From 30fae5753d9c2a5eb57da4accaba0b3303ec2dac Mon Sep 17 00:00:00 2001 From: Jin Hou Date: Mon, 18 May 2026 21:15:48 -0700 Subject: [PATCH] fix: connector close leaks for in-flight queries --- src/cloud-sql-instance.ts | 30 ++++++++++++++++-------------- src/connector.ts | 13 +++++++++++-- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/cloud-sql-instance.ts b/src/cloud-sql-instance.ts index 23b1506b..3dc2e329 100644 --- a/src/cloud-sql-instance.ts +++ b/src/cloud-sql-instance.ts @@ -364,12 +364,14 @@ export class CloudSQLInstance { this.checkDomainID = null; } for (const socket of this.sockets) { - socket.destroy( - new CloudSQLConnectorError({ - code: 'ERRCLOSED', - message: 'The connector was closed.', - }) - ); + if (typeof socket.destroy === 'function') { + socket.destroy( + new CloudSQLConnectorError({ + code: 'ERRCLOSED', + message: 'The connector was closed.', + }) + ); + } } } @@ -391,15 +393,15 @@ export class CloudSQLInstance { } } addSocket(socket: DestroyableSocket) { - if (!this.instanceInfo.domainName) { - // This was not connected by domain name. Ignore all sockets. - return; - } - - // Add the socket to the list + // Track all active sockets created by this instance so they can + // be forcefully cleaned up during a domain change or when + // the connector is explicitly closed. this.sockets.add(socket); - // When the socket is closed, remove it. - socket.once('closed', () => { + + // When the socket is closed by the driver or peer, remove it + // from our tracking set to prevent reference memory leaks. + // Note: Node.js TLSSocket/Socket emits 'close', not 'closed'. + socket.once('close', () => { this.sockets.delete(socket); }); } diff --git a/src/connector.ts b/src/connector.ts index 37848473..111ad141 100644 --- a/src/connector.ts +++ b/src/connector.ts @@ -358,8 +358,17 @@ export class Connector { // // Also clear up any local proxy servers and socket connections. close(): void { - for (const instance of this.instances.values()) { - instance.promise.then(inst => inst.close()); + for (const entry of this.instances.values()) { + if (entry.isResolved() && entry.instance) { + // If the instance is already resolved, close it synchronously. + // This prevents a race condition with immediate connection pool close + // (e.g., pool.end()) where asynchronous microtasks would execute too late. + entry.instance.close(); + } else { + // Otherwise, close the instance asynchronously once its initial + // refresh has finished. + entry.promise.then(inst => inst.close()).catch(() => {}); + } } for (const server of this.localProxies) { server.close();