From 49611cfcddc7555fcd502b4f13a41166b1feebf7 Mon Sep 17 00:00:00 2001 From: Aditya Jain Date: Fri, 21 Aug 2026 23:37:49 -0700 Subject: [PATCH] fix(cdk-assets-lib): pLimit.dispose() doesn't stop new jobs from starting dispatch() never checked the `stopped` flag, only resumeNext() (called when an already-active job finishes) did. So calling dispose() and then submitting a new task via the limiter, while a concurrency slot happened to be free, would start that task immediately instead of rejecting it - silently violating dispose()'s documented/tested contract that "new jobs aren't started after dispose is called". Guard dispatch() with the stopped flag, and reject immediately when a task is submitted after dispose() so callers don't hang waiting on a promise that will never settle. Co-Authored-By: Claude Sonnet 5 --- .../cdk-assets-lib/lib/private/p-limit.ts | 7 +++++++ .../cdk-assets-lib/test/private/p-limit.test.ts | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/@aws-cdk/cdk-assets-lib/lib/private/p-limit.ts b/packages/@aws-cdk/cdk-assets-lib/lib/private/p-limit.ts index fb62e18da..a909baf3a 100644 --- a/packages/@aws-cdk/cdk-assets-lib/lib/private/p-limit.ts +++ b/packages/@aws-cdk/cdk-assets-lib/lib/private/p-limit.ts @@ -10,6 +10,9 @@ export function pLimit(concurrency: number): PLimit { let stopped = false; function dispatch() { + if (stopped) { + return; + } if (activeCount < concurrency && queue.length > 0) { const [fac, resolve, reject] = queue.shift()!; activeCount++; @@ -41,6 +44,10 @@ export function pLimit(concurrency: number): PLimit { const ret = (promiseFactory: PromiseFactory) => { return new Promise((resolve, reject) => { + if (stopped) { + reject(new Error('Task has been cancelled')); + return; + } queue.push([promiseFactory, resolve, reject]); dispatch(); }); diff --git a/packages/@aws-cdk/cdk-assets-lib/test/private/p-limit.test.ts b/packages/@aws-cdk/cdk-assets-lib/test/private/p-limit.test.ts index 209e1d691..f4693aaac 100644 --- a/packages/@aws-cdk/cdk-assets-lib/test/private/p-limit.test.ts +++ b/packages/@aws-cdk/cdk-assets-lib/test/private/p-limit.test.ts @@ -43,6 +43,23 @@ test('new jobs arent started after dispose is called', async () => { expect(started).toBeLessThanOrEqual(3); }); +test('no new job is started once dispose is called, even with a free slot', async () => { + const limit = pLimit(1); + + limit.dispose(); + + let ran = false; + await expect( + limit(async () => { + ran = true; + }), + ).rejects.toThrow(/cancelled/); + + // activeCount was 0 and concurrency is 1, so before the fix dispatch() + // would start this job immediately despite dispose() having been called. + expect(ran).toBe(false); +}); + function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); }