From 08e3a85370f2229a8ff7c477bd4905a60764c273 Mon Sep 17 00:00:00 2001 From: Tom Yang <82199152+tomyang11@users.noreply.github.com> Date: Tue, 16 Jun 2026 02:26:40 -0700 Subject: [PATCH] Fix orphaned queued reads when a freed worker slot is stolen concurrently (#404) * Fix orphaned queued reads when a freed worker slot is stolen concurrently ReadOrchestrator.runWorker's finally callback dequeues the oldest queued read and asserts that createWorker succeeds ("we just freed up a worker"). That assumption races: the callback runs on a later microtask than the worker's stop, and concurrent read() calls in that gap can LRU-evict the freed worker and saturate every slot. The assert then throws as an unhandled rejection after the read was removed from the queue but before it was attached to any worker - its pending slices' promises never settle and the awaiting reads hang forever. Observed in production-like load (a 4-source composition player): 25 back-to-back occurrences saturating both workers, leaving clips permanently undecodable. Fix: create the worker first; only dequeue the read once a slot was actually obtained. If every slot is busy, leave the read queued - each running worker drains the queue from this same block when it stops, so the read is picked up by whichever worker stops next. Co-Authored-By: Claude Opus 4.8 * Update logic --------- Co-authored-by: Claude Opus 4.8 Co-authored-by: Vanilagy <1696106+Vanilagy@users.noreply.github.com> --- src/source.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/source.ts b/src/source.ts index 1fb266a..d181a14 100644 --- a/src/source.ts +++ b/src/source.ts @@ -2108,7 +2108,7 @@ class ReadOrchestrator { } }) .finally(() => { - if (worker.running) { + if (worker.running || this.workers.length >= this.options.maxWorkerCount) { // Rare, but can happen with multiple concurrent reads. In this case, don't do anything. return; }