fix(compression): drain the worker pool queue when spawn throws synchronously - #3
fix(compression): drain the worker pool queue when spawn throws synchronously#3insoln wants to merge 14 commits into
Conversation
…ronously A synchronous spawn failure (Turbopack standalone moduleContext MODULE_NOT_FOUND) escaped dispatch() before queue.shift(), stranding every queued job — each holding a full parsed request body — in pool.queue for the process lifetime (unbounded main-isolate heap leak, #2). - dispatch() now fail-opens the whole queue on a spawn throw and marks the pool broken: subsequent run() calls resolve unchanged immediately instead of re-jamming - constructor accepts a workerFactory test seam - strategySelector fail-open catch now logs (rate-limited): the leak was invisible precisely because the catch was silent - llmlingua worker passes a URL object to new Worker (ERR_WORKER_PATH on Node >= 21 for file:// strings)
…and dark llmlingua catches - extract notifyCompressionFailOpen into failOpenNotifier.ts (import-cycle-free) - broken-pool run() now notifies instead of failing open silently forever - rate limiter keys on detail so distinct new failure modes are never swallowed - llmlingua pump/postMessage catches log through the notifier
- errorText/strategySelector/llmlingua now pass the raw unknown to sanitizeErrorMessage (never String(error), which throws on hostile toString/valueOf) so run() cannot reject on a non-Error spawn failure - add an injectable worker harness to the llmlingua backend and cover the spawn-throw and postMessage-throw fail-open paths (original text, queue continuation, respawn, rate-limited warn) - add hostile-throwable regression test for the pool
There was a problem hiding this comment.
🟡 Changes recommended
CompressionWorkerPool can be marked permanently broken (and drains queued jobs) on a structural spawn failure even when a healthy worker already exists, which can unnecessarily disable compression despite available capacity.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR fixes a production memory leak/jam class in the compression worker path by ensuring CompressionWorkerPool cannot strand queued jobs when worker spawning fails synchronously (e.g., Turbopack standalone MODULE_NOT_FOUND), and adds rate-limited observability for all fail-open paths (including LLMLingua worker failures).
Changes:
- Make
CompressionWorkerPoolfail-open and drain queued jobs on synchronous spawn errors, and short-circuit future runs when the pool is broken. - Add a shared, rate-limited fail-open notifier used by strategy selection, the pool, and the LLMLingua worker (with sanitization).
- Fix LLMLingua worker spawning on modern Node by passing a
URLobject (not afile://string) tonew Worker(...), and add regression tests.
File summaries
| File | Description |
|---|---|
| tests/unit/compression/compression-worker-pool-jam.test.ts | Adds regression tests covering synchronous spawn throws, queue draining, broken-pool behavior, notifier rate limiting, and LLMLingua worker specifier behavior. |
| stryker.conf.json | Includes the new unit test file in the mutation-test allowlist input set. |
| open-sse/services/compression/strategySelector.ts | Logs (rate-limited) when dynamic import / worker-path execution fails open, rather than silently swallowing. |
| open-sse/services/compression/failOpenNotifier.ts | Introduces the shared rate-limited + sanitized console.warn notifier used across compression worker fail-open paths. |
| open-sse/services/compression/engines/llmlingua/worker.ts | Uses a URL object specifier for Worker entry; adds logging on spawn/postMessage fail-open paths; adds test harness seam. |
| open-sse/services/compression/compressionWorkerPool.ts | Wraps spawn/postMessage failure paths to ensure queue draining and observability; adds test seam for worker construction. |
| changelog.d/fixes/3-compression-worker-pool-queue-jam.md | Documents the fix and related observability/LLMLingua worker specifier adjustment. |
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🔵 Needs a closer look
The changelog fragment links to a fork (insoln/OmniRoute) instead of the canonical repository link pattern used elsewhere in changelog.d entries.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
changelog.d/fixes/3-compression-worker-pool-queue-jam.md:1
- The changelog fragment links to the
insoln/OmniRoutefork and PR#3, but existing changelog.d entries consistently link to the canonicaldiegosouzapw/OmniRouterepository. This will produce a broken/misleading reference when merged upstream; please update the link target (and PR/issue number if needed) to match the canonical repo’s URL pattern.
- Files reviewed: 7/7 changed files
- Comments generated: 0 new
- Review effort level: Lite
|
Follow-up review addressed in f62ceff: the changelog now references canonical upstream PR diegosouzapw#12755 instead of fork PR #3. |
Summary
Fixes #2.
In the Turbopack standalone production build,
CompressionWorkerPooljams its job queue on the very first job:spawn()throws a synchronousMODULE_NOT_FOUND(Turbopack rewritesnew Worker(resolveWorkerFile())into its module-context map, which only contains.tskeys while the runtime value is a.jspath) before the job is dequeued. The rejected caller's promise is silently fail-opened bystrategySelector, but the queuedPendingJob— retaining the full parsed request body and its closures — stays inpool.queueforever. Under Claude Code traffic this leaked ~2.5–3 GB of main-isolate heap per hour until the heap-pressure guard began shedding with 503s.Changes
compressionWorkerPool.ts— the queue can no longer jam.dispatch()wrapsspawn()in try/catch; on a synchronous throw it fail-opens every queued job (queue.splice(0)→ resolve unchanged) and marks the poolbroken, so subsequentrun()calls resolve immediately instead of re-jamming. This kills the whole leak class regardless of which bundler/module-loader breaks the spawn.compressionWorkerPool.ts— test seam. The constructor accepts an optionalworkerFactory; tests inject a synchronously-throwing factory (exactly the production Turbopack failure shape).failOpenNotifier.ts+ callers — fail-open is no longer silent. Spawn failure, broken-pool short-circuit, strategy-level fallback, and LLMLingua spawn/post failures emit a rate-limitedconsole.warn. Repeated identical details are suppressed for 60 s, while a distinct failure mode is still logged immediately; tracked detail keys are bounded.engines/llmlingua/worker.ts—ERR_WORKER_PATH.new Worker(pathToFileURL(...).href, …)passed afile://string, which Node ≥ 21 rejects synchronously. ExtractedllmlinguaWorkerSpecifier()returns a URL object.What this PR deliberately does NOT fix
uuidinto the esbuild'd worker: those are build-pipeline changes; this PR guarantees they can never again leak memory when they break.Validation
Change type: performance/memory (fail-open semantics unchanged — every failure path still returns
{ body, compressed: false, stats: null }).tests/unit/compression/compression-worker-pool-jam.test.tsfails on the untouched release base (jobs hang → timeout) and passes after the fix:EMFILE,ERR_WORKER_INIT_FAILED) are retried on the next wave; structural ones (MODULE_NOT_FOUNDetc.) permanently break the poolpostMessagethrow, workererror/exit, per-job timeout andclose()all settle fail-open —run()never rejects and never strands a promisefile://stringnode --import tsx/esm --test tests/unit/compression/compression-worker-pool-jam.test.ts— 14 passedcompression-worker.test.ts,llmlingua-worker.test.ts,llmlingua-worker-resolution.test.ts,llmlingua-failopen.test.ts,compression-worker-file-resolution.test.ts,strategySelector.test.ts— 68 passednode scripts/check/check-mutation-test-coverage.mjs— the new test file is listed instryker.conf.jsontap.testFiles(the remaining drift,quotaScoring.ts, is pre-existing on the release base)npm run typecheck:coregit diff --checkcheck:file-size/check:test-file-sizeOK)Tests Added
tests/unit/compression/compression-worker-pool-jam.test.ts— the permanent regression guard for the jam/leak class.