Skip to content

Commit 8aa2052

Browse files
authored
fix(pool): resolve potential memory leak (#4111)
1 parent 0e06e02 commit 8aa2052

File tree

2 files changed

+109
-15
lines changed

2 files changed

+109
-15
lines changed

lib/base/connection.js

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -835,27 +835,31 @@ class BaseConnection extends EventEmitter {
835835
if (!cb) {
836836
return;
837837
}
838+
838839
if (this._fatalError || this._protocolError) {
839840
return cb(this._fatalError || this._protocolError);
840841
}
842+
841843
if (this._handshakePacket) {
842844
return cb(null, this);
843845
}
844-
let connectCalled = 0;
845-
function callbackOnce(isErrorHandler) {
846-
return function (param) {
847-
if (!connectCalled) {
848-
if (isErrorHandler) {
849-
cb(param);
850-
} else {
851-
cb(null, param);
852-
}
853-
}
854-
connectCalled = 1;
855-
};
856-
}
857-
this.once('error', callbackOnce(true));
858-
this.once('connect', callbackOnce(false));
846+
847+
/* eslint-disable prefer-const */
848+
let onError, onConnect;
849+
850+
onError = (param) => {
851+
this.removeListener('connect', onConnect);
852+
cb(param);
853+
};
854+
855+
onConnect = (param) => {
856+
this.removeListener('error', onError);
857+
cb(null, param);
858+
};
859+
/* eslint-enable prefer-const */
860+
861+
this.once('error', onError);
862+
this.once('connect', onConnect);
859863
}
860864

861865
// ===================================
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import type { Pool, PoolConnection } from '../../index.js';
2+
import { assert, describe, it } from 'poku';
3+
import { createPool } from '../common.test.mjs';
4+
5+
/** Returns the raw `PoolConnection` with access to `EventEmitter` listeners. */
6+
const getConnection = (pool: Pool) =>
7+
new Promise<PoolConnection>((resolve, reject) => {
8+
pool.getConnection((err, conn) => {
9+
if (err) return reject(err);
10+
resolve(conn);
11+
});
12+
});
13+
14+
await describe('Pool Memory Leak (issue #3904)', async () => {
15+
await it('should not retain stale connect/error listeners after pool connection is established', async () => {
16+
const pool = createPool({ connectionLimit: 1 });
17+
const conn = await getConnection(pool);
18+
19+
try {
20+
const errorListenerCount = conn.listenerCount('error');
21+
22+
assert.strictEqual(
23+
errorListenerCount,
24+
1,
25+
`Expected 1 error listener, but found ${errorListenerCount}.`
26+
);
27+
} finally {
28+
conn.release();
29+
await pool.promise().end();
30+
}
31+
});
32+
33+
await it('should not accumulate listeners across multiple pool.query calls', async () => {
34+
const pool = createPool({ connectionLimit: 1 });
35+
const promisePool = pool.promise();
36+
37+
await promisePool.query('SELECT 1');
38+
39+
const conn = await getConnection(pool);
40+
41+
try {
42+
const errorListeners = conn.listenerCount('error');
43+
const connectListeners = conn.listenerCount('connect');
44+
45+
assert.strictEqual(
46+
errorListeners,
47+
1,
48+
`Expected 1 error listener after queries, but found ${errorListeners}.`
49+
);
50+
51+
assert.strictEqual(
52+
connectListeners,
53+
0,
54+
`Expected 0 connect listeners, but found ${connectListeners}.`
55+
);
56+
} finally {
57+
conn.release();
58+
await promisePool.end();
59+
}
60+
});
61+
62+
await it('should release query result references after query completes', async () => {
63+
const pool = createPool({ connectionLimit: 1 });
64+
const promisePool = pool.promise();
65+
66+
await promisePool.query(
67+
'SELECT REPEAT("x", 1024) AS padding FROM (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t'
68+
);
69+
70+
const conn = await getConnection(pool);
71+
72+
try {
73+
const errorListeners = conn.rawListeners('error');
74+
75+
const hasStaleListener = errorListeners.some((listener) => {
76+
// @ts-expect-error: TODO: implement typings
77+
const original = listener.listener ?? listener;
78+
return original.toString().includes('connectCalled');
79+
});
80+
81+
assert(
82+
!hasStaleListener,
83+
'Found a stale callbackOnce error listener that retains query result references.'
84+
);
85+
} finally {
86+
conn.release();
87+
await promisePool.end();
88+
}
89+
});
90+
});

0 commit comments

Comments
 (0)