v0.6 Plan — Collected-Map Engine Unification¶
Status: amended after Round 1 review — ready to implement. This document is the implementation contract for the first v0.6 work item: every design decision carries its rationale, behavior changes, edge cases, and test plan. Deviations during implementation are recorded in the review amendments log below, not silently absorbed.
Read together with the Roadmap, DevX Principles, and the v0.5.0 Plan (whose design review produced this item).
Theme¶
v0.5 shipped the sliding-window engine for streaming and for
max_errors collected maps, but left the plain collected path on the
old chunk-and-barrier machinery. The result is the coupling the v0.5
design review flagged: batch_size means "chunk with a barrier" on
parallel_map(...) and "in-flight window" the moment you add
max_errors or switch to .stream(). One keyword, two meanings,
switched by an unrelated option.
This plan deletes the barrier engine. After it, every non-sequential
execution path in the library — collected and streaming, sync and async
— runs through one windowed engine, and batch_size means exactly one
thing everywhere: the maximum number of items admitted but not yet
resolved.
This is a deletion-shaped refactor: no new API parameters, ~150 lines of
engine code and a planning module removed. The one surface addition is
ParallelResult.timed_out/.aborted (D7) — required so the no-drain
policy cannot silently truncate results.
Current state (what exists today)¶
Per runtime there are three collected engines:
| Engine | Selected by | Semantics |
|---|---|---|
barrier path (inline in parallel_map / async_parallel_map) |
default | _plan_collected_map chunks input; all futures per chunk submitted upfront; barrier between chunks |
_collected_map_windowed / _async_collected_map_windowed |
max_errors= |
bounded admission window, lazy input, no-drain aftermath |
_sequential_collected_map |
sequential=True |
inline in caller thread (mostly untouched by this plan; gains D7 status) |
The barrier path also retains three pre-v0.5 behaviors the windowed engine deliberately fixed:
- Upfront materialization:
batch_size=Nonerunslist(items)— a 1M-item generator becomes 1M futures in memory at once. - Drain-on-timeout: unsized input +
batch_size+ timeout expiry drains the remaining source to append failure placeholders (_append_timeout_failures) — the exact "never touch the source after a stop" violation the v0.5 review banned from the windowed engine (poison/infinite/blocking iterators). - Barrier stalls: one straggler in chunk N delays every item of chunk N+1.
Additionally, sync parallel_starmap's non-process path materializes
its input before delegating (packed = [(fn, args) for args in
items]) — so no engine change alone can make starmap lazy. (Round 1
finding; fixed by this plan, see D6.)
Design¶
Generalize the two windowed engines to accept max_errors: int | None
and route all non-sequential collected calls through them:
_collected_map_windowed→ renamed_collected_map; the abort branch (failures >= max_errors) guards onmax_errors is not None._async_collected_map_windowed→ renamed_async_collected_map; same guard.parallel_mapbody: validate →sequentialbranch →_collected_map. The ~100-line inline barrier loop is deleted.async_parallel_mapbody: validate →timeout <= 0pre-check stays in the engine (already there) →_async_collected_map. The_consume/TaskGroup barrier loop is deleted.parallel_starmapnon-process path: replace input packing with a module-level_apply_star(fn, args)+functools.partial(_apply_star, fn)passed straight toparallel_mapwithitemsuntouched — the shape the async starmap and the sync process branch already have. Sized-ness and laziness are preserved; the pickling constraint is unchanged (the packed tuples carriedfntoo).
Deleted from _plan.py: _CollectedMapPlan, _plan_collected_map,
_make_chunks, _iter_batches_from_iterator,
_append_timeout_failures. Retained: _validate_max_errors,
_total_if_known, _progress_total, _timeout_failure,
_mark_timeout_indices. (Round 1 verified: the delete list has no
other users; every kept helper has live uses.)
Window size: batch_size when given, else
2 × _effective_workers(workers, executor) sync / 2 × concurrency
async — everywhere, including sized unconstrained inputs. Resolved
by Phase 1 data (see amendments log): the small window passed the R1
gate on all cases, and the pre-designed fallback (window = total for
sized inputs) was refuted — wait() re-registers waiters across the
whole in-flight set per wake, so a total-sized window is O(N²) and
measured 4–7× slower on threads and 72× on processes. The small window
is not a compromise; it is what keeps the driver loop cheap.
Mid-fill absorb sweep (sync): the zero-timeout wait() after each
paced submission exists so a dead API stops admission mid-fill under
max_errors, and to keep on_progress timely while a rate limiter
stretches the fill over wall-clock seconds. Both purposes are inactive
without max_errors and without a limiter, so the sweep is gated:
if max_errors is not None or bucket is not None:. One boolean, not a
second engine; removes an unconditional O(window) scan per admission
from the hot path. (Round 1 amendment — the draft kept it
unconditional.)
Streaming engines, decorators, TypedDicts, checkpoint, limiter: untouched. The public signatures of every entry point are byte-identical. The streaming and collected admission loops stay separate — their control flow genuinely differs (yield-driven vs return-at-end, reorder buffer only in streaming); a shared abstraction would be the premature kind.
Contract decisions¶
- D1 —
batch_size= admission window, everywhere. The chunk-with-barrier semantics are removed from the library. Theparallel_mapdocstring caveat "withmax_errorsset,batch_sizebecomes the admission window" is deleted because it is now unconditional. - D2 — collected maps consume input lazily, always. Exactly one
window ahead,
batch_sizeor not.parallel_map(fn, generator)no longer materializes the generator. (Results are still O(n) — it is a collected map.) Docs carry the atomicity callout: if no-side-effects-before-input-validation matters, materialize the input yourself. - D3 — no-drain everywhere. On timeout or abort, the source is never touched again: sized inputs get one failure placeholder per unseen item (by count), unsized inputs return a shorter result. This extends the v0.5 windowed policy to the plain path and deletes the drain behavior. Behavior change, documented below. D7 is the companion: truncation is never silent.
- D4 —
on_progresstotal for unsized inputs = items seen so far. Previously the plain path materialized unsized input, sototalwas the true final count; now it grows as the source is consumed — the same contract streaming andmax_errorspaths already have. Behavior change, documented below. Docs say plainly: a progress bar over a generator readsn/n(100%) on every tick; pass a sized input for a real total. - D5 — exception propagation gets simpler, and the changelog says
so.
CheckpointError: the async barrier path neededexcept*to unwrap the TaskGroup's ExceptionGroup; the windowed engine has no TaskGroup, so it already escapes plain — the unwrap block is deleted, the observable contract (except CheckpointErrorworks, cause preserved) unchanged and still tested. Also changed, deliberately: a non-CheckpointError escaping fromon_progress/store.lookupon the async plain path was ExceptionGroup-wrapped (raised inside a TaskGroup child) and now propagates plain; on the sync plain path an escaping exception previously hitpool.shutdown(wait=True)(drain) and now cancels via thesys.exc_info()check. Both are strictly-better behaviors from the windowed engine; both go in the changelog rather than being asserted away as "unchanged". - D6 — starmap surfaces keep their signatures, and sync starmap
joins the contract. No new starmap parameters
(
max_errors/checkpointremain out of scope). But the sync non-process delegation is rewritten (see Design) so D1–D4 genuinely hold forparallel_starmapand.starmap()— the draft's "unchanged, inherits the engine" claim was false while the packing list-comp stood. (Round 1 amendment.) - D7 —
ParallelResultcarries run status:timed_outandaborted(read-only bools, default False). Without this, the no-drain policy has a silent-truncation hole: a timeout that fires while the admission window happens to be empty, on an unsized input, returns only successes —ok=True, indistinguishable from "the generator simply ended". Set by all three collected engines (windowed sync/async and sequential) from their existingtimed_out/abortedlocals; constructor takes them as optional keywords (existingParallelResult(entries)call sites and tests unaffected). Streaming is untouched — its consumer sees every yielded item and the documented end-of-stream contract. (Round 1 amendment; the one scope addition of this plan.)
Behavior changes to document loudly¶
Pre-1.0, no deprecation dance — changelog and docs state these plainly:
parallel_map/async_parallel_mapwithbatch_sizeno longer pause at chunk boundaries. Strictly faster; only timing-sensitive code could notice.- Unsized input +
timeoutexpiry returns a result covering only the items actually pulled from the source (previously: source drained to append failures). Sized inputs are unaffected — still one failure entry per item.result.timed_out(D7) is the reliable signal. - Unsized input without
batch_sizeis no longer materialized;on_progresstotalreports items seen so far instead of the final count. - Source-iterator errors surface mid-run instead of upfront, and the
stop contract differs by runtime: sync stops admission, cancels
queued futures, and propagates promptly — tasks already running
cannot be interrupted and may complete, side effects included, in
the background (documented loudly in the docstring, with the
list(items)-first mitigation); async cancels and awaits in-flight tasks. (Round 2: Codex demonstrated the background mutations and proposed settle-before-raise; the fix was implemented, then rejected — it collides with the v0.5-mandated no-hang contract (test_poison_source_with_slow_tasks_raises_promptly): one hung task would block the source error from ever surfacing. Promptness + loud documentation is the deliberate trade.) - The default, no-kwargs
parallel_map(fn, sized_list)moves from eager upfront submission to windowed admission. Phase 1 measured it: parity on the library's niche (1ms tasks: 0.94×), 1.27–1.74× on degenerate no-op/µs-scale tasks — within the R1 budget, and the measured numbers still include the mid-fill sweep this plan gates off. - Exception-shape changes from D5: plain (not ExceptionGroup-wrapped) propagation of callback errors on the async path; cancel-not-drain pool shutdown on sync escape.
Edge cases (and their required outcomes)¶
- Empty input →
ParallelResult([]), no task ever submitted. Note: the windowed engine opens the checkpoint store (creating the file) and constructs the pool before discovering emptiness, where the barrier path returned first — accepted as an intended minor difference (an empty file that validates on reuse beats a special case). timeout=0/ already-expired deadline → zero tasks run, sync and async. Sync: deadline check precedes admission. Async: thetimeout <= 0pre-check precedes_drive()(asyncio.timeout alone cancels only at the first suspension point). Sized inputs: fully marked by count;timed_out=Trueeither way. Both already tested for themax_errorspath; tests extend to the plain path.- Timeout with an empty in-flight window (unsized input) → result
contains no TimeoutError entries (nothing was in flight) but
timed_out=True— the D7 case. timeout=vs rate-limit pacing → deadline-awareLimiter.wait(timeout=)already threaded through the windowed engine; the plain path inherits it. The v0.5 repro (rate 1/s,timeout=0.1taking 2s) must stay fixed withmax_errorsunset.- Checkpoint write failure (
store.putraises) → engine stops cold:sys.exc_info()check in the syncfinallycancels instead of draining; async cancels in-flight tasks. PlainCheckpointErrorreaches the caller in both runtimes. - Cached (checkpointed) items during admission → looked up at admit time, filling slots without occupying window capacity; a fully-cached run submits nothing. (Existing windowed behavior, now on the plain path.)
batch_size=1→ serial admission, window of one. Results identical, order preserved in the results list (positional slots).- Lazy starmap →
parallel_starmap(fn, generator_of_tuples)now consumes one window ahead; sized tuple lists keep theirtotal.
Risks and how the plan retires them¶
- R1 — micro-task throughput. The mechanism, named: upfront
submission registers each future's completion handling once
(
as_completedfor the batch's lifetime) and the pool queue feeds workers with no driver involvement; the windowed driver callswait()per completion batch, which re-registers waiters across the whole in-flight set — O(N·window) waiter churn vs O(N) for many tiny tasks. Retirement: throwaway benchmark before the engine switch is committed — (a) 100k no-op items, (b) 100ksleep(0.001)items, (c) 100k cheap-real-function items (dict transform) on a sized list with default kwargs — the most common call shape; thread executor, plus a process-pool spot check. Target: parity on (b) and (c); no worse than ~2× on (a), which is outside the library's niche. If breached: absorb in batches per wake (already the shape) and/or the sized-input window-scaling fallback from Design — decided on data, recorded here. Outcome: gate passed on all cases; the window-scaling fallback refuted. Numbers in the amendments log (Phase 1 results). - R2 — tests encoding barrier behavior. One existing test pins the
drain-on-timeout contract and must change:
tests/test_parity.py::TestAsyncTotalTimeout::test_lazy_input_remainder_marked(generator +batch_size=2+timeout=0.3, assertslen(result) == 6from post-timeout source draining). It is rewritten to the no-drain contract: shorter result,timed_out=True, source consumption stopped. (Round 1 correction — the draft claimed no such test existed.)test_batch.pyasserts in-flight caps, lazy consumption, totals, and error isolation — all window-compatible. Any further test whose assertion must change is listed in the amendments log with its reason. - R3 — async structural change. The plain async path swaps
TaskGroup for manual task bookkeeping (create/cancel/await-suppress in
finally). The windowed engine has carried exactly this shape since v0.5 with its cancellation tested; parity tests (below) extend coverage to the plain path. - R4 — silent semantic drift. The plain path inherits ~40 existing
plain-
parallel_map/async_parallel_maptests untouched (results, ordering, errors, progress, checkpoint, timeout on sized inputs). They run against the new engine unmodified — that suite is the primary regression net.
Test plan¶
New tests, each naming the production bug it prevents:
- plain collected map source-lead bound (sync + async): no
max_errors, generator input — source never more thanwindowahead of completions. Prevents: silent return of upfront materialization. batch_size=Nonedoes not materialize: workers blocked, produced count stays ≤ default window. Prevents: regression tolist(items).- timeout + unsized input (sync + async): result shorter,
timed_out=True, source consumption stops at the stop. Prevents: drain-on-timeout returning; silent truncation. - timeout + sized input on the plain path: every slot filled by count,
timed_out=True. Prevents:_PENDINGleak through the constructor guard. timeout=0plain path, sync + async: zero tasks run. Prevents: the asyncio.timeout first-suspension gap reopening on the plain path.- D7 status:
timed_out/abortedset by windowed sync, windowed async, and sequential engines; both default False on a clean run and on manual construction. Prevents: the empty-window unsized-timeout result masquerading as complete. - source iterator raises mid-run: exception propagates, sync cancels queued futures / async cancels tasks, no hang. Prevents: a wedged pool on poison input. (Assertions match the per-runtime contract in behavior change 4 — no sync assertion may require interrupting a running thread.)
- deadline-beats-pacing on the plain path (port of the v0.5 repro
without
max_errors). Prevents: the bypassed-timeout bug returning through the routing change. - starmap laziness (sync): generator of tuples, blocked workers, bounded consumption; sized tuple list keeps exact progress total. Prevents: the packing list-comp returning.
- benchmark script (throwaway, not committed as a test) — results recorded in the amendments log.
Removed/updated tests are individually justified in the amendments log
(test_lazy_input_remainder_marked already listed under R2).
Phases¶
| Phase | Work | Commit gate |
|---|---|---|
| 1 | Benchmark harness; baseline numbers on the barrier path; window-default decision for sized unconstrained inputs | numbers + decision recorded below |
| 2 | Sync unification: generalize _collected_map, route parallel_map, delete barrier loop; lazy starmap delegation; D7 on ParallelResult (all engines); gated mid-fill sweep; new sync tests |
full suite green; benchmark within target |
| 3 | Async unification: generalize _async_collected_map, route async_parallel_map, delete _consume/except* unwrap; rewrite test_lazy_input_remainder_marked; new async tests |
full suite green |
| 4 | Delete dead _plan.py machinery; docs sweep — docstrings (incl. core.py "total … when batch_size is set" qualifier, aio.py drain sentence), docs/api-reference/core.md (the "not the chunked meaning of the plain collected path" passage becomes false), docs/api-reference/async.md, docs/getting-started/quickstart.md, docs/user-guide/advanced-features.md, best-practices.md, real-world-patterns.md, README.md, roadmap |
mkdocs --strict, examples run, grep for barrier/chunk prose comes back clean |
| 5 | Review cycle (four-review: Codex, Codex adversarial, code review, adversarial design review) + merge gates | all findings triaged here |
Each phase is its own commit on a v0.6-engine-unification branch.
Gates before merge: uv run pytest -q, uv run ruff check + format
--check, uv run mypy pyarallel tests/typing_assertions.py,
uv run mkdocs build --strict, all examples run.
Explicitly out of scope¶
- Free-threaded CI and
executor="interpreter"(separate v0.6 items). max_errors/checkpointparameters on the starmap surfaces.- Any change to streaming, decorator, limiter, or checkpoint code.
- Version bump / release (v0.5.0 is still unpublished; versioning is a release-time decision).
Review amendments log¶
Round 1 — pre-implementation plan review (2026-07-06)¶
Reviewers: Codex adversarial (working tree), independent fact-check review (Opus), adversarial design review (Sonnet). Verdicts: needs-attention / not implementable as written / proceed with amendments. All dispositions below are reflected in the body above.
Fixed in plan:
- (Codex high / design-review-adjacent) Sync
parallel_starmapmaterializes input viapacked = [(fn, args) for args in items]before the engine sees it — D6's "unchanged, inherits the engine" was false for a public surface. → Lazy delegation via module-level_apply_starpartial; D6 rewritten; starmap laziness tests added. - (Codex high) "In-flight work is cancelled" on source error is impossible for sync threads. → Behavior change 4 split by runtime, matching the v0.5 honesty wording; test assertions constrained accordingly.
- (Codex medium + fact-check high) R2's "no existing test pins
unsized-timeout drain" was false:
test_parity.py::test_lazy_input_remainder_markedassertslen(result) == 6from post-timeout draining. → R2 corrected; test listed for rewrite in Phase 3. - (Design review high) Silent-truncation hole: timeout firing on an
empty in-flight window with unsized input returns only successes,
ok=True. → D7 added:ParallelResult.timed_out/.aborted, set by all three collected engines; edge case and tests added. The one scope addition of the plan. - (Design review high) The eager→windowed shift for the default no-kwargs sized call was an unnamed behavior change buried in "window size unchanged". → Named as behavior change 5; Phase 1 benchmark gains the cheap-real-function sized-list case; sized-input window-scaling fallback (window = total, same engine) pre-designed, decided on Phase 1 data.
- (Design review medium) Unconditional mid-fill absorb sweep is a
hot-path tax whose purposes (mid-fill abort, paced progress) are
inactive without
max_errors/limiter. → Gated onmax_errors is not None or bucket is not None. - (Fact-check medium) D5's "observable contract unchanged" understated real changes: callback exceptions on the async plain path lose their ExceptionGroup wrapper; sync escape switches drain→cancel shutdown. → D5 rewritten to name both; changelog item 6 added.
- (Fact-check medium) Phase 4 doc list missed
docs/api-reference/core.md(whosemax_errors-vs-plain-path passage becomes actively false),api-reference/async.md, andgetting-started/quickstart.md, plus two load-bearing docstring sentences. → Phase 4 list expanded with the specific passages; gate extended with a barrier/chunk-prose grep. - (Fact-check low) Empty input +
checkpoint=now creates the checkpoint file (windowed engine opens the store before discovering emptiness; barrier path returned first). → Recorded as intended in edge cases. - (Design review low ×2) D4 progress-bar reads 100% throughout on generators; D2 moves source-failure timing after side effects may have fired. → Both get explicit docs callouts (D2/D4 text).
- (Design review low) R1 named neither the mechanism nor why 2× — → R1 rewritten: waiter-re-registration churn named, third benchmark case added.
Accepted, no action:
- (Design review) Benchmark-first sequencing, four-review Phase 5 proportionality, keeping streaming/collected loops separate, and the breaking-change set being one conceptual fix plus its entailed consequences — all endorsed as-is.
- (Fact-check) Delete/keep helper lists verified clean;
cached-items-don't-occupy-window verified; no starmap or progress
test depends on barrier timing; sized-input timeout and
timeout=0tests remain green under the windowed path.
Phase 1 results — benchmark (2026-07-06)¶
Environment: darwin arm64, 10 cores, Python 3.12.8; default thread
workers = 14 → default window 28. Method: the windowed engine measured
via the existing max_errors=10**9 routing (never triggers);
window=total simulated with batch_size=len(items) on top. Median of
3 runs (2 for process). The windowed numbers include the mid-fill
absorb sweep that Phase 2 gates off — they are the pessimistic bound.
| Case | eager (barrier) | windowed 2×workers | windowed window=total |
|---|---|---|---|
| (a) no-op ×100k, thread | 1021 ms | 1772 ms (1.74×) | 5595 ms (5.5×) |
| (b) sleep(1ms) ×20k, thread | 1923 ms | 1811 ms (0.94×) | 12967 ms (6.7×) |
| (c) dict transform ×100k, thread | 1364 ms | 1823 ms (1.34×) | 5722 ms (4.2×) |
| (a′) no-op ×10k, process, workers=4 | 839 ms | 1063 ms (1.27×) | 60459 ms (72×) |
R1 gate: passed. Parity (actually faster) on the 1ms niche workload; 1.27–1.74× on degenerate micro-tasks, inside the ≤2× budget.
Window-default decision: keep 2 × workers everywhere; the
window=total fallback is rejected — refuted by its own benchmark.
Mechanism: concurrent.futures.wait() re-registers a waiter on every
in-flight future per wake, and the (currently ungated) mid-fill sweep
scans the in-flight set per admission — with window = N both go
O(N²). The small window is what keeps the driver loop cheap; it is the
design, not a compromise. The 72× process-pool number is the same
effect amplified by pickling every pending work item into the pool
upfront.
Round 2 — pre-merge review of the branch (2026-07-06)¶
Reviewers: Codex review, Codex adversarial, independent code review (Opus), adversarial design review (Sonnet) — all against the full branch diff. Verdicts: needs-attention ×2 / merge-ready / merge-with-amendments. All Round-1 amendments were independently verified as implemented (letter and spirit).
Fixed:
- (Codex adversarial high — reproduced: 275ms elapsed on a 50ms
timeout, ok=True) Cached checkpoint hits bypass
timeout=: the cached-admission loop pulls from the source and runs lookups without ever reaching a wait, so a checkpoint-heavy run overruns the deadline and reports a clean success. → Deadline check at the top of_submit_nextin both engines (async gets a monotonic mirror of theasyncio.timeoutdeadline, which cannot preempt synchronous code); regression tests with a slowcheckpoint_keyon both runtimes. - (Codex adversarial high — reproduced: exception at 0ms, side
effects landing 300ms later; fix attempted, then REJECTED) Sync
driver-side errors (source, checkpoint write, progress callback)
return while admitted tasks keep mutating state in the background.
The proposed settle-before-raise shutdown was implemented and
immediately failed the v0.5-mandated regression test
test_poison_source_with_slow_tasks_raises_promptly— the two reviews demand opposite contracts. Resolution: promptness stands (one hung task must not block the error from ever surfacing, and every other stop in the library is cancel-and-return); the background-completion hazard is now documented loudly in theparallel_mapdocstring with thelist(items)-first mitigation, and recorded as behavior change 4 + a changelog entry. - (Codex review P2) A
TimeoutErrorraised by the source iterable itself was repackaged as deadline expiry by the async engine's broadexcept TimeoutError(AssertionError withtimeout=None, fabricated timed-out result otherwise). → Handler scoped to real expiry viaasyncio.timeout'sscope.expired(); propagation test for both timeout=None and timeout-set. - (Design review medium + code review low — same finding) With
timeout=andmax_errors=both live, a mid-fill abort fell through to the blocking wait: the abort was delayed by a task completion, and a deadline expiry in that window set both flags and marked would-be-Abortedslots asTimeoutError. → Earlybreakon abort after the admit loop; flags made exclusive at construction in both engines (aborted and not timed_out— first stop reason wins, also covering the async timeout-salvage flip); documented inParallelResult; exclusivity test added. - (Design review medium)
ParallelResult.__repr__hidtimed_out/aborted— the all-successes truncation printed identically to a complete run. → Flags appear in the repr; tests. - (Design review high) Stale
asyncio.TaskGroupclaims on the two most-read pages (api-reference/async.mdintro,docs/index.md) and stale roadmap Current-section lines ("chunking for collected maps", "via asyncio.TaskGroup"). → All rewritten. - (Design review high) No user-facing changelog for the named
behavior changes — they lived only in this plan. →
CHANGELOG.mdcreated (Unreleased v0.6 section, plus 0.5.0/0.4.0 records). - (Code review low) The freshly-written "progress bar reads 100% on
every tick" claim was false —
totalcounts items admitted (one window ahead), so the ratio hovers below 100% and is non-monotonic. → Reworded at all four sites (both docstrings, advanced-features, async.md); D4's rationale stands corrected here.
Accepted, no action:
- (Design review low)
_plan.pycould fold intoresult.py— kept as-is: it is genuinely shared bookkeeping between the two runtime modules; folding saves a file, not complexity. - (Design review low) Duplicated sync/async docstring prose — inherent to the intentionally separate surfaces (DevX principle 2).
- Streaming keeps cancel-and-return on generator close and source errors (consumer-driven stops; promptness is the documented contract there).
- The abort/deadline race fixed in (4) could not be pinned with a deterministic test (needs a mid-fill completion racing a deadline); the exclusivity guarantee is enforced structurally at construction and asserted by the timeout+max_errors engine tests.