myxce/src/scheduler/testsuite/test_sim_contention_parity.c
Rongsong Shen 8931833132
feat(scheduler-sim-decision-trace): compile trace sources into REPL/sim tests and the sim isolation checks
Task 5.1: the 26 white-box tests that include repl.c or sim_loop.c now
include the REPL trace sources (and the core trace sources where no
hooked source brings them); sched_sim_isolation_lib carries all trace
sources and the sim isolation test scans the REPL trace files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 14:56:31 +00:00

2006 lines
82 KiB
C

/*
* 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 <errno.h>
#include <limits.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <unity/unity.h>
#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();
}