Skip to content

Commit 0e4853a

Browse files
authored
refactor: Improve the refresh to allow connectors to close, part of #421. (#424)
Add CloudSqlInstance.close() to stop the refresh cycle permanently. Change CloudSqlInstance.forceRefresh() so that it schedules a refresh and returns immediately. This should not return a promise. Update test cases to account for changes. Part of #421
1 parent 0d0adaf commit 0e4853a

File tree

3 files changed

+92
-14
lines changed

3 files changed

+92
-14
lines changed

src/cloud-sql-instance.ts

+48-3
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export class CloudSQLInstance {
6969
private scheduledRefreshID?: ReturnType<typeof setTimeout> | null = undefined;
7070
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
7171
private throttle?: any;
72+
private closed = false;
7273
public readonly instanceInfo: InstanceConnectionInfo;
7374
public ephemeralCert?: SslCert;
7475
public host?: string;
@@ -106,17 +107,45 @@ export class CloudSQLInstance {
106107
}) as ReturnType<typeof pThrottle>;
107108
}
108109

109-
forceRefresh(): Promise<RefreshResult> {
110+
forceRefresh(): Promise<void> {
110111
// if a refresh is already ongoing, just await for its promise to fulfill
111112
// so that a new instance info is available before reconnecting
112113
if (this.next) {
113-
return this.next;
114+
return new Promise(resolve => {
115+
if (this.next) {
116+
this.next.finally(resolve);
117+
} else {
118+
resolve();
119+
}
120+
});
114121
}
122+
115123
this.cancelRefresh();
116-
return this.refresh();
124+
this.scheduleRefresh(0);
125+
126+
return new Promise(resolve => {
127+
// setTimeout() to yield execution to allow other refresh background
128+
// task to start.
129+
setTimeout(() => {
130+
if (this.next) {
131+
// If there is a refresh promise in progress, resolve this promise
132+
// when the refresh is complete.
133+
this.next.finally(resolve);
134+
} else {
135+
// Else resolve immediately.
136+
resolve();
137+
}
138+
}, 0);
139+
});
117140
}
118141

119142
refresh(): Promise<RefreshResult> {
143+
if (this.closed) {
144+
this.scheduledRefreshID = undefined;
145+
this.next = undefined;
146+
return Promise.reject('closed');
147+
}
148+
120149
const currentRefreshId = this.scheduledRefreshID;
121150

122151
// Since forceRefresh might be invoked during an ongoing refresh
@@ -183,6 +212,12 @@ export class CloudSQLInstance {
183212
// used to create new connections to a Cloud SQL instance. It throws in
184213
// case any of the internal steps fails.
185214
private async performRefresh(): Promise<RefreshResult> {
215+
if (this.closed) {
216+
// The connector may be closed while the rate limiter delayed
217+
// a call to performRefresh() so check this.closed before continuing.
218+
return Promise.reject('closed');
219+
}
220+
186221
const rsaKeys: RSAKeys = await generateKeys();
187222
const metadata: InstanceMetadata =
188223
await this.sqlAdminFetcher.getInstanceMetadata(this.instanceInfo);
@@ -244,6 +279,9 @@ export class CloudSQLInstance {
244279
}
245280

246281
private scheduleRefresh(delay: number): void {
282+
if (this.closed) {
283+
return;
284+
}
247285
this.scheduledRefreshID = setTimeout(() => this.refresh(), delay);
248286
}
249287

@@ -260,4 +298,11 @@ export class CloudSQLInstance {
260298
setEstablishedConnection(): void {
261299
this.establishedConnection = true;
262300
}
301+
302+
// close stops any refresh process in progress and prevents future refresh
303+
// connections.
304+
close(): void {
305+
this.closed = true;
306+
this.cancelRefresh();
307+
}
263308
}

src/connector.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,8 @@ export class Connector {
230230
serverCaMode,
231231
dnsName,
232232
});
233-
tlsSocket.once('error', async () => {
234-
await cloudSqlInstance.forceRefresh();
233+
tlsSocket.once('error', () => {
234+
cloudSqlInstance.forceRefresh();
235235
});
236236
tlsSocket.once('secureConnect', async () => {
237237
cloudSqlInstance.setEstablishedConnection();
@@ -333,7 +333,7 @@ export class Connector {
333333
// Also clear up any local proxy servers and socket connections.
334334
close(): void {
335335
for (const instance of this.instances.values()) {
336-
instance.cancelRefresh();
336+
instance.close();
337337
}
338338
for (const server of this.localProxies) {
339339
server.close();

test/cloud-sql-instance.ts

+41-8
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,9 @@ t.test('CloudSQLInstance', async t => {
269269
await CloudSQLInstance.prototype.refresh.call(instance);
270270
instance.refresh = CloudSQLInstance.prototype.refresh;
271271
};
272+
272273
await instance.forceRefresh();
274+
273275
t.ok(
274276
cancelRefreshCalled,
275277
'should cancelRefresh current refresh cycle on force refresh'
@@ -290,7 +292,7 @@ t.test('CloudSQLInstance', async t => {
290292
let cancelRefreshCalled = false;
291293
let refreshCalled = false;
292294

293-
const refreshPromise = instance.refresh();
295+
instance.refresh();
294296

295297
instance.cancelRefresh = () => {
296298
cancelRefreshCalled = true;
@@ -302,13 +304,7 @@ t.test('CloudSQLInstance', async t => {
302304
return CloudSQLInstance.prototype.refresh.call(instance);
303305
};
304306

305-
const forceRefreshPromise = instance.forceRefresh();
306-
t.strictSame(
307-
refreshPromise,
308-
forceRefreshPromise,
309-
'forceRefresh should return same promise ref from initial refresh call'
310-
);
311-
await forceRefreshPromise;
307+
await instance.forceRefresh();
312308

313309
t.ok(
314310
!cancelRefreshCalled,
@@ -481,6 +477,43 @@ t.test('CloudSQLInstance', async t => {
481477
}
482478
);
483479

480+
t.test(
481+
'close on established connection and ongoing failed cycle',
482+
async t => {
483+
let metadataCount = 0;
484+
const failAndSlowFetcher = {
485+
...fetcher,
486+
async getInstanceMetadata() {
487+
await (() => new Promise(res => setTimeout(res, 50)))();
488+
metadataCount++;
489+
return fetcher.getInstanceMetadata();
490+
},
491+
};
492+
493+
const instance = new CloudSQLInstance({
494+
ipType: IpAddressTypes.PUBLIC,
495+
authType: AuthTypes.PASSWORD,
496+
instanceConnectionName: 'my-project:us-east1:my-instance',
497+
sqlAdminFetcher: failAndSlowFetcher,
498+
limitRateInterval: 50,
499+
});
500+
501+
await instance.refresh();
502+
instance.setEstablishedConnection();
503+
504+
// starts a new refresh cycle but do not await on it
505+
instance.close();
506+
await instance.forceRefresh();
507+
t.equal(metadataCount, 1, 'No refresh after close');
508+
509+
await t.rejects(
510+
instance.refresh(),
511+
'closed',
512+
'Refresh after close rejected.'
513+
);
514+
}
515+
);
516+
484517
t.test(
485518
'get invalid certificate data while having a current valid',
486519
async t => {

0 commit comments

Comments
 (0)