Skip to content

Commit 0e1eb46

Browse files
committed
feat: Periodicaly check if domain name changed and close connections to old database.
wip: periodic check for domain change wip: periodic checks wip: close sockets on instance closed
1 parent dbd3ae8 commit 0e1eb46

File tree

4 files changed

+216
-4
lines changed

4 files changed

+216
-4
lines changed

src/cloud-sql-instance.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,23 @@
1414

1515
import {IpAddressTypes, selectIpAddress} from './ip-addresses';
1616
import {InstanceConnectionInfo} from './instance-connection-info';
17-
import {resolveInstanceName} from './parse-instance-connection-name';
17+
import {isSameInstance, resolveInstanceName} from './parse-instance-connection-name';
1818
import {InstanceMetadata} from './sqladmin-fetcher';
1919
import {generateKeys} from './crypto';
2020
import {RSAKeys} from './rsa-keys';
2121
import {SslCert} from './ssl-cert';
2222
import {getRefreshInterval, isExpirationTimeValid} from './time';
2323
import {AuthTypes} from './auth-types';
24+
import {CloudSQLConnectorError} from './errors';
25+
26+
// Private types that describe exactly the methods
27+
// needed from tls.Socket to be able to close
28+
// sockets when the DNS Name changes.
29+
type EventFn = () => void;
30+
type ClosableSocket = {
31+
destroy: (error?: Error) => void;
32+
once: (name: string, handler: EventFn) => void;
33+
};
2434

2535
interface Fetcher {
2636
getInstanceMetadata({
@@ -42,6 +52,7 @@ interface CloudSQLInstanceOptions {
4252
ipType: IpAddressTypes;
4353
limitRateInterval?: number;
4454
sqlAdminFetcher: Fetcher;
55+
checkDomainInterval?: number;
4556
}
4657

4758
interface RefreshResult {
@@ -74,9 +85,13 @@ export class CloudSQLInstance {
7485
// The ongoing refresh promise is referenced by the `next` property
7586
private next?: Promise<RefreshResult>;
7687
private scheduledRefreshID?: ReturnType<typeof setTimeout> | null = undefined;
88+
private checkDomainID?: ReturnType<typeof setInterval> | null = undefined;
7789
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
7890
private throttle?: any;
7991
private closed = false;
92+
private checkDomainInterval: number;
93+
private sockets = new Set<ClosableSocket>();
94+
8095
public readonly instanceInfo: InstanceConnectionInfo;
8196
public ephemeralCert?: SslCert;
8297
public host?: string;
@@ -98,6 +113,7 @@ export class CloudSQLInstance {
98113
this.ipType = options.ipType || IpAddressTypes.PUBLIC;
99114
this.limitRateInterval = options.limitRateInterval || 30 * 1000; // 30 seconds
100115
this.sqlAdminFetcher = options.sqlAdminFetcher;
116+
this.checkDomainInterval = options.checkDomainInterval || 30 * 1000;
101117
}
102118

103119
// p-throttle library has to be initialized in an async scope in order to
@@ -142,7 +158,7 @@ export class CloudSQLInstance {
142158
// Else resolve immediately.
143159
resolve();
144160
}
145-
}, 0);
161+
});
146162
});
147163
}
148164

@@ -152,6 +168,14 @@ export class CloudSQLInstance {
152168
this.next = undefined;
153169
return Promise.reject('closed');
154170
}
171+
if (this?.instanceInfo?.domainName && !this.checkDomainID) {
172+
this.checkDomainID = setInterval(
173+
() => {
174+
this.checkDomainChanged();
175+
},
176+
this.checkDomainInterval || 30 * 1000
177+
);
178+
}
155179

156180
const currentRefreshId = this.scheduledRefreshID;
157181

@@ -296,8 +320,8 @@ export class CloudSQLInstance {
296320
// If refresh has not yet started, then cancel the setTimeout
297321
if (this.scheduledRefreshID) {
298322
clearTimeout(this.scheduledRefreshID);
323+
this.scheduledRefreshID = null;
299324
}
300-
this.scheduledRefreshID = null;
301325
}
302326

303327
// Mark this instance as having an active connection. This is important to
@@ -312,9 +336,48 @@ export class CloudSQLInstance {
312336
close(): void {
313337
this.closed = true;
314338
this.cancelRefresh();
339+
if (this.checkDomainID) {
340+
clearInterval(this.checkDomainID);
341+
this.checkDomainID = null;
342+
}
343+
for (const socket of this.sockets) {
344+
socket.destroy(
345+
new CloudSQLConnectorError({
346+
code: 'ERRCLOSED',
347+
message: 'The connector was closed.',
348+
})
349+
);
350+
}
315351
}
316352

317353
isClosed(): boolean {
318354
return this.closed;
319355
}
356+
async checkDomainChanged() {
357+
if (!this.instanceInfo.domainName) {
358+
return;
359+
}
360+
361+
const newInfo = await resolveInstanceName(
362+
undefined,
363+
this.instanceInfo.domainName
364+
);
365+
if (!isSameInstance(this.instanceInfo, newInfo)) {
366+
// Domain name changed. Close and remove, then create a new map entry.
367+
this.close();
368+
}
369+
}
370+
addSocket(socket: ClosableSocket) {
371+
if (!this.instanceInfo.domainName) {
372+
// This was not connected by domain name. Ignore all sockets.
373+
return;
374+
}
375+
376+
// Add the socket to the list
377+
this.sockets.add(socket);
378+
// When the socket is closed, remove it.
379+
socket.once('closed', () => {
380+
this.sockets.delete(socket);
381+
});
382+
}
320383
}

src/connector.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export declare interface ConnectionOptions {
4444
ipType?: IpAddressTypes;
4545
instanceConnectionName: string;
4646
domainName?: string;
47+
checkDomainInterval?: number;
4748
limitRateInterval?: number;
4849
}
4950

@@ -125,6 +126,7 @@ class CloudSQLInstanceMap extends Map<string, CacheEntry> {
125126
} else {
126127
// Check the domain name, then if the instance is still open
127128
// return it
129+
await entry.instance?.checkDomainChanged();
128130
if (!entry.instance?.isClosed()) {
129131
// The instance is open and the domain has not changed.
130132
// use the cached instance.
@@ -141,6 +143,7 @@ class CloudSQLInstanceMap extends Map<string, CacheEntry> {
141143
ipType: opts.ipType || IpAddressTypes.PUBLIC,
142144
limitRateInterval: opts.limitRateInterval || 30 * 1000, // 30 sec
143145
sqlAdminFetcher: this.sqlAdminFetcher,
146+
checkDomainInterval: opts.checkDomainInterval,
144147
});
145148
this.set(this.cacheKey(opts), new CacheEntry(promise));
146149

@@ -244,6 +247,9 @@ export class Connector {
244247
tlsSocket.once('secureConnect', async () => {
245248
cloudSqlInstance.setEstablishedConnection();
246249
});
250+
251+
cloudSqlInstance.addSocket(tlsSocket);
252+
247253
return tlsSocket;
248254
}
249255

test/cloud-sql-instance.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ t.test('CloudSQLInstance', async t => {
6767
instanceConnectionName: 'my-project:us-east1:my-instance',
6868
sqlAdminFetcher: fetcher,
6969
});
70+
t.after(() => instance.close());
7071

7172
t.same(
7273
instance.ephemeralCert.cert,
@@ -115,6 +116,7 @@ t.test('CloudSQLInstance', async t => {
115116
limitRateInterval: 50,
116117
},
117118
});
119+
t.after(() => instance.close());
118120

119121
await t.rejects(
120122
instance.refresh(),
@@ -135,6 +137,7 @@ t.test('CloudSQLInstance', async t => {
135137
limitRateInterval: 50,
136138
},
137139
});
140+
t.after(() => instance.close());
138141
instance.refresh = () => {
139142
if (refreshCount === 2) {
140143
const end = Date.now();
@@ -177,6 +180,7 @@ t.test('CloudSQLInstance', async t => {
177180
limitRateInterval: 50,
178181
},
179182
});
183+
t.after(() => instance.close());
180184
await (() =>
181185
new Promise((res): void => {
182186
let refreshCount = 0;
@@ -233,6 +237,7 @@ t.test('CloudSQLInstance', async t => {
233237
limitRateInterval: 50,
234238
},
235239
});
240+
t.after(() => instance.close());
236241
await (() =>
237242
new Promise((res): void => {
238243
let refreshCount = 0;
@@ -263,6 +268,7 @@ t.test('CloudSQLInstance', async t => {
263268
limitRateInterval: 50,
264269
},
265270
});
271+
t.after(() => instance.close());
266272

267273
await instance.refresh();
268274

@@ -301,6 +307,7 @@ t.test('CloudSQLInstance', async t => {
301307
limitRateInterval: 50,
302308
},
303309
});
310+
t.after(() => instance.close());
304311

305312
let cancelRefreshCalled = false;
306313
let refreshCalled = false;
@@ -338,6 +345,7 @@ t.test('CloudSQLInstance', async t => {
338345
sqlAdminFetcher: fetcher,
339346
},
340347
});
348+
t.after(() => instance.close());
341349

342350
const start = Date.now();
343351
// starts regular refresh cycle
@@ -377,6 +385,7 @@ t.test('CloudSQLInstance', async t => {
377385
sqlAdminFetcher: fetcher,
378386
},
379387
});
388+
t.after(() => instance.close());
380389
const start = Date.now();
381390
// starts out refresh logic
382391
let refreshCount = 1;
@@ -424,6 +433,7 @@ t.test('CloudSQLInstance', async t => {
424433
limitRateInterval: 50,
425434
},
426435
});
436+
t.after(() => instance.close());
427437

428438
// starts a new refresh cycle but do not await on it
429439
instance.refresh();
@@ -451,6 +461,7 @@ t.test('CloudSQLInstance', async t => {
451461
limitRateInterval: 50,
452462
},
453463
});
464+
t.after(() => instance.close());
454465

455466
// simulates an ongoing instance, already has data
456467
await instance.refresh();
@@ -487,6 +498,7 @@ t.test('CloudSQLInstance', async t => {
487498
limitRateInterval: 50,
488499
},
489500
});
501+
t.after(() => instance.close());
490502

491503
await instance.refresh();
492504
instance.setEstablishedConnection();
@@ -522,6 +534,7 @@ t.test('CloudSQLInstance', async t => {
522534
limitRateInterval: 50,
523535
},
524536
});
537+
t.after(() => instance.close());
525538

526539
await instance.refresh();
527540
instance.setEstablishedConnection();
@@ -589,6 +602,7 @@ t.test('CloudSQLInstance', async t => {
589602
limitRateInterval: 0,
590603
},
591604
});
605+
t.after(() => instance.close());
592606
await (() =>
593607
new Promise((res): void => {
594608
let refreshCount = 0;

0 commit comments

Comments
 (0)