Adds a NULL-by-default lookup/populate callback seam on xce_sched_core_t, mirroring cache_clear_cb's proven shape, so the allocation dispatch chokepoint can consult the unsatisfiable-resreq cache without a normal-mode-only field access in shared code. Adapters live in alloc_cache.c and are registered only during normal-mode startup; simulation mode leaves both callbacks NULL and always computes fresh. Adds the scheduler-sim-live-allocation spec (requirements/design/ research/tasks) as the planning artifact this and subsequent tasks implement against. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
15 KiB
15 KiB
Research & Design Decisions
Summary
- Feature:
scheduler-sim-live-allocation - Discovery Scope: Extension (existing system) — light discovery per
design-discovery-light.md, since the integration points, existing patterns, and constraints were already exhaustively traced against live source during requirements development (seerequirements.md's Project Description for the full evidence trail; this document focuses on decisions, alternatives, and risk, not re-deriving what's already cited there). - Key Findings:
- Six functions (
plugin_begin_session,plugin_end_session,sched_resolve_job_picker,sched_get_job,sched_account_jobs,sched_commit_job_alloc) already completed the exactxce_scheduler_t*→xce_sched_core_t*migration this spec needs to repeat for the remaining allocation chain — a proven, six-times-repeated pattern, not a new one. - Every function in the host-scoped allocation chain (
sched_meet_host_selection,sched_check_shared_satisfy,sched_check_host_satisfy,plugin_prepare_allocation,sched_try_alloc_on_host_,sched_commit_decision,plugin_commit_decision) only ever touchessched->core.*or worker/ctx fields — zero genuinexce_scheduler_t-specific dependency, confirmed by reading every function body, not inferred from signatures. - The one genuine exception,
xce_scheduler_t.unsatisfiable_cache, is a pure performance optimization with a documented prior memory-corruption incident (scheduler-allocation task 5.2 round 1) from trying to shortcut it via a struct-prefix cast — the fix pattern (a driver-registered callback,cache_clear_cb) is proven and must be repeated, not re-invented. sched_allocator()'s full body (verified, not assumed) shows every single call it makes becomes core-safe once the above migration lands — the function itself needs only its top cast changed (xce_scheduler_t* sched→xce_sched_core_t* core).- Sim mode's thread-spawn point is
sched_sim_driver_run()(sim_loop.c), NOTsched_sim_driver_start()(sim.c) — the latter only does core init and snapshot/replay-file setup; thepthread_create(&picker_tid, NULL, sim_picker_thread_fn, &(sim->core))call lives inside the former.
- Six functions (
Research Log
Is the allocation chain's xce_scheduler_t* coupling mechanical or genuine?
- Context: The spec's central bet is that sim mode can reuse normal mode's allocation code verbatim. This only holds if the coupling is superficial (never-migrated signature) rather than load-bearing (genuinely needs normal-mode-only state).
- Sources Consulted: Direct reads of
resource.c(sched_meet_host_selection:221,sched_check_shared_satisfy:305/sched_check_host_satisfy:378/sched_try_alloc_on_host_:497),plugin.c(plugin_prepare_allocation:591,plugin_commit_decision:442,plugin_job_election:554),scheduler.c(sched_commit_decision:1607,sched_allocator:429),schedobj.c(sched_ctx_get_uncomit_host:522, already core-safe). - Findings: Every one of the 8 functions in the allocation chain reads/writes only
sched->core.*fields (or worker/ctx fields passed in separately) — no function readssched->apictx,sched->cluster_cfg,sched->unsatisfiable_cache, or any other genuinelyxce_scheduler_t-only field, with the single exception noted below. - Implications: The migration is safe to do mechanically, function by function, following the exact precedent of the six already-migrated functions. No redesign of the allocation algorithm itself is needed.
Is there a genuinely xce_scheduler_t-only dependency anywhere in the chain?
- Context: A prior investigation (scheduler-allocation spec, task 5.2) hit exactly this problem once already, for a different field.
- Sources Consulted:
scheduler-allocation's own Implementation Notes (task 5.1, 5.2),core.h'sxce_core_cache_clear_cb_tdoc comment. - Findings:
xce_scheduler_t.unsatisfiable_cache(a fingerprint → pend-reason-category cache, populated whensched_make_primary_alloc_()exhausts all candidates for a job, consulted before attempting allocation again for an identically-shaped job) has no sim-mode equivalent field, by deliberate design (Req 5 AC 17 simulation-isolation boundary fromscheduler-core). A first attempt to bridge this via__attribute__((weak))+ a cast of the sharedxce_sched_core_t coreprefix back toxce_scheduler_t*silently corruptedxce_sched_sim_driver_t.alt_events_file(same byte offset as the cache field) — invisible to ASAN and the whole test suite until caught by review. - Implications: This one field needs a callback seam, not a cast, mirroring
cache_clear_cbexactly. Consult/populate calls must go through a NULL-checked function-pointer pair the normal-mode driver registers and sim mode leaves NULL. A NULL seam means "always take the slow path" (recompute fully), which is correct behavior, not degraded correctness — see Design Decision below.
Where does sim mode actually spawn its picker/allocator thread?
- Context: Needed to know precisely which function's thread-spawn code changes.
- Sources Consulted:
sim.c'ssched_sim_driver_start()(full body read) andsim_loop.c'ssched_sim_driver_run()(partial body read, confirmedpthread_createcall site at line ~974). - Findings:
sched_sim_driver_start()only initializessim->coreand opens/validates the replay file and snapshot — it spawns no threads.sched_sim_driver_run()is the actual long-running entry point (design.md's own "literal SimDriver entry point" per its header comment) and is where the picker thread is created, once, for the process's sim-session lifetime. - Implications: The unification touches
sched_sim_driver_run()'s thread-spawn call, notsched_sim_driver_start().
Open question not fully resolved — flagged as a risk, not asserted either way
- Context:
sched_register_plugins()(scheduler.c:596) also callsregister_seqjob_allocator(sched)/register_parjob_allocator(sched), andxce_sched_plugin_thas ajob_alloc_fnfield (plugin_job_allocate_fn_t) alongsidejob_elect_fn. This raises the question of whether primary-resreq allocation is ALSO dispatched through a generic plugin-hook indirection (aplugin_job_allocate()analogous toplugin_job_election()) that would need the same core-safety migration. - Findings so far: The
scheduler-allocationspec's own Implementation Notes describesched_make_primary_alloc_()as directly branching betweenseqjob_allocator.c/parjob_allocator.c's ownmy_alloc()functions by name (based onprimary->maxTasks), which reads as a direct function-call dispatch, not a generic plugin-hook indirection — suggestingjob_alloc_fn/register_seqjob_allocator/register_parjob_allocatormay be a legacy/inactive registration path (parallel tojob_elect_fn's own documented legacy-compat status for pickers) rather than something on the live call path. - Not confirmed with certainty. Flagged in Risks below as a design-time verification item for whoever implements task work in this area — if
sched_make_primary_alloc_()turns out to dispatch through a generic hook after all, that hook needs the identical core-safe treatment asplugin_job_election/plugin_prepare_allocation/plugin_commit_decision.
Architecture Pattern Evaluation
| Option | Description | Strengths | Risks / Limitations | Notes |
|---|---|---|---|---|
| Unify picker/allocator (selected) | Re-type sched_job_picker()/sched_allocator() to xce_sched_core_t*, spawn the identical functions from both drivers |
Single implementation, cannot drift (directly satisfies Requirement 5); reuses 6x-proven migration pattern | Sim mode gains a real two-thread jbuf hand-off for the first time — needs the 3 named regression tests re-verified |
Selected: matches the project's own stated governing principle and its documented drift-prevention rationale |
| Behavioral-equivalence only (rejected) | Keep sim_picker_thread_fn() as a separate implementation, extend it to call the same core-safe sub-functions, verify equivalence via tests |
Smaller diff, no jbuf behavior change in sim mode |
Reintroduces exactly the two-implementations-must-independently-stay-in-sync failure mode this project has already hit three times (bug-1611, bug-1613, bug-1681) | Rejected per Requirement 5 and the user's explicit direction during spec discussion |
| Recording-authoritative (rejected) | Keep sim_reconcile_unaccounted_jobs() as the general-case decision source; new election/allocation logic only populates side effects (limit vres) for an already-known outcome |
Smallest behavior change; no live/recorded divergence handling needed | Contradicts the project's own driver-equivalence invariant (decisions should be independently reproduced, not merely replayed) and doesn't scale to future plugin hooks with other side effects | Rejected during requirements review — see requirements.md Requirement 3 AC3-5 |
Design Decisions
Decision: Live election/allocation is authoritative; recorded-decision reconciliation narrows to a timing-gap safety net
- Context: Once sim mode can independently compute real decisions, two sources exist for a job's outcome: the live picker/allocator, and
sim_reconcile_unaccounted_jobs()'s trust-the-recording reconstruction. - Alternatives Considered:
- Recording stays authoritative always; live path only supplies side effects — rejected (see table above).
- Live path authoritative, recording is a fallback only for jobs it hasn't reached in time — selected.
- Selected Approach: The live picker/allocator decides for any job it reaches.
sim_reconcile_unaccounted_jobs()'s existingjob->accounts == NULLtrigger guard already means it only acts on jobs the live path hasn't yet processed — no new guard needed, this narrowing falls out of the existing idempotency check once the live path starts actually producing outcomes. - Rationale: Matches
scheduler-driver-equivalence.md's own state-equivalence invariant (decisions should be independently reproduced by the core state machine, not replayed from a trusted log) and the project's explicit governing principle for this spec. - Trade-offs: A live-computed decision that disagrees with the recording is now possible and must be surfaced (Requirement 3 AC5) rather than silently reconciled either way — this is by design: a divergence is a genuine finding (a migration defect, or a previously-hidden issue the old fake-PEND behavior could never have exposed).
- Follow-up: Verify via
--sim-verifyagainst real recorded sessions (Scope item 9 in requirements.md) before shipping.
Decision: unsatisfiable_cache becomes an optional, driver-registered callback pair, not a relocated field
- Context: Need the primary-allocation dispatch chokepoint to remain core-safe without giving sim mode a cache it structurally cannot support cleanly today.
- Alternatives Considered:
- Move the cache itself onto
xce_sched_core_tso both drivers have it — rejected: sim mode has no natural place to construct/own it (nosched_fini()-equivalent teardown exists in sim mode to destroy it), and giving sim mode a working cache is not something any requirement asks for (explicitly a non-goal — see Boundary Context, "achieving simulation mode's allocation-decision throughput or latency parity"). - A NULL-safe callback pair (
lookup/populate) onxce_sched_core_t, registered only by normal mode'smain.c, mirroringcache_clear_cb— selected.
- Move the cache itself onto
- Selected Approach:
xce_sched_core_tgains two optional function-pointer fields (unsatisfiable_lookup_cb/unsatisfiable_populate_cb), each takingxce_sched_core_t* coredirectly — the exact same shape as the existingcache_clear_cbfield, not a new wrapper struct with an opaqueimpl/user-data pointer (an earlier design draft added one; removed as unnecessary indirection with only one registrant and no foreseeable second). Normal mode'smain.cregisters both, pointing at two small adapter functions inalloc_cache.c(alongside the existingxce_alloc_cache_clear_cb()adapter) that safely recoverxce_scheduler_t*via(xce_scheduler_t*)core— safe specifically because these adapters are normal-mode-exclusive and only ever invoked with acoregenuinely embedded in anxce_scheduler_t, unlike the round-1 mistake which cast inside code shared by both drivers. Sim mode leaves both NULL.sched_make_primary_alloc_()treats NULL as "no cache available — always compute fresh," which is correct (never wrong), not a degraded-correctness mode. - Rationale: Directly reuses the proven pattern and its documented rationale (
core.h'sxce_core_cache_clear_cb_tdoc comment); avoids repeating the exact class of memory-corruption bug the earlier cast-based attempt caused. - Trade-offs: Sim mode never gets the cache's performance benefit — accepted, explicitly out of scope (see Boundary Context).
Risks & Mitigations
- Decision-timing/ordering shift in sim mode once
jbufis genuinely used — Mitigation: re-runtest_repl_equivalence.c,test_sim_contention_parity.c,test_sim_equivalence.cbefore considering this feature done; treat a broken assertion as a signal to investigate (it may be asserting on an artifact of the old single-fused-thread sequencing), not a reason to revert to two implementations. - Unconfirmed
job_alloc_fn/register_seqjob_allocator/register_parjob_allocatordispatch path — Mitigation: verify at implementation time whethersched_make_primary_alloc_()dispatches through a generic plugin hook or callsseqjob/parjobdirectly; if a hook exists, it needs the same core-safe migration as the other four plugin-hook dispatchers. - A live-computed decision may legitimately diverge from a real recording during validation (Scope item 9) — Mitigation: this is treated as expected and valuable, not a blocker; root-cause every divergence found against real recordings before shipping, per Requirement 3 AC5.
plugin->sched(barevoid*, set only byplugin_register()) staying NULL for sim-registered plugins — Mitigation: extend thelimit_seed_config()/limit_seed_config_impl()honest-core-parameter pattern (already proven by bug-1825) to the job-election and prepare-allocation call chains, rather than relying onplugin->sched.
References
.kiro/steering/scheduler-driver-equivalence.md— the governing state-equivalence invariant..wolf/buglog.jsonbug-1611, bug-1613, bug-1681, bug-1825, bug-1826, bug-1827 — the evidence trail of this project's own recurring drift failure mode and its prior fixes..kiro/specs/scheduler-allocation/— completed spec whose task 5.2 Implementation Notes document theunsatisfiable_cachememory-corruption incident and thecache_clear_cbfix pattern being reused here.src/scheduler/CLAUDE.md— "one core, two consumers" architecture statement.