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>
2006 lines
82 KiB
C
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();
|
|
}
|