14
14
15
15
import { IpAddressTypes , selectIpAddress } from './ip-addresses' ;
16
16
import { 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' ;
18
21
import { InstanceMetadata } from './sqladmin-fetcher' ;
19
22
import { generateKeys } from './crypto' ;
20
23
import { RSAKeys } from './rsa-keys' ;
21
24
import { SslCert } from './ssl-cert' ;
22
25
import { getRefreshInterval , isExpirationTimeValid } from './time' ;
23
26
import { 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
+ } ;
24
37
25
38
interface Fetcher {
26
39
getInstanceMetadata ( {
@@ -42,6 +55,7 @@ interface CloudSQLInstanceOptions {
42
55
ipType : IpAddressTypes ;
43
56
limitRateInterval ?: number ;
44
57
sqlAdminFetcher : Fetcher ;
58
+ failoverPeriod ?: number ;
45
59
}
46
60
47
61
interface RefreshResult {
@@ -74,9 +88,13 @@ export class CloudSQLInstance {
74
88
// The ongoing refresh promise is referenced by the `next` property
75
89
private next ?: Promise < RefreshResult > ;
76
90
private scheduledRefreshID ?: ReturnType < typeof setTimeout > | null = undefined ;
91
+ private checkDomainID ?: ReturnType < typeof setInterval > | null = undefined ;
77
92
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
78
93
private throttle ?: any ;
79
94
private closed = false ;
95
+ private failoverPeriod : number ;
96
+ private sockets = new Set < DestroyableSocket > ( ) ;
97
+
80
98
public readonly instanceInfo : InstanceConnectionInfo ;
81
99
public ephemeralCert ?: SslCert ;
82
100
public host ?: string ;
@@ -98,6 +116,7 @@ export class CloudSQLInstance {
98
116
this . ipType = options . ipType || IpAddressTypes . PUBLIC ;
99
117
this . limitRateInterval = options . limitRateInterval || 30 * 1000 ; // 30 seconds
100
118
this . sqlAdminFetcher = options . sqlAdminFetcher ;
119
+ this . failoverPeriod = options . failoverPeriod || 30 * 1000 ; // 30 seconds
101
120
}
102
121
103
122
// p-throttle library has to be initialized in an async scope in order to
@@ -153,6 +172,14 @@ export class CloudSQLInstance {
153
172
return Promise . reject ( 'closed' ) ;
154
173
}
155
174
175
+ // Lazy instantiation of the checkDomain interval on the first refresh
176
+ // This avoids issues with test cases that instantiate a CloudSqlInstance.
177
+ if ( this ?. instanceInfo ?. domainName && ! this . checkDomainID ) {
178
+ this . checkDomainID = setInterval ( ( ) => {
179
+ this . checkDomainChanged ( ) ;
180
+ } , this . failoverPeriod ) ;
181
+ }
182
+
156
183
const currentRefreshId = this . scheduledRefreshID ;
157
184
158
185
// Since forceRefresh might be invoked during an ongoing refresh
@@ -312,9 +339,48 @@ export class CloudSQLInstance {
312
339
close ( ) : void {
313
340
this . closed = true ;
314
341
this . cancelRefresh ( ) ;
342
+ if ( this . checkDomainID ) {
343
+ clearInterval ( this . checkDomainID ) ;
344
+ this . checkDomainID = null ;
345
+ }
346
+ for ( const socket of this . sockets ) {
347
+ socket . destroy (
348
+ new CloudSQLConnectorError ( {
349
+ code : 'ERRCLOSED' ,
350
+ message : 'The connector was closed.' ,
351
+ } )
352
+ ) ;
353
+ }
315
354
}
316
355
317
356
isClosed ( ) : boolean {
318
357
return this . closed ;
319
358
}
359
+ async checkDomainChanged ( ) {
360
+ if ( ! this . instanceInfo . domainName ) {
361
+ return ;
362
+ }
363
+
364
+ const newInfo = await resolveInstanceName (
365
+ undefined ,
366
+ this . instanceInfo . domainName
367
+ ) ;
368
+ if ( ! isSameInstance ( this . instanceInfo , newInfo ) ) {
369
+ // Domain name changed. Close and remove, then create a new map entry.
370
+ this . close ( ) ;
371
+ }
372
+ }
373
+ addSocket ( socket : DestroyableSocket ) {
374
+ if ( ! this . instanceInfo . domainName ) {
375
+ // This was not connected by domain name. Ignore all sockets.
376
+ return ;
377
+ }
378
+
379
+ // Add the socket to the list
380
+ this . sockets . add ( socket ) ;
381
+ // When the socket is closed, remove it.
382
+ socket . once ( 'closed' , ( ) => {
383
+ this . sockets . delete ( socket ) ;
384
+ } ) ;
385
+ }
320
386
}
0 commit comments