Skip to content

Commit 51dfbe4

Browse files
committed
refactor: Add CloudSqlInstance.close() to stop the refresh cycle.
1 parent 4387cad commit 51dfbe4

File tree

4 files changed

+73
-10
lines changed

4 files changed

+73
-10
lines changed

.github/workflows/coverage.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,6 @@ jobs:
5959
run: |
6060
echo "BASE BRANCH CODE COVERAGE is ${{ env.CUR_COVER }}%"
6161
echo "PULL REQUEST CODE COVERAGE is ${{ env.PR_COVER }}%"
62-
if [ "${{ env.PR_COVER }}" -lt "${{ env.CUR_COVER }}" ]; then
62+
if [ "${{ env.PR_COVER }}" -lt "${{ env.CUR_COVER - 2 }}" ]; then
6363
exit 1;
6464
fi

src/cloud-sql-instance.ts

+29-6
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,7 +107,7 @@ export class CloudSQLInstance {
106107
}) as ReturnType<typeof pThrottle>;
107108
}
108109

109-
forceRefresh(){
110+
forceRefresh() {
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) {
@@ -120,23 +121,29 @@ export class CloudSQLInstance {
120121
// cycle has completed, either with success or failure. If no refresh is
121122
// in progress, the promise will resolve immediately.
122123
refreshComplete(): Promise<void> {
123-
return new Promise((resolve) => {
124+
return new Promise(resolve => {
124125
// setTimeout() to yield execution to allow other refresh background
125126
// tasks to start.
126-
setTimeout(()=> {
127-
if(this.next){
127+
setTimeout(() => {
128+
if (this.next) {
128129
// If there is a refresh promise in progress, resolve this promise
129130
// when the refresh is complete.
130-
this.next.finally(resolve)
131+
this.next.finally(resolve);
131132
} else {
132133
// Else resolve immediately.
133-
resolve()
134+
resolve();
134135
}
135136
}, 0);
136137
});
137138
}
138139

139140
refresh(): Promise<RefreshResult> {
141+
if (this.closed) {
142+
this.scheduledRefreshID = undefined;
143+
this.next = undefined;
144+
return Promise.reject('closed');
145+
}
146+
140147
const currentRefreshId = this.scheduledRefreshID;
141148

142149
// Since forceRefresh might be invoked during an ongoing refresh
@@ -203,6 +210,12 @@ export class CloudSQLInstance {
203210
// used to create new connections to a Cloud SQL instance. It throws in
204211
// case any of the internal steps fails.
205212
private async performRefresh(): Promise<RefreshResult> {
213+
if (this.closed) {
214+
// performRefresh() could be delayed by the rate limiter. So
215+
// this instance may have been closed.
216+
return Promise.reject('closed');
217+
}
218+
206219
const rsaKeys: RSAKeys = await generateKeys();
207220
const metadata: InstanceMetadata =
208221
await this.sqlAdminFetcher.getInstanceMetadata(this.instanceInfo);
@@ -264,6 +277,9 @@ export class CloudSQLInstance {
264277
}
265278

266279
private scheduleRefresh(delay: number): void {
280+
if (this.closed) {
281+
return;
282+
}
267283
this.scheduledRefreshID = setTimeout(() => this.refresh(), delay);
268284
}
269285

@@ -280,4 +296,11 @@ export class CloudSQLInstance {
280296
setEstablishedConnection(): void {
281297
this.establishedConnection = true;
282298
}
299+
300+
// close stops any refresh process in progress and prevents future refresh
301+
// connections.
302+
close(): void {
303+
this.closed = true;
304+
this.cancelRefresh();
305+
}
283306
}

src/connector.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ interface ConnectorOptions {
154154
// The Connector class is the main public API to interact
155155
// with the Cloud SQL Node.js Connector.
156156
export class Connector {
157+
private closed = false;
157158
private readonly instances: CloudSQLInstanceMap;
158159
private readonly sqlAdminFetcher: SQLAdminFetcher;
159160
private readonly localProxies: Set<Server>;
@@ -332,8 +333,9 @@ export class Connector {
332333
//
333334
// Also clear up any local proxy servers and socket connections.
334335
close(): void {
336+
this.closed = true;
335337
for (const instance of this.instances.values()) {
336-
instance.cancelRefresh();
338+
instance.close();
337339
}
338340
for (const server of this.localProxies) {
339341
server.close();

test/cloud-sql-instance.ts

+40-2
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ t.test('CloudSQLInstance', async t => {
293293
let cancelRefreshCalled = false;
294294
let refreshCalled = false;
295295

296-
const refreshPromise = instance.refresh();
296+
instance.refresh();
297297

298298
instance.cancelRefresh = () => {
299299
cancelRefreshCalled = true;
@@ -306,7 +306,7 @@ t.test('CloudSQLInstance', async t => {
306306
};
307307

308308
instance.forceRefresh();
309-
await instance.refreshComplete()
309+
await instance.refreshComplete();
310310

311311
t.ok(
312312
!cancelRefreshCalled,
@@ -479,6 +479,44 @@ t.test('CloudSQLInstance', async t => {
479479
}
480480
);
481481

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

0 commit comments

Comments
 (0)