Skip to content

fix(concurrency-parallel): pull-based dispatch, crash recovery, timeout semantics - #5

Open
frankstupak wants to merge 1 commit into
SkinnnyJay:mainfrom
frankstupak:lumen-uplift/concurrency-parallel
Open

fix(concurrency-parallel): pull-based dispatch, crash recovery, timeout semantics#5
frankstupak wants to merge 1 commit into
SkinnnyJay:mainfrom
frankstupak:lumen-uplift/concurrency-parallel

Conversation

@frankstupak

@frankstupak frankstupak commented Jul 4, 2026

Copy link
Copy Markdown

Lumen Industries uplift #4. The worker pool assigned tasks round-robin without regard to load. Here is what was happening and what changed.

What was wrong

ParallelManager

  • Eager round-robin dispatch = head-of-line blocking. Every task was posted to a pre-picked worker up front. One slow task starved everything pinned behind it on that worker while the other workers sat idle. (The taskQueue field existed and was never used — the queue this dispatch model needs.)
  • A worker crash after init crashed the whole process. The error/exit listeners were removed once the worker reported ready, so a post-init error event had no listener (Node throws), and any in-flight task on a dead worker hung until timeout.
  • activeTasksCount only ever went up. Incremented on dispatch, decremented only in a fallback path that the normal flow never hits.
  • Explicit workerScript argument was silently ignored whenever dist/worker.js existed.
  • Timed-out tasks poisoned accounting; late results were mishandled instead of being discarded and freeing the worker.
  • fibonacci was O(2^n) recursion. fib(42) = ~866M calls. The cap says 50; fib(50) is ~40 billion calls — the endpoint would hang for minutes on a legal input.

ConcurrencyManager

  • timeout and retries are declared in ConcurrencyConfig and were never read. The existing test admitted it: "the current implementation doesn't have built-in timeout handling." It does now, and that test finally tests what its name says.
  • Fail-fast was fake. After one task rejected in executeLimitedConcurrent/executePriorityQueue, the returned promise rejected but the other lanes kept happily executing side effects behind the back.
  • completedTasks grew unbounded (ParallelManager had a cap; this class didn't).

What's better

  • Pull-based dispatch (the Piscina model): one shared FIFO, next idle worker takes next task. Plus maxPendingTasks backpressure, getActiveTaskCount()/getQueuedTaskCount()/getWorkerCount()/getIdleWorkerCount() observability.
  • Crash recovery: persistent post-ready handlers fail the in-flight task loudly and auto-respawn a replacement (bounded by maxWorkerRestarts, default 3). cleanup() rejects queued tasks instead of stranding them.
  • Real timeout/retry/cancellation in ConcurrencyManager: per-attempt timeout, retries with error propagation after exhaustion, optional signal: AbortSignal, genuine fail-fast (lanes stop pulling after a rejection), executeAllSettled() for per-task outcomes, bounded metrics memory.
  • Fast-doubling fibonacci: O(log n), exact for every n <= the existing cap of 50.
  • Public API fully backward compatible; all config additions optional.

Numbers (Node 22.22, Xeon E5 T5810, median of 3)

Benchmark before after delta
16 mixed-duration tasks, 2 workers (alternating heavy/light) 26,261 ms 13,347 ms 1.95x faster
fibonacci(42) via worker 4,604 ms 1 ms ~4,600x faster

Repro: npx tsx src/api/concurrency-parallel/benchmarks/bench.ts (script included).

Verification

  • 42/42 tests green in the subproject (23 original — one updated: the timeout test now asserts timeouts actually happen — plus 19 new covering dispatch fairness, crash recovery + respawn, timeout recovery with late-result discard, backpressure, retries, abort, allSettled, bounded metrics, fibonacci exactness incl. fib(50)=12586269025).
  • tsc --noEmit clean, eslint clean, root jest run: zero new failures vs baseline.

— Lumen Industries

…y, timeout/retry/abort policy, fast-doubling fibonacci

- ParallelManager: shared-FIFO pull dispatch (idle worker takes next task)
  replaces eager round-robin that head-of-line-blocked short tasks behind a
  slow task while other workers idled — 1.95x faster on skewed workloads
- Worker crash recovery: persistent post-ready error/exit handlers fail the
  in-flight task loudly and auto-respawn a replacement (previously an
  unhandled 'error' event crashed the process and in-flight tasks hung)
- Fixed activeTasksCount leak (incremented but never decremented on the
  success path); added getActiveTaskCount/getQueuedTaskCount/getWorkerCount/
  getIdleWorkerCount; maxPendingTasks backpressure; timeout now frees the
  worker when its late result arrives; cleanup rejects queued tasks
- Explicit workerScript argument now honored when the file exists
  (previously silently ignored whenever dist/worker.js existed)
- ConcurrencyManager: timeout and retries (declared in ConcurrencyConfig,
  previously ignored) are enforced; AbortSignal support; genuine fail-fast
  (other lanes stop pulling tasks after a rejection); executeAllSettled;
  bounded completedTasks memory
- Worker fibonacci: fast doubling O(log n) replaces O(2^n) recursion —
  fib(42) 4550ms -> 1ms
- +19 tests (42 total green), benchmarks/bench.ts included
@frankstupak frankstupak changed the title concurrency-parallel: pull-based worker dispatch (1.95x on skewed loads), crash recovery, real timeout/retry semantics, fib(42) 4550ms -> 1ms fix(concurrency-parallel): pull-based dispatch, crash recovery, timeout semantics Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant