From a454d04db1fa25d790e63389cbff2991725baa7c Mon Sep 17 00:00:00 2001 From: Tim Griesser Date: Wed, 8 Jul 2026 10:57:14 -0400 Subject: [PATCH] feat: add fifo option for controlling the pool connection acquisition strategy --- README.md | 14 +++++++++++++- src/Pool.ts | 8 ++++++-- tests.test.ts | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e707b3f..8b0ec97 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,19 @@ const pool = new Pool({ // rejected with the error. If this is false (the default) then // create is retried until acquireTimeoutMillis milliseconds has // passed. - propagateCreateError: false + propagateCreateError: false, + + // Order in which free resources are handed back out on acquire. + // false (the default) is LIFO: the most recently released resource + // is reused first, keeping a small hot set active and letting colder + // resources age out via idleTimeoutMillis. + // true is FIFO (round-robin): the least recently used free resource + // is reused first, cycling evenly across all resources. + // + // Note: with fifo: true, connections are rotated evenly, so idle + // connections rarely reach idleTimeoutMillis; the pool shrinks toward + // min far less aggressively than with the default LIFO. + fifo: false }); // acquires a resource. The promise is rejected with `tarn.TimeoutError` diff --git a/src/Pool.ts b/src/Pool.ts index cd7ee10..57f1fc7 100644 --- a/src/Pool.ts +++ b/src/Pool.ts @@ -18,6 +18,7 @@ export interface PoolOptions { log?: (msg: string) => any; validate?: (resource: T) => boolean; propagateCreateError?: boolean; + fifo?: boolean; } export class Pool { @@ -33,6 +34,7 @@ export class Pool { protected interval: ReturnType | null; protected destroyed = false; protected propagateCreateError: boolean; + protected fifo: boolean; protected idleTimeoutMillis: number; protected createRetryIntervalMillis: number; protected reapIntervalMillis: number; @@ -119,7 +121,8 @@ export class Pool { idleTimeoutMillis: true, reapIntervalMillis: true, createRetryIntervalMillis: true, - propagateCreateError: true + propagateCreateError: true, + fifo: true }; for (const key of Object.keys(opt)) { @@ -140,6 +143,7 @@ export class Pool { this.reapIntervalMillis = opt.reapIntervalMillis || 1000; this.createRetryIntervalMillis = opt.createRetryIntervalMillis || 200; this.propagateCreateError = !!opt.propagateCreateError; + this.fifo = !!opt.fifo; this.min = opt.min; this.max = opt.max; @@ -400,7 +404,7 @@ export class Pool { // on validation result to either leave the free resource to used resources array // or destroy the free resource if validation did fail. const pendingAcquire = this.pendingAcquires.shift(); - const free = this.free.pop(); + const free = this.fifo ? this.free.shift() : this.free.pop(); if (free === undefined || pendingAcquire === undefined) { const errMessage = 'this.free was empty while trying to acquire resource'; diff --git a/tests.test.ts b/tests.test.ts index 9e55468..a767b19 100644 --- a/tests.test.ts +++ b/tests.test.ts @@ -2362,6 +2362,53 @@ describe('Tarn', () => { return Math.round(Math.random() * max); } }); + + describe('reuse order (fifo option)', () => { + function makePool(opt) { + let created = 0; + return new Pool({ + create() { + return Promise.resolve({ id: created++ }); + }, + destroy() {}, + min: 0, + max: 3, + ...opt + }); + } + + it('reuses the most recently released resource by default (LIFO)', async () => { + pool = makePool(); + + const r0 = await pool.acquire().promise; + const r1 = await pool.acquire().promise; + const r2 = await pool.acquire().promise; + expect([r0.id, r1.id, r2.id]).to.eql([0, 1, 2]); + + pool.release(r0); + pool.release(r1); + pool.release(r2); + + const reused = await pool.acquire().promise; + expect(reused.id).to.equal(2); + }); + + it('reuses the least recently released resource when fifo is true (round-robin)', async () => { + pool = makePool({ fifo: true }); + + const r0 = await pool.acquire().promise; + const r1 = await pool.acquire().promise; + const r2 = await pool.acquire().promise; + expect([r0.id, r1.id, r2.id]).to.eql([0, 1, 2]); + + pool.release(r0); + pool.release(r1); + pool.release(r2); + + const reused = await pool.acquire().promise; + expect(reused.id).to.equal(0); + }); + }); }); function sortBy(arr, key) {