1414
1515import { IpAddressTypes , selectIpAddress } from './ip-addresses' ;
1616import { InstanceConnectionInfo } from './instance-connection-info' ;
17- import { resolveInstanceName } from './parse-instance-connection-name' ;
17+ import {
18+ isSameInstance ,
19+ resolveInstanceName ,
20+ } from './parse-instance-connection-name' ;
1821import { InstanceMetadata } from './sqladmin-fetcher' ;
1922import { generateKeys } from './crypto' ;
2023import { RSAKeys } from './rsa-keys' ;
2124import { SslCert } from './ssl-cert' ;
2225import { getRefreshInterval , isExpirationTimeValid } from './time' ;
2326import { AuthTypes } from './auth-types' ;
27+ import { CloudSQLConnectorError } from './errors' ;
28+
29+ // Private types that describe exactly the methods
30+ // needed from tls.Socket to be able to close
31+ // sockets when the DNS Name changes.
32+ type EventFn = ( ) => void ;
33+ type DestroyableSocket = {
34+ destroy : ( error ?: Error ) => void ;
35+ once : ( name : string , handler : EventFn ) => void ;
36+ } ;
2437
2538interface Fetcher {
2639 getInstanceMetadata ( {
@@ -42,6 +55,7 @@ interface CloudSQLInstanceOptions {
4255 ipType : IpAddressTypes ;
4356 limitRateInterval ?: number ;
4457 sqlAdminFetcher : Fetcher ;
58+ failoverPeriod ?: number ;
4559}
4660
4761interface RefreshResult {
@@ -74,9 +88,13 @@ export class CloudSQLInstance {
7488 // The ongoing refresh promise is referenced by the `next` property
7589 private next ?: Promise < RefreshResult > ;
7690 private scheduledRefreshID ?: ReturnType < typeof setTimeout > | null = undefined ;
91+ private checkDomainID ?: ReturnType < typeof setInterval > | null = undefined ;
7792 /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
7893 private throttle ?: any ;
7994 private closed = false ;
95+ private failoverPeriod : number ;
96+ private sockets = new Set < DestroyableSocket > ( ) ;
97+
8098 public readonly instanceInfo : InstanceConnectionInfo ;
8199 public ephemeralCert ?: SslCert ;
82100 public host ?: string ;
@@ -98,6 +116,7 @@ export class CloudSQLInstance {
98116 this . ipType = options . ipType || IpAddressTypes . PUBLIC ;
99117 this . limitRateInterval = options . limitRateInterval || 30 * 1000 ; // 30 seconds
100118 this . sqlAdminFetcher = options . sqlAdminFetcher ;
119+ this . failoverPeriod = options . failoverPeriod || 30 * 1000 ; // 30 seconds
101120 }
102121
103122 // p-throttle library has to be initialized in an async scope in order to
@@ -153,6 +172,19 @@ export class CloudSQLInstance {
153172 return Promise . reject ( 'closed' ) ;
154173 }
155174
175+ // Lazy instantiation of the checkDomain interval on the first refresh
176+ // This avoids issues with test cases that instantiate a CloudSqlInstance.
177+ // If failoverPeriod is 0 (or negative) don't check for DNS updates.
178+ if (
179+ this ?. instanceInfo ?. domainName &&
180+ ! this . checkDomainID &&
181+ this . failoverPeriod > 0
182+ ) {
183+ this . checkDomainID = setInterval ( ( ) => {
184+ this . checkDomainChanged ( ) ;
185+ } , this . failoverPeriod ) ;
186+ }
187+
156188 const currentRefreshId = this . scheduledRefreshID ;
157189
158190 // Since forceRefresh might be invoked during an ongoing refresh
@@ -312,9 +344,48 @@ export class CloudSQLInstance {
312344 close ( ) : void {
313345 this . closed = true ;
314346 this . cancelRefresh ( ) ;
347+ if ( this . checkDomainID ) {
348+ clearInterval ( this . checkDomainID ) ;
349+ this . checkDomainID = null ;
350+ }
351+ for ( const socket of this . sockets ) {
352+ socket . destroy (
353+ new CloudSQLConnectorError ( {
354+ code : 'ERRCLOSED' ,
355+ message : 'The connector was closed.' ,
356+ } )
357+ ) ;
358+ }
315359 }
316360
317361 isClosed ( ) : boolean {
318362 return this . closed ;
319363 }
364+ async checkDomainChanged ( ) {
365+ if ( ! this . instanceInfo . domainName ) {
366+ return ;
367+ }
368+
369+ const newInfo = await resolveInstanceName (
370+ undefined ,
371+ this . instanceInfo . domainName
372+ ) ;
373+ if ( ! isSameInstance ( this . instanceInfo , newInfo ) ) {
374+ // Domain name changed. Close and remove, then create a new map entry.
375+ this . close ( ) ;
376+ }
377+ }
378+ addSocket ( socket : DestroyableSocket ) {
379+ if ( ! this . instanceInfo . domainName ) {
380+ // This was not connected by domain name. Ignore all sockets.
381+ return ;
382+ }
383+
384+ // Add the socket to the list
385+ this . sockets . add ( socket ) ;
386+ // When the socket is closed, remove it.
387+ socket . once ( 'closed' , ( ) => {
388+ this . sockets . delete ( socket ) ;
389+ } ) ;
390+ }
320391}
0 commit comments