Skip to content

Commit a97891b

Browse files
authored
fix: pending connection handling (#661)
1 parent bdf1e87 commit a97891b

File tree

5 files changed

+202
-20
lines changed

5 files changed

+202
-20
lines changed

.changeset/nice-cameras-march.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"slonik": minor
3+
---
4+
5+
fix: connection pool edge cases that could lead to hanging connections or effectively incorrect pool sizing

packages/slonik/src/factories/createConnectionPool.ts

+43-20
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export type ConnectionPoolClient = {
4646
type ConnectionPoolState = {
4747
acquiredConnections: number;
4848
idleConnections: number;
49+
pendingConnections: number;
4950
pendingDestroyConnections: number;
5051
pendingReleaseConnections: number;
5152
state: ConnectionPoolStateName;
@@ -144,22 +145,30 @@ export const createConnectionPool = ({
144145
pendingConnections.push(pendingConnection);
145146

146147
const connection = await pendingConnection.catch((error) => {
147-
pendingConnections.pop();
148+
const index = pendingConnections.indexOf(pendingConnection);
149+
150+
if (index === -1) {
151+
logger.error(
152+
'Unable to find pendingConnection in `pendingConnections` array to remove.',
153+
);
154+
} else {
155+
pendingConnections.splice(index, 1);
156+
}
148157

149158
throw error;
150159
});
151160

152161
const onRelease = () => {
162+
if (connection.state() !== 'IDLE') {
163+
return;
164+
}
165+
153166
const waitingClient = waitingClients.shift();
154167

155168
if (!waitingClient) {
156169
return;
157170
}
158171

159-
if (connection.state() !== 'IDLE') {
160-
throw new Error('Connection is not idle.');
161-
}
162-
163172
connection.acquire();
164173

165174
waitingClient.deferred.resolve(connection);
@@ -171,35 +180,48 @@ export const createConnectionPool = ({
171180
connection.removeListener('release', onRelease);
172181
connection.removeListener('destroy', onDestroy);
173182

174-
connections.splice(connections.indexOf(connection), 1);
183+
const indexOfConnection = connections.indexOf(connection);
184+
185+
if (indexOfConnection === -1) {
186+
logger.error(
187+
'Unable to find connection in `connections` array to remove.',
188+
);
189+
} else {
190+
connections.splice(indexOfConnection, 1);
191+
}
175192

176193
const waitingClient = waitingClients.shift();
177194

178-
if (!isEnding && !isEnded && connections.length < minimumPoolSize) {
179-
addConnection();
195+
if (waitingClient) {
196+
// eslint-disable-next-line promise/prefer-await-to-then
197+
acquire().then(
198+
waitingClient.deferred.resolve,
199+
waitingClient.deferred.reject,
200+
);
180201

181202
return;
182203
}
183204

184-
if (!waitingClient) {
185-
return;
205+
// In the case that there are no waiting clients and we're below the minimum pool size, add a new connection
206+
if (!isEnding && !isEnded && connections.length < minimumPoolSize) {
207+
addConnection();
186208
}
187-
188-
// eslint-disable-next-line promise/prefer-await-to-then
189-
acquire().then(
190-
waitingClient.deferred.resolve,
191-
waitingClient.deferred.reject,
192-
);
193209
};
194210

195211
connection.on('destroy', onDestroy);
196212

197213
connections.push(connection);
198214

199-
pendingConnections.splice(
200-
pendingConnections.indexOf(pendingConnection),
201-
1,
202-
);
215+
const indexOfPendingConnection =
216+
pendingConnections.indexOf(pendingConnection);
217+
218+
if (indexOfPendingConnection === -1) {
219+
logger.error(
220+
'Unable to find pendingConnection in `pendingConnections` array to remove.',
221+
);
222+
} else {
223+
pendingConnections.splice(indexOfPendingConnection, 1);
224+
}
203225

204226
return connection;
205227
};
@@ -279,6 +301,7 @@ export const createConnectionPool = ({
279301
const state = {
280302
acquiredConnections: 0,
281303
idleConnections: 0,
304+
pendingConnections: pendingConnections.length,
282305
pendingDestroyConnections: 0,
283306
pendingReleaseConnections: 0,
284307
};

packages/slonik/src/helpers.test/createIntegrationTests.ts

+84
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,7 @@ export const createIntegrationTests = (
840840
t.deepEqual(pool.state(), {
841841
acquiredConnections: 0,
842842
idleConnections: 0,
843+
pendingConnections: 0,
843844
pendingDestroyConnections: 0,
844845
pendingReleaseConnections: 0,
845846
state: 'ACTIVE',
@@ -851,6 +852,7 @@ export const createIntegrationTests = (
851852
t.deepEqual(pool.state(), {
852853
acquiredConnections: 0,
853854
idleConnections: 0,
855+
pendingConnections: 0,
854856
pendingDestroyConnections: 0,
855857
pendingReleaseConnections: 0,
856858
state: 'ENDED',
@@ -867,6 +869,7 @@ export const createIntegrationTests = (
867869
t.deepEqual(pool.state(), {
868870
acquiredConnections: 0,
869871
idleConnections: 0,
872+
pendingConnections: 0,
870873
pendingDestroyConnections: 0,
871874
pendingReleaseConnections: 0,
872875
state: 'ACTIVE',
@@ -880,6 +883,7 @@ export const createIntegrationTests = (
880883
t.deepEqual(pool.state(), {
881884
acquiredConnections: 0,
882885
idleConnections: 1,
886+
pendingConnections: 0,
883887
pendingDestroyConnections: 0,
884888
pendingReleaseConnections: 0,
885889
state: 'ACTIVE',
@@ -891,6 +895,7 @@ export const createIntegrationTests = (
891895
t.deepEqual(pool.state(), {
892896
acquiredConnections: 0,
893897
idleConnections: 0,
898+
pendingConnections: 0,
894899
pendingDestroyConnections: 0,
895900
pendingReleaseConnections: 0,
896901
state: 'ENDED',
@@ -906,6 +911,7 @@ export const createIntegrationTests = (
906911
t.deepEqual(pool.state(), {
907912
acquiredConnections: 0,
908913
idleConnections: 0,
914+
pendingConnections: 0,
909915
pendingDestroyConnections: 0,
910916
pendingReleaseConnections: 0,
911917
state: 'ACTIVE',
@@ -921,6 +927,7 @@ export const createIntegrationTests = (
921927
t.deepEqual(pool.state(), {
922928
acquiredConnections: 1,
923929
idleConnections: 0,
930+
pendingConnections: 0,
924931
pendingDestroyConnections: 0,
925932
pendingReleaseConnections: 0,
926933
state: 'ACTIVE',
@@ -932,6 +939,7 @@ export const createIntegrationTests = (
932939
t.deepEqual(pool.state(), {
933940
acquiredConnections: 0,
934941
idleConnections: 0,
942+
pendingConnections: 0,
935943
pendingDestroyConnections: 0,
936944
pendingReleaseConnections: 0,
937945
state: 'ENDED',
@@ -951,6 +959,7 @@ export const createIntegrationTests = (
951959
t.deepEqual(pool.state(), {
952960
acquiredConnections: 0,
953961
idleConnections: 0,
962+
pendingConnections: 0,
954963
pendingDestroyConnections: 0,
955964
pendingReleaseConnections: 0,
956965
state: 'ACTIVE',
@@ -978,6 +987,7 @@ export const createIntegrationTests = (
978987
t.deepEqual(pool.state(), {
979988
acquiredConnections: 0,
980989
idleConnections: 5,
990+
pendingConnections: 0,
981991
pendingDestroyConnections: 0,
982992
pendingReleaseConnections: 0,
983993
state: 'ACTIVE',
@@ -989,6 +999,7 @@ export const createIntegrationTests = (
989999
t.deepEqual(pool.state(), {
9901000
acquiredConnections: 0,
9911001
idleConnections: 0,
1002+
pendingConnections: 0,
9921003
pendingDestroyConnections: 0,
9931004
pendingReleaseConnections: 0,
9941005
state: 'ENDED',
@@ -1442,6 +1453,7 @@ export const createIntegrationTests = (
14421453
{
14431454
acquiredConnections: 0,
14441455
idleConnections: 0,
1456+
pendingConnections: 0,
14451457
pendingDestroyConnections: 0,
14461458
pendingReleaseConnections: 0,
14471459
state: 'ACTIVE',
@@ -1466,6 +1478,7 @@ export const createIntegrationTests = (
14661478
{
14671479
acquiredConnections: 1,
14681480
idleConnections: 0,
1481+
pendingConnections: 0,
14691482
pendingDestroyConnections: 0,
14701483
pendingReleaseConnections: 0,
14711484
state: 'ACTIVE',
@@ -2004,6 +2017,7 @@ export const createIntegrationTests = (
20042017
{
20052018
acquiredConnections: 0,
20062019
idleConnections: 0,
2020+
pendingConnections: 0,
20072021
pendingDestroyConnections: 0,
20082022
pendingReleaseConnections: 0,
20092023
state: 'ACTIVE',
@@ -2021,6 +2035,7 @@ export const createIntegrationTests = (
20212035
{
20222036
acquiredConnections: 0,
20232037
idleConnections: 1,
2038+
pendingConnections: 0,
20242039
pendingDestroyConnections: 0,
20252040
pendingReleaseConnections: 0,
20262041
state: 'ACTIVE',
@@ -2036,6 +2051,7 @@ export const createIntegrationTests = (
20362051
{
20372052
acquiredConnections: 0,
20382053
idleConnections: 0,
2054+
pendingConnections: 0,
20392055
pendingDestroyConnections: 0,
20402056
pendingReleaseConnections: 0,
20412057
state: 'ACTIVE',
@@ -2175,6 +2191,7 @@ export const createIntegrationTests = (
21752191
t.like(pool.state(), {
21762192
acquiredConnections: 0,
21772193
idleConnections: 0,
2194+
pendingConnections: 0,
21782195
pendingDestroyConnections: 0,
21792196
pendingReleaseConnections: 0,
21802197
waitingClients: 0,
@@ -2193,6 +2210,7 @@ export const createIntegrationTests = (
21932210
t.like(pool.state(), {
21942211
acquiredConnections: 0,
21952212
idleConnections: 0,
2213+
pendingConnections: 0,
21962214
pendingDestroyConnections: 0,
21972215
pendingReleaseConnections: 0,
21982216
waitingClients: 0,
@@ -2216,6 +2234,7 @@ export const createIntegrationTests = (
22162234
acquiredConnections: 0,
22172235
// TODO we might want to add an option to warm up the pool, in which case this value should be 1
22182236
idleConnections: 0,
2237+
pendingConnections: 0,
22192238
pendingDestroyConnections: 0,
22202239
pendingReleaseConnections: 0,
22212240
state: 'ACTIVE',
@@ -2233,6 +2252,7 @@ export const createIntegrationTests = (
22332252
{
22342253
acquiredConnections: 0,
22352254
idleConnections: 1,
2255+
pendingConnections: 0,
22362256
pendingDestroyConnections: 0,
22372257
pendingReleaseConnections: 0,
22382258
state: 'ACTIVE',
@@ -2248,6 +2268,7 @@ export const createIntegrationTests = (
22482268
{
22492269
acquiredConnections: 0,
22502270
idleConnections: 1,
2271+
pendingConnections: 0,
22512272
pendingDestroyConnections: 0,
22522273
pendingReleaseConnections: 0,
22532274
state: 'ACTIVE',
@@ -2259,6 +2280,67 @@ export const createIntegrationTests = (
22592280
await pool.end();
22602281
});
22612282

2283+
test('destroy creates a new connection to be used by waiting client', async (t) => {
2284+
const pool = await createPool(t.context.dsn, {
2285+
driverFactory,
2286+
idleTimeout: 30_000,
2287+
maximumPoolSize: 1,
2288+
minimumPoolSize: 1,
2289+
});
2290+
2291+
pool
2292+
.query(
2293+
sql.unsafe`
2294+
DO $$
2295+
BEGIN
2296+
PERFORM pg_sleep(1); -- Sleep for 1 second
2297+
RAISE EXCEPTION 'Test error after 1 second delay';
2298+
END $$;
2299+
`,
2300+
)
2301+
// eslint-disable-next-line promise/prefer-await-to-then
2302+
.catch(() => {
2303+
// Ignoring intentional error
2304+
});
2305+
2306+
const waitingClientPromise = pool.oneFirst(sql.unsafe`
2307+
SELECT 1
2308+
`);
2309+
2310+
t.deepEqual(
2311+
pool.state(),
2312+
{
2313+
acquiredConnections: 0,
2314+
idleConnections: 0,
2315+
pendingConnections: 1,
2316+
pendingDestroyConnections: 0,
2317+
pendingReleaseConnections: 0,
2318+
state: 'ACTIVE',
2319+
waitingClients: 1,
2320+
},
2321+
'pool state has waiting client',
2322+
);
2323+
2324+
const waitingClientResult = await waitingClientPromise;
2325+
t.is(waitingClientResult, 1);
2326+
2327+
t.deepEqual(
2328+
pool.state(),
2329+
{
2330+
acquiredConnections: 0,
2331+
idleConnections: 1,
2332+
pendingConnections: 0,
2333+
pendingDestroyConnections: 0,
2334+
pendingReleaseConnections: 0,
2335+
state: 'ACTIVE',
2336+
waitingClients: 0,
2337+
},
2338+
'pool state after all queries complete',
2339+
);
2340+
2341+
await pool.end();
2342+
});
2343+
22622344
test('retains explicit transaction beyond the idle timeout', async (t) => {
22632345
const pool = await createPool(t.context.dsn, {
22642346
driverFactory,
@@ -2363,6 +2445,7 @@ export const createIntegrationTests = (
23632445
{
23642446
acquiredConnections: 0,
23652447
idleConnections: 1,
2448+
pendingConnections: 0,
23662449
pendingDestroyConnections: 0,
23672450
pendingReleaseConnections: 0,
23682451
state: 'ACTIVE',
@@ -2408,6 +2491,7 @@ export const createIntegrationTests = (
24082491
t.deepEqual(pool.state(), {
24092492
acquiredConnections: 2,
24102493
idleConnections: 0,
2494+
pendingConnections: 0,
24112495
pendingDestroyConnections: 0,
24122496
pendingReleaseConnections: 0,
24132497
state: 'ACTIVE',

0 commit comments

Comments
 (0)