Skip to content

003 concurrent scheduler#9

Open
S2thend wants to merge 11 commits into
mainfrom
003-concurrent-scheduler
Open

003 concurrent scheduler#9
S2thend wants to merge 11 commits into
mainfrom
003-concurrent-scheduler

Conversation

@S2thend

@S2thend S2thend commented Jul 8, 2026

Copy link
Copy Markdown
Owner

feat(scheduler): concurrent execution of ready nodes within a single run (parallel fan-out before merge) #8

S2thend added 9 commits July 8, 2026 14:59
…rtifacts

Full speckit artifact set for intra-run concurrent node execution
(eager dispatch, first-k merges, graph-definition dispatch order):

- spec.md: 4 user stories, FR-001..FR-018, SC-001..SC-008, constitution
  alignment, clarifications session (eager dispatch + first-k decided)
- plan.md: technical context, 15-point constitution check (all pass)
- research.md: decisions R1-R11 grounded in the 0.3.0 compatibility
  audit and empirical probes
- data-model.md: state occupancy changes (multi-RUNNING snapshots,
  straggler records), run-loop entities; snapshot format stays v2
- contracts/scheduler-behavior.md: observable behavior contract C1-C9
- quickstart.md: parallel diamond, first-k racing, k-successes pattern
- tasks.md: 33 tasks across setup/foundational/US1-US4/polish
- checklists/requirements.md: spec quality gate (all pass)
…nt-scheduler

Six findings from /speckit.analyze clarified with maintainer and applied:

- U1 (HIGH): define edge classification — an edge is in-loop (back) iff
  it lies on any cycle (derived from the validator's cycle enumeration);
  in-loop edges keep 0.3.0 re-entry/hard-stop semantics, forward edges
  get the record-only straggler rule (spec FR-006, research R5,
  data-model, contracts C3.6, T007)
- I1 (MEDIUM): merge payload snapshotted at fire decision, not slot
  admission — exactly k results regardless of capacity queuing
  (FR-005, research R5, data-model, contracts C3.2, T014)
- C1 (MEDIUM): record-only straggler rule extended uniformly to
  partial-input targets (the k=1 case) (FR-005, C3.3, T015)
- C2 (MEDIUM): T020 gains constitution-XIV defensive-fallback sub-test
- C3 (MEDIUM): new T034 [US2] — stall-detection tests (no false stall
  with in-flight work; genuine stall still raises) (FR-010)
- C4 (LOW): T013 gains queued-node event-silence assertions (FR-007)
- D1 (LOW, editorial): FR-004 annotated as the k=fan-in case of FR-005

Total tasks: 34.
…01-T003)

Four fixture files generated by the actual 0.3.0 engine plus an 11-test
gate module. Baseline verified: 54 passed (43 existing + 11 golden) on
the unchanged engine.
…n (T004-T010)

Rework Scheduler.run into a completion-driven loop: every ready node is
dispatched immediately as an asyncio task (graph-definition order),
bounded by the ConcurrencyManager; all finished tasks are harvested
before any readiness re-scan. mark_running + NODE_SCHEDULED move to
slot admission so queued nodes stay PENDING everywhere observable.
Downstream dispatch now classifies edges (in-loop iff on any cycle, from
the validator's cycle enumeration): in-loop edges keep exact 0.3.0
re-entry/hard-stop semantics (constitution XIV); forward edges into an
already-activated target record the completion only (first-k straggler
rule). Partial-input nodes receive their triggering input's result.
No-progress error now requires nothing ready AND nothing in flight.

Sanctioned test update: the reentry hard-stop unit test pinned the old
undifferentiated behavior on an acyclic edge (the audited crash class
spec 003 removes); split into back-edge hard-stop (preserved, XIV) +
forward-edge record-only. The doctest predicted to need updating in the
spec passes unchanged, so the net sanctioned-edit count remains one.

Gate: 55 passed (incl. 11-test frozen golden suite), ruff + mypy clean.
Smoke: diamond fan-out at capacity 2 runs in 50% of capacity-1 time.
…011-T021, T034)

US1: speedup targets met (2 branches/cap2 <=60%, 3/cap3 <=45%),
definition-ordered merge payloads under reversed completion, capacity
bound with queued-node event silence, first-k exactly-once with
straggler recording and fire-decision payload freeze under saturation,
partial-input triggering payloads, dispatch-order determinism across
10 hash seeds.

US2: golden gate green post-change, sequential-equivalence corpus
(pipeline/switch/loop) byte-identical across 10 seeds with strict event
alternation, capacity-1 equivalence (gauge + spy-store max-one-running),
max_visits under concurrent forward routers, stall detection both ways
(no false stall in flight; genuine stall still raises).

Spec assumption updated to record the actual sanctioned test edit (the
hard-stop unit test split; the predicted doctest passed unchanged).

Suite: 70 passed.
…22-T026)

US3: original exception types surface unwrapped with a sibling in
flight (handler ValueError, unmatched switch route, snapshot-store
RuntimeError); after failure surfaces there are zero further events and
zero snapshot writes; the cancelled sibling observes CancelledError and
never emits completion. Cancellation implementation (cancel-and-await,
no TaskGroup) landed in the foundational phase; these tests are its
acceptance (T023).

US4: resume from a crafted v2 snapshot with two RUNNING nodes re-runs
exactly the in-flight nodes and reuses completed results; crash-and-
resume roundtrip from a real intermediate snapshot re-executes only
unfinished work; every persisted snapshot in both runs stays format
version 2.

Suite: 76 passed; ruff + mypy clean.
…027-T033)

README gains a 'Concurrency Within a Run' section with upgrade notes;
the edge-heavy design principle now reflects in-process concurrent
dispatch under an explicit capacity policy. Scheduler class docstring
gains a fan-out doctest demonstrating definition-ordered merge payloads.
New CHANGELOG.md documents 0.4.0 semantics (first-k merges, triggering
payloads, deterministic dispatch order, edge classification, failure
teardown) and the observable changes for fan-out workflows. docs/ page
has no execution-order claims - nothing to sync.

Verified: 76 tests passing, doctests green, quickstart patterns run as
written, debug tracing silent under default config, ruff + mypy clean.
…ersarial review)

An Opus multi-lens review of the implementation found two real defects,
both fixed with tests:

1. First-k merge cardinality (C3.2/FR-005 contract violation): the
   aggregate payload builder appended every completed upstream, ignoring
   k. Because the run loop harvests a whole asyncio.wait batch before the
   next readiness scan, when more than k inputs complete in one batch the
   merge fired once but with >k results, and cardinality varied with
   capacity (probe: required=1 diamond -> ['A','B']). Cap the payload at
   exactly k in graph-definition order; a no-op when k == fan-in, so
   wait-for-all and all single-frontier golden behavior stay
   0.3.0-identical.

2. Self-loop re-entry lost (0.3.0 regression): _compute_loop_pairs
   derived in-loop edges from Graph._find_cycles, which never reports
   length-1 self-cycles, so a self-edge (a->a) was misclassified as a
   forward straggler and lost re-entry. Add self-edges to the loop-pair
   set explicitly; verified equal against main (3 runs -> {'i': 3}).

Tests: exactly-k cardinality on the batched-completion path,
capacity-invariant cardinality/membership, default-merge wait-for-all,
self-loop re-entry parity, and a priority-admission test closing the
FR-014/C1.3 coverage gap (NODE_SCHEDULED order follows priority, not
definition order). Tightened the golden quorum test to assert exactly-k
(the missing assertion that had masked the bug). Contract C3.2/C3.6 and
research evidence updated.

Suite: 81 passed (was 76); new timing tests stable across 5 runs; ruff,
mypy, doctests clean.
@S2thend S2thend self-assigned this Jul 8, 2026
S2thend added 2 commits July 9, 2026 13:14
…ion, partial-input tiebreak wording

Addresses four external-review findings:

1. (bug, FR-012/Snapshot-First Recovery) _initial_results_from_snapshot
   dropped completed nodes whose payload was None, so a resumed aggregate
   saw a shorter input list than a fresh run. Restore completed payloads
   unconditionally. Regression test asserts fresh == resumed == [None,'V'].

2. (constitution XV) EventBus.emit silently returned listener exceptions
   when no on_error was set. Now log them at warning level (still returned,
   still not raised). test_eventbus asserts the warning fires without
   on_error and does NOT fire when on_error handles it.

3. (test weakness) The priority-admission test put the best-priority node
   first in graph order, so it could not distinguish priority from
   dispatch order. Rewritten: the graph-first node gets the WORST priority
   and still wins the free slot (FR-018 dispatch order), while the queued
   nodes are admitted in strict priority order. Behavior was already
   correct per C1.3 ('reorder admission among queued nodes'); only the test
   and its docstring overclaimed. No code change — priority governing the
   free slot would contradict FR-018 deterministic dispatch.

4. (doc precision) data-model/FR-017/C3.4 said a partial-input node gets
   'the input whose completion made it ready', which is ill-defined when
   several inputs complete in one batch. Reworded to the deterministic
   graph-definition-order tiebreak (matching first-k membership). Tracking
   the event-loop-first task was rejected as it would be nondeterministic.
   New test pins the same-batch tiebreak.

Suite: 84 passed (was 81); new/reworked tests stable across 5 runs;
ruff, mypy, doctests clean.
…s (membership)

Third external review found the earlier cardinality fix insufficient:
capping the aggregate payload at k in graph-definition order kept exactly
k results but chose the graph-first k, not the k earliest-completing. So
an input that completed in an earlier harvest batch could be dropped when
the k-th completion arrived in a later multi-completion batch. Repro:
graph order a,b,c, required=2, c completes first then a,b in one batch ->
returned ['A','B'], but the k=2 earliest are {c,a} -> ['A','C']. This
violated FR-005/C3.2 ('the k earliest-completing inputs').

Fix: track a monotonic completion sequence in the run loop, assigned in
graph order within each harvest batch (so same-batch ties are
deterministic and results-dict insertion is now deterministic too).
_build_input_payload selects the k winners by that sequence and emits
them in graph-definition order. When k == fan-in this selects all inputs,
so wait-for-all and golden behavior stay 0.3.0-identical. Resume seeds
the sequence for already-completed nodes in graph order (their true
pre-crash order is not persisted; snapshot format stays v2).

This is the fix the FIRST review's fix_suggestion originally proposed and
that I wrongly simplified to graph-first-k. Three regression tests added:
in-run membership, determinism across 10 hash seeds, and resume.

Suite: 87 passed (was 84); new tests stable across 5 runs; ruff, mypy,
doctests clean.
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