The session-level unsatisfiable-resreq cache stored only the exhausted attempt's main reason (an int). The allocators record shortfalls as per-host/per-resource entries and leave main_reason at 0, so every cache hit in the live scheduler pended its job with an empty reason. The simulator registered no cache at all (its table lived on xce_scheduler_t), so it searched every identical pending job again and its traces differed from production. - The cache table moves to xce_sched_core_t; cache_lookup/insert/clear and the hook adapters take the core, with no cast to a driver struct. sched_init() builds it, cache_clear() rebuilds it per session, xce_sched_core_fini() destroys it. - A cache entry is a deep copy of the attempt's full xce_job_preason_t plus the job that proved the request unsatisfiable (core->alloc_jobid, set by sched_allocator()). A hit moves a copy into ctx->reason; a miss leaves ctx->reason untouched. - main.c passes the same three adapters to the simulator through new sched_sim_args_t fields; sched_sim_driver_start() sets them on the sim core (plain field writes: white-box tests include sim.c alone). - Decision trace: a hit renders "no plan generated: skipped, same request as <job> proved unsatisfiable earlier this session" (new XCE_TRACE_NOPLAN_KNOWN_UNSAT via sched_trace_known_unsat()); the outcome shows the restored reasons. - sim_verify keeps its main-only PEND compare (old recordings carry main-only hits); comments and the scheduler-sim-live-allocation design note updated. Tests: full-reason assertions in the cache dispatch tests (RED before: "a cache hit must restore the original attempt's FULL pend reason"), test_alloc_cache.c ported to the new API (deep copy, clear frees, -EEXIST frees), a sim-registered clear test, a renderer test, and a sim e2e scenario with two identical unsatisfiable jobs (204, 205) where 205 is skipped with 204's full reasons (RED before: no skip line). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
57 KiB
Technical Design: scheduler-sim-live-allocation
Overview
Purpose: This feature gives the scheduler's simulation/replay driver (xce_scheduler -i / -s <replay>) a genuine, live job-election and allocation path, sharing a single implementation with the normal-mode driver, instead of unconditionally pending every job as it does today.
Users: Scheduler operators and developers debugging production scheduling behavior offline via replay, and this codebase's own maintainers, who need normal-mode and simulation-mode to stay behaviorally identical without relying on two independently-verified implementations.
Impact: Today, sim_picker_thread_fn() (sim_loop.c) unconditionally PENDs every job it considers; every real dispatch decision in simulation mode instead comes from a separate mechanism, sim_reconcile_unaccounted_jobs(), that trusts a recorded decision rather than computing one. This feature replaces sim_picker_thread_fn() with the literal sched_job_picker()/sched_allocator() functions normal mode already uses (after migrating their remaining xce_scheduler_t*-coupled dependencies to the shared xce_sched_core_t*), makes limit matching (the concrete acceptance test, bug-1826) work as a side effect of that shared path, and narrows sim_reconcile_unaccounted_jobs() to a safety net for the async timing gap between an event's arrival and the live path's processing of it.
Goals
- Job election and allocation run as one implementation, used verbatim by both drivers.
- Global and host-scoped resource-limit matching work in simulation mode with the same fidelity as normal mode.
- A divergence between a live-computed decision and its recorded counterpart is surfaced, never silently resolved.
- Zero change to normal mode's own observable behavior.
Non-Goals
- Simulation-mode throughput/latency parity with normal mode (the
unsatisfiable_cacheoptimization stays normal-mode-only). - A new clean shutdown/teardown path for simulation-mode worker threads (out of scope; pre-existing asymmetry, not introduced by this feature).
- Any new REPL command,
infosubcommand, or replay-file format change. - Resolving the (currently unconfirmed)
job_alloc_fn/register_seqjob_allocator/register_parjob_allocatorplugin-hook question definitively before implementation — flagged as a verify-at-implementation-time risk inresearch.md, not blocking this design.
Boundary Commitments
This Spec Owns
- The core-safe (
xce_sched_core_t*) signatures of the allocation-chain functions listed in the Architecture section's migration table. plugin_job_election(),plugin_prepare_allocation(), andplugin_commit_decision()'s dispatcher signatures and their single shared implementation across both drivers.- The limit plugin's honest-core-parameter entry points for job election and host-scoped matching (extending the
limit_seed_config()pattern bug-1825 established for the config-hook path). sched_job_picker()/sched_allocator()'s core-safe signatures and their use as the sole picker/allocator implementation for both drivers, replacingsim_picker_thread_fn().- The
unsatisfiable_cachecallback seam onxce_sched_core_tand its NULL-safe fallback behavior. - The authority rule between live-computed decisions and
sim_reconcile_unaccounted_jobs()'s recorded-decision reconstruction, and the divergence-reporting behavior when they disagree.
Out of Boundary
- The
scheduler-allocationspec's own algorithm internals (host_iter.c,parjob_allocator.c, the unsatisfiable-resreq cache's own lookup/populate logic) — this spec only wraps that cache behind a seam, it does not change what the cache does or how it decides. - bug-1827 (the
core_clock.cisolation-test coverage gap) — unrelated, tracked separately. - Any change to
.replayfile format, recording/capture (recording.c/recording_events.c), or snapshot format (snapshot_wire.c/snapshot_load.c) — confirmed field-complete and unaffected by the audit (dimension 4). - Giving simulation mode a working
unsatisfiable_cacheimplementation of its own.
Superseded (2026-10-09, bug-1932): the two cache non-goals above no longer hold. The cache table moved from
xce_scheduler_tontoxce_sched_core_t, the simulator registers the same three hooks as normal mode (passed in throughsched_sim_args_t), and a hit now restores the original attempt's full pend reason instead of only its main reason. The seam itself is unchanged in role: no hook casts the core to a driver struct.
- Simulation-mode thread lifecycle/teardown (
sched_fini()-equivalent for sim mode does not exist today and this spec does not add one).
Allowed Dependencies
core.c/core_events.c/core_session.c/core_clock.c(the mechanically-isolated core module) — read-only from this spec's perspective; no changes needed there beyond the new optional callback field onxce_sched_core_t.- The completed
scheduler-allocationspec'shost_iter.c/parjob_allocator.c/seqjob_allocator.c— this design depends on their algorithm (host iteration order, bucket/plan construction) staying unmodified; it only touches the type threaded through their existing entry points (see File Structure Plan for the specific, minimal changes each file needs). - bug-1825's plugin-bootstrap functions (
sim_bootstrap_picker(),sim_bootstrap_limit_plugin(),limit_seed_config()) — this spec's honest-core-parameter treatment of job-election/prepare-allocation extends the same pattern those functions already established. cbuf/mylock(src/lib) — the existing rendezvous primitive; no changes.
Revalidation Triggers
- A future change to
xce_sched_plugin_t's hook signatures (plugin_hooks_t) must re-verify every hook dispatcher this spec touches (job_election_hook,prepare_allocation_hook,commit_hook) stays core-safe. - A future change to
xce_scheduler_t.unsatisfiable_cache's data shape must re-verify the callback seam's contract still matches. - A future spec giving simulation mode a working live allocator with its own cache, or a clean teardown path, must re-check this spec's Non-Goals are still accurate.
- Any change to
sched_job_picker()/sched_allocator()'s signature or behavior automatically changes simulation mode too (single implementation) — normal-mode-only test coverage is no longer sufficient; both drivers' test suites must be re-run.
Architecture
Existing Architecture Analysis
This is an extension of the already-established "one core, two consumers" split (scheduler-core spec): xce_sched_core_t (passive state + synchronization primitives, mechanically isolated from controller I/O) is driven by two interchangeable top-level structs, xce_scheduler_t (normal mode) and xce_sched_sim_driver_t (simulation mode), each embedding xce_sched_core_t core as a real field. Six functions in the picker/allocator path (plugin_begin_session, plugin_end_session, sched_resolve_job_picker, sched_get_job, sched_account_jobs, sched_commit_job_alloc) already completed the xce_scheduler_t* → xce_sched_core_t* migration and are called identically by both drivers today. This design completes that migration for the remaining allocation-chain functions and the picker/allocator thread bodies themselves.
Project invariants checked (/xce-audit-invariants scheduler, run during this spec's requirements phase — see requirements.md's Project Description and research.md for full evidence):
- Driver state-equivalence (
scheduler-driver-equivalence.md) — this spec's entire purpose is closing the remaining gap in this dimension. Checked against every function in the migration table below by reading its body, not inferring from its signature. - Core module I/O isolation — not touched by this spec (only
xce_sched_core_t's field list grows by one optional callback pointer; no I/O, no new controller/netio dependency). The unrelated coverage gap found in this dimension (bug-1827) does not block this design. - jbuf capacity-1 rendezvous — this spec deliberately changes simulation mode from bypassing
jbufentirely to using the same already-verified two-thread producer/consumer pattern normal mode uses. The invariant (capacity hardcoded to 1, one producer, one consumer) is unaffected because it's the same code, not a new implementation. - Snapshot field-completeness — not touched; no new persisted field, no change to
snapshot_wire.c/snapshot_load.c.
Architecture Pattern & Boundary Map
graph TB
subgraph NormalMode
NormalDriver[NormalModeRound]
end
subgraph SimMode
SimDriver[SimDriverRun]
end
subgraph SharedCore
CoreState[xce_sched_core_t]
PickerThread[SchedJobPicker]
AllocatorThread[SchedAllocator]
ElectionDispatch[PluginJobElection]
AllocationChain[AllocationChain]
CommitDispatch[PluginCommitDecision]
end
subgraph SharedPlugins
LimitPlugin[LimitPlugin GlobalAndHostMatch]
end
subgraph NormalOnly
CacheSeam[UnsatisfiableCacheOps]
end
subgraph SimOnly
Reconcile[SimReconcileUnaccountedJobs]
end
NormalDriver --> CoreState
SimDriver --> CoreState
CoreState --> PickerThread
PickerThread --> AllocatorThread
AllocatorThread --> ElectionDispatch
ElectionDispatch --> LimitPlugin
AllocatorThread --> AllocationChain
AllocationChain --> LimitPlugin
AllocationChain --> CacheSeam
AllocatorThread --> CommitDispatch
SimDriver --> Reconcile
Reconcile -.-> CoreState
Architecture Integration:
- Selected pattern: single shared implementation for election/allocation (Requirement 5), with mode-specific differences isolated behind two narrow seams — the
UnsatisfiableCacheOpscallback (normal-mode-only, NULL in sim mode) andSimReconcileUnaccountedJobs(sim-mode-only, narrowed to the timing-gap safety net). - Domain/feature boundaries:
SharedCoreis driver-agnostic and callable from both;NormalOnly/SimOnlysubgraphs are the only places behavior legitimately differs. - Existing patterns preserved:
xce_sched_core_tembedding, thecache_clear_cb-style optional-callback seam, the plugin-hook-dispatcher pattern (plugin_begin_session/plugin_end_sessionalready core-safe). - New components rationale:
UnsatisfiableCacheOpsis new (a seam that didn't need to exist while the allocation chain was normal-mode-only); everything else is an existing component moved to a core-safe signature, not a new component. - Steering compliance: directly satisfies
scheduler-driver-equivalence.md's state-equivalence invariant andsrc/scheduler/CLAUDE.md's "one core, two consumers" statement.
Technology Stack
| Layer | Choice / Version | Role in Feature | Notes |
|---|---|---|---|
| Backend / Core | C (gnu17) | All changes are within the existing scheduler C codebase | No new language/runtime |
| Concurrency | POSIX pthreads, existing cbuf/mylock (src/lib) |
Simulation mode gains a genuine two-thread producer/consumer pair, reusing the exact primitives normal mode already uses | No new synchronization primitive introduced |
| Build | Meson | New/changed files wired into sched_sim_isolation_lib and normal build targets |
Follows existing meson.build conventions per src/scheduler/CLAUDE.md |
File Structure Plan
Modified Files
-
src/scheduler/include/core.h— addunsatisfiable_lookup_cb/unsatisfiable_populate_cb(two optional function-pointer fields, NULL unless registered) toxce_sched_core_t, alongside the existingcache_clear_cbfield and documented with the samexce_core_cache_clear_cb_t-style rationale comment. -
src/scheduler/src/alloc_cache.c— add the two small adapter functions implementingunsatisfiable_lookup_cb/unsatisfiable_populate_cb, alongside the existingxce_alloc_cache_clear_cb()adapter (same file, same reason: normal-mode-exclusive code that safely recoversxce_scheduler_t*fromcore, kept out ofdriver.cso the ~11 white-box test files that#include driver.cwithout linkingalloc_cache.care unaffected). -
src/scheduler/src/schedobj.c—sched_ctx_init()populatesxce_sched_ctx_t.schedwithcoreinstead ofsched;sched_ctx_set_primary()'s existing(xce_scheduler_t*)ctx->schedcast becomes(xce_sched_core_t*)ctx->sched;sched_ctx_get_uncomit_host()is already core-safe, unchanged. -
src/scheduler/src/host_iter.c,src/scheduler/src/parjob_allocator.c,src/scheduler/src/seqjob_allocator.c(scheduler-allocationspec's own files) — no algorithm change; their entry points (my_alloc(),host_iter_init()) accept whatever type their caller (sched_make_primary_allocation(), now core-safe) andctx->sched(nowxce_sched_core_t*) hand them. Confirm the exact signature ripple at implementation time (seeresearch.mdRisks) — this design commits to the seam (ctx->sched's type), not to enumerating every internal call site in these files. -
src/scheduler/include/plugin.h— no signature change toplugin_hooks_titself;plugin_job_election(),plugin_prepare_allocation(),plugin_commit_decision()declarations change fromvoid*toxce_sched_core_t*;xce_sched_plugin_tgains a newxce_sched_core_t* corefield (alongside the existing, untouchedschedfield — see Limit Plugin Core-Safe Matching for why they stay separate). -
src/scheduler/src/plugin.c— re-typeplugin_job_election(),plugin_prepare_allocation(),plugin_commit_decision()to takexce_sched_core_t* coreand iteratecore->pluginsdirectly (mirrorsplugin_begin_session()'s existing body);plugin_register()additionally setsplugin->core = &(sched->core);;plugin_register_core()additionally setsplugin->core = core;. -
src/scheduler/src/resource.c— re-typesched_meet_host_selection(),sched_check_shared_satisfy()/sched_check_shared_satisfy_(),sched_check_host_satisfy()/sched_check_host_satisfy_(),sched_try_alloc_on_host_()to takexce_sched_core_t* core. -
src/scheduler/src/scheduler.c— re-typesched_commit_decision()and the full allocation-dispatch switch (sched_make_allocation(),sched_make_primary_allocation(),sched_make_compound_allocation(),sched_make_alter_allocation(), and whichever internal function turns out to hold the directsched->unsatisfiable_cachetouch — believed to besched_make_primary_alloc_(), confirm at implementation time) to takexce_sched_core_t* core; update their bodies' internalsched->core.Xreferences tocore->X; replace the one directsched->unsatisfiable_cachetouch with NULL-checked calls tocore->unsatisfiable_lookup_cb/core->unsatisfiable_populate_cb; removesched_job_picker()/sched_allocator()(moved out, see new file below); updatesched_init()to spawn the moved functions and pass&(sched->core). -
src/scheduler/src/sched_election.c(new file) — houses the core-safesched_job_picker()/sched_allocator(), extracted verbatim (signature migrated) fromscheduler.c. Two reasons, not one: (1) it is the single implementation both drivers spawn, so it should not live inside a file (scheduler.c) that is otherwise entirely normal-mode-only and controller-coupled; (2)scheduler.cis already 1382 lines, over this project's own 800-line cap, andsrc/scheduler/CLAUDE.mdexplicitly asks for opportunistic splitting rather than further growth — this migration is exactly such an opportunity, not scope creep invented for its own sake. Declared in a small new header or an existing shared one (scheduler.h), whichever this codebase's own convention for a two-function extraction favors — confirm at implementation time. -
src/scheduler/src/plugins/limit.c— addlimit_job_election_core()/limit_prepare_allocation_core()(or equivalently-named honest-core entry points) alongside the existing hook-registeredlimit_job_election()/limit_prepare_allocation(), following thelimit_seed_config()/limit_seed_config_impl()split bug-1825 established;match_job_global_limit_()/match_job_host_limit_()takexce_sched_core_t* coredirectly instead of derivingschedfromplugin->sched. -
src/scheduler/src/sim_loop.c— replace thepthread_create(&picker_tid, NULL, sim_picker_thread_fn, &(sim->core))call insched_sim_driver_run()with twopthread_create()calls againstsched_job_picker/sched_allocator, passing&(sim->core); removesim_picker_thread_fn()entirely;sim_reconcile_unaccounted_jobs()gains a call to the core-safe job-election entry point before its FOUND-branch decision reconstruction (idempotency already guarded byobj->configver >= core.configver). -
src/scheduler/src/main.c— registerunsatisfiable_lookup_cb/unsatisfiable_populate_cbonly in the normal-mode (controller-connected) startup branch, mirroring the existingcache_clear_cbregistration's placement. -
src/scheduler/meson.build— addsched_election.cto the normal build'sscheduler_srcs(both drivers link it, since it lives in the singlexce_schedulerbinary alongsidescheduler.c/sim_loop.c); confirm whether it also needs adding tosched_sim_isolation_lib's source set (Req 5 AC 17 coverage), mirroring howsim_limit.cwas added there for the analogous reason duringscheduler-sim-diagnosticstask 6. -
White-box test files (
test_sim_*.c,test_repl_*.c,test_scheduler_*.c, per this codebase's established "duplicate, don't share" stub convention) — any file that#includesplugin.c/resource.c/scheduler.cwithout linking its full dependency set will need its link-satisfying stubs updated for the newxce_sched_core_t*signatures, following the exact ripple-fix pattern already used forlimit_get_configured_limits()/limit_get_usage_instances()(scheduler-sim-diagnostics task 2.1's Implementation Note) andlimit_seed_config()(bug-1825's own remediation). Extractingsched_job_picker()/sched_allocator()intosched_election.cspecifically limits this ripple forsim_loop.c's own white-box tests: they now only need to link the small new file to exercise real election/allocation, not the whole ofscheduler.c. -
src/scheduler/src/sim_alloc.c(new file, added during task 4.1's implementation) —sim_bootstrap_seqjob_allocator()/sim_bootstrap_parjob_allocator(), bootstrappingcore->seqjob_allocator/core->parjob_allocatorfor simulation mode, mirroring the establishedsim_picker.c/sim_limit.cbootstrap precedent exactly. Fixes bug-1829: these two fields (read directly bysched_make_seqjob_allocation()/sched_make_parjob_allocation(), not looked up viacore->plugins) were never populated for the simulation driver at all, which was undiscovered until task 4.1 madesched_allocator()genuinely reachsched_make_allocation()in sim mode for the first time — before that, this code path was structurally unreachable in simulation mode, so the gap had zero prior symptoms. Without this fix, task 4.1's own acceptance criteria (a live dispatch decision, not an unconditional pend) could not be met at all. See "Known Seam Gap: Host-Scoped Limit Fan-Out During Live Seqjob/Parjob Dispatch" below for a limitation this fix's own mechanism introduces.
Two new files are introduced:
sched_election.c(task 3.1) andsim_alloc.c(task 4.1, fixing bug-1829). Both extract or bootstrap existing behavior rather than adding new behavior — justified by the isolation-boundary/file-size reasons above (sched_election.c) and by a genuinely blocking pre-existing gap discovered only once live sim-mode allocation became reachable (sim_alloc.c), not by any new component or abstraction layer. Every other change is a signature/call-site migration of an existing function, or a small addition inside an existing file.
Known Seam Gap: Host-Scoped Limit Fan-Out During Live Seqjob/Parjob Dispatch
Confirmed during task 4.1's review, mechanism-traced end to end — not resolved by that task, closed by task 4.3 (see task 4.3's own commit/buglog-1829 entry for the actual fix: plugin->core-based retyping of seqjob_allocator.c/parjob_allocator.c's init_fn and runtime hooks, plus direct plugin_register_core() registration against sim->core, eliminating the wrapper-registry entirely from the fan-out path). register_seqjob_allocator()/register_parjob_allocator()'s own init_fn (a pre-existing, real, out-of-boundary dependency this feature's tasks 1.2/2.3 never touched, since those covered job_election_hook/prepare_allocation_hook/commit_hook, not init_fn) still requires a real xce_scheduler_t*, which simulation mode does not have. sim_bootstrap_seqjob_allocator()/sim_bootstrap_parjob_allocator() satisfy this with a throwaway xce_scheduler_t wrapper, calling plugin_register() (not plugin_register_core()) against it — which means the seqjob/parjob plugin instances' own xce_sched_plugin_t.core field (task 1.2) ends up honestly pointing at the wrapper's core, not sim->core. seqjob_allocator.c/parjob_allocator.c's my_alloc() then derives its own sched/core reference from plugin->sched/plugin->core (its OWN plugin instance, not the core parameter threaded down from sched_make_allocation()), so sched_try_alloc_on_host_() → plugin_prepare_allocation()'s fan-out during a REAL sim-mode seqjob/parjob dispatch attempt iterates the wrapper's plugin registry, not sim->core.plugins — meaning any plugin registered only against sim->core directly (the limit plugin, via sim_bootstrap_limit_plugin()) is never reached for host-scoped matching during a live sim-mode dispatch.
This is a genuine functional gap (not a harmless unused-registry artifact — the wrapper's registry is on the real, executed code path), confined specifically to host-scoped limit matching during a live seqjob/parjob allocation attempt in simulation mode. It does not affect: global-limit matching (reached via plugin_job_election(), called directly against sim->core before allocation, per the "Plugin Hook Dispatchers"/"Limit Plugin Core-Safe Matching" components above); normal mode (unaffected, its own xce_scheduler_t is the real one throughout); or the jbuf rendezvous, decision-commit, or accounting-state correctness of the dispatch itself (the allocation DECISION and its resource charge are correct; only a specific plugin hook's visibility during that decision's host search is narrowed). Closed by task 4.3 (see above) — task 5.2 verifies the resulting end-to-end behavior as part of its own acceptance test, rather than closing the gap itself.
System Flows
sequenceDiagram
participant Driver as DriverPerMode
participant Core as SchedCore
participant Picker as SchedJobPicker
participant Allocator as SchedAllocator
participant Election as PluginJobElection
participant Limit as LimitPlugin
participant Alloc as AllocationChain
participant Commit as PluginCommitDecision
Driver->>Core: PushEvents
Driver->>Core: BeginSession
Driver->>Core: SignalGo
Core->>Picker: wbuf go signal
Picker->>Core: next_job via jbuf
Core->>Allocator: jbuf handoff
Allocator->>Election: job election
Election->>Limit: match global limit
Allocator->>Alloc: attempt allocation
Alloc->>Limit: match host scoped limit
Alloc-->>Allocator: dispatch or pend
Allocator->>Commit: commit decision
Commit-->>Core: decisions cbuf
Core-->>Driver: drain decisions
This flow is identical for both drivers — the only per-mode difference is what Driver does before pushing events (capnp translation for normal mode, replay decode for simulation mode) and after draining decisions (publish/commit for normal mode, print/record/--sim-verify-compare for simulation mode).
flowchart TD
Start[Job accounts is null this round] --> LiveReached{Live election allocation already reached this job}
LiveReached -->|Yes| LiveWins[Live decision is authoritative]
LiveReached -->|No| RecordingFound{Recorded dispatch found for this job}
RecordingFound -->|Yes| Reconstruct[Reconstruct outcome from recording]
RecordingFound -->|No| Wait[Leave pending re-check next round]
LiveWins --> Compare{Verify mode enabled}
Reconstruct --> Compare
Compare -->|Yes and outcomes differ| Report[Report divergence to operator]
Compare -->|Yes and outcomes match| Continue[Continue]
Compare -->|No| Continue
Key decision: Reconstruct never re-fires for a job the LiveWins path already handled — both branches set job->accounts, and the existing job->accounts == NULL trigger guard on sim_reconcile_unaccounted_jobs() naturally excludes an already-decided job from being reconsidered (Requirement 3 AC4).
Requirements Traceability
| Requirement | Summary | Components | Interfaces | Flows |
|---|---|---|---|---|
| 1.1 | Job election/allocation uses same logic in both modes | Core-Safe Allocation Chain, Unified Picker/Allocator Threads | sched_allocator(core), plugin_job_election(core, job) |
Round flow (sequence) |
| 1.2 | Sim mode no longer always pends | Unified Picker/Allocator Threads | sched_allocator(core) |
Round flow |
| 1.3 | Dispatch applies identical accounting effects | Core-Safe Allocation Chain | sched_commit_decision(core, decision), sched_commit_job_alloc(core, job) |
Round flow |
| 2.1 | Global limit match recorded same way | Limit Plugin Core-Safe Matching | limit_job_election_core(core, job) |
Round flow (Election → Limit) |
| 2.2 | Host-scoped limit match recorded same way | Limit Plugin Core-Safe Matching, Core-Safe Allocation Chain | limit_prepare_allocation_core(core, ctx) |
Round flow (Alloc → Limit) |
| 2.3 | info limit shows real usage |
Limit Plugin Core-Safe Matching (no REPL change; reads existing accessors) | limit_get_usage_instances() (unchanged, from scheduler-sim-diagnostics) |
— |
| 2.4 | Unmatched limit still reports no instance | Limit Plugin Core-Safe Matching | (unchanged accessor behavior) | — |
| 3.1 | Identical decisions/state at every replay point | Core-Safe Allocation Chain, Unified Picker/Allocator Threads | full chain | Round flow |
| 3.2 | Same contention outcome | Core-Safe Allocation Chain | sched_check_host_satisfy, sched_check_shared_satisfy |
Round flow |
| 3.3 | Live decision authoritative when reached | Live/Recorded Authority Rule | — | Decision-authority flowchart |
| 3.4 | Recording safety net for timing gap | Sim-Mode Reconciliation Narrowing | sim_reconcile_unaccounted_jobs (unchanged guard) |
Decision-authority flowchart |
| 3.5 | Divergence reported, not resolved silently | Live/Recorded Authority Rule | existing --sim-verify comparison/sim_log_mismatch |
Decision-authority flowchart |
| 4.1 | --sim-verify continues comparing/reporting |
Live/Recorded Authority Rule | unchanged comparison mechanism | Decision-authority flowchart |
| 4.2 | Interactive/batch remain decision-equivalent | Unified Picker/Allocator Threads | unchanged (sim->interactive gate) |
— |
| 4.3 | One-job-at-a-time preserved | Unified Picker/Allocator Threads | jbuf (capacity 1, unchanged) |
Round flow |
| 5.1 | Single shared implementation | Unified Picker/Allocator Threads, Core-Safe Allocation Chain | all core-safe signatures | — |
| 5.2 | Mode-specific differences isolated behind a seam | Unsatisfiable-Cache Callback Seam, Sim-Mode Reconciliation Narrowing | unsatisfiable_lookup_cb, unsatisfiable_populate_cb |
— |
Components and Interfaces
| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies (P0/P1) | Contracts |
|---|---|---|---|---|---|
| Core-Safe Allocation Chain | Scheduler Core | Migrate the 7 audit-identified functions plus the 4-function sched_make_allocation() dispatch switch from xce_scheduler_t* to xce_sched_core_t* |
1.1, 1.3, 3.1, 3.2 | Existing resource.c/scheduler.c internals (P0) |
Service |
| Unsatisfiable-Cache Callback Seam | Scheduler Core | Isolate the one genuinely mode-specific piece of state behind a NULL-safe seam | 5.2 | main.c registration (P0) |
Service |
| Plugin Hook Dispatchers | Plugin Framework | Core-safe plugin_job_election/plugin_prepare_allocation/plugin_commit_decision |
1.1, 2.1, 2.2 | core->plugins registry (P0) |
Service |
| Limit Plugin Core-Safe Matching | Limit Plugin | Honest-core-parameter entry points for global/host-scoped matching | 2.1, 2.2, 2.3, 2.4 | Plugin Hook Dispatchers (P0), bug-1825's limit_seed_config pattern (P1) |
Service |
| Unified Picker/Allocator Threads | Driver Threading | Single sched_job_picker/sched_allocator implementation, spawned by both drivers |
1.1, 1.2, 4.2, 4.3, 5.1 | Core-Safe Allocation Chain (P0), jbuf/decisions cbufs (P0) |
Service |
| Live/Recorded Authority Rule | Sim Driver | Live decision wins when reached; divergence from recording is reported | 3.3, 3.5, 4.1 | Unified Picker/Allocator Threads (P0), existing --sim-verify comparison (P0) |
Service |
| Sim-Mode Reconciliation Narrowing | Sim Driver | sim_reconcile_unaccounted_jobs() continues, scoped to the async timing gap |
3.4 | Unified Picker/Allocator Threads (P1) | Service |
Scheduler Core
Core-Safe Allocation Chain
| Field | Detail |
|---|---|
| Intent | Every function between "a job is picked" and "a decision is committed" operates on xce_sched_core_t*, callable from either driver |
| Requirements | 1.1, 1.3, 3.1, 3.2 |
Responsibilities & Constraints
- Owns the per-host/shared capacity check and commit-decision state transition (
job->allocs/accounts/state). - Must not read or write anything outside
core->*and thexce_sched_worker_t*/xce_sched_ctx_t*passed to it — this is the boundary that makes verbatim reuse safe. - Must not introduce a first-member struct cast to reach normal-mode-only state (the documented anti-pattern,
core.h'sxce_core_cache_clear_cb_tcomment) — the one case that needs mode-specific state (the cache) goes through the seam below instead.
Dependencies
- Inbound: Unified Picker/Allocator Threads — the sole caller (P0)
- Outbound: Unsatisfiable-Cache Callback Seam — consulted at the primary-dispatch chokepoint (P0)
- Outbound: Plugin Hook Dispatchers —
plugin_prepare_allocation/plugin_commit_decision(P0)
Contracts: Service [x]
Service Interface
/* resource.c */
int sched_meet_host_selection(xce_sched_core_t* core, xce_sched_worker_t* w, xce_select_t* s);
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted, xvec_idval_t* wanted, xvec_idval_t* unmeet);
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w, xvec_t* uncomitted, xvec_idval_t* wanted, xvec_idval_t* unmeet);
int sched_try_alloc_on_host_(xce_sched_worker_t* w, xce_sched_core_t* core, xce_sched_ctx_t* ctx);
/* scheduler.c -- the full dispatch switch, not just the leaf commit, must migrate
together: sched_allocator() (core-safe, see Unified Picker/Allocator Threads)
calls sched_make_allocation() directly, so every function between that call
and the resource-check leaves above must share the same xce_sched_core_t*
the caller now has -- there is no xce_scheduler_t* left in that call chain
to pass down otherwise. */
int sched_make_allocation(xce_sched_core_t* core, xce_sched_obj_t* obj);
int sched_make_primary_allocation(xce_sched_core_t* core, xce_sched_obj_t* obj);
int sched_make_compound_allocation(xce_sched_core_t* core, xce_sched_obj_t* obj);
int sched_make_alter_allocation(xce_sched_core_t* core, xce_sched_obj_t* obj);
int sched_commit_decision(xce_sched_core_t* core, xce_decision_t* decision);
- Preconditions:
corenon-NULL;w/ctx/decision/objnon-NULL where used (matches existing NULL-guard behavior of the pre-migration functions). - Postconditions: identical to today's
xce_scheduler_t*-typed behavior — this is a pure signature migration, not a behavior change. - Invariants: no function in this chain reads any field outside
core->*/w->*/ctx->*, except the one primary-resreq dispatch chokepoint insidesched_make_primary_allocation()'s own call chain (believed to besched_make_primary_alloc_(), perscheduler-allocation's task 5.1 notes — not yet directly read in full; verify at implementation time) that currently readssched->unsatisfiable_cachedirectly. That one call site routes through the Unsatisfiable-Cache Callback Seam instead of a direct field read once migrated — it is the seam's entire reason to exist.
Implementation Notes
- Integration: this migration is wider than just the 7 functions originally identified during the audit (the table in
requirements.md) — it necessarily also includes the wholesched_make_allocation()dispatch switch and its three primary/compound/alter sub-functions, sincesched_allocator()calls into this switch directly and there is noxce_scheduler_t*available to hand it aftersched_allocator()itself becomes core-safe. Confirm during implementation thatsched_make_primary_allocation()/sched_make_compound_allocation()/sched_make_alter_allocation()(not yet read in full at design time) touch nothing beyondcore->*, worker/ctx fields, and the one cache touch point — if any of them reads otherxce_scheduler_t-only state, that state needs its own seam, following the same NULL-safe-callback pattern as the cache. - Validation: contention-fixture tests (per
scheduler-driver-equivalence.md's testing discipline) exercising this chain from both drivers. - Risks: (1) the open question about
job_alloc_fn/register_seqjob_allocator/register_parjob_allocator(seeresearch.md) — verify before assuming this chain is fully closed; (2)sched_make_primary_allocation/sched_make_compound_allocation/sched_make_alter_allocation's full bodies were not read in full during design — treat their migration as believed-mechanical based on the pattern established by every sibling function in this chain, but confirm by reading each body before migrating it, not by assumption.
Unsatisfiable-Cache Callback Seam
| Field | Detail |
|---|---|
| Intent | Isolate the one genuinely xce_scheduler_t-only piece of state behind a NULL-safe seam, so the surrounding allocation code never needs a mode-specific branch |
| Requirements | 5.2 |
Responsibilities & Constraints
- Owns nothing itself — a thin, optional indirection over
xce_scheduler_t.unsatisfiable_cache's existing lookup/populate behavior. - A NULL callback must mean "always compute fresh" — never an error, never a degraded-correctness path, only a degraded-performance one (Non-Goals).
- Must never be bridged via a struct-prefix cast inside code shared by both drivers — but the small adapter function that implements each callback is itself normal-mode-exclusive (only ever registered by
main.c's normal-mode branch, only ever invoked with acorepointer that is genuinely embedded in anxce_scheduler_t, since sim mode never registers it), so it may safely recoverxce_scheduler_t*via(xce_scheduler_t*)core— the identical, already-reviewed-safe patternalloc_cache.c's existingxce_alloc_cache_clear_cb()adapter uses forcache_clear_cb. No separate opaqueimpl/user-data pointer is needed; simplification over an earlier draft of this design that added one unnecessarily (single registrant, no foreseeable second implementation).
Dependencies
- Inbound: Core-Safe Allocation Chain, at
sched_make_primary_alloc_()'s existing chokepoint (P0) - Outbound:
xce_scheduler_t.unsatisfiable_cache's existing lookup/populate implementation, reached via a small adapter inalloc_cache.c(P0), the same file the existingxce_alloc_cache_clear_cb()adapter already lives in and for the same reason (white-box tests that#include driver.cwithout linkingalloc_cache.c)
Contracts: Service [x]
Service Interface
/* core.h -- mirrors cache_clear_cb's existing shape exactly: a bare function
pointer taking xce_sched_core_t*, NULL unless registered, no wrapper struct */
typedef int (*xce_unsatisfiable_lookup_cb_t)(xce_sched_core_t* core, xce_sched_obj_t* obj, int* out_reason_category);
typedef void (*xce_unsatisfiable_populate_cb_t)(xce_sched_core_t* core, xce_sched_obj_t* obj, int reason_category);
/* two new fields on xce_sched_core_t, alongside the existing cache_clear_cb field */
xce_unsatisfiable_lookup_cb_t unsatisfiable_lookup_cb; /* NULL unless registered */
xce_unsatisfiable_populate_cb_t unsatisfiable_populate_cb; /* NULL unless registered */
Corrected during task 1.1's implementation: the parameter list is xce_sched_obj_t* obj + int*/int reason-category, matching the REAL cache_lookup()/cache_insert() API (alloc_cache.c) exactly, not the uint64_t fingerprint/int category shape drafted above at design time — the real cache already keys on the full xce_sched_obj_t* (its embedded xce_resreq_fingerprint_t plus user/queue/project identity), so this seam needed no invented fingerprint abstraction on top of it.
- Preconditions: registered (non-NULL) only by normal mode's
main.c, exactly once, beforesched_init()spawns any thread — identical registration discipline tocache_clear_cb. - Postconditions: NULL callbacks mean every call to
sched_make_primary_alloc_()computes fully, bypassing the cache fast path — behaviorally correct, matching what happens today whenever the cache has no entry for a fingerprint. - Invariants: the allocation chain (shared code) only ever NULL-checks and calls these — it never interprets
core's contents itself; only the mode-exclusive adapter functions inalloc_cache.cdo, and only when they are guaranteed (by registration discipline, not by a runtime check) to have a normal-modecore.
Implementation Notes
- Integration: registered in
main.c's normal-mode branch only, alongside the existingcache_clear_cbregistration — same placement pattern, same rationale, same file for the adapter implementation (alloc_cache.c). - Validation: a unit test confirming NULL callbacks never crash and always take the compute-fresh path; a normal-mode test confirming the cache is still consulted/populated exactly as before this change.
- Risks: none identified beyond the general struct-cast anti-pattern this design explicitly avoids in shared code.
Plugin Framework
Plugin Hook Dispatchers
| Field | Detail |
|---|---|
| Intent | plugin_job_election, plugin_prepare_allocation, plugin_commit_decision iterate core->plugins directly, callable from either driver |
| Requirements | 1.1, 2.1, 2.2 |
Responsibilities & Constraints
- Mirrors
plugin_begin_session()/plugin_end_session()'s already-completed migration exactly — same iteration pattern, same NULL-guard shape. - Does not change
plugin_hooks_t's function-pointer signatures (job_election_hook,prepare_allocation_hook,commit_hookkeep their existing(void* arg, ...)shape, sinceargthere is the plugin instance, not the driver struct — only the dispatcher functions that iterate plugins change).
Dependencies
- Inbound: Unified Picker/Allocator Threads, Core-Safe Allocation Chain (P0)
- Outbound: Limit Plugin Core-Safe Matching, via the hook function pointers (P0)
Contracts: Service [x]
Service Interface
int plugin_job_election(xce_sched_core_t* core, xce_sched_job_t* job);
int plugin_prepare_allocation(xce_sched_core_t* core, xce_sched_ctx_t* ctx);
int plugin_commit_decision(xce_sched_core_t* core, xce_decision_t* decision);
- Preconditions:
corenon-NULL. - Postconditions: every registered plugin's corresponding hook is invoked exactly once per call, in registration order — unchanged from today's behavior.
- Invariants: iterates
core->plugins(a field bothxce_scheduler_t.coreandxce_sched_sim_driver_t.coregenuinely have), never a driver-specific field.
Implementation Notes
- Integration: single normal-mode call site each (
sched_allocator(),sched_try_alloc_on_host_(),sched_commit_decision()) gets updated to pass&(sched->core)/core(already core-typed after that function's own migration); simulation mode'ssched_allocator()(now the same function) passes&(sim->core). - Validation: existing plugin-hook unit tests re-run unchanged (behavior-preserving refactor); white-box test files that
#include plugin.cneed their link-satisfying stubs' signatures updated. - Risks: none beyond the standard ripple-to-white-box-stubs risk already seen in bug-1825/scheduler-sim-diagnostics. Separately, bug-1771 (an unfixed ASAN SEGV in
fcfs_del_job(), reached viaplugin_commit_decision()'scommit_hookdispatch tofcfs_commit) is a pre-existing, unrelated defect in exactly this dispatch path, discovered duringscheduler-allocation's own end-to-end testing. This migration is a pure signature change with no behavior change, so it neither fixes nor worsens bug-1771 — noted here only so an implementer who hits this crash during testing attributes it correctly instead of assuming their own migration caused it.
Limit Plugin
Limit Plugin Core-Safe Matching
| Field | Detail |
|---|---|
| Intent | match_job_global_limit_()/match_job_host_limit_() derive xce_sched_core_t* honestly instead of via plugin->sched (a bare void*, NULL for sim-registered plugins) |
| Requirements | 2.1, 2.2, 2.3, 2.4 |
Responsibilities & Constraints
- Extends the exact split bug-1825 established for the config-hook path (
limit_update_config()/sim_bootstrap_limit_plugin()both delegating to a sharedlimit_seed_config_impl()) to the job-election and prepare-allocation call chains. - Concrete mechanism (corrected during task planning — the hook signature itself cannot carry
core):job_election_hook/prepare_allocation_hookkeep their existing(void* arg, ...)shape whereargis the plugin instance, notcore— soplugin_job_election(core, job)'s owncoreparameter does not automatically reach the leaf hook function it calls.xce_sched_plugin_t(plugin.h) gains a new, honestly-typed field,xce_sched_core_t* core, set by BOTHplugin_register()(plugin->core = &(sched->core);) andplugin_register_core()(plugin->core = core;— closing the gap bug-1825 left, since that function never sets anything today).limit_job_election()/limit_prepare_allocation()/match_job_global_limit_()/match_job_host_limit_()readplugin->core(never NULL after either registration path), notplugin->sched. - The existing
plugin->schedfield is deliberately left untouched — stillxce_scheduler_t*-flavored, still set only byplugin_register(), still read byplugins/fcfs.c's legacyjob_elect_fnpath exactly as today. Repurposingplugin->scheditself (an earlier draft of this design considered this) was rejected: it is a shared field across every plugin category, andfcfs.c's legacy picker also reads it expectingxce_scheduler_t*— changing its meaning would silently break that unrelated, normal-mode-only consumer. Adding a new field costs one pointer per plugin and avoids that regression entirely, consistent with this codebase's own preference (thelimit_seed_config()precedent) for an honest new entry point over reinterpreting an existing ambiguous one. - The plugin-hook-registered entry points (
limit_job_election(),limit_prepare_allocation(), unchanged(void* arg, ...)signature) become thin wrappers that readplugin->coreand delegate to shared_implfunctions takingxce_sched_core_t* corehonestly.
Dependencies
- Inbound: Plugin Hook Dispatchers, via
job_election_hook/prepare_allocation_hook(P0) - Outbound:
instantiate_limit()/update_limit_res_value()(xcelimit.c, unchanged) — registers the matched limit as a virtual resource intocore->sharedInst/w->resvals, the same arrayssched_account_jobs()already resums every round in both drivers (P0)
Contracts: Service [x]
Service Interface
/* plugins/limit.c */
static int limit_job_election_impl(xce_sched_core_t* core, xce_sched_plugin_t* plugin, xce_sched_job_t* job);
static int limit_prepare_allocation_impl(xce_sched_core_t* core, xce_sched_plugin_t* plugin, xce_sched_ctx_t* ctx);
/* existing hook-registered wrappers, now delegating to the _impl functions above
using the core-safe xce_sched_core_t* the (now core-safe) dispatcher supplies */
static int limit_job_election(void* arg, xce_sched_job_t* job);
static int limit_prepare_allocation(void* arg, xce_sched_ctx_t* ctx);
- Preconditions:
plugin->corenon-NULL (guaranteed by either registration path once this fix lands),plugin->data(the limit plugin's privatelimits/instanceskvmgr handles) already initialized bylimit_init()— unchanged. - Postconditions: a global limit match registers a vres into
core->sharedInst; a host-scoped match registers intow->resvals— identical outcome in both drivers. - Invariants:
match_job_global_limit_()'s existingobj->configver >= core.configveridempotency guard is preserved unchanged — safe to call from both the live path and, for global limits only, fromsim_reconcile_unaccounted_jobs().
Implementation Notes
- Integration: no change to
info limit's REPL display logic (repl_info.c) or its read-only accessors (limit_get_configured_limits()/limit_get_usage_instances(), fromscheduler-sim-diagnostics) — this component only makes the data those accessors read become real. - Validation:
test_repl_limit_table.c's existing fixtures, extended with a real sim-mode-driven (not test-only-registered) matching scenario for both global and host-scoped limits under contention. - Risks: none beyond the standard signature-migration ripple to white-box stubs.
Driver Threading
Unified Picker/Allocator Threads
| Field | Detail |
|---|---|
| Intent | sched_job_picker()/sched_allocator() become the sole picker/allocator implementation, spawned by both drivers |
| Requirements | 1.1, 1.2, 4.2, 4.3, 5.1 |
Responsibilities & Constraints
sim_picker_thread_fn()is removed entirely, not deprecated alongside the new path.- Thread-spawn call sites:
sched_init()(normal mode, unchanged call site, unchanged behavior) andsched_sim_driver_run()(simulation mode, changed from onesim_picker_thread_fnspawn to two spawns of the shared functions). - Preserves the capacity-1
jbufrendezvous — this component reuses the existing cbuf, it does not reimplement it. - No new teardown/lifecycle work for simulation mode (Non-Goals) — the pre-existing "never joined, lives for process lifetime" behavior is preserved for both threads in sim mode, matching today's
sim_picker_thread_fn()'s own documented lifecycle.
Dependencies
- Inbound:
sched_init()(normal mode),sched_sim_driver_run()(simulation mode) (P0) - Outbound: Core-Safe Allocation Chain, Plugin Hook Dispatchers (P0)
Contracts: Service [x]
Service Interface
/* sched_election.c (new file, extracted from scheduler.c -- see File Structure Plan) */
static void* sched_job_picker(void* arg); /* arg is xce_sched_core_t* */
static void* sched_allocator(void* arg); /* arg is xce_sched_core_t* */
- Preconditions:
sched_resolve_job_picker(core, ...)must succeed before either thread is spawned (existing gate, unchanged) — for simulation mode, this means the picker plugin must already be bootstrapped (sim_bootstrap_picker(), bug-1825) beforesched_sim_driver_run()spawns these threads. - Postconditions: identical per-round behavior to today's normal mode — pull a job, elect it, attempt allocation, commit a decision, hand off via
jbuf/decisions. - Invariants: exactly one producer (
sched_job_picker) and one consumer (sched_allocator) per session, per the jbuf rendezvous invariant.
Implementation Notes
- Integration:
sched_sim_driver_run()'s existingpthread_create(&picker_tid, NULL, sim_picker_thread_fn, &(sim->core))becomes twopthread_create()calls (sched_job_picker,sched_allocator), both passed&(sim->core). - Validation:
test_repl_equivalence.c,test_sim_contention_parity.c,test_sim_equivalence.cmust pass unchanged or with investigated, understood deltas (seeresearch.mdRisks) — this is the highest-risk integration point in the whole design. - Risks: (1) decision-timing/ordering shift from the fused-thread-to-two-thread change in simulation mode; mitigated by the validation above, not by avoiding the change. (2) Recurring bug class, not a one-off:
xce_sched_sim_driver_t simheld stack-local while a never-joined thread (sim_picker_thread_fn()today) keeps a pointer into it past the owning function's return has caused ASAN stack-use-after-return at least three times in this codebase's history (bug-1598, and its own explicitly-documented recurrences bug-1632 and bug-1763, the latter still marked NOT FIXED, out of scope for the spec that found it). This design doubles the number of long-lived threads holding&(sim->core)(two instead of one), doubling the surface area for any remaining unaudited instance of this same pattern. Do not assume bug-1598's single fixture fix closed this class — any new or touched test fixture for this component must be positively confirmed to use astaticor heap-allocatedxce_sched_sim_driver_t, not merely assumed safe by analogy.
Sim Driver
Live/Recorded Authority Rule
| Field | Detail |
|---|---|
| Intent | The live picker/allocator's decision is authoritative wherever it reaches a job; a disagreement with the recording is reported, not resolved silently |
| Requirements | 3.3, 3.5, 4.1 |
Responsibilities & Constraints
- This is a behavioral rule, not a new function — it falls out of (a) the live path now producing real decisions and (b)
sim_reconcile_unaccounted_jobs()'s existingjob->accounts == NULLguard naturally skipping any job the live path already decided. - The existing
--sim-verifypositional comparison (sim_log_mismatch()) is the reporting mechanism — unchanged, now actually exercised meaningfully for the first time against live-computed decisions instead of only against reconstructed-from-recording ones.
Dependencies
- Inbound: none (a property of the interaction between the two components below) (P0)
- Outbound: Unified Picker/Allocator Threads, Sim-Mode Reconciliation Narrowing (P0)
Contracts: State [x]
State Management
- State model: a job's
accounts == NULLmarks it as not-yet-decided; either the live allocator (viasched_commit_decision) orsim_reconcile_unaccounted_jobs()'s FOUND branch sets it, whichever reaches the job first this round. - Persistence & consistency: no new persisted state; this is in-memory per-session state, already covered by the field-completeness audit (dimension 4, PASS, unaffected).
- Concurrency strategy: the existing per-round ordering (
sim_reconcile_unaccounted_jobs()runs beforexce_core_signal_go()unblocks the picker thread each round) means reconciliation always gets first look at whether a job is already decided from an EARLIER round's live processing before deciding whether to reconstruct it from the recording this round — no race, since the live path's own decision-commit for a job from a prior round has already setjob->accountsbefore this round's reconciliation check runs.
Implementation Notes
- Integration: no new code path — the reporting behavior already exists (
--sim-verify); this component's job in the design is to make sure nothing is added that would suppress or override a genuine mismatch. - Validation: Scope item 9 (
requirements.md) — run--sim-verifyagainst real recorded sessions and treat any reported mismatch as a finding to root-cause. - Risks: a mismatch found this way could indicate either a defect in this feature's own migration, or a genuine pre-existing issue the old always-PEND behavior could never have exposed — both are valid, actionable outcomes, not a reason to suppress the report.
Sim-Mode Reconciliation Narrowing
| Field | Detail |
|---|---|
| Intent | sim_reconcile_unaccounted_jobs() keeps existing, narrowed to the async timing-gap safety net |
| Requirements | 3.4 |
Responsibilities & Constraints
- No structural change to this function's existing logic — the narrowing is a consequence of the live path now handling most jobs, not a new code path inside this function.
- One addition: call the core-safe global-limit-election entry point (
limit_job_election_impl, via the now-core-safeplugin_job_election) for a job this function reconstructs, so a job whose outcome comes from the recording still gets its global-limit vres registered (idempotency guard already prevents double-registration if the live path also happened to touch it).
Dependencies
- Inbound:
sched_sim_driver_run()'s per-round sequence, unchanged position (beforesched_account_jobs()) (P0) - Outbound: Plugin Hook Dispatchers (new, for the limit-election call) (P1)
Contracts: Batch [x]
Batch / Job Contract
- Trigger: once per round, unconditionally, for every job in
core->jobswithaccounts == NULLand not terminal. - Input / validation: the round's decoded
ReplaySession.decisionsarray (unchanged). - Output / destination:
job->allocs/accounts/stateset directly from the recording; decision reported via the existingsched_record_session_add_decision()/sim_print_decision_gate()/xce_repl_check_decision()triple (unchanged). - Idempotency & recovery:
job->accounts != NULLpermanently excludes the job from reconsideration (unchanged, existing guard).
Implementation Notes
- Integration: add one call (
plugin_job_election(&(sim->core), job)— global-limit matching only, since host-scoped matching needs a resolved worker context this function doesn't independently construct) in the FOUND branch, before or alongside the existing account-reconstruction logic. - Validation:
test_sim_reconcile_unaccounted.c's existing fixtures, extended to confirm a reconciled job's global limit is also registered. - Risks: none beyond the standard ripple risk; this function's own scope and trigger condition are unchanged.
Error Handling
Error Strategy
No new error categories. Every migrated function preserves its existing error/return-code contract (-EINVAL, -ENOENT, etc.) — this is a signature migration, not a behavior change, so existing error-handling call sites (ERROR(...), WARN(...), fail-return-NULL patterns) are preserved verbatim.
Error Categories and Responses
- Picker resolution failure at thread start (both drivers): existing fail-return-NULL-and-log behavior, unchanged (
sched_resolve_job_pickerfailing insched_job_picker()/sched_allocator()). unsatisfiable_lookup_cb/unsatisfiable_populate_cbNULL: not an error — the documented, correct fallback (always compute fresh).- Live/recorded decision divergence: not an error in the traditional sense — reported via the existing
--sim-verifymismatch mechanism (a diagnostic finding, not a crash or an aborted session).
Monitoring
No new logging infrastructure. Existing TRACE/WARN/ERROR call sites in the migrated functions are preserved; sim_log_mismatch() (unchanged) remains the mechanism for surfacing a live/recorded divergence.
Testing Strategy
- Unit Tests:
- Each migrated function (
sched_meet_host_selection,sched_check_host_satisfy,sched_try_alloc_on_host_,sched_commit_decision,plugin_job_election,plugin_prepare_allocation,plugin_commit_decision) called directly with anxce_sched_core_t*from EITHER anxce_scheduler_t.coreorxce_sched_sim_driver_t.corefixture, confirming identical results. unsatisfiable_lookup_cb/unsatisfiable_populate_cbNULL fallback: confirmsched_make_primary_alloc_()always computes fresh with no crash and no incorrect skip.limit_job_election_impl/limit_prepare_allocation_implcalled against a sim-registered (plugin_register_core()) plugin instance, confirming a match registers correctly even thoughplugin->schedis NULL.
- Each migrated function (
- Integration Tests:
- A real
xce_sched_sim_driver_tdriven throughsched_sim_driver_run()against a replay whose original session had global AND host-scoped limits configured and actively enforced —info limitshows real usage for both (Requirement 2.1-2.4, the concrete bug-1826 acceptance test). - A resource-contention fixture (multiple jobs, insufficient capacity, including limit-constrained capacity) driven through both drivers, confirming identical admission/pend outcomes (Requirement 3.2, per
scheduler-driver-equivalence.md's testing discipline). sim_reconcile_unaccounted_jobs()and the live picker/allocator both active in the same session, confirming no double-processing of a single job (Requirement 3.4).
- A real
- E2E / Real-Recording Tests (Scope item 9):
--sim-verifyrun against a range of real recorded sessions (not only crafted fixtures), treating any reported mismatch as a finding to root-cause.test_repl_equivalence.c,test_sim_contention_parity.c,test_sim_equivalence.cre-run in full, with any assertion change investigated and explained, not silently updated to match new output.
- Performance: none targeted (Non-Goals) — no throughput/latency test required for this feature.