Skip to content

Commit 7e57db6

Browse files
wellwelwelclaudeCopilot
authored
fix: prevent double release from corrupting the connection pool (#4186)
* fix: prevent double release from corrupting the connection pool Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: defer _released reset until connection is delivered in getConnection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 92d0724 commit 7e57db6

3 files changed

Lines changed: 144 additions & 2 deletions

File tree

lib/base/pool.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ class BasePool extends EventEmitter {
6262
if (this._freeConnections.length > 0) {
6363
connection = this._freeConnections.pop();
6464
this.emit('acquire', connection);
65-
return process.nextTick(() => cb(null, connection));
65+
return process.nextTick(() => {
66+
connection._released = false;
67+
cb(null, connection);
68+
});
6669
}
6770
if (
6871
this.config.connectionLimit === 0 ||
@@ -126,7 +129,10 @@ class BasePool extends EventEmitter {
126129
}
127130
} else if (this._connectionQueue.length) {
128131
cb = this._connectionQueue.shift();
129-
process.nextTick(cb.bind(null, null, connection));
132+
process.nextTick(() => {
133+
connection._released = false;
134+
cb(null, connection);
135+
});
130136
} else {
131137
this._freeConnections.push(connection);
132138
this.emit('release', connection);

lib/pool_connection.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ class PoolConnection extends Connection {
66
constructor(pool, options) {
77
super(options);
88
this._pool = pool;
9+
this._released = false;
910
this.lastActiveTime = Date.now();
1011
this.once('end', () => {
1112
this._removeFromPool();
@@ -16,9 +17,13 @@ class PoolConnection extends Connection {
1617
}
1718

1819
release() {
20+
if (this._released) {
21+
return;
22+
}
1923
if (!this._pool || this._pool._closed) {
2024
return;
2125
}
26+
this._released = true;
2227
this.lastActiveTime = Date.now();
2328
this._pool.releaseConnection(this);
2429
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import type { PoolConnection } from '../../index.js';
2+
import { describe, it, strict } from 'poku';
3+
import { createPool } from '../common.test.mjs';
4+
5+
await describe('Pool Double Release', async () => {
6+
await describe('should not duplicate _freeConnections on double release', async () => {
7+
const pool = createPool({
8+
connectionLimit: 5,
9+
maxIdle: 5,
10+
idleTimeout: 15000,
11+
});
12+
13+
let freeConnectionsLength = -1;
14+
15+
await new Promise<void>((resolve, reject) => {
16+
pool.getConnection(
17+
(err: NodeJS.ErrnoException | null, connection: PoolConnection) => {
18+
if (err) return reject(err);
19+
20+
connection.release();
21+
connection.release();
22+
23+
// @ts-expect-error: internal access
24+
freeConnectionsLength = pool._freeConnections.length;
25+
resolve();
26+
}
27+
);
28+
});
29+
30+
it('should have exactly 1 free connection, not 2', () => {
31+
strict.equal(freeConnectionsLength, 1);
32+
});
33+
34+
await pool.promise().end();
35+
});
36+
37+
await describe('should not give the same connection to two different handlers', async () => {
38+
const pool = createPool({
39+
connectionLimit: 2,
40+
idleTimeout: 15000,
41+
});
42+
43+
let conn1!: PoolConnection;
44+
let conn2!: PoolConnection;
45+
46+
await new Promise<void>((resolve, reject) => {
47+
pool.getConnection(
48+
(err: NodeJS.ErrnoException | null, connection: PoolConnection) => {
49+
if (err) return reject(err);
50+
51+
connection.release();
52+
connection.release();
53+
54+
pool.getConnection(
55+
(err1: NodeJS.ErrnoException | null, c1: PoolConnection) => {
56+
if (err1) return reject(err1);
57+
conn1 = c1;
58+
59+
pool.getConnection(
60+
(err2: NodeJS.ErrnoException | null, c2: PoolConnection) => {
61+
if (err2) return reject(err2);
62+
conn2 = c2;
63+
64+
conn1.release();
65+
conn2.release();
66+
resolve();
67+
}
68+
);
69+
}
70+
);
71+
}
72+
);
73+
});
74+
75+
it('should return unique connections', () => {
76+
strict.notStrictEqual(conn1, conn2);
77+
});
78+
79+
await pool.promise().end();
80+
});
81+
82+
await describe('should not have duplicate references in _freeConnections after double release', async () => {
83+
const pool = createPool({
84+
connectionLimit: 3,
85+
maxIdle: 3,
86+
idleTimeout: 15000,
87+
});
88+
89+
let hasDuplicates = true;
90+
91+
await new Promise<void>((resolve, reject) => {
92+
pool.getConnection(
93+
(err1: NodeJS.ErrnoException | null, c1: PoolConnection) => {
94+
if (err1) return reject(err1);
95+
96+
pool.getConnection(
97+
(err2: NodeJS.ErrnoException | null, c2: PoolConnection) => {
98+
if (err2) return reject(err2);
99+
100+
c1.release();
101+
c1.release();
102+
c2.release();
103+
c2.release();
104+
105+
// @ts-expect-error: internal access
106+
const freeConns = pool._freeConnections;
107+
const seen = new Set();
108+
hasDuplicates = false;
109+
for (let i = 0; i < freeConns.length; i++) {
110+
const conn = freeConns.get(i);
111+
if (seen.has(conn)) {
112+
hasDuplicates = true;
113+
break;
114+
}
115+
seen.add(conn);
116+
}
117+
118+
resolve();
119+
}
120+
);
121+
}
122+
);
123+
});
124+
125+
it('should have no duplicate connection references', () => {
126+
strict.equal(hasDuplicates, false);
127+
});
128+
129+
await pool.promise().end();
130+
});
131+
});

0 commit comments

Comments
 (0)