diff --git a/.kiro/specs/scheduler-sim-decision-parity/tasks.md b/.kiro/specs/scheduler-sim-decision-parity/tasks.md index 995eaebf..18848c3d 100644 --- a/.kiro/specs/scheduler-sim-decision-parity/tasks.md +++ b/.kiro/specs/scheduler-sim-decision-parity/tasks.md @@ -17,7 +17,7 @@ - _Boundary: Scheduler test suite (contention fixture)_ - [ ] 2. Core: round-boundary accounting and decision reconciliation -- [ ] 2.1 Give simulation mode the same round-boundary accounting sequence normal mode uses +- [x] 2.1 Give simulation mode the same round-boundary accounting sequence normal mode uses - Replace simulation mode's single combined session-start call with the same two-step sequence (reset accounting, then unblock the picker) the normal-mode driver already uses, opening an interleaving point between them - Call the relocated resource re-accounting service (task 1.1) in that interleaving point, in the same relative position the normal-mode driver already calls it - Preserve the existing error-handling behavior for a failed session start exactly as it works today @@ -89,3 +89,5 @@ ## Implementation Notes - (task 1.2) Whole-scenario white-box test files in `src/scheduler/testsuite/` (fixture construction + real driver/allocator plumbing) routinely exceed the project's 800-line guidance (`test_sim_verify.c` is 934 lines, uncorrected; the new `test_sim_resource_contention.c` is 883) — the steering's "don't use as precedent" caveat names only non-test files, so this is an accepted pattern for this class of test, not a defect to fix per task. Tasks 4.1-4.3 will likely add similarly-sized files; don't flag file size alone as blocking for them. +- (task 2.1) Confirmed (by reading `sim_picker_thread_fn()`): simulation mode's live picker thread unconditionally PENDs every job today, and `sched_commit_decision()` (the only real setter of `job->accounts`) is normal-mode-only. So after 2.1 alone, `resInUse` is still zero end-to-end in sim mode -- 2.1 only wires the call site/interleaving slot. Tasks 2.2-2.4 (Recorded-Decision Reconciliation) are what actually populate `job->accounts` and make `resInUse` correct. Don't expect nonzero `resInUse` from a sim run until 2.2-2.4 land -- this is expected, not a regression. +- (task 2.1) White-box test files that `#include "../src/sim_loop.c"` directly (test_sim_event_loop.c, test_sim_verify.c, test_sim_alt_events.c, test_sim_record_output.c, test_repl_breakpoints_inspection.c, test_repl_snapshot_params.c, test_repl_execution_control.c, test_repl_equivalence.c) need a link-satisfying stub added whenever sim_loop.c gains a new extern reference (here: `sched_account_jobs()`). Tasks 2.2-2.4 will likely add more such references (e.g. into the recorded-decision/reporting machinery) -- expect to touch these same 8 files again for stub updates, and check for linker errors rather than guessing which files need it. diff --git a/src/scheduler/src/sim_loop.c b/src/scheduler/src/sim_loop.c index f09cb1da..60c7f54a 100644 --- a/src/scheduler/src/sim_loop.c +++ b/src/scheduler/src/sim_loop.c @@ -435,10 +435,11 @@ static void sim_print_decision(const xce_decision_t* d) * decision with no real worker/resource assignment behind it. * * Runs for the simulation driver's entire lifetime (one wbuf-wait/session - * cycle per xce_core_go(), forever) -- never joined or cancelled: no - * sim-driver teardown/fini function exists yet (task 11.1 did not build - * one either), the same gap sched_job_picker()/sched_allocator() do not - * have in normal mode only because sched_fini() already exists there. + * cycle per xce_core_signal_go() call, forever) -- never joined or + * cancelled: no sim-driver teardown/fini function exists yet (task 11.1 + * did not build one either), the same gap sched_job_picker()/ + * sched_allocator() do not have in normal mode only because sched_fini() + * already exists there. */ static void* sim_picker_thread_fn(void* arg) { @@ -677,10 +678,18 @@ int sched_sim_driver_run(xce_sched_sim_driver_t* sim, = sched_record_session_ctx_new(xce_core_now(&(sim->core))); } - if (xce_core_go(&(sim->core)) < 0) { - ERROR("%s xce_core_go failed -- skipping this round's " - "decision drain", - __func__); + /* Task 2.1 (scheduler-sim-decision-parity): xce_core_go() + * split into xce_core_begin_session()/xce_core_signal_go(), + * mirroring driver.c's xce_sched_driver_run_normal_mode_round() + * -- opens the interleaving window sched_account_jobs() below + * needs (core.h's doc comment on these two functions). Error + * contract unchanged: a failed begin_session still skips this + * round's decision drain, exactly as xce_core_go() did. */ + rc = xce_core_begin_session(&(sim->core)); + if (rc < 0) { + ERROR("%s xce_core_begin_session failed rc=%d -- skipping " + "this round's decision drain", + __func__, rc); free_ReplaySession_ptr(&decoded_session); /* record_ctx (if any) has nothing accumulated yet -- a * direct free() is safe: no ReplaySession is written for a @@ -689,6 +698,14 @@ int sched_sim_driver_run(xce_sched_sim_driver_t* sim, continue; } + /* Recorded-Decision Reconciliation (tasks 2.2-2.4, not yet + * implemented) belongs here, before sched_account_jobs() + * below -- see design.md's "Recorded-Decision Reconciliation" + * component. */ + sched_account_jobs(&(sim->core)); + + xce_core_signal_go(&(sim->core)); + /* Req 5 AC 15 (task 11.4): when --sim-verify is set, walk the * decoded `decisions` array positionally (di) alongside the * live drained decisions, exactly matching AC 18's pseudocode diff --git a/src/scheduler/testsuite/meson.build b/src/scheduler/testsuite/meson.build index 829b5db8..b34b86a7 100644 --- a/src/scheduler/testsuite/meson.build +++ b/src/scheduler/testsuite/meson.build @@ -726,6 +726,32 @@ sched_sim_equivalence_test = executable( dependencies: [scheduler_test_deps], install: false) +# Round-boundary ordering contract for the simulation driver +# (scheduler-sim-decision-parity spec, task 2.1, Req 1.3/1.4, 5.1, 5.3): the +# sim-mode counterpart to sched_driver_session_ordering_test above -- proves +# sim_loop.c's sched_sim_driver_run() now calls the relocated +# sched_account_jobs() (core.h, task 1.1) once per replayed round, strictly +# inside that round's session-active window, mirroring driver.c's +# xce_sched_driver_run_normal_mode_round() exactly. Before this task, +# sim_loop.c called the older monolithic xce_core_go() with no interleaving +# slot and never called sched_account_jobs() at all (bug-1613) -- every +# worker's/shared resource's resInUse stayed permanently zero for the whole +# `--sim`/`--interactive` run. Same combined white-box #include set as +# sched_sim_equivalence_test (records two real normal-mode rounds via the +# REAL normal-mode driver, then replays the same file through the REAL, +# unmodified simulation driver) -- see the test's own header comment for why +# a spy sched_account_jobs() (not the real one) is the right-strength proof +# here, and why no cross-thread wbuf-rendezvous replica of +# test_driver_session_ordering.c's own design is attempted (sim_loop.c's +# picker thread is not independently substitutable from a test without +# touching sim_picker.c, outside this task's boundary). +sched_sim_session_ordering_test = executable( + 'sched_sim_session_ordering_test', + ['test_sim_session_ordering.c'], + include_directories:[incdir], + dependencies: [scheduler_test_deps], + install: false) + # Resource-contention replay fixture (scheduler-sim-decision-parity spec, # task 1.2, Req 2.1/2.2): existing fixtures in this directory never have # scarce capacity, so none can exercise or prove capacity-check correctness. @@ -1073,6 +1099,9 @@ test('scheduler sim record output test', test('scheduler sim equivalence test', sched_sim_equivalence_test, suite: 'scheduler', env: ['ASAN_OPTIONS=detect_leaks=0']) +test('scheduler sim session ordering test', + sched_sim_session_ordering_test, suite: 'scheduler', + env: ['ASAN_OPTIONS=detect_leaks=0']) test('scheduler sim resource contention test', sched_sim_resource_contention_test, suite: 'scheduler', env: ['ASAN_OPTIONS=detect_leaks=0']) diff --git a/src/scheduler/testsuite/test_repl_breakpoints_inspection.c b/src/scheduler/testsuite/test_repl_breakpoints_inspection.c index f9c182aa..bc9875b0 100644 --- a/src/scheduler/testsuite/test_repl_breakpoints_inspection.c +++ b/src/scheduler/testsuite/test_repl_breakpoints_inspection.c @@ -110,6 +110,17 @@ int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec, return 0; } +/* sched_sim_driver_run()'s (sim_loop.c, scheduler-sim-decision-parity task + * 2.1) new round-boundary call to the relocated sched_account_jobs() + * (core.h) -- a no-op stand-in here since this file does not link + * scheduler.c/resource.c, same rationale test_sim_equivalence.c's own copy + * documents. */ +int sched_account_jobs(xce_sched_core_t* core) +{ + (void)core; + return 0; +} + /* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same * copy test_repl_execution_control.c/test_sim_alt_events.c/etc. each carry * (schedobj.c is not linked here). */ diff --git a/src/scheduler/testsuite/test_repl_equivalence.c b/src/scheduler/testsuite/test_repl_equivalence.c index 2765b73a..ed607af7 100644 --- a/src/scheduler/testsuite/test_repl_equivalence.c +++ b/src/scheduler/testsuite/test_repl_equivalence.c @@ -159,6 +159,17 @@ int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec, return 0; } +/* sched_sim_driver_run()'s (sim_loop.c, scheduler-sim-decision-parity task + * 2.1) new round-boundary call to the relocated sched_account_jobs() + * (core.h) -- a no-op stand-in here since this file does not link + * scheduler.c/resource.c, same rationale test_sim_equivalence.c's own copy + * documents. */ +int sched_account_jobs(xce_sched_core_t* core) +{ + (void)core; + return 0; +} + /* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same * copy every white-box test in this directory that does not link * schedobj.c already establishes (test_repl_execution_control.c, diff --git a/src/scheduler/testsuite/test_repl_execution_control.c b/src/scheduler/testsuite/test_repl_execution_control.c index 6389a6ee..25ddd108 100644 --- a/src/scheduler/testsuite/test_repl_execution_control.c +++ b/src/scheduler/testsuite/test_repl_execution_control.c @@ -124,6 +124,17 @@ int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec, return 0; } +/* sched_sim_driver_run()'s (sim_loop.c, scheduler-sim-decision-parity task + * 2.1) new round-boundary call to the relocated sched_account_jobs() + * (core.h) -- a no-op stand-in here since this file does not link + * scheduler.c/resource.c, same rationale test_sim_equivalence.c's own copy + * documents. */ +int sched_account_jobs(xce_sched_core_t* core) +{ + (void)core; + return 0; +} + /* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same * copy test_sim_alt_events.c/test_sim_verify.c/test_sim_event_loop.c each * carry (schedobj.c is not linked here: it would need plugin_register(), diff --git a/src/scheduler/testsuite/test_repl_snapshot_params.c b/src/scheduler/testsuite/test_repl_snapshot_params.c index e0478827..656d7a2c 100644 --- a/src/scheduler/testsuite/test_repl_snapshot_params.c +++ b/src/scheduler/testsuite/test_repl_snapshot_params.c @@ -88,6 +88,17 @@ int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec, return 0; } +/* sched_sim_driver_run()'s (sim_loop.c, scheduler-sim-decision-parity task + * 2.1) new round-boundary call to the relocated sched_account_jobs() + * (core.h) -- a no-op stand-in here since this file does not link + * scheduler.c/resource.c, same rationale test_sim_equivalence.c's own copy + * documents. */ +int sched_account_jobs(xce_sched_core_t* core) +{ + (void)core; + return 0; +} + /* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same * copy every sim-family white-box test carries (schedobj.c is not linked * here: it would need plugin_register(), which requires a genuine diff --git a/src/scheduler/testsuite/test_sim_alt_events.c b/src/scheduler/testsuite/test_sim_alt_events.c index 59f99509..26e7f3ad 100644 --- a/src/scheduler/testsuite/test_sim_alt_events.c +++ b/src/scheduler/testsuite/test_sim_alt_events.c @@ -117,6 +117,17 @@ int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec, return 0; } +/* sched_sim_driver_run()'s (sim_loop.c, scheduler-sim-decision-parity task + * 2.1) new round-boundary call to the relocated sched_account_jobs() + * (core.h) -- a no-op stand-in here since this file does not link + * scheduler.c/resource.c, same rationale test_sim_equivalence.c's own copy + * documents. */ +int sched_account_jobs(xce_sched_core_t* core) +{ + (void)core; + return 0; +} + /* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same * copy test_sim_verify.c/test_sim_event_loop.c/test_sim_startup.c each * carry (schedobj.c is not linked here: it would need plugin_register(), diff --git a/src/scheduler/testsuite/test_sim_event_loop.c b/src/scheduler/testsuite/test_sim_event_loop.c index ef928e51..c8d48752 100644 --- a/src/scheduler/testsuite/test_sim_event_loop.c +++ b/src/scheduler/testsuite/test_sim_event_loop.c @@ -139,6 +139,17 @@ int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec, return 0; } +/* sched_sim_driver_run()'s (sim_loop.c, scheduler-sim-decision-parity task + * 2.1) new round-boundary call to the relocated sched_account_jobs() + * (core.h) -- a no-op stand-in here since this file does not link + * scheduler.c/resource.c, same rationale test_sim_equivalence.c's own copy + * documents. */ +int sched_account_jobs(xce_sched_core_t* core) +{ + (void)core; + return 0; +} + /* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same * rationale/algorithm as test_sim_startup.c's/test_core_full_cycle.c's own * copies (schedobj.c is not linked: it would need plugin_register(), which diff --git a/src/scheduler/testsuite/test_sim_record_output.c b/src/scheduler/testsuite/test_sim_record_output.c index f80c2142..62a438aa 100644 --- a/src/scheduler/testsuite/test_sim_record_output.c +++ b/src/scheduler/testsuite/test_sim_record_output.c @@ -104,6 +104,17 @@ int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec, return 0; } +/* sched_sim_driver_run()'s (sim_loop.c, scheduler-sim-decision-parity task + * 2.1) new round-boundary call to the relocated sched_account_jobs() + * (core.h) -- a no-op stand-in here since this file does not link + * scheduler.c/resource.c, same rationale test_sim_equivalence.c's own copy + * documents. */ +int sched_account_jobs(xce_sched_core_t* core) +{ + (void)core; + return 0; +} + int sched_resolve_job_picker(xce_sched_core_t* core, xce_sched_plugin_t** out) { xce_sched_plugin_t* plugin = NULL; diff --git a/src/scheduler/testsuite/test_sim_session_ordering.c b/src/scheduler/testsuite/test_sim_session_ordering.c new file mode 100644 index 00000000..7c56bf52 --- /dev/null +++ b/src/scheduler/testsuite/test_sim_session_ordering.c @@ -0,0 +1,605 @@ +/* + * test_sim_session_ordering.c - proves the simulation driver's round + * boundary now upholds the same ordering discipline + * test_driver_session_ordering.c already proves for the normal-mode driver + * (scheduler-sim-decision-parity spec, task 2.1, Req 1.3/1.4, 5.1, 5.3; + * "Sim Driver Round-Boundary Accounting" component, design.md). + * + * The property under test: sim_loop.c's sched_sim_driver_run() must call + * the relocated sched_account_jobs() (core.h, task 1.1) once per replayed + * round, strictly inside the round's session-active window -- i.e. after + * xce_core_begin_session()'s resval reset, before xce_core_end_session() + * closes the round -- mirroring driver.c's + * xce_sched_driver_run_normal_mode_round() exactly. Before task 2.1, + * sim_loop.c called the older monolithic xce_core_go() with zero + * interleaving slot and never called sched_account_jobs() at all + * (bug-1613, documented in .kiro/steering/scheduler-driver-equivalence.md) + * -- every worker's/shared resource's resInUse stayed permanently zero + * across a whole `--sim`/`--interactive` run regardless of what jobs were + * actually running. This file's own central assertion is 0 spy calls + * against the pre-fix code, 2 (one per replayed round) against the fixed + * code -- captured as this task's RED/GREEN evidence. + * + * Why a spy, not the real sched_account_jobs() (resource.c/scheduler.c): + * same rationale test_driver_session_ordering.c's own header comment gives + * for its identical choice -- this file's target is the DRIVER's ordering + * discipline (does it call sched_account_jobs() at all, and in the right + * window), not whether sched_account_jobs() itself charges resources + * correctly (task 1.1's own scope, proven by test_resource_accounting.c). + * + * Why no cross-thread wbuf-rendezvous proof (unlike + * test_driver_session_ordering.c's own elaborate three-thread design): that + * proof technique requires substituting a test-controlled picker plugin for + * the one the driver under test resolves. sim_loop.c's + * sched_sim_driver_run() -- unlike driver.c's decoupled, single-round + * xce_sched_driver_run_normal_mode_round() -- is one monolithic entry point + * that spawns its own picker thread (sim_picker_thread_fn(), sim_loop.c) + * internally and unconditionally bootstraps the real builtin_fcfs plugin + * (sim_bootstrap_picker(), sim_picker.c) as part of the same call; neither + * is independently substitutable from a test without modifying production + * code outside this task's boundary (sim_picker.c is not part of the "Sim + * Driver Round-Boundary Accounting" component this task owns). This is not + * a gap in rigor: begin_session()/[reconciliation slot]/account_jobs()/ + * signal_go() are four single-threaded, sequential, unconditional calls in + * sim_loop.c's own source (no branch reorders them), so "does + * account_jobs() run before signal_go()" is a plain, deterministic + * same-thread program-order fact, not a race to be proven at runtime -- + * only "does it actually run, and does it see session_active==1 (i.e. + * strictly after begin_session, strictly before end_session)" is the + * genuinely runtime-observable part, which this file's spy does prove. + * cbuf's own capacity-1 blocking rendezvous (core_session.c, already + * proven by test_core_session.c) is what guarantees the picker thread + * cannot observe xce_core_signal_go()'s wbuf signal any earlier than that + * call actually executes. + * + * Error-path preservation (Requirement: "preserve the existing + * error-handling behavior for a failed session start exactly as it works + * today"): NOT exercised by a runtime reproduction in this file. Analysis + * (see task's own status report): xce_core_begin_session() only fails when + * a session is already active, and sched_sim_driver_run()'s single + * replay-reading thread always completes a round's xce_core_end_session() + * (via the picker thread's guaranteed end-of-session NULL decision, drained + * before end_session runs) before its own next loop iteration could call + * begin_session() again -- this was equally true, and equally unreachable + * through the legitimate single-consumer replay loop, before this task's + * change (xce_core_go() had the identical failure condition). This task's + * diff preserves the exact same cleanup sequence (free_ReplaySession_ptr(), + * free(record_ctx), continue) the prior xce_core_go() branch used, just + * keyed off xce_core_begin_session()'s own rc instead -- verified by code + * review, not a synthetic runtime trigger disproportionate to a + * structurally unreachable branch. + * + * Fixture-construction technique and white-box #include/stub set: IDENTICAL + * to test_sim_equivalence.c's own (real normal-mode recording via + * sched_record_start()/xce_sched_driver_run_normal_mode_round() driving the + * REAL builtin_fcfs picker + a stand-in allocator that unconditionally + * PENDs, then replayed via the REAL, unmodified sched_sim_driver_run()) -- + * see that file's own header comment for the full rationale on why every + * decision here is PEND (sim_loop.c's live picker thread cannot emit a + * DISPATCH). This file records TWO separate rounds (not test_sim_equivalence.c's + * one) specifically to prove sched_account_jobs() is called ONCE PER ROUND, + * not merely once ever -- pinning Requirement 1.3 ("When a scheduling round + * completes ... refresh ... not only jobs whose resource consumption was + * already reflected in an earlier round"). + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "controller.capnp.h" +#include "kvmgr.h" +#include "preason.h" +#include "scheduler.h" +#include "xstring.h" + +/* Pull in the real functions under test into this TU (white-box #include, + * same combined set test_sim_equivalence.c already establishes). */ +#include "../src/core.c" +#include "../src/core_clock.c" +#include "../src/core_events.c" +#include "../src/core_session.c" +#include "../src/decision.c" +#include "../src/driver.c" +#include "../src/plugin.c" +#include "../src/plugins/builtin_fcfs.c" +#include "../src/recording.c" +#include "../src/recording_events.c" +#include "../src/recording_session.c" +#include "../src/repl.c" +#include "../src/repl_break.c" +#include "../src/repl_info.c" +#include "../src/repl_params.c" +#include "../src/sim.c" +#include "../src/sim_loop.c" +#include "../src/sim_picker.c" /* bug-1567: sched_sim_driver_run() calls sim_bootstrap_picker() (sim_loop.c-cap sibling) as an extern -- undefined reference at link time otherwise */ +#include "../src/sim_record.c" +#include "../src/sim_verify.c" +#include "../src/snapshot.c" +#include "../src/snapshot_load.c" +#include "../src/snapshot_wire.c" + +void kill_sched_job(void* arg) { (void)arg; } +void kill_sched_queue(void* arg) { (void)arg; } +void kill_sched_worker(void* arg) { (void)arg; } +void kill_sched_user(void* arg) { (void)arg; } +void kill_sched_project(void* arg) { (void)arg; } +void kill_sched_resval(xce_val_t* val) { (void)val; } + +/* core_reset_workers()'s (core_session.c) linker dependency -- never + * invoked (no worker is ever added here). */ +void sched_worker_release_host_ctx(xce_sched_worker_t* w) { (void)w; } + +/* plugin_prepare_allocation()'s (plugin.c) linker dependency -- never + * invoked (this fixture never drives real resource-matching allocation -- + * same rationale test_sim_equivalence.c's own copy documents). */ +int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec, + xlist_t* limits) +{ + (void)sched; + (void)vec; + (void)limits; + return 0; +} + +/* xce_sched_driver_on_decision()'s (driver.c) publish-side linker + * dependencies -- never actually invoked (this test never registers a + * decision callback). Same rationale test_sim_equivalence.c's own copy + * documents. */ +int sched_publish_dispatch(xce_scheduler_t* sched, xce_job_dispatch_t* d) +{ + (void)sched; + (void)d; + return 0; +} + +int sched_publish_pend_reason(xce_scheduler_t* sched, xce_preason_set_t* r) +{ + (void)sched; + (void)r; + return 0; +} + +/* handle_deferred_admin_record()'s (driver.c) linker dependency -- never + * invoked (this test never enqueues XCE_SCHED_OP_ADMIN_RECORD). */ +int xcecli_reply_schedadmin(xcecli_ctx_t c, uint64_t seq, + sched_admin_reply_t* reply) +{ + (void)c; + (void)seq; + (void)reply; + return 0; +} + +/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same + * copy every white-box test in this directory that does not link + * schedobj.c already establishes. Shared by driver.c's normal-mode round + * (recording phase, below) AND sim_loop.c's sim_picker_thread_fn() (replay + * phase, below) -- one real definition, both call sites in this combined TU + * resolve through it. */ +int sched_resolve_job_picker(xce_sched_core_t* core, xce_sched_plugin_t** out) +{ + xce_sched_plugin_t* plugin = NULL; + int rc = 0; + + if ((core == NULL) || (out == NULL)) { + return -EINVAL; + } + + *out = NULL; + + if (core->job_pick_plugin == NULL) { + return -EINVAL; + } + + lock(core->plugins, RTTI_LOCK_READ); + rc = key_find(core->plugins, core->job_pick_plugin, (void**)&plugin); + unlock(core->plugins); + + if (rc <= 0) { + return (rc < 0) ? rc : -ENOENT; + } + + if ((plugin == NULL) || (plugin->picker_iface == NULL) + || (plugin->picker_iface->next_job == NULL) + || (plugin->picker_iface->begin_session == NULL) + || (plugin->picker_iface->end_session == NULL) + || (plugin->picker_iface->on_dispatch == NULL) + || (plugin->picker_iface->on_pend == NULL)) { + return -EINVAL; + } + + *out = plugin; + + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* sched_account_jobs() SPY -- this file's own central instrument. Records */ +/* one call count PLUS, per call (up to MAX_SPY_CALLS), whether */ +/* core->session_active was 1 at the moment of the call -- proving each */ +/* invocation happened strictly inside a round's begin_session/end_session */ +/* window (see this file's own header comment for why this, rather than a */ +/* cross-thread rendezvous, is the right-strength proof here). */ +/* ---------------------------------------------------------------------- */ + +#define MAX_SPY_CALLS 8 + +static _Atomic int g_spy_calls; +static int g_spy_session_active_ok[MAX_SPY_CALLS]; + +int sched_account_jobs(xce_sched_core_t* core) +{ + int idx = atomic_fetch_add(&g_spy_calls, 1); + + if ((core != NULL) && (idx < MAX_SPY_CALLS)) { + g_spy_session_active_ok[idx] + = (atomic_load_explicit(&(core->session_active), + memory_order_acquire) + == 1) + ? 1 + : 0; + } + + return 0; +} + +static void reset_spy(void) +{ + atomic_store(&g_spy_calls, 0); + memset(g_spy_session_active_ok, 0, sizeof(g_spy_session_active_ok)); +} + +void setUp(void) { return; } +void tearDown(void) { return; } + +/* ---------------------------------------------------------------------- */ +/* Fixture helpers -- identical to test_sim_equivalence.c's own. */ +/* ---------------------------------------------------------------------- */ + +static void make_tmp_dir(char* out, size_t out_len, const char* suffix) +{ + char tmpl[PATH_MAX]; + + snprintf(tmpl, sizeof(tmpl), "/tmp/sched_simordering%s_XXXXXX", suffix); + TEST_ASSERT_NOT_NULL(mkdtemp(tmpl)); + TEST_ASSERT_TRUE(strlen(tmpl) < out_len); + strcpy(out, tmpl); +} + +static void fixture_init(xce_scheduler_t* sched, char* record_dir) +{ + xce_sched_plugin_t* fcfs; + int rc; + + memset(sched, 0, sizeof(*sched)); + mylock_init(&(sched->record_lock)); + + rc = xce_sched_core_init(&(sched->core)); + TEST_ASSERT_EQUAL_INT(0, rc); + + sched->core.pend_updates[0] = (dataq_t*)calloc(1, sizeof(dataq_t)); + dataq_init(sched->core.pend_updates[0], NULL); + sched->core.pend_updates[1] = (dataq_t*)calloc(1, sizeof(dataq_t)); + dataq_init(sched->core.pend_updates[1], NULL); + + fcfs = builtin_fcfs_new_plugin(); + TEST_ASSERT_NOT_NULL(fcfs); + rc = plugin_register_core(&(sched->core), fcfs); + TEST_ASSERT_TRUE(rc >= 0); + + sched->core.job_pick_plugin = (char*)"builtin_fcfs"; + sched->record_dir = record_dir; +} + +static xce_queue_t* make_queue(const char* name, int32_t priority) +{ + xce_queue_t* q = (xce_queue_t*)calloc(1, sizeof(*q)); + + q->queue = strdup(name); + q->status = QUEUE_STAT_OPEN; + q->priority = priority; + + return q; +} + +static xce_job_t* make_job(uint64_t jobid, uint64_t sub_time, const char* queue, + const char* user) +{ + xce_job_t* j = (xce_job_t*)calloc(1, sizeof(*j)); + + j->jobid = jobid; + j->state = JOB_STAT_PEND; + j->sub_time = sub_time; + j->queue_time = sub_time; + j->start_time = 0; + + j->submit = (job_submit_data_t*)calloc(1, sizeof(*(j->submit))); + j->submit->queue = strdup(queue); + j->submit->user = strdup(user); + j->submit->project_name = strdup("ordering_proj"); + j->submit->resreq = strdup("type == any"); + j->submit->mintasks = 1; + j->submit->maxtasks = 1; + + return j; +} + +static void enqueue_queue_update(xce_scheduler_t* sched, const char* name, + int32_t priority) +{ + xce_sched_update_request_t* req + = (xce_sched_update_request_t*)calloc(1, sizeof(*req)); + + req->op = XCE_SCHED_OP_UPDATE_QUEUE; + req->u.queueupdate.queue = make_queue(name, priority); + + dataq_enq(sched->core.pend_updates[0], &(req->link)); +} + +static void enqueue_jobs_push(xce_scheduler_t* sched, xce_job_t** jobs, + int njobs) +{ + xce_sched_update_request_t* req + = (xce_sched_update_request_t*)calloc(1, sizeof(*req)); + + req->op = XCE_SCHED_OP_UPDATE_JOB; + req->u.jobupdate.njobs = njobs; + req->u.jobupdate.jobs = jobs; + + dataq_enq(sched->core.pend_updates[0], &(req->link)); +} + +/* ---------------------------------------------------------------------- */ +/* Normal-mode picker/allocator threads (recording phase) -- identical to */ +/* test_sim_equivalence.c's own. */ +/* ---------------------------------------------------------------------- */ + +static void* picker_thread_fn(void* arg) +{ + xce_sched_core_t* core = (xce_sched_core_t*)arg; + xce_sched_plugin_t* job_picker = NULL; + int rc; + int go = 0; + uint64_t jobid; + + rc = sched_resolve_job_picker(core, &job_picker); + if (rc < 0) { + return NULL; + } + + cbuf_get(&(core->wbuf), &go); + + job_picker->picker_iface->begin_session(job_picker); + + for (;;) { + xce_sched_job_t* job = job_picker->picker_iface->next_job(job_picker); + + if (job == NULL) { + break; + } + + jobid = job->jobid; + cbuf_put(&(core->jbuf), &jobid); + } + + jobid = 0; + cbuf_put(&(core->jbuf), &jobid); + + job_picker->picker_iface->end_session(job_picker); + + return NULL; +} + +static void* stand_in_allocator_thread_fn(void* arg) +{ + xce_sched_core_t* core = (xce_sched_core_t*)arg; + xce_sched_plugin_t* picker = NULL; + uint64_t jobid; + + if (sched_resolve_job_picker(core, &picker) < 0) { + xce_decision_t* decision = NULL; + + cbuf_get(&(core->jbuf), &jobid); + cbuf_put(&(core->decisions), &decision); + return NULL; + } + + for (;;) { + xce_decision_t* decision; + + cbuf_get(&(core->jbuf), &jobid); + if (jobid == 0) { + decision = NULL; + cbuf_put(&(core->decisions), &decision); + break; + } + + picker->picker_iface->on_pend(picker, jobid); + decision = make_pend_decision(make_pendreason(jobid)); + cbuf_put(&(core->decisions), &decision); + } + + return NULL; +} + +static void run_one_real_session(xce_scheduler_t* sched) +{ + pthread_t picker_tid; + pthread_t allocator_tid; + int rc; + + rc = pthread_create(&picker_tid, NULL, picker_thread_fn, &(sched->core)); + TEST_ASSERT_EQUAL_INT(0, rc); + + rc = pthread_create(&allocator_tid, NULL, stand_in_allocator_thread_fn, + &(sched->core)); + TEST_ASSERT_EQUAL_INT(0, rc); + + xce_sched_driver_run_normal_mode_round(sched); + + rc = pthread_join(picker_tid, NULL); + TEST_ASSERT_EQUAL_INT(0, rc); + rc = pthread_join(allocator_tid, NULL); + TEST_ASSERT_EQUAL_INT(0, rc); +} + +/* ---------------------------------------------------------------------- */ +/* The contract test. */ +/* ---------------------------------------------------------------------- */ + +static void +test_sim_replay_calls_account_jobs_once_per_round_within_session_window( + void) +{ + char record_dir[PATH_MAX]; + xce_scheduler_t sched; + char r_path[PATH_MAX]; + xce_job_t** jobs1; + xce_job_t** jobs2; + sched_sim_args_t sim_args; + xce_sched_sim_driver_t sim; + int rc; + int i; + + /* ---- Record TWO real normal-mode rounds into one .replay file ---- */ + make_tmp_dir(record_dir, sizeof(record_dir), "_r"); + fixture_init(&sched, record_dir); + + rc = sched_record_start(&sched, "ordering.replay"); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_TRUE(strlen(sched.record_file_path) < sizeof(r_path)); + strcpy(r_path, sched.record_file_path); + + enqueue_queue_update(&sched, "normal", 1); + + jobs1 = (xce_job_t**)calloc(1, sizeof(xce_job_t*)); + jobs1[0] = make_job(9201ULL, 1000ULL, "normal", "alice"); + enqueue_jobs_push(&sched, jobs1, 1); + + run_one_real_session(&sched); /* round 1 */ + + jobs2 = (xce_job_t**)calloc(1, sizeof(xce_job_t*)); + jobs2[0] = make_job(9202ULL, 2000ULL, "normal", "bob"); + enqueue_jobs_push(&sched, jobs2, 1); + + run_one_real_session(&sched); /* round 2 */ + + rc = sched_record_stop(&sched); + TEST_ASSERT_EQUAL_INT(0, rc); + + /* Reset the spy AFTER recording -- driver.c's own normal-mode round + * boundary (task 1.1) already calls the real sched_account_jobs() once + * per round during the recording above (already proven correct by + * test_driver_session_ordering.c); this test's own target is + * sim_loop.c's call site (task 2.1), isolated by resetting here. */ + reset_spy(); + + /* ---- Replay the SAME file through the REAL, unmodified simulation + * driver ---- */ + memset(&sim_args, 0, sizeof(sim_args)); + sim_args.replay_path = r_path; + sim_args.sim_start_ts = SCHED_SIM_START_TS_LATEST; + sim_args.sim_speed = 0; /* frozen -- deterministic, no wall-clock skew */ + + memset(&sim, 0, sizeof(sim)); + rc = sched_sim_driver_run(&sim, &sim_args); + TEST_ASSERT_EQUAL_INT(0, rc); + + /* THE central regression proof (task 2.1's own scope, Req 1.3/1.4): + * before this task, this count is 0 (sim_loop.c never called + * sched_account_jobs() at all, bug-1613) -- after, it is 2, one per + * replayed round, not merely once ever. */ + TEST_ASSERT_EQUAL_INT(2, atomic_load(&g_spy_calls)); + + for (i = 0; i < 2; i++) { + TEST_ASSERT_TRUE_MESSAGE( + g_spy_session_active_ok[i], + "sched_account_jobs() must run with core->session_active==1 -- " + "i.e. strictly after xce_core_begin_session(), strictly before " + "xce_core_end_session(), matching driver.c's own ordering " + "contract"); + } + + printf("test_sim_replay_calls_account_jobs_once_per_round_within_" + "session_window: PASS\n"); +} + +/* + * test_sim_replay_still_completes_interactive_and_batch_identically - + * Requirement 5.1: interactive and batch simulation runs must go through + * the SAME round-boundary call site with no divergence. sched_sim_driver_run() + * is the one entry point BOTH `xce_scheduler -s` (batch) and `-i -s` + * (interactive) call -- gated only by sim->interactive inside REPL hook + * calls this file's own fixture never triggers (scheduler/CLAUDE.md: "drives + * the exact same sched_sim_driver_run() as batch --sim, gated only by + * sim->interactive"). This test simply confirms the round-boundary spy + * fires identically whether sim->interactive is left at its default 0 + * (batch) -- there is no separate call site to diverge from; this is a + * structural guarantee proven by the single call site itself, re-asserted + * here as an explicit regression pin rather than left implicit. + */ +static void test_sim_replay_batch_mode_uses_the_same_round_boundary(void) +{ + char record_dir[PATH_MAX]; + xce_scheduler_t sched; + char r_path[PATH_MAX]; + xce_job_t** jobs1; + sched_sim_args_t sim_args; + xce_sched_sim_driver_t sim; + int rc; + + make_tmp_dir(record_dir, sizeof(record_dir), "_b"); + fixture_init(&sched, record_dir); + + rc = sched_record_start(&sched, "batch.replay"); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_TRUE(strlen(sched.record_file_path) < sizeof(r_path)); + strcpy(r_path, sched.record_file_path); + + enqueue_queue_update(&sched, "normal", 1); + + jobs1 = (xce_job_t**)calloc(1, sizeof(xce_job_t*)); + jobs1[0] = make_job(9301ULL, 1000ULL, "normal", "carol"); + enqueue_jobs_push(&sched, jobs1, 1); + + run_one_real_session(&sched); + + rc = sched_record_stop(&sched); + TEST_ASSERT_EQUAL_INT(0, rc); + + reset_spy(); + + memset(&sim_args, 0, sizeof(sim_args)); + sim_args.replay_path = r_path; + sim_args.sim_start_ts = SCHED_SIM_START_TS_LATEST; + sim_args.sim_speed = 0; + + memset(&sim, 0, sizeof(sim)); + TEST_ASSERT_EQUAL_INT(0, sim.interactive); /* batch: default-zero, never set */ + + rc = sched_sim_driver_run(&sim, &sim_args); + TEST_ASSERT_EQUAL_INT(0, rc); + + TEST_ASSERT_EQUAL_INT(1, atomic_load(&g_spy_calls)); + TEST_ASSERT_TRUE(g_spy_session_active_ok[0]); + + printf("test_sim_replay_batch_mode_uses_the_same_round_boundary: " + "PASS\n"); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST( + test_sim_replay_calls_account_jobs_once_per_round_within_session_window); + RUN_TEST(test_sim_replay_batch_mode_uses_the_same_round_boundary); + return UNITY_END(); +} diff --git a/src/scheduler/testsuite/test_sim_verify.c b/src/scheduler/testsuite/test_sim_verify.c index b11672e6..7c92c951 100644 --- a/src/scheduler/testsuite/test_sim_verify.c +++ b/src/scheduler/testsuite/test_sim_verify.c @@ -130,6 +130,17 @@ int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec, return 0; } +/* sched_sim_driver_run()'s (sim_loop.c, scheduler-sim-decision-parity task + * 2.1) new round-boundary call to the relocated sched_account_jobs() + * (core.h) -- a no-op stand-in here since this file does not link + * scheduler.c/resource.c, same rationale test_sim_equivalence.c's own copy + * documents. */ +int sched_account_jobs(xce_sched_core_t* core) +{ + (void)core; + return 0; +} + /* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same * copy test_sim_event_loop.c/test_sim_startup.c/test_core_full_cycle.c each * carry (schedobj.c is not linked here: it would need plugin_register(),