/* * test_sim_contention_parity.c - scheduler-sim-decision-parity spec, task * 4.1 (Req 1.4, 2.1, 2.2): the record-then-replay-and-diff comparison the * resource-contention fixture (task 1.2) was BUILT for but never itself * asserted -- task 1.2 only proved the fixture's own shape + basic * replayability (test_sim_resource_contention.c); task 3's own test * (test_sim_round_boundary_reconciliation.c) proved the reconciliation -> * accounting -> capacity-check chain works via direct function calls * against a hand-built `.replay`, NOT via an actual "record via normal * mode, replay via simulation mode, diff round-by-round" comparison. This * file is that comparison. * * Why this is NOT simply "replay task 1.2's own recorded file and diff it" * (a load-bearing scope decision -- read this before changing the fixture): * * sim_picker_thread_fn() (sim_loop.c) structurally can only ever PEND a * candidate it independently evaluates -- real resource-matching * allocation belongs to the not-yet-designed scheduler-allocation spec * (confirmed by test_sim_equivalence.c's own header comment and this * spec's design.md, which scopes Recorded-Decision Reconciliation * strictly to jobs "already resolved... BEFORE this replay run started * tracking them live"). Task 1.2's own recorded fixture has jobs A/B/C * pushed PEND and dispatched/pended WITHIN the very first round the * picker ever sees them -- i.e. NOT "already resolved before the replay * reached them". Replaying that exact file through sim mode unmodified * would therefore see sim's own live picker independently (and * correctly, per its documented scope) PEND all three jobs, diverging * from the real recording's genuine DISPATCH/DISPATCH/PEND outcome for a * reason this spec does NOT try to fix (that gap is scheduler-allocation * spec territory, not scheduler-sim-decision-parity's). * * What THIS spec's Requirement 2 is actually about ("while resource * capacity is already consumed by RUNNING jobs, ... treat that * already-consumed capacity as unavailable") is: given jobs A and C are * ALREADY resolved (dispatched, running, consuming capacity) by the time * this replay's own live picker evaluates a NEW candidate, does sim mode * correctly deny that new candidate the same way normal mode would? This * is exactly Recorded-Decision Reconciliation's designed trigger shape * (RUN-state job, job->accounts == NULL, matching recorded DISPATCH * decision in the SAME round's window) -- test_sim_round_boundary_ * reconciliation.c's own construction (a JOB_STATE_CHANGE event * immediately preceding a ReplaySession carrying the matching DISPATCH * decision, for ONE job). This file extends that exact, proven mechanism * to BOTH jobs A and C together (task 1.2's full 3-job contention shape: * worker capacity 10.0, jobs wanting 8.0/5.0/2.0), and -- the genuinely * NEW part this task requires -- independently, separately produces "the * equivalent normal-mode recording" via a REAL, driven session (task * 1.2's own picker+capacity-aware-allocator technique, reused verbatim) * so the comparison is against actual observed normal-mode behavior, not * a hand-typed expectation. * * Two-fixture design: * - Part A (normal mode): a REAL xce_scheduler_t, real builtin_fcfs * picker, this file's own capacity-aware allocator thread (same * technique as test_sim_resource_contention.c's own * capacity_aware_allocator_thread_fn(), extended to ALSO set * job->state = JOB_STAT_RUN immediately after a real dispatch -- see * that code's own comment for why this is required for round-2 * cross-round accounting persistence to mean anything at all: without * it, sched_account_jobs()'s JOB_IS_RUN() guard would never re-charge * jobs A/C in round 2). TWO real, driven rounds: round 1 (jobs A/B/C: * A+C dispatch, B denied) and round 2 (job D: denied, capacity fully * saturated by A+C -- and B is genuinely RE-OFFERED as a candidate in * round 2 too, per builtin_fcfs_on_pend()'s own documented "no-op -- * job remains in pendJobs, retried next round" contract; re-denied * again, not a bug). This is the "equivalent normal-mode recording" * ground truth this task's own text requires -- inspected directly via * sched.core (the SAME struct sched_record_start()/sched_record_stop() * wrap), not by re-decoding the file (test_sim_round_boundary_ * reconciliation.c's own precedent for this style of direct * inspection). * - Part B (simulation mode): a hand-crafted `.replay` file (same * write_capnp_message()/encode_ReplayEvent_ptr()/encode_ReplaySession_ptr() * technique test_sim_round_boundary_reconciliation.c's own header * comment documents), modeling the IDENTICAL facts from simulation * mode's own perspective: jobs A and C already RUN (JOB_STATE_CHANGE * events immediately preceding round 1's own session) with matching * recorded DISPATCH decisions (SAME want-values: JOBA_WANT=8.0, * JOBC_WANT=2.0, taken from the SAME constants Part A uses, never * re-typed by hand) for reconciliation to reconstruct; job B genuinely * PEND (fresh, sim's own live decision, no reconciliation needed). * Round 2 pushes job D fresh; sim's own live picker independently PENDs * it (matching normal mode's real denial for a genuinely-shared reason * this time: the SAME capacity fact, proven directly below via * sched_check_host_satisfy() against sim's own post-replay state, not * merely "sim always pends anyway"). * * Round-boundary resInUse observation mechanism: sched_sim_driver_run() * (sim_loop.c) is a single monolithic call that runs the WHOLE replay to * completion in one shot (cerebrum's own documented note: unlike driver.c's * decoupled single-round xce_sched_driver_run_normal_mode_round(), there is * no way to pause sim mode mid-replay and inspect intermediate state * without either interactive REPL stepping (stdin-driven, awkward to * drive from a test) or a hook this test itself controls). This file uses * the SAME technique test_sim_round_boundary_reconciliation.c already * establishes for driving a REAL (not stubbed) sched_account_jobs() body * into this TU -- but additionally augments it with a snapshot side effect * (g_capturing/g_round_resinuse[]/g_round_idx below) that records worker-a's * resInUse EVERY time sched_account_jobs() is called. Since this function is * called EXACTLY ONCE per round, by EITHER driver, at the identical * relative round-boundary position (design.md's own Architecture * Integration note), this gives a genuine PER-ROUND resInUse observation * point for simulation mode -- the SAME mechanism the real production code * already uses at the exact point Requirement 1.4 cares about, not a * separate/parallel measurement path. * * White-box #include set: identical to test_sim_resource_contention.c's own * combined set (core.c/core_clock.c/core_events.c/core_session.c/ * decision.c/driver.c/plugin.c/plugins/builtin_fcfs.c/recording.c/ * recording_events.c/recording_session.c/repl.c/repl_break.c/repl_info.c/ * repl_params.c/resource.c/sim.c/sim_loop.c/sim_picker.c/sim_record.c/ * sim_verify.c/snapshot.c/snapshot_load.c/snapshot_wire.c) -- this file * needs BOTH driver.c (Part A's real driven rounds) AND resource.c (Part * A's real capacity-check/charge AND Part B's real reconciliation * accounting), exactly like that file. * * Stub surface: the same set test_sim_resource_contention.c's own header * comment documents (kill_sched_* family, sched_publish_dispatch/ * sched_publish_pend_reason, xcecli_reply_schedadmin, * sched_ctx_get_uncomit_host, sched_resolve_job_picker real reimpl) MINUS * that file's own no-op sched_account_jobs() stub -- THIS file needs the * REAL body (test_sim_round_boundary_reconciliation.c's own precedent for * why: "this file's entire purpose is to prove what this function does to * resInUse... a no-op stub would defeat the test's own premise"), augmented * with the round-boundary snapshot side effect described above. * * Discriminating-power note (validated live, not merely asserted): this * test was run against a deliberately-reverted tree (task 3's * sim_reconcile_unaccounted_jobs() call site temporarily removed from * sim_loop.c) and confirmed to fail -- specifically, the array slot for * round 1's own resInUse snapshot (g_round_resinuse[1] as of task 4.6's * added load-time slot at index 0; g_round_resinuse[0] at the time of this * historical run, before that slot existed) read back 0.0 instead of 10.0 * (jobs A/C never reconciled, so sched_account_jobs()'s * JOB_IS_RUN()+accounts!=NULL guard skipped them), * and the direct sched_check_host_satisfy() capacity-awareness proof below * WRONGLY granted a hypothetical job wanting JOBB_WANT (5.0) against sim's * post-replay state -- then confirmed to pass again once the call site was * restored. See this task's own Status Report for the exact commands run. * * RECONCILIATION (scheduler-sim-live-allocation spec, task 5.3, 2026-09-19): * the "sim_picker_thread_fn() ... structurally can only ever PEND" and * "real resource-matching allocation belongs to the not-yet-designed * scheduler-allocation spec" framing above is now historical -- that * function was removed (scheduler-sim-live-allocation task 3.1/4.1), * scheduler-allocation has since been completed, and this feature makes * production sim mode genuinely dispatch via the shared sched_job_picker()/ * sched_allocator() pair (sched_election.c). This file's OWN rationale for * NOT simply replaying task 1.2's recorded file is still correct and * load-bearing for a different reason than originally stated, though: this * TU supplies its own link-satisfying sched_job_picker()/sched_allocator() * stub (below, added unmodified by task 4.1) that deliberately preserves * the old always-PEND contract, so it could not detect real resource-based * dispatch even if the fixture were replayed unmodified -- Recorded- * Decision Reconciliation's own narrow trigger shape (a job already RUN by * the time the live picker reaches it) is genuinely the mechanism this file * tests, independent of whether the live allocator can also dispatch a * fresh candidate for itself. `git show` on task 4.1's commit confirms the * only change made to this file was adding that stub -- zero pre-existing * assertions were touched, and this file's own test still passes for the * same reason it always did. The genuine question this feature raised -- * does production sim mode's real live allocator (not reconciliation) * produce the same admission/pend outcome as normal mode under contention, * including limit-constrained contention -- is answered by task 5.1's * sched_sim_contention_resource_driver_parity_test and * sched_sim_contention_limit_driver_parity_test (meson.build), which link * real scheduler_srcs and drive the real sched_job_picker()/sched_allocator() * pair directly; this file does not duplicate that proof. */ #include #include #include #include #include #include #include #include #include #include "controller.capnp.h" #include "kvmgr.h" #include "preason.h" #include "resobj.h" #include "scheduler.h" #include "xstring.h" /* Pull in the real functions under test into this TU (white-box #include, * same convention every other file in this directory establishes -- see * this file's own header comment for the exact combined set). */ #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_fmt.c" #include "../src/repl_rollup.c" #include "../src/repl_break.c" #include "../src/repl_info.c" #include "../src/repl_params.c" #include "../src/repl_trace_render.c" #include "../src/repl_trace_render_outcome.c" #include "../src/repl_trace_store.c" #include "../src/repl_trace.c" #include "../src/repl_trace_emit.c" #include "../src/resource.c" /* Hooked sources compile SCHED_TRACE bodies (scheduler-sim-decision-trace). */ #include "../src/sched_trace.c" #include "../src/sched_trace_scan.c" #include "../src/sched_trace_capture.c" #include "../src/sched_trace_outcome.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_limit.c" /* bug-1825: sched_sim_driver_run() now also calls sim_bootstrap_limit_plugin() (sim_loop.c-cap sibling, sim_picker.c's own bug-1567 precedent) 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; } /* repl_info.c (scheduler-sim-diagnostics spec, task 2.1) now calls * limit_get_configured_limits() (plugins/limit.c, not part of this TU) to * resolve `info limit`'s name/all argument -- link-satisfying stub only, * same rationale as kill_sched_job()/kill_sched_resval() above; this * file's own fixtures never register the limit plugin, so NULL * (limit_get_configured_limits()'s own documented "plugin absent" return) * is also the behaviorally correct answer here, not just a link * placeholder. repl_info_limit() itself is not yet wired into * repl_cmd_info() (task 4), so no test in this file can reach it anyway. */ var limit_get_configured_limits(xce_sched_core_t* core) { (void)core; return NULL; } /* scheduler-sim-diagnostics spec, task 3.1: repl_info.c now also calls * limit_get_usage_instances() (plugins/limit.c, not part of this TU) to * read a limit's live usage instances -- identical link-satisfying stub, * same rationale as limit_get_configured_limits() immediately above; this * file's own fixtures never register the limit plugin, so NULL * (limit_get_usage_instances()'s own documented "plugin absent" return) is * also the behaviorally correct answer here, not just a link placeholder. */ var limit_get_usage_instances(xce_sched_core_t* core) { (void)core; return NULL; } /* scheduler-sim-diagnostics spec, task 6 (bug-1825): sim_loop.c's * sched_sim_driver_run() now also calls sim_bootstrap_limit_plugin() * (sim_limit.c, pulled into this TU via the #include above), which in * turn calls limit_new_plugin() (plugins/limit.c, not part of this TU) -- * identical link-satisfying stub rationale as limit_get_configured_ * limits()/limit_get_usage_instances() immediately above. Returning NULL * here is also the behaviorally correct answer, not just a link * placeholder: sim_bootstrap_limit_plugin() already handles a NULL * limit_new_plugin() return gracefully (logs an ERROR, registers nothing, * returns) -- the exact same "out of memory" path sim_bootstrap_picker() * (sim_picker.c) takes for a NULL builtin_fcfs_new_plugin() -- so this * file's fixtures simply end up with no limit plugin registered, same * observable state as before this task. */ xce_sched_plugin_t* limit_new_plugin(void) { return NULL; } /* limit_seed_config() (plugins/limit.c, not part of this TU) -- bug-1825 * remediation round 2's new honestly-typed config-seed entry point. * sim_bootstrap_limit_plugin() (sim_limit.c) only calls it after * plugin_register_core() has already succeeded against a real, non-NULL * limit_new_plugin() plugin -- unreachable in this file since the * limit_new_plugin() stub immediately above always returns NULL, but the * symbol must still resolve at link time. Same link-satisfying-stub * rationale as limit_new_plugin() immediately above. */ int limit_seed_config(xce_sched_plugin_t* plugin, xce_sched_core_t* core, xce_conf4sched_t* config) { (void)plugin; (void)core; (void)config; return 0; } /* xce_sched_driver_on_decision()'s (driver.c) publish-side linker * dependencies -- never actually invoked: this file never registers a * decision callback. Same rationale test_sim_resource_contention.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 file 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; } /* resource.c's sched_try_alloc_on_host_()'s own link-time dependency -- * never actually invoked (this file calls sched_check_host_satisfy()/ * sched_charge_account()/sched_commit_job_alloc() directly). Same * link-satisfying-only stub every resource.c-including sibling in this * directory already establishes needing. */ int sched_ctx_get_uncomit_host(xvec_t** v, xce_sched_ctx_t* c, int i) { (void)v; (void)c; (void)i; 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. */ 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; } /* ---------------------------------------------------------------------- */ /* Round-boundary resInUse observation (see this file's own header */ /* comment for the full rationale). */ /* ---------------------------------------------------------------------- */ static char SLOTS_RESNAME[] = "slots"; static int g_capturing = 0; static float g_round_resinuse[8]; static int g_round_idx = 0; /* Real reimplementation of scheduler.c's sched_account_jobs() (scheduler.c: * 1258, scheduler-sim-decision-parity task 1.1's relocated xce_sched_core_t* * signature) -- body copied verbatim from test_sim_round_boundary_ * reconciliation.c's own copy (itself copied from scheduler.c), augmented * ONLY with the round-boundary snapshot side effect below. This file's * whole purpose (like that file's) is to prove what this function does to * resInUse, so a no-op stub would defeat the test's own premise. */ int sched_account_jobs(xce_sched_core_t* core) { iterator_t it; int rc = 0; rc = iter_make(&it, core->jobs); if (rc < 0) { return rc; } for (;;) { xce_sched_job_t* job = NULL; rc = iter_next(&it, (void**)&job); if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) { break; } if (!JOB_IS_RUN(job->state)) { continue; } if (job->accounts != NULL) { sched_charge_account(core, job->accounts); } } if (rc == RTTI_ITER_NO_DATA) { rc = 0; } iter_destroy(&it); /* Task 4.1's own addition: snapshot worker-a's resInUse into * g_round_resinuse[] every time this is called, when the test has * armed g_capturing. Gated so Part A's (normal mode) OWN calls to this * SAME function -- driver.c calls it too, at the identical relative * position -- never pollute the simulation-side snapshot array; Part A * observes its own resInUse via direct get_resvals() calls instead * (see the test function below for why: normal mode's real dispatch * happens mid-round, AFTER this function's own entering-the-round * recompute, so this function's OWN snapshot at round 1 would * trivially read 0.0 on the normal side regardless of correctness -- * not a useful comparison point there). */ if (g_capturing && (g_round_idx < (int)(sizeof(g_round_resinuse) / sizeof(g_round_resinuse[0])))) { xce_sched_worker_t* w = sched_get_worker(core, (char*)"worker-a"); float got = -1.0f; if (w != NULL) { xce_resource_t* r = NULL; if ((key_find(core->resmgr, (void*)SLOTS_RESNAME, (void**)&r) >= 0) && (r != NULL)) { int residx = r->idx; get_resvals(&(w->resInUse), 1, &residx, &got); } } g_round_resinuse[g_round_idx++] = got; } return rc; } /* scheduler-sim-live-allocation spec, task 4.1: sim_loop.c's real * sched_sim_driver_run() (white-box #include'd into this TU above) now * spawns sched_job_picker()/sched_allocator() (sched_election.c) directly * instead of its own, now-removed sim_picker_thread_fn() -- an undefined * reference at link time in this white-box TU otherwise, since * sched_election.c's own sched_allocator() calls the REAL * sched_make_allocation()/sched_commit_decision() (scheduler.c), neither * part of this file's own hand-picked white-box #include set (this file's * own header comment lists that set). This file's own test(s) do not * exercise genuine resource-matching allocation, so this link-satisfying * pair faithfully reproduces the exact PEND-only contract the REMOVED * sim_picker_thread_fn() itself used to provide -- split across the SAME * two-thread jbuf hand-off the real production pair now uses (one * producer, one consumer, matching the project's jbuf capacity-1 * rendezvous invariant) instead of the old single fused thread -- so this * file's own existing assertions are unaffected by this task's production * change. Genuine resource-based dispatch through the real, unmodified * sched_job_picker()/sched_allocator() pair is proved separately by * test_sim_live_allocation.c (task 4.1's own dedicated fixture, which * links the real scheduler_srcs instead of white-boxing individual files, * precisely so it can reach the real allocation chain this stub * deliberately does not). */ void* sched_job_picker(void* arg) { xce_sched_core_t* core = (xce_sched_core_t*)arg; xce_sched_plugin_t* picker = NULL; for (;;) { int go = 0; uint64_t jobid = 0; cbuf_get(&(core->wbuf), &go); if (sched_resolve_job_picker(core, &picker) < 0) { jobid = 0; cbuf_put(&(core->jbuf), &jobid); continue; } picker->picker_iface->begin_session(picker); for (;;) { xce_sched_job_t* job = picker->picker_iface->next_job(picker); if (job == NULL) { break; } jobid = job->jobid; cbuf_put(&(core->jbuf), &jobid); } jobid = 0; cbuf_put(&(core->jbuf), &jobid); picker->picker_iface->end_session(picker); } return NULL; } void* sched_allocator(void* arg) { xce_sched_core_t* core = (xce_sched_core_t*)arg; xce_sched_plugin_t* picker = NULL; for (;;) { uint64_t jobid = 0; xce_decision_t* decision = NULL; xce_sched_job_t* job; cbuf_get(&(core->jbuf), &jobid); if (jobid == 0) { decision = NULL; cbuf_put(&(core->decisions), &decision); continue; } if (jobid == XCE_SCHED_RECONCILE_WAKE) { /* bug-1837: this file's stand-in sched_allocator() must service * the same Recorded-Decision Reconciliation relay protocol the * real sched_election.c implementation does (see that file's * matching branch for the full rationale) -- this file's own * sched_sim_driver_run() calls set sim->allocator_thread_live, * so sim_reconcile_unaccounted_jobs() routes on_dispatch() * through jbuf/reconcile_req/reconcile_ack exactly as * production does; without this branch, a reconciliation * success here would hang forever waiting on an ack this stub * never sends. */ uint64_t reconcile_jobid = 0; int reconcile_rc = 0; cbuf_get(&(core->reconcile_req), &reconcile_jobid); (void)sched_resolve_job_picker(core, &picker); if (picker != NULL) { reconcile_rc = picker->picker_iface->on_dispatch( picker, reconcile_jobid); } cbuf_put(&(core->reconcile_ack), &reconcile_rc); continue; } job = sched_get_job(core, jobid); if (job == NULL) { continue; } (void)sched_resolve_job_picker(core, &picker); if (picker != NULL) { picker->picker_iface->on_pend(picker, job->jobid); } decision = make_pend_decision(make_pendreason(job->jobid)); cbuf_put(&(core->decisions), &decision); } return NULL; } /* scheduler-sim-live-allocation spec, task 4.1 (bug-1829 fix): sim_loop.c's * real sched_sim_driver_run() (white-box #include'd into this TU above) now * also calls sim_bootstrap_seqjob_allocator()/sim_bootstrap_parjob_ * allocator() (sim_alloc.c) -- undefined references at link time in this * white-box TU otherwise, since neither symbol is part of this file's own * hand-picked white-box #include set. This file's own test(s) exercise the * link-satisfying sched_job_picker()/sched_allocator() stub pair above * (which never reaches real primary allocation), so a true no-op is the * correct, behavior-preserving stand-in here -- the real bootstrap logic is * exercised separately by test_sim_live_allocation.c (task 4.1's own * dedicated fixture, which links the real scheduler_srcs). */ void sim_bootstrap_seqjob_allocator(xce_sched_sim_driver_t* sim) { (void)sim; } void sim_bootstrap_parjob_allocator(xce_sched_sim_driver_t* sim) { (void)sim; } void setUp(void) { return; } void tearDown(void) { return; } /* ---------------------------------------------------------------------- */ /* Fixture constants -- SAME job set/want-values as task 1.2's own */ /* resource-contention fixture (test_sim_resource_contention.c), plus one */ /* new job D for this file's own round-2 persistence proof. */ /* ---------------------------------------------------------------------- */ #define WORKER_NAME "worker-a" #define WORKER_CAPACITY 10.0f #define JOBA_ID 9101ULL #define JOBB_ID 9102ULL #define JOBC_ID 9103ULL #define JOBD_ID 9104ULL #define JOBA_WANT 8.0f #define JOBB_WANT 5.0f #define JOBC_WANT 2.0f #define JOBD_WANT 1.0f /* any positive amount -- remaining headroom is * exactly 0.0 once A+C are charged, so this must * always be denied regardless of its exact value */ /* ---------------------------------------------------------------------- */ /* Shared fixture helpers */ /* ---------------------------------------------------------------------- */ static void make_tmp_dir(char* out, size_t out_len, const char* suffix) { char tmpl[PATH_MAX]; snprintf(tmpl, sizeof(tmpl), "/tmp/sched_contparity%s_XXXXXX", suffix); TEST_ASSERT_NOT_NULL(mkdtemp(tmpl)); TEST_ASSERT_TRUE(strlen(tmpl) < out_len); strcpy(out, tmpl); } /* Registers "slots" via the real makeResource() and reads back its * assigned ->idx -- same technique test_sim_round_boundary_ * reconciliation.c's own fixture_init() establishes, and for the SAME * reason: reconciliation's real make_job_account()/make_res_account() * path always produces residx == -1, lazily resolved via * sched_update_res_accounts()'s own key_find(core->resmgr, ...) -- rather * than assume which index that lazy path would land on, this fixture * pre-registers the resource so BOTH the worker's own resvals/resInUse * setup below AND every capacity-check `wanted` vector built later use the * SAME, actually-resolved index. Returns the resolved residx. */ static int register_slots_resource(xce_sched_core_t* core) { xce_resource_t* res_template = (xce_resource_t*)calloc(1, sizeof(*res_template)); xce_resource_t* registered; TEST_ASSERT_NOT_NULL(res_template); res_template->name = strdup("slots"); res_template->valtype = XCE_VAL_NUMERIC; res_template->ordtype = XCE_ORD_DECR; res_template->flags = XCE_RESF_LIMIT | XCE_RESF_CONSUMABLE | XCE_RESF_RELEASE | XCE_RESF_DEFAULT_HOST; registered = makeResource(core->resmgr, res_template); TEST_ASSERT_NOT_NULL(registered); return registered->idx; } /* Adds a real xce_sched_worker_t "worker-a" with WORKER_CAPACITY finite * units of `residx`, added directly via sched_add_worker() -- same * technique every fixture in this directory that adds a worker directly * establishes ("sched_add_worker() itself does not initialize resvals/ * resInUse -- callers own it"). */ static void add_worker(xce_sched_core_t* core, int residx) { xce_sched_worker_t* w; int rc; float val[1] = { WORKER_CAPACITY }; w = (xce_sched_worker_t*)calloc(1, sizeof(*w)); TEST_ASSERT_NOT_NULL(w); w->name = strdup(WORKER_NAME); TEST_ASSERT_NOT_NULL(w->name); rc = zarray_init(&(w->resvals), 8, NULL); TEST_ASSERT_EQUAL_INT(0, rc); rc = zarray_init(&(w->resInUse), 8, NULL); TEST_ASSERT_EQUAL_INT(0, rc); rc = put_resvals(&(w->resvals), 1, &residx, val); TEST_ASSERT_TRUE(rc >= 0); rc = sched_add_worker(core, w); TEST_ASSERT_TRUE(rc >= 0); } /* ====================================================================== * Part A: real normal-mode driven fixture (the "equivalent normal-mode * recording" ground truth) -- same technique test_sim_resource_ * contention.c's own fixture_init()/make_job()/enqueue_*()/ * picker_thread_fn()/run_one_real_session() establish. * ====================================================================== */ static int g_slots_residx_normal = -1; /* set once by fixture_init_normal(), * read by capacity_aware_allocator_ * thread_fn() below (thread entry * points can't take extra args * beyond the single void* pthread * gives them) */ static void fixture_init_normal(xce_scheduler_t* sched, char* record_dir) { xce_sched_plugin_t* fcfs; int rc; int residx; 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; residx = register_slots_resource(&(sched->core)); add_worker(&(sched->core), residx); g_slots_residx_normal = residx; } 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("contparity_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)); } 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 float lookup_want(uint64_t jobid) { if (jobid == JOBA_ID) { return JOBA_WANT; } if (jobid == JOBB_ID) { return JOBB_WANT; } if (jobid == JOBC_ID) { return JOBC_WANT; } if (jobid == JOBD_ID) { return JOBD_WANT; } return 0.0f; } /* SESSION_FROM - the `from` an allocation decided in a session that began at * `begin_ns` (epoch nanoseconds) carries: the session start in whole * seconds, floor division (libxce-alloc-preason-equality Decision 8; * scheduler.c's sched_make_primary_alloc_() stamps it, and in simulation * mode the session starts at the recorded beginRound). Hand-built recorded * DISPATCH fixtures derive their `from` from the begin_round they are * recorded under, so a live replay of the same round reproduces it. */ #define SESSION_FROM(begin_ns) ((uint64_t)(begin_ns) / 1000000000ULL) /* The recorded round-1 beginRound the hand-built DISPATCH fixtures below * are appended under (logical nanoseconds). */ #define ROUND1_BEGIN_NS 2000ULL /* Builds the wire-shaped xce_job_alloc_t a DISPATCH decision carries -- * shared by BOTH Part A's real dispatch construction below AND Part B's * hand-crafted decisions further down -- same field shape * test_sim_verify.c's own make_test_alloc()/make_test_host_alloc()/ * make_test_job_alloc() establish. */ static xce_job_alloc_t* make_dispatch_alloc(uint64_t jobid, const char* host, float value, uint64_t from) { xce_alloc_t* res = (xce_alloc_t*)calloc(1, sizeof(*res)); xce_host_alloc_t* h = (xce_host_alloc_t*)calloc(1, sizeof(*h)); xce_host_alloc_t** hosts = (xce_host_alloc_t**)calloc(1, sizeof(*hosts)); xce_alloc_t** allocs = (xce_alloc_t**)calloc(1, sizeof(*allocs)); xce_job_alloc_t* j = (xce_job_alloc_t*)calloc(1, sizeof(*j)); TEST_ASSERT_NOT_NULL(res); TEST_ASSERT_NOT_NULL(h); TEST_ASSERT_NOT_NULL(hosts); TEST_ASSERT_NOT_NULL(allocs); TEST_ASSERT_NOT_NULL(j); res->resname = strdup("slots"); res->reserve = strdup("run"); res->instid = 1; res->value = value; res->from = from; res->duration = 0; allocs[0] = res; h->host = strdup(host); h->nallocs = 1; h->allocs = allocs; hosts[0] = h; j->jobid = jobid; j->nhosts = 1; j->allocs = hosts; j->nres_shared = 0; j->allocs_shared = NULL; return j; } /* Builds the accounting-charge shape sched_charge_account()/ * sched_commit_job_alloc() consume -- same field shape * test_resource_accounting.c's own make_test_account() establishes, * parametrized on `residx` (this file's own register_slots_resource() * result) rather than a hardcoded index. */ static xce_job_account_t* make_charge_account(const char* host, int residx, float value) { xce_job_account_t* acct = (xce_job_account_t*)calloc(1, sizeof(*acct)); TEST_ASSERT_NOT_NULL(acct); acct->nres_shared = 0; acct->accounts_shared = NULL; acct->nhosts = 1; acct->accounts = (xce_host_account_t**)calloc(1, sizeof(*acct->accounts)); TEST_ASSERT_NOT_NULL(acct->accounts); acct->accounts[0] = (xce_host_account_t*)calloc(1, sizeof(xce_host_account_t)); TEST_ASSERT_NOT_NULL(acct->accounts[0]); acct->accounts[0]->host = strdup(host); acct->accounts[0]->naccount = 1; acct->accounts[0]->accounts = (xce_res_account_t**)calloc(1, sizeof(*acct->accounts[0]->accounts)); TEST_ASSERT_NOT_NULL(acct->accounts[0]->accounts); acct->accounts[0]->accounts[0] = (xce_res_account_t*)calloc(1, sizeof(xce_res_account_t)); TEST_ASSERT_NOT_NULL(acct->accounts[0]->accounts[0]); acct->accounts[0]->accounts[0]->residx = residx; acct->accounts[0]->accounts[0]->used = value; acct->accounts[0]->accounts[0]->reserved = 0.0; return acct; } /* Capacity-aware stand-in allocator -- same technique test_sim_resource_ * contention.c's own capacity_aware_allocator_thread_fn() establishes * (drives the REAL sched_check_host_satisfy()/sched_charge_account()/ * sched_commit_job_alloc()), with ONE addition this file's own header * comment documents: after a real dispatch, job->state is set directly to * JOB_STAT_RUN. This is required for sched_account_jobs()'s own * JOB_IS_RUN() guard to recognize the job as accounted-for in a LATER * round -- without it, round 2's re-charge would skip jobs A/C entirely * (their `state` would still read JOB_STAT_PEND, since dispatching a job * via sched_commit_job_alloc()/on_dispatch() alone -- exactly like the * real production sched_commit_decision() path -- never touches * job->state; only a separate JOB_STATE_CHANGE mutates it). This mirrors * what a real system eventually confirms via a worker-reported state * change, done directly here rather than through the full * sched_job_state_change()/plugin_job_state_change() machinery (out of * this file's own boundary -- scheduler.c is not white-box included). */ static void* capacity_aware_allocator_thread_fn(void* arg) { xce_scheduler_t* sched = (xce_scheduler_t*)arg; xce_sched_core_t* core = &(sched->core); 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); /* drain the lone 0 sentinel */ cbuf_put(&(core->decisions), &decision); return NULL; } for (;;) { xce_decision_t* decision; xce_sched_job_t* job; xce_sched_worker_t* w; xvec_t uncomitted; xvec_idval_t wanted; xvec_idval_t unmeet; float want; int rc; cbuf_get(&(core->jbuf), &jobid); if (jobid == 0) { decision = NULL; cbuf_put(&(core->decisions), &decision); break; } job = sched_get_job(core, jobid); w = sched_get_worker(core, (char*)WORKER_NAME); TEST_ASSERT_NOT_NULL(job); TEST_ASSERT_NOT_NULL(w); want = lookup_want(jobid); rc = xvec_init(&uncomitted, 1, sizeof(float)); TEST_ASSERT_TRUE(rc >= 0); rc = xvec_idval_init(&wanted, 1); TEST_ASSERT_EQUAL_INT(0, rc); rc = xvec_idval_store(&wanted, g_slots_residx_normal, want); TEST_ASSERT_TRUE(rc >= 0); rc = sched_check_host_satisfy(core, w, &uncomitted, &wanted, &unmeet); xvec_fini(&uncomitted); xvec_idval_fini(&wanted); xvec_idval_fini(&unmeet); if (rc == 1) { job->accounts = make_charge_account(WORKER_NAME, g_slots_residx_normal, want); sched_commit_job_alloc(core, job); /* Stamp `from` exactly as sched_make_primary_alloc_() * (scheduler.c) stamps a real allocation: the deciding * session's start in whole seconds. */ job->allocs = make_dispatch_alloc( jobid, WORKER_NAME, want, SESSION_FROM(core->session_start)); /* This file's own addition -- see this function's header * comment for why this is required. */ job->state = JOB_STAT_RUN; picker->picker_iface->on_dispatch(picker, jobid); decision = make_dispatch_decision(job->allocs); } else { picker->picker_iface->on_pend(picker, jobid); decision = make_pend_decision(make_pendreason(jobid)); } if (decision != NULL) { 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, capacity_aware_allocator_thread_fn, sched); 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); } /* ====================================================================== * Part B: hand-crafted `.replay` fixture for simulation-mode reconciliation * -- same technique test_sim_round_boundary_reconciliation.c's own header * comment documents (sched_record_start() for a real header+snapshot * prefix, then hand-constructed ReplayEvent/ReplaySession messages * appended via the real encode_ReplayEvent_ptr()/encode_ReplaySession_ptr()/ * write_capnp_message() codec). * ====================================================================== */ static void fixture_init_seed(xce_scheduler_t* sched, char* record_dir) { int residx; memset(sched, 0, sizeof(*sched)); mylock_init(&(sched->record_lock)); TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched->core))); sched->core.job_pick_plugin = (char*)"builtin_fcfs"; sched->record_dir = record_dir; residx = register_slots_resource(&(sched->core)); add_worker(&(sched->core), residx); } static void append_queue_update_event(FILE* f, uint64_t ts, const char* queue_name) { xce_queue_t queue; replay_event_t src; struct capn c; struct capn_segment* cs; ReplayEvent_ptr p; memset(&queue, 0, sizeof(queue)); queue.queue = (char*)queue_name; queue.status = QUEUE_STAT_OPEN; queue.priority = 1; memset(&src, 0, sizeof(src)); src.ts = ts; src.body_which = ReplayEvent_body_queueUpdate; src.body.queue_update = &queue; capn_init_malloc(&c); cs = capn_root(&c).seg; encode_ReplayEvent_ptr(cs, &p, &src); capn_setp(capn_root(&c), 0, p.p); TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c)); capn_free(&c); } static void append_jobs_push_event(FILE* f, uint64_t ts, uint64_t jobid, const char* queue_name) { job_submit_data_t submit; xce_job_t job; xce_job_t* jobs[1]; xce_jobs_data_t jobs_data; replay_event_t src; struct capn c; struct capn_segment* cs; ReplayEvent_ptr p; memset(&submit, 0, sizeof(submit)); submit.queue = (char*)queue_name; submit.user = (char*)"alice"; submit.mintasks = 1; submit.maxtasks = 1; memset(&job, 0, sizeof(job)); job.jobid = jobid; job.state = JOB_STAT_PEND; job.submit = &submit; jobs[0] = &job; memset(&jobs_data, 0, sizeof(jobs_data)); jobs_data.n_total = 1; jobs_data.count = 1; jobs_data.jobs = jobs; memset(&src, 0, sizeof(src)); src.ts = ts; src.body_which = ReplayEvent_body_jobsPush; src.body.jobs_push = &jobs_data; capn_init_malloc(&c); cs = capn_root(&c).seg; encode_ReplayEvent_ptr(cs, &p, &src); capn_setp(capn_root(&c), 0, p.p); TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c)); capn_free(&c); } static void append_job_state_change_event(FILE* f, uint64_t ts, uint64_t jobid, uint32_t old_state, uint32_t new_state) { job_state_change_event_t jsc; replay_event_t src; struct capn c; struct capn_segment* cs; ReplayEvent_ptr p; memset(&jsc, 0, sizeof(jsc)); jsc.jobid = jobid; jsc.old_state = old_state; jsc.new_state = new_state; jsc.exit_code = 0; memset(&src, 0, sizeof(src)); src.ts = ts; src.body_which = ReplayEvent_body_jobStateChange; src.body.job_state_change = &jsc; capn_init_malloc(&c); cs = capn_root(&c).seg; encode_ReplayEvent_ptr(cs, &p, &src); capn_setp(capn_root(&c), 0, p.p); TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c)); capn_free(&c); } static void append_end_of_stream_marker(FILE* f) { replay_event_t src; struct capn c; struct capn_segment* cs; ReplayEvent_ptr p; memset(&src, 0, sizeof(src)); src.body_which = ReplayEvent_body_endSession; capn_init_malloc(&c); cs = capn_root(&c).seg; encode_ReplayEvent_ptr(cs, &p, &src); capn_setp(capn_root(&c), 0, p.p); TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c)); capn_free(&c); } static replay_decision_t* make_decision_dispatch(uint64_t ts, xce_job_alloc_t* alloc) { sched_dispatch_decision_t* dispatch = (sched_dispatch_decision_t*)calloc(1, sizeof(*dispatch)); replay_decision_t* d = (replay_decision_t*)calloc(1, sizeof(*d)); TEST_ASSERT_NOT_NULL(dispatch); TEST_ASSERT_NOT_NULL(d); dispatch->alloc = alloc; d->ts = ts; d->body_which = ReplayDecision_body_dispatch; d->body.dispatch = dispatch; return d; } static replay_decision_t* make_decision_pend(uint64_t ts, uint64_t jobid, int main_reason) { sched_pend_decision_t* pend = (sched_pend_decision_t*)calloc(1, sizeof(*pend)); xce_job_preason_t* reason = (xce_job_preason_t*)calloc(1, sizeof(*reason)); replay_decision_t* d = (replay_decision_t*)calloc(1, sizeof(*d)); TEST_ASSERT_NOT_NULL(pend); TEST_ASSERT_NOT_NULL(reason); TEST_ASSERT_NOT_NULL(d); reason->main_reason = main_reason; pend->jobid = jobid; pend->reason = reason; d->ts = ts; d->body_which = ReplayDecision_body_pend; d->body.pend = pend; return d; } static void append_replay_session(FILE* f, uint64_t begin_round, replay_decision_t** decisions, int n_decisions) { replay_session_t session; struct capn c; struct capn_segment* cs; ReplaySession_ptr sp; memset(&session, 0, sizeof(session)); session.begin_round = begin_round; session.decisions = decisions; session.n_decisions = n_decisions; session.summary = NULL; capn_init_malloc(&c); cs = capn_root(&c).seg; encode_ReplaySession_ptr(cs, &sp, &session); capn_setp(capn_root(&c), 0, sp.p); TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c)); capn_free(&c); } /* ---------------------------------------------------------------------- */ /* The central regression test (task 4.1's own Observable Completion). */ /* ---------------------------------------------------------------------- */ static void test_contention_parity_matches_real_normal_mode_recording_round_by_round( void) { /* ---- Part A: real, driven normal-mode recording, 2 rounds ---- */ char record_dir_normal[PATH_MAX]; xce_scheduler_t sched; char r_path[PATH_MAX]; xce_job_t** jobs3; xce_job_t** jobs1; xce_sched_worker_t* w_normal; xce_sched_job_t* jobA_normal; xce_sched_job_t* jobB_normal; xce_sched_job_t* jobC_normal; xce_sched_job_t* jobD_normal; xce_resource_t* r_normal = NULL; int residx_normal = -1; float got; float normal_resinuse_after_round1; float normal_resinuse_after_round2; int rc; /* ---- Part B: hand-crafted simulation-mode fixture, 2 rounds ---- */ char record_dir_sim[PATH_MAX]; xce_scheduler_t sched_seed; FILE* f; char sim_r_path[PATH_MAX]; xce_job_alloc_t* allocA; xce_job_alloc_t* allocC; replay_decision_t* round1_decisions[3]; replay_decision_t* round2_decisions[1]; sched_sim_args_t sim_args; xce_sched_sim_driver_t sim; xce_sched_worker_t* w_sim; xce_sched_job_t* jobA_sim; xce_sched_job_t* jobB_sim; xce_sched_job_t* jobC_sim; xce_sched_job_t* jobD_sim; xce_resource_t* r_sim = NULL; int residx_sim = -1; /* ================================================================== * Part A * ================================================================== */ make_tmp_dir(record_dir_normal, sizeof(record_dir_normal), "_normal"); fixture_init_normal(&sched, record_dir_normal); residx_normal = g_slots_residx_normal; rc = sched_record_start(&sched, "contention_parity_normal.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); jobs3 = (xce_job_t**)calloc(3, sizeof(xce_job_t*)); jobs3[0] = make_job(JOBA_ID, 1000ULL, "normal", "alice"); jobs3[1] = make_job(JOBB_ID, 1001ULL, "normal", "bob"); jobs3[2] = make_job(JOBC_ID, 1002ULL, "normal", "carol"); enqueue_jobs_push(&sched, jobs3, 3); run_one_real_session(&sched); /* round 1: A+C real dispatch, B real deny */ jobA_normal = sched_get_job(&(sched.core), JOBA_ID); jobB_normal = sched_get_job(&(sched.core), JOBB_ID); jobC_normal = sched_get_job(&(sched.core), JOBC_ID); TEST_ASSERT_NOT_NULL(jobA_normal); TEST_ASSERT_NOT_NULL(jobB_normal); TEST_ASSERT_NOT_NULL(jobC_normal); TEST_ASSERT_NOT_NULL_MESSAGE( jobA_normal->accounts, "sanity: normal mode must have really dispatched job A this round"); TEST_ASSERT_NULL_MESSAGE( jobB_normal->accounts, "sanity: normal mode must have really denied job B this round"); TEST_ASSERT_NOT_NULL_MESSAGE( jobC_normal->accounts, "sanity: normal mode must have really dispatched job C this round"); w_normal = sched_get_worker(&(sched.core), (char*)WORKER_NAME); TEST_ASSERT_NOT_NULL(w_normal); rc = key_find(sched.core.resmgr, (void*)SLOTS_RESNAME, (void**)&r_normal); TEST_ASSERT_TRUE(rc >= 0); TEST_ASSERT_NOT_NULL(r_normal); TEST_ASSERT_EQUAL_INT(residx_normal, r_normal->idx); got = -1.0f; rc = get_resvals(&(w_normal->resInUse), 1, &residx_normal, &got); TEST_ASSERT_EQUAL_INT(1, rc); /* get_resvals() returns nres on success */ normal_resinuse_after_round1 = got; TEST_ASSERT_EQUAL_FLOAT_MESSAGE( JOBA_WANT + JOBC_WANT, normal_resinuse_after_round1, "sanity: worker-a's real resInUse after round 1 must be A+C's " "combined charge (8.0+2.0=10.0, exactly saturating capacity)"); /* Round 2: push job D. B is genuinely re-offered as a candidate too * (builtin_fcfs_on_pend()'s own documented "no-op -- retried next * round" contract), so this round's real session evaluates BOTH B and * D -- both must be denied, capacity is fully saturated by A+C. */ jobs1 = (xce_job_t**)calloc(1, sizeof(xce_job_t*)); jobs1[0] = make_job(JOBD_ID, 2000ULL, "normal", "dave"); enqueue_jobs_push(&sched, jobs1, 1); run_one_real_session(&sched); /* round 2 */ jobD_normal = sched_get_job(&(sched.core), JOBD_ID); TEST_ASSERT_NOT_NULL(jobD_normal); TEST_ASSERT_NULL_MESSAGE( jobD_normal->accounts, "sanity: normal mode must genuinely deny job D too -- capacity is " "fully saturated by A+C, zero headroom remains"); TEST_ASSERT_NULL_MESSAGE( jobB_normal->accounts, "sanity: job B must still be denied in round 2 too (re-offered, " "re-evaluated, still no capacity)"); got = -1.0f; rc = get_resvals(&(w_normal->resInUse), 1, &residx_normal, &got); TEST_ASSERT_EQUAL_INT(1, rc); normal_resinuse_after_round2 = got; TEST_ASSERT_EQUAL_FLOAT_MESSAGE( normal_resinuse_after_round1, normal_resinuse_after_round2, "sanity: normal mode's resInUse must be unchanged in round 2 -- " "nothing new was charged (D denied)"); rc = sched_record_stop(&sched); TEST_ASSERT_EQUAL_INT(0, rc); /* ================================================================== * Part B: hand-crafted simulation-mode fixture modeling the SAME facts * ================================================================== */ make_tmp_dir(record_dir_sim, sizeof(record_dir_sim), "_sim"); fixture_init_seed(&sched_seed, record_dir_sim); rc = sched_record_start(&sched_seed, "contention_parity_sim_input.replay"); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_TRUE(strlen(sched_seed.record_file_path) < sizeof(sim_r_path)); strcpy(sim_r_path, sched_seed.record_file_path); f = sched_seed.record_file; /* Round 1: jobs A/B/C pushed PEND, then A and C flip to RUN via * JOB_STATE_CHANGE (the signature of "already resolved before this * replay's own live picker reaches them", design.md's own Recorded- * Decision Reconciliation trigger condition) -- BEFORE round 1's own * ReplaySession, which carries the matching recorded DISPATCH * decisions for A and C (SAME want-values as Part A's real dispatch: * JOBA_WANT/JOBC_WANT, never re-typed) plus a genuine PEND for B (no * reconciliation needed -- sim's own live picker independently PENDs * it, matching normal mode's real denial). */ append_queue_update_event(f, 1000ULL, "normal"); append_jobs_push_event(f, 1500ULL, JOBA_ID, "normal"); append_jobs_push_event(f, 1500ULL, JOBB_ID, "normal"); append_jobs_push_event(f, 1500ULL, JOBC_ID, "normal"); append_job_state_change_event(f, 1800ULL, JOBA_ID, JOB_STAT_PEND, JOB_STAT_RUN); append_job_state_change_event(f, 1800ULL, JOBC_ID, JOB_STAT_PEND, JOB_STAT_RUN); allocA = make_dispatch_alloc(JOBA_ID, WORKER_NAME, JOBA_WANT, SESSION_FROM(ROUND1_BEGIN_NS)); allocC = make_dispatch_alloc(JOBC_ID, WORKER_NAME, JOBC_WANT, SESSION_FROM(ROUND1_BEGIN_NS)); round1_decisions[0] = make_decision_dispatch(ROUND1_BEGIN_NS, allocA); round1_decisions[1] = make_decision_dispatch(ROUND1_BEGIN_NS, allocC); round1_decisions[2] = make_decision_pend(ROUND1_BEGIN_NS, JOBB_ID, 0); append_replay_session(f, ROUND1_BEGIN_NS, round1_decisions, 3); /* Round 2: job D pushed fresh -- no reconciliation involved (D was * never dispatched anywhere); sim's own live picker independently * PENDs it. The recorded decision embedded here (a PEND, matching what * a real system would ALSO record) is not itself consulted by * reconciliation or asserted on directly -- it exists only so this * round has a structurally normal, non-empty ReplaySession boundary * message. */ append_jobs_push_event(f, 3000ULL, JOBD_ID, "normal"); round2_decisions[0] = make_decision_pend(4000ULL, JOBD_ID, 0); append_replay_session(f, 4000ULL, round2_decisions, 1); append_end_of_stream_marker(f); fclose(f); memset(&sim_args, 0, sizeof(sim_args)); sim_args.replay_path = sim_r_path; sim_args.sim_start_ts = SCHED_SIM_START_TS_LATEST; memset(&sim, 0, sizeof(sim)); g_capturing = 1; g_round_idx = 0; rc = sched_sim_driver_run(&sim, &sim_args); g_capturing = 0; TEST_ASSERT_EQUAL_INT(0, rc); /* 3, not 2: scheduler-snapshot-job-allocation task 4.6 added ONE extra * sched_account_jobs() call, inside xce_sched_core_load_snapshot() * itself (snapshot_load.c), which runs during sched_sim_driver_start() * -- strictly before either replayed round -- so it lands at * g_round_resinuse[0], pushing round 1/round 2's own snapshots to * indices 1/2 (see the shifted indices below). This fixture's initial * snapshot has no RUN-state jobs yet (A/B/C/D are all dispatched via * recorded round decisions, never present at the replay's own starting * snapshot), so this extra call charges nothing -- g_round_resinuse[0] * is expected to read 0.0, not a real round observation. */ TEST_ASSERT_EQUAL_INT_MESSAGE( 3, g_round_idx, "sched_account_jobs() must have been called exactly once per " "replayed round (2 rounds in this fixture) PLUS once at snapshot- " "load time (task 4.6) -- if this isn't 3, either the round-boundary " "snapshot mechanism itself is miscounting, or the load-time call " "is missing/duplicated, independent of the parity assertions below"); /* ================================================================== * Part C: the actual parity assertions * ================================================================== */ /* Requirement 1.4: sim mode's round-by-round resInUse must match what * the real normal-mode recording observed at the SAME point -- not * just at the final round. Indices start at 1, not 0: index 0 is the * load-time call above, not a round. */ TEST_ASSERT_EQUAL_FLOAT_MESSAGE( normal_resinuse_after_round1, g_round_resinuse[1], "round 1: simulation mode's reconciled resInUse must byte-for-byte " "match what the real normal-mode recording actually observed after " "its own round 1 (jobs A+C dispatched, consuming 10.0 of 10.0 " "total capacity)"); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( normal_resinuse_after_round2, g_round_resinuse[2], "round 2: simulation mode's resInUse must still match normal " "mode's -- the already-consumed capacity must PERSIST correctly " "into a SECOND round, not just the first"); /* Requirement 2.1/2.2 (decision-level): job A/C dispatched with the * SAME allocation amount, job B/D denied, in BOTH modes. */ jobA_sim = sched_get_job(&(sim.core), JOBA_ID); jobB_sim = sched_get_job(&(sim.core), JOBB_ID); jobC_sim = sched_get_job(&(sim.core), JOBC_ID); jobD_sim = sched_get_job(&(sim.core), JOBD_ID); TEST_ASSERT_NOT_NULL(jobA_sim); TEST_ASSERT_NOT_NULL(jobB_sim); TEST_ASSERT_NOT_NULL(jobC_sim); TEST_ASSERT_NOT_NULL(jobD_sim); TEST_ASSERT_NOT_NULL_MESSAGE( jobA_sim->accounts, "simulation mode must have reconciled job A's dispatch, matching " "normal mode's real dispatch decision"); TEST_ASSERT_NULL_MESSAGE( jobB_sim->accounts, "simulation mode must show job B denied, matching normal mode's " "real denial (dispatch where normal pended, or vice versa, must " "never happen)"); TEST_ASSERT_NOT_NULL_MESSAGE( jobC_sim->accounts, "simulation mode must have reconciled job C's dispatch, matching " "normal mode's real dispatch decision"); TEST_ASSERT_NULL_MESSAGE( jobD_sim->accounts, "simulation mode must show job D denied too, matching normal " "mode's real denial in round 2"); /* NOTE: the reconciled xce_job_account_t is built by the REAL * production make_job_account()/make_res_account() (xcealloc.c), which * stores the dispatched amount into .reserved/.allocated (and leaves * .used == 0.0 until sched_charge_account() actually charges it into * the WORKER's resInUse -- .used on the per-job xce_res_account_t * itself is never separately updated by that charge). This differs * from Part A's own make_charge_account() helper above (a test-only * construction that sets .used directly) -- check .reserved here, * matching make_res_account()'s own field mapping * (account->reserved = alloc->value). */ TEST_ASSERT_EQUAL_FLOAT_MESSAGE( JOBA_WANT, jobA_sim->accounts->accounts[0]->accounts[0]->reserved, "job A's reconciled charge amount must match the real dispatch's " "own amount (8.0)"); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( JOBC_WANT, jobC_sim->accounts->accounts[0]->accounts[0]->reserved, "job C's reconciled charge amount must match the real dispatch's " "own amount (2.0)"); /* Requirement 2's capacity-awareness, proven directly against BOTH * drivers' independently-derived post-round state via the SAME real, * unmodified sched_check_host_satisfy() -- mirroring * test_sim_round_boundary_reconciliation.c's own technique, run here * against BOTH sides, not just sim's. A hypothetical job wanting * JOBB_WANT (5.0 -- the SAME amount the real recording genuinely * denied, reused rather than inventing a new number) must be denied * against BOTH post-round-2 states: if reconciliation were broken * (job A/C never reconciled, resInUse wrongly reads 0.0), this SAME * check would wrongly GRANT it on the simulation side -- this is * this test's own discriminating-power proof (see this file's header * comment; independently confirmed live by reverting the production * fix). */ { xce_scheduler_t wrapper_sim; xvec_t uncomitted; xvec_idval_t wanted; xvec_idval_t unmeet; w_sim = sched_get_worker(&(sim.core), (char*)WORKER_NAME); TEST_ASSERT_NOT_NULL(w_sim); rc = key_find(sim.core.resmgr, (void*)SLOTS_RESNAME, (void**)&r_sim); TEST_ASSERT_TRUE(rc >= 0); TEST_ASSERT_NOT_NULL(r_sim); residx_sim = r_sim->idx; memset(&wrapper_sim, 0, sizeof(wrapper_sim)); wrapper_sim.core = sim.core; /* Normal mode's own post-round-2 state. */ rc = xvec_init(&uncomitted, 1, sizeof(float)); TEST_ASSERT_TRUE(rc >= 0); rc = xvec_idval_init(&wanted, 1); TEST_ASSERT_EQUAL_INT(0, rc); rc = xvec_idval_store(&wanted, residx_normal, JOBB_WANT); TEST_ASSERT_TRUE(rc >= 0); rc = sched_check_host_satisfy(&(sched.core), w_normal, &uncomitted, &wanted, &unmeet); TEST_ASSERT_EQUAL_INT_MESSAGE( 0, rc, "normal mode's real post-round-2 state must deny a hypothetical " "job wanting 5.0 -- zero headroom remains"); xvec_fini(&uncomitted); xvec_idval_fini(&wanted); xvec_idval_fini(&unmeet); /* Simulation mode's own post-replay state -- the SAME real, * unmodified capacity-check function, against sim's OWN * independently-derived resInUse. */ rc = xvec_init(&uncomitted, 1, sizeof(float)); TEST_ASSERT_TRUE(rc >= 0); rc = xvec_idval_init(&wanted, 1); TEST_ASSERT_EQUAL_INT(0, rc); rc = xvec_idval_store(&wanted, residx_sim, JOBB_WANT); TEST_ASSERT_TRUE(rc >= 0); rc = sched_check_host_satisfy(&(wrapper_sim.core), w_sim, &uncomitted, &wanted, &unmeet); TEST_ASSERT_EQUAL_INT_MESSAGE( 0, rc, "simulation mode's post-replay state must ALSO deny a " "hypothetical job wanting 5.0 -- matching normal mode's own " "denial above, proving Requirement 2's capacity-awareness " "holds identically in both drivers"); xvec_fini(&uncomitted); xvec_idval_fini(&wanted); xvec_idval_fini(&unmeet); } printf("test_contention_parity_matches_real_normal_mode_recording_" "round_by_round: PASS\n"); } /* ====================================================================== * scheduler-sim-eager-reconciliation task 3.1 (Req 1.1-1.4), REVISED * 2026-09-11 (user-directed, requirements.md Requirement 1's revision): * a resource-contention fixture where a job's recorded DISPATCH decision * and its JOB_STATE_CHANGE-to-RUN confirmation are separated by SEVERAL * rounds -- not the same-round shape the test above already covers (there, * JOB_STATE_CHANGE lands in the SAME round window as the matching DISPATCH * decision, which is exactly the shape the OLD, pre-task-1.1 trigger gate * (`!JOB_IS_RUN(job->state)` in sim_reconcile_unaccounted_jobs(), removed * by task 1.1) already handled correctly). Jobs A and C are PEND at the * exact round their own DISPATCH decision is recorded (round 1), and only * reach a SEPARATELY-RECORDED RUN confirmation several rounds later * (rounds 4 and 5 respectively) via a bare JOB_STATE_CHANGE event with no * DISPATCH decision of its own -- by which point round 1's own * decoded_session (the only place their DISPATCH ever existed) is long * freed. * * Pre-task-1.1: job->accounts stayed NULL forever for both jobs (the OLD * `!JOB_IS_RUN` gate never re-searched a PEND job past its own round), so * resInUse read a PERMANENTLY-ZERO 0.0 for the rest of the replay. * * Task 1.1 through 2026-09-07: `sim_reconcile_unaccounted_jobs()` captured * job->allocs/job->accounts at round 1 regardless of state, but LEFT * job->state untouched -- charging stayed deferred to sched_account_jobs()'s * unchanged JOB_IS_RUN gate until the SEPARATELY-recorded JOB_STATE_CHANGE * event replayed at round 4/5, several rounds after capture. * * 2026-09-11 revision (current, this fixture): capture now ALSO sets * job->state = JOB_STAT_RUN synchronously (mirroring scheduler.c's * sched_commit_decision(), bug-1624) -- so BOTH jobs A and C are * genuinely RUN and charged starting round 1 itself, the SAME round their * DISPATCH decision is captured, not deferred to rounds 4/5. The later, * separately-recorded JOB_STATE_CHANGE-to-RUN events at rounds 4/5 are * still replayed (this fixture keeps them, unchanged, to prove they * remain safe/idempotent) but no longer change resInUse at all -- they * re-affirm an already-RUN job. This test's core value survives * unchanged: prove capacity accounting is CORRECT across a realistic * multi-round dispatch-to-confirmation gap, not permanently wrong for the * rest of the replay (the pre-task-1.1 bug) -- but "correct" now means * "already fully accounted from round 1", not "deferred until * confirmation." * * Fixture shape (reuses this file's own established constants verbatim -- * WORKER_CAPACITY=10.0, JOBA_WANT=8.0, JOBB_WANT=5.0, JOBC_WANT=2.0, * JOBD_WANT=1.0 -- never re-typed): * Round 1 (begin_round=2000): A/B/C pushed PEND; A(8.0) and C(2.0) * dispatched (A+C=10.0, exactly saturating capacity) -- BOTH * genuinely RUN and charged from this round onward (2026-09-11 * revision), B genuinely denied. NEITHER A NOR C's JOB_STATE_CHANGE * appears anywhere near this round. * Round 2, 3 (begin_round=3000/4000): pure filler -- B is genuinely * re-offered and re-denied each round (builtin_fcfs's own documented * "no-op, retried next round" contract), widening the gap before A/C's * eventual (now redundant) confirmations below to genuinely "several * rounds", and proving the round-1 charge persists correctly across * MULTIPLE idle rounds, not just one. * Round 4 (begin_round=5000): job A's SEPARATELY-recorded JOB_STATE_ * CHANGE to RUN replays -- 3 rounds after its own round-1 DISPATCH. * A safe no-op re-affirmation (already RUN since round 1); resInUse * stays at the SAME full 10.0 it already reached at round 1. * Round 5 (begin_round=6000): job C's SEPARATELY-recorded JOB_STATE_ * CHANGE to RUN replays -- 4 rounds after ITS OWN round-1 DISPATCH (a * deliberately DIFFERENT separation than job A's 3 rounds, proving * this isn't a single-job coincidence). Also a safe no-op; resInUse * is unchanged, still 10.0. * * The single test function below drives this SAME fixture shape three * times, truncated after round 1, round 4, and round 5 respectively (via * `write_multiround_contention_rounds()`'s `upto_round` parameter) -- * sched_sim_driver_run() has no mid-replay pause point outside the REPL * (this file's own earlier header comment already documents this gap), so * a direct sched_check_host_satisfy() proof at an EARLY and a MID-replay * point each need their own independently-run, truncated copy of the * stream. The round-boundary g_round_resinuse[] snapshot mechanism (this * file's own earlier addition) is reused for the full 5-round run to prove * every round in between as well, not just the three checkpoints. * ====================================================================== */ static void write_multiround_contention_rounds(FILE* f, int upto_round) { xce_job_alloc_t* allocA; xce_job_alloc_t* allocC; replay_decision_t* round1_decisions[3]; replay_decision_t* filler_decisions[1]; if (upto_round < 1) { return; } append_queue_update_event(f, 1000ULL, "normal"); append_jobs_push_event(f, 1500ULL, JOBA_ID, "normal"); append_jobs_push_event(f, 1500ULL, JOBB_ID, "normal"); append_jobs_push_event(f, 1500ULL, JOBC_ID, "normal"); allocA = make_dispatch_alloc(JOBA_ID, WORKER_NAME, JOBA_WANT, SESSION_FROM(ROUND1_BEGIN_NS)); allocC = make_dispatch_alloc(JOBC_ID, WORKER_NAME, JOBC_WANT, SESSION_FROM(ROUND1_BEGIN_NS)); round1_decisions[0] = make_decision_dispatch(ROUND1_BEGIN_NS, allocA); round1_decisions[1] = make_decision_dispatch(ROUND1_BEGIN_NS, allocC); round1_decisions[2] = make_decision_pend(ROUND1_BEGIN_NS, JOBB_ID, 0); append_replay_session(f, ROUND1_BEGIN_NS, round1_decisions, 3); if (upto_round < 2) { return; } /* Round 2: idle filler -- no new events. A and C are still PEND; * proves the deferred-zero state from round 1 persists across an * extra round without a spurious re-capture attempt. */ filler_decisions[0] = make_decision_pend(3000ULL, JOBB_ID, 0); append_replay_session(f, 3000ULL, filler_decisions, 1); if (upto_round < 3) { return; } /* Round 3: a SECOND idle filler round, widening the gap before A's * confirmation below to genuinely "several rounds", not just one. */ filler_decisions[0] = make_decision_pend(4000ULL, JOBB_ID, 0); append_replay_session(f, 4000ULL, filler_decisions, 1); if (upto_round < 4) { return; } /* Round 4: job A's genuine worker-confirmed RUN transition, recorded 3 * rounds after its own round-1 DISPATCH decision -- the realistic * dispatch-to-confirmation latency this spec exists to fix. */ append_job_state_change_event(f, 4500ULL, JOBA_ID, JOB_STAT_PEND, JOB_STAT_RUN); filler_decisions[0] = make_decision_pend(5000ULL, JOBB_ID, 0); append_replay_session(f, 5000ULL, filler_decisions, 1); if (upto_round < 5) { return; } /* Round 5: job C's genuine worker-confirmed RUN transition, recorded 4 * rounds after ITS OWN round-1 DISPATCH decision -- a deliberately * DIFFERENT round-separation than job A's (3 rounds), proving this * isn't a single-job coincidence. */ append_job_state_change_event(f, 5500ULL, JOBC_ID, JOB_STAT_PEND, JOB_STAT_RUN); filler_decisions[0] = make_decision_pend(6000ULL, JOBB_ID, 0); append_replay_session(f, 6000ULL, filler_decisions, 1); } /* Builds+runs a truncated copy of write_multiround_contention_rounds()'s * fixture (up to `upto_round`) and returns the resulting driver state via * `*sim_out` -- shared setup for all three checkpoints below. `label` is * only used to keep each checkpoint's tmp dir/file name distinct. */ static void run_multiround_contention_fixture(xce_sched_sim_driver_t* sim_out, int upto_round, const char* label) { char record_dir[PATH_MAX]; xce_scheduler_t sched_seed; FILE* f; char r_path[PATH_MAX]; sched_sim_args_t sim_args; int rc; make_tmp_dir(record_dir, sizeof(record_dir), label); fixture_init_seed(&sched_seed, record_dir); rc = sched_record_start(&sched_seed, "contention_mr.replay"); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_TRUE(strlen(sched_seed.record_file_path) < sizeof(r_path)); strcpy(r_path, sched_seed.record_file_path); f = sched_seed.record_file; write_multiround_contention_rounds(f, upto_round); append_end_of_stream_marker(f); fclose(f); memset(&sim_args, 0, sizeof(sim_args)); sim_args.replay_path = r_path; sim_args.sim_start_ts = SCHED_SIM_START_TS_LATEST; memset(sim_out, 0, sizeof(*sim_out)); rc = sched_sim_driver_run(sim_out, &sim_args); TEST_ASSERT_EQUAL_INT(0, rc); } /* Runs a real sched_check_host_satisfy() capacity check against `sim`'s * post-replay worker-a state for a hypothetical job wanting `want` units * of "slots" -- returns sched_check_host_satisfy()'s own rc (1 == would be * granted, 0 == would be denied). Same technique (xce_scheduler_t wrapper * carrying a copy of sim->core) the test above already establishes. */ static int check_host_satisfy_want(xce_sched_sim_driver_t* sim, float want) { xce_scheduler_t wrapper; xce_sched_worker_t* w; xce_resource_t* r = NULL; int residx = -1; xvec_t uncomitted; xvec_idval_t wanted; xvec_idval_t unmeet; int rc; w = sched_get_worker(&(sim->core), (char*)WORKER_NAME); TEST_ASSERT_NOT_NULL(w); rc = key_find(sim->core.resmgr, (void*)SLOTS_RESNAME, (void**)&r); TEST_ASSERT_TRUE(rc >= 0); TEST_ASSERT_NOT_NULL(r); residx = r->idx; memset(&wrapper, 0, sizeof(wrapper)); wrapper.core = sim->core; rc = xvec_init(&uncomitted, 1, sizeof(float)); TEST_ASSERT_TRUE(rc >= 0); rc = xvec_idval_init(&wanted, 1); TEST_ASSERT_EQUAL_INT(0, rc); rc = xvec_idval_store(&wanted, residx, want); TEST_ASSERT_TRUE(rc >= 0); rc = sched_check_host_satisfy(&(wrapper.core), w, &uncomitted, &wanted, &unmeet); xvec_fini(&uncomitted); xvec_idval_fini(&wanted); xvec_idval_fini(&unmeet); return rc; } /* * CORRECTED (2026-09-21, bug-1838, scheduler-driver-equivalence): this * test's entire fixture (write_multiround_contention_rounds() above) is * built around jobs A/B/C pushed fresh PEND at round 1 with a matching * RECORDED DISPATCH decision for A and C -- relying on * sim_reconcile_unaccounted_jobs() to bridge them, exactly the mechanism * bug-1838 narrowed back to JOB_IS_RUN-only. Compounding this, THIS file's * own sched_allocator() override (used whenever sched_sim_driver_run() is * called, as it is here) is a deliberate PEND-only stub (see that * function's own header comment) -- "genuine resource-based dispatch... is * proved separately by test_sim_live_allocation.c". With reconciliation no * longer touching PEND jobs, and this file's own live path structurally * unable to dispatch anything, jobs A/B/C now stay PEND for the entire * fixture, in every round -- resInUse never leaves 0.0, and every * assertion this test used to make (capacity SATURATED by round 1, stays * saturated through rounds 2-5) is now backwards. Inverted below to lock * in the new, correct behavior instead of deleting the coverage outright. * * The genuine "capacity is correctly tracked across a real dispatch-then- * later-confirmation gap" property is now proved via test_sim_live_ * allocation.c's test_sim_verify_no_false_mismatch_after_live_dispatch_ * and_later_confirmation, which links the real scheduler_srcs and can * genuinely dispatch a job -- something this file's PEND-only stub never * could, reconciliation or not. */ static void test_contention_capacity_check_correct_across_multi_round_dispatch_confirm_gap( void) { static xce_sched_sim_driver_t sim1; static xce_sched_sim_driver_t sim4; static xce_sched_sim_driver_t sim5; int rc; g_capturing = 0; g_round_idx = 0; /* ---- Checkpoint 1: right after round 1 -- A and C are pushed PEND * with a matching recorded DISPATCH, but bug-1838's RUN-only * reconciliation trigger does not touch them (still PEND), and this * file's own PEND-only sched_allocator() stub cannot dispatch them * either. Capacity must therefore be WIDE OPEN: even a job wanting * the entire WORKER_CAPACITY must be GRANTED, since nothing has * genuinely been charged. ---- */ run_multiround_contention_fixture(&sim1, 1, "_mr1"); rc = check_host_satisfy_want(&sim1, WORKER_CAPACITY); TEST_ASSERT_EQUAL_INT_MESSAGE( 1, rc, "bug-1838: A and C stay PEND (neither reconciliation -- now " "RUN-only -- nor this file's PEND-only live stub can dispatch " "them), so resInUse stays 0.0 and a job wanting the entire " "WORKER_CAPACITY must be GRANTED, not denied"); /* ---- Checkpoint 2: right after round 4 -- job A's separately- * recorded confirmation event has replayed, but A was never captured * in the first place (still PEND), so this is a WARN-and-skip no-op * (core_events.c logs an unknown-jobid warning for a job it has no * prior RUN record to reconcile against, per that handler's existing * contract) -- it does not fabricate a charge. Capacity remains wide * open. ---- */ run_multiround_contention_fixture(&sim4, 4, "_mr4"); rc = check_host_satisfy_want(&sim4, JOBC_WANT); TEST_ASSERT_EQUAL_INT_MESSAGE( 1, rc, "bug-1838: A/C are still PEND at round 4 -- JOBC_WANT (2.0) " "must be granted against a fully-open worker"); rc = check_host_satisfy_want(&sim4, JOBB_WANT); TEST_ASSERT_EQUAL_INT_MESSAGE( 1, rc, "bug-1838: JOBB_WANT (5.0) must likewise be granted -- " "nothing has ever been charged against this worker"); /* ---- Checkpoint 3: the full 5-round replay -- per-round resInUse * snapshots proving the round-1-through-5 gap stays permanently * zero-charged, not just at the two checkpoints above. ---- */ g_capturing = 1; g_round_idx = 0; run_multiround_contention_fixture(&sim5, 5, "_mr5"); g_capturing = 0; /* 6, not 5: scheduler-snapshot-job-allocation task 4.6's extra * snapshot-load-time sched_account_jobs() call (see the sibling test * above for the full rationale) lands at g_round_resinuse[0], pushing * every round's own snapshot to index+1 below. */ TEST_ASSERT_EQUAL_INT_MESSAGE( 6, g_round_idx, "sched_account_jobs() must have been called exactly once per " "replayed round (5 rounds in this fixture) PLUS once at " "snapshot-load time (task 4.6)"); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( 0.0f, g_round_resinuse[0], "snapshot load: no RUN-state jobs exist yet -- resInUse reads 0.0"); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( 0.0f, g_round_resinuse[1], "bug-1838: round 1 -- A and C are pushed PEND with a matching " "recorded DISPATCH, but RUN-only reconciliation ignores them and " "this file's live stub is PEND-only -- resInUse stays 0.0, not " "the old 10.0 (WORKER_CAPACITY)"); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( 0.0f, g_round_resinuse[2], "round 2: idle filler round -- still 0.0, nothing to charge"); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( 0.0f, g_round_resinuse[3], "round 3: a second idle filler round -- still 0.0"); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( 0.0f, g_round_resinuse[4], "bug-1838: round 4 -- job A's separately-recorded JOB_STATE_" "CHANGE-to-RUN event replays, but A was never captured (still " "PEND, no job->accounts) -- core_events.c's own unknown-jobid " "warn-and-skip path applies; no charge is fabricated"); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( 0.0f, g_round_resinuse[5], "bug-1838: round 5 -- job C's separately-recorded confirmation " "event likewise finds C never captured -- still 0.0, not 10.0"); rc = check_host_satisfy_want(&sim5, JOBD_WANT); TEST_ASSERT_EQUAL_INT_MESSAGE( 1, rc, "bug-1838: capacity remains wide open through round 5 -- even " "JOBD_WANT must be granted, since A/B/C were never genuinely " "dispatched in this PEND-only-stub environment"); printf("test_contention_capacity_check_correct_across_multi_round_" "dispatch_confirm_gap: PASS\n"); } int main(void) { UNITY_BEGIN(); RUN_TEST( test_contention_parity_matches_real_normal_mode_recording_round_by_round); RUN_TEST( test_contention_capacity_check_correct_across_multi_round_dispatch_confirm_gap); return UNITY_END(); }