From ba1534d852ee5c94c44b022f6d47f1bf90e47130 Mon Sep 17 00:00:00 2001 From: Samuel Tinnerholm Date: Fri, 5 Jun 2026 14:40:04 +0000 Subject: [PATCH] fix: reject throttler queue overflow --- core/src/utils/throttler.ts | 8 +++----- core/test/utils/throttler.test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 core/test/utils/throttler.test.ts diff --git a/core/src/utils/throttler.ts b/core/src/utils/throttler.ts index 7673f5e1..740e4fc9 100644 --- a/core/src/utils/throttler.ts +++ b/core/src/utils/throttler.ts @@ -22,12 +22,10 @@ export class Throttler { } async throttle(cost: number = 1): Promise { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { if (this.queue.length >= this.maxQueueDepth) { - const dropped = this.queue.shift(); - if (dropped) { - dropped.resolve(); - } + reject(new Error(`Throttler queue full (max depth ${this.maxQueueDepth})`)); + return; } this.queue.push({ resolve, cost }); if (!this.running) { diff --git a/core/test/utils/throttler.test.ts b/core/test/utils/throttler.test.ts new file mode 100644 index 00000000..517be987 --- /dev/null +++ b/core/test/utils/throttler.test.ts @@ -0,0 +1,24 @@ +import { Throttler } from '../../src/utils/throttler'; + +describe('Throttler', () => { + it('rejects new requests instead of silently resolving the oldest queued waiter', async () => { + const throttler = new Throttler({ + refillRate: 1, + capacity: 1, + delay: 1, + maxQueueDepth: 1, + }); + + let oldestResolved = false; + (throttler as any).queue.push({ + resolve: () => { + oldestResolved = true; + }, + cost: 1, + }); + + await expect(throttler.throttle(1)).rejects.toThrow(/queue full/i); + expect(oldestResolved).toBe(false); + expect((throttler as any).queue).toHaveLength(1); + }); +});