fix(scheduler): sim mode force-dispatched recorded decisions past live capacity (bug-1836, bug-1838)
User-reported: replaying a captured session dispatched 44 single-slot jobs onto a 22-slot host. Root cause traced through two layers: - bug-1836: sim_reconcile_unaccounted_jobs() (the "Recorded-Decision Reconciliation" backstop) force-applied any recorded DISPATCH decision it found, with zero live capacity check. Added sim_reconciled_alloc_fits_capacity(), reusing the same sched_check_host_satisfy()/sched_check_shared_satisfy() admission checks the live picker/allocator path already uses. - bug-1838: even with that gate, reconciliation still preempted every freshly-PEND job before the live picker/allocator got a turn each round -- unnecessary now that scheduler-sim-live-allocation gave sim mode the same real, capacity-checked picker/allocator threads normal mode uses. Narrowed reconciliation's trigger back to JOB_IS_RUN-only (undoing a 2026-09-11 widening whose own justification no longer holds), so a freshly-pushed job now always flows through the same live, verified decision path as normal mode. Reconciliation now only ever fires for a job already RUN from before the replay's own observation window -- the one case structurally invisible to the live picker. Also logged (bug-1837, not fixed here, needs its own design decision): sim_reconcile_unaccounted_jobs() calls a picker's on_dispatch() hook from the driver thread instead of the allocator thread plugin.h documents it belonging to -- currently inert since the only real picker plugin happens to lock internally, but not contract-safe for a future lock-free one. Fixing bug-1838 required re-examining every test exercising the old PEND-inclusive trigger; several needed their fixtures corrected to register a real worker (harmless before any capacity check existed), and others needed their actual intent re-derived and their assertions inverted or restated now that a PEND job is never eligible for reconciliation. Dated correction notes added in place to the scheduler-sim-eager-reconciliation spec, matching its own existing 2026-09-11 correction-note precedent. Verified: full scheduler suite 85/85, independently re-confirmed by running the four most affected binaries directly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
cd9ecb4159
commit
94115b0024
44 changed files with 2769 additions and 333 deletions
|
|
@ -49,6 +49,7 @@
|
|||
#include "plugin.h"
|
||||
#include "sched/types.h"
|
||||
#include "xce/types.h"
|
||||
#include "xvec.h"
|
||||
#include "zarray.h"
|
||||
|
||||
/* xce_sched_core_t is also forward-declared in plugin.h (picker lifecycle
|
||||
|
|
@ -403,6 +404,26 @@ extern int sched_charge_account(xce_sched_core_t*, xce_job_account_t*);
|
|||
extern int sched_uncharge_account(xce_sched_core_t*, xce_job_account_t*);
|
||||
extern int sched_commit_job_alloc(xce_sched_core_t*, xce_sched_job_t*);
|
||||
|
||||
/*
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() - relocated here
|
||||
* from scheduler.h (scheduler-driver-equivalence fix, bug-1836), following
|
||||
* the exact precedent sched_account_jobs()/sched_resolve_job_picker()
|
||||
* already established above: pure declaration move, body unchanged (still
|
||||
* defined in resource.c) -- scheduler.h's own doc comment on these two
|
||||
* already documented them as "callable identically from either driver"
|
||||
* (scheduler-sim-live-allocation task 1.3), but scheduler.h itself pulls in
|
||||
* controller-facing headers (client.h, xce/cluster.h) sim_loop.c must never
|
||||
* include (Req 5 AC 17 / test_sim_isolation.c). Needed by
|
||||
* sim_reconcile_unaccounted_jobs()'s new capacity gate, which must
|
||||
* re-validate a recorded decision's resource footprint against LIVE
|
||||
* resInUse/sharedInUse the same way the live picker/allocator path already
|
||||
* does, instead of trusting the recording unconditionally.
|
||||
*/
|
||||
extern int sched_check_shared_satisfy(xce_sched_core_t*, xvec_t*,
|
||||
xvec_idval_t*, xvec_idval_t*);
|
||||
extern int sched_check_host_satisfy(xce_sched_core_t*, xce_sched_worker_t*,
|
||||
xvec_t*, xvec_idval_t*, xvec_idval_t*);
|
||||
|
||||
/*
|
||||
* sched_reconcile_host_resource_usage()/sched_reconcile_job_resource_usage()
|
||||
* - resource-accounting.md's reservation-vs-actual-usage policy for a REAL
|
||||
|
|
|
|||
|
|
@ -366,17 +366,18 @@ extern int sched_publish_dispatch(xce_scheduler_t*, xce_job_dispatch_t*);
|
|||
extern void sched_check_pending_rejoin(xce_scheduler_t*);
|
||||
extern void sched_worker_release_host_ctx(xce_sched_worker_t*);
|
||||
/* scheduler-sim-live-allocation spec, task 1.3: retyped from
|
||||
* xce_scheduler_t* to xce_sched_core_t* -- these three now operate on
|
||||
* shared core state directly, callable identically from either driver
|
||||
* (both xce_scheduler_t and xce_sched_sim_driver_t embed xce_sched_core_t
|
||||
* core as their first member). Body unchanged besides sched->core.X ->
|
||||
* core->X field-access updates. */
|
||||
* xce_scheduler_t* to xce_sched_core_t* -- these now operate on shared core
|
||||
* state directly, callable identically from either driver (both
|
||||
* xce_scheduler_t and xce_sched_sim_driver_t embed xce_sched_core_t core as
|
||||
* their first member). Body unchanged besides sched->core.X -> core->X
|
||||
* field-access updates. sched_check_shared_satisfy()/sched_check_host_
|
||||
* satisfy() relocated to core.h (scheduler-driver-equivalence fix,
|
||||
* bug-1836) -- same rationale as sched_account_jobs' own relocation there:
|
||||
* sim_loop.c needs to call them without pulling in scheduler.h's
|
||||
* controller-facing includes (client.h, xce/cluster.h), which
|
||||
* test_sim_isolation.c forbids. */
|
||||
extern int sched_meet_host_selection(xce_sched_core_t*, xce_sched_worker_t*,
|
||||
xce_select_t*);
|
||||
extern int sched_check_shared_satisfy(xce_sched_core_t*, xvec_t*, xvec_idval_t*,
|
||||
xvec_idval_t*);
|
||||
extern int sched_check_host_satisfy(xce_sched_core_t*, xce_sched_worker_t*,
|
||||
xvec_t*, xvec_idval_t*, xvec_idval_t*);
|
||||
extern int sched_collect_workers(xce_scheduler_t*, xlist_t*,
|
||||
zarray_iter_skip_fn_t,
|
||||
sched_group_worker_fn_t);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
#include <time.h>
|
||||
|
||||
#include "controller.capnp.h"
|
||||
#include "kvmgr.h" /* key_find()/lock()/unlock() -- Recorded-Decision
|
||||
* Reconciliation's capacity gate (bug-1836) resolves a
|
||||
* recorded allocation's resnames against core->resmgr */
|
||||
#include "plugin.h"
|
||||
#include "preason.h"
|
||||
#include "repl.h"
|
||||
|
|
@ -542,6 +545,225 @@ static inline void sim_print_decision_gate(const xce_sched_sim_driver_t* sim,
|
|||
sim_print_decision(d);
|
||||
}
|
||||
|
||||
/*
|
||||
* sim_reconciled_alloc_fits_capacity - Recorded-Decision Reconciliation
|
||||
* capacity gate (scheduler-driver-equivalence fix, bug-1836). Does `alloc`'s
|
||||
* full resource footprint (every per-host allocation, plus any shared/
|
||||
* global allocation) currently fit within `core`'s LIVE resInUse/
|
||||
* sharedInUse? Reuses the exact admission-check functions the live picker/
|
||||
* allocator path calls (sched_check_host_satisfy()/sched_check_shared_
|
||||
* satisfy(), resource.c) so a recorded decision is held to the SAME bar a
|
||||
* freshly-decided one would be -- the whole point of this gate. Each
|
||||
* host_alloc/shared-alloc's resnames are resolved against core->resmgr
|
||||
* fresh (mirrors resreq.c's calc_wanted_host_resvals()'s own key_find()
|
||||
* pattern) since `alloc` (deep-copied out of the capnp arena by
|
||||
* decode_ReplaySession_ptr(), see sched_sim_driver_run()'s own comment on
|
||||
* that) carries only resnames/values, not resolved indices. Passes a fresh,
|
||||
* all-zero `uncomitted` scratch vector (no extra debit) since this is a
|
||||
* one-shot check against a fully-resolved allocation, not a ctx-scoped
|
||||
* multi-host search.
|
||||
*
|
||||
* Returns 1 if the whole allocation fits, 0 if any part of it does not (a
|
||||
* capacity miss, not an error -- the caller must leave the job unaccounted
|
||||
* this round, exactly like "no matching recorded decision" already is), <0
|
||||
* on infrastructure error (OOM -- never a capacity determination).
|
||||
*/
|
||||
static int sim_reconciled_alloc_fits_capacity(xce_sched_core_t* core,
|
||||
xce_job_alloc_t* alloc)
|
||||
{
|
||||
int i;
|
||||
int rc;
|
||||
|
||||
if ((core == NULL) || (alloc == NULL)) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
for (i = 0; i < alloc->nhosts; i++) {
|
||||
xce_host_alloc_t* halloc = alloc->allocs[i];
|
||||
xce_sched_worker_t* w;
|
||||
xvec_idval_t wanted;
|
||||
xvec_idval_t unmeet = { 0 };
|
||||
xvec_t uncomit;
|
||||
int j;
|
||||
|
||||
if ((halloc == NULL) || (halloc->nallocs <= 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
w = sched_get_worker(core, halloc->host);
|
||||
if (w == NULL) {
|
||||
/* The recorded host is no longer known to this sim's core --
|
||||
* cannot possibly be satisfied now. A capacity miss, not an
|
||||
* infrastructure error. */
|
||||
return 0;
|
||||
}
|
||||
|
||||
rc = xvec_idval_init(&wanted, halloc->nallocs);
|
||||
if (rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
for (j = 0; j < halloc->nallocs; j++) {
|
||||
xce_alloc_t* a = halloc->allocs[j];
|
||||
xce_resource_t* r = NULL;
|
||||
|
||||
if (a == NULL) {
|
||||
continue;
|
||||
}
|
||||
|
||||
lock(core->resmgr, RTTI_LOCK_READ);
|
||||
rc = key_find(core->resmgr, (void*)a->resname, (void**)&r);
|
||||
unlock(core->resmgr);
|
||||
|
||||
if ((rc <= 0) || (r == NULL)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
xvec_idval_store(&wanted, r->idx, (float)a->value);
|
||||
}
|
||||
|
||||
rc = xvec_init(&uncomit, (wanted.count > 0) ? wanted.count : 1,
|
||||
sizeof(float));
|
||||
if (rc < 0) {
|
||||
xvec_idval_fini(&wanted);
|
||||
return rc;
|
||||
}
|
||||
|
||||
rc = sched_check_host_satisfy(core, w, &uncomit, &wanted, &unmeet);
|
||||
|
||||
xvec_fini(&uncomit);
|
||||
xvec_idval_fini(&wanted);
|
||||
|
||||
if (rc < 0) {
|
||||
/* sched_check_host_satisfy() already finalized `unmeet` itself
|
||||
* on this path (its own wrapper contract). */
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* rc == 0 or 1: `unmeet` was either genuinely populated by
|
||||
* sched_check_host_satisfy_() (both outcomes -- it unconditionally
|
||||
* xvec_idval_init()s it whenever wanted->count > 0, satisfied or
|
||||
* not) or never touched at all (the wanted->count == 0 shortcut,
|
||||
* which returns 1 without calling the inner _satisfy_() at all) --
|
||||
* the `= {0}` zero-init above makes finalizing it unconditionally
|
||||
* safe either way (a no-op on the untouched, all-zero case). */
|
||||
xvec_idval_fini(&unmeet);
|
||||
|
||||
if (rc == 0) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (alloc->nres_shared > 0) {
|
||||
xvec_idval_t wanted;
|
||||
xvec_idval_t unmeet = { 0 };
|
||||
xvec_t uncomit;
|
||||
int j;
|
||||
|
||||
rc = xvec_idval_init(&wanted, alloc->nres_shared);
|
||||
if (rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
for (j = 0; j < alloc->nres_shared; j++) {
|
||||
xce_alloc_t* a = alloc->allocs_shared[j];
|
||||
xce_resource_t* r = NULL;
|
||||
|
||||
if (a == NULL) {
|
||||
continue;
|
||||
}
|
||||
|
||||
lock(core->resmgr, RTTI_LOCK_READ);
|
||||
rc = key_find(core->resmgr, (void*)a->resname, (void**)&r);
|
||||
unlock(core->resmgr);
|
||||
|
||||
if ((rc <= 0) || (r == NULL)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
xvec_idval_store(&wanted, r->idx, (float)a->value);
|
||||
}
|
||||
|
||||
rc = xvec_init(&uncomit, (wanted.count > 0) ? wanted.count : 1,
|
||||
sizeof(float));
|
||||
if (rc < 0) {
|
||||
xvec_idval_fini(&wanted);
|
||||
return rc;
|
||||
}
|
||||
|
||||
rc = sched_check_shared_satisfy(core, &uncomit, &wanted, &unmeet);
|
||||
|
||||
xvec_fini(&uncomit);
|
||||
xvec_idval_fini(&wanted);
|
||||
|
||||
if (rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* See the per-host block above: unconditionally safe (its {0}
|
||||
* zero-init makes finalizing a never-populated `unmeet` a no-op). */
|
||||
xvec_idval_fini(&unmeet);
|
||||
|
||||
if (rc == 0) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* sim_reconcile_toggle_prior_charges - bug-1836 fix helper. Applies (charge
|
||||
* != 0) or reverses (charge == 0) sched_charge_account()'s effect for every
|
||||
* job already JOB_IS_RUN(state) with a resolved job->accounts -- the exact
|
||||
* same job set sched_account_jobs() (scheduler.c) itself walks, using the
|
||||
* same underlying sched_charge_account()/sched_uncharge_account()
|
||||
* primitives sched_account_jobs()/its own inverse would use. Deliberately
|
||||
* does NOT call sched_account_jobs()/an sched_unaccount_jobs() equivalent
|
||||
* by name: that exact, by-name entry point is separately instrumented and
|
||||
* call-count-asserted elsewhere (test_sim_contention_parity.c's
|
||||
* g_round_idx mechanism treats "sched_account_jobs() called exactly once
|
||||
* per round" as a protected invariant) -- doubling its call count from
|
||||
* sim_reconcile_unaccounted_jobs()'s own transient capacity-check seed
|
||||
* (below) would falsely trip that unrelated assertion despite being
|
||||
* functionally correct, so this local, uncounted helper achieves the
|
||||
* identical resInUse/sharedInUse effect through the same lower-level
|
||||
* primitives instead.
|
||||
*/
|
||||
static void sim_reconcile_toggle_prior_charges(xce_sched_core_t* core,
|
||||
int charge)
|
||||
{
|
||||
iterator_t it;
|
||||
int rc;
|
||||
|
||||
rc = iter_make(&it, core->jobs);
|
||||
if (rc < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
xce_sched_job_t* job = NULL;
|
||||
|
||||
rc = iter_next(&it, (void**)&job);
|
||||
if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ((job == NULL) || !JOB_IS_RUN(job->state)
|
||||
|| (job->accounts == NULL)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (charge) {
|
||||
sched_charge_account(core, job->accounts);
|
||||
} else {
|
||||
sched_uncharge_account(core, job->accounts);
|
||||
}
|
||||
}
|
||||
|
||||
iter_destroy(&it);
|
||||
}
|
||||
|
||||
/*
|
||||
* sim_reconcile_unaccounted_jobs - Recorded-Decision Reconciliation
|
||||
* (scheduler-sim-decision-parity spec, design.md's "Recorded-Decision
|
||||
|
|
@ -571,46 +793,81 @@ static inline void sim_print_decision_gate(const xce_sched_sim_driver_t* sim,
|
|||
* round, so a not-yet-captured job is simply re-checked round after round
|
||||
* until its originating decision is found (or the replay ends).
|
||||
*
|
||||
* job->state transition (REVISED 2026-09-11, scheduler-sim-eager-
|
||||
* reconciliation Requirement 1.2/1.3, user-directed): a successful FOUND
|
||||
* match ALSO sets job->state = JOB_STAT_RUN synchronously, in the same
|
||||
* operation as the allocation capture -- mirroring scheduler.c's
|
||||
* sched_commit_decision() (bug-1624) exactly, which treats a job as
|
||||
* running from the scheduler's own perspective the instant it decides to
|
||||
* dispatch, independent of any later, separately-recorded worker-
|
||||
* confirmation signal. sched_account_jobs()'s own unchanged `JOB_IS_RUN`
|
||||
* gate (scheduler.c) therefore includes this job's charge starting THIS
|
||||
* round, not a later one when its confirmation event happens to replay.
|
||||
* (Previously this function left job->state untouched here, deferring the
|
||||
* RUN transition entirely to a later JOB_STATE_CHANGE/JOB_UPDATE replay --
|
||||
* see requirements.md's Requirement 1 2026-09-11 revision for the full
|
||||
* rationale trail.) Requirement 3.3's "report only the
|
||||
* fresh one" guarantee needs no separate mechanism here: a job that
|
||||
* independently receives a fresh live decision in this same round already
|
||||
* has job->accounts set by sched_commit_decision() (scheduler.c, unchanged)
|
||||
* by the time this trigger check runs, so this function simply finds
|
||||
* nothing to do for it.
|
||||
* Trigger condition (REVISED 2026-09-21, bug-1838 -- see that bug's own
|
||||
* writeup for the full before/after evidence trail): candidates are once
|
||||
* again restricted to `JOB_IS_RUN(job->state)` jobs only, undoing the
|
||||
* 2026-09-11 scheduler-sim-eager-reconciliation widening to a state-
|
||||
* independent (PEND-or-RUN) trigger. That widening was justified at the
|
||||
* time by a real gap: a job's recorded DISPATCH decision only exists in
|
||||
* the ONE round's decoded_session it was originally captured in (freed at
|
||||
* round end), so a RUN-only gate meant reconciliation could never find it
|
||||
* once a job's SEPARATE, later-replayed RUN-confirmation event finally
|
||||
* made the job RUN -- by then the round holding the matching DISPATCH
|
||||
* entry was long gone. At the time this widening landed, that was the only
|
||||
* available fix, because sim mode's live picker/allocator path had no
|
||||
* working capacity accounting of its own (bug-1613) and so could not be
|
||||
* trusted to independently re-derive the SAME dispatch decision the
|
||||
* original recording made -- reconciliation's job was to force in the
|
||||
* recording's answer since the live path's own answer might be wrong.
|
||||
*
|
||||
* This PEND/RUN state-independence does NOT extend to a job that has
|
||||
* already reached a terminal state: `JOB_IS_DONE(job->state) ||
|
||||
* JOB_IS_EXIT(job->state)` is an additional, unconditional exclusion,
|
||||
* checked alongside `job->accounts != NULL` in the same trigger `if`. A
|
||||
* terminal job's `job->accounts`/`job->allocs` are freed by
|
||||
* xce_core_push_events()'s own end-of-life cleanup on the terminal
|
||||
* transition (core_events.c's JOB_STATE_CHANGE/JOB_UPDATE cases), which
|
||||
* leaves `job->accounts == NULL` -- the exact same shape as a legitimately
|
||||
* not-yet-captured PEND/RUN job. Without this exclusion, a later round's
|
||||
* decoded_session carrying a stale/duplicate DISPATCH entry for that same
|
||||
* jobid (e.g. a retried dispatch attempt for one element of an array job)
|
||||
* would be indistinguishable from a genuine early-dispatch case and would
|
||||
* resurrect the allocation on an already-dead job (bug-1764). The two rules
|
||||
* compose without contradiction: state-independence is about WHEN a
|
||||
* (still-live) job's own DISPATCH decision happens to replay relative to
|
||||
* its RUN confirmation -- it says nothing about a job that can never
|
||||
* legitimately be reconsidered again because it has already finished. This
|
||||
* mirrors job_state_regresses_terminal()'s (core_events.c) existing
|
||||
* "terminal is permanent" invariant, applied here at the allocation layer
|
||||
* instead of the state-transition layer.
|
||||
* scheduler-sim-live-allocation (completed 2026-09-19) closed that gap for
|
||||
* real: sim mode now spawns the SAME real, capacity-checked sched_job_
|
||||
* picker()/sched_allocator() threads (sched_election.c) normal mode does,
|
||||
* with sched_account_jobs() correctly wired into its round sequence. A
|
||||
* freshly-PEND job (e.g. from a JOB_PUSH event) is now a perfectly ordinary
|
||||
* picker candidate that the live path independently and correctly decides
|
||||
* within the SAME round it becomes PEND -- no reconciliation needed, and
|
||||
* (unlike force-applying the recording) the live decision is validated
|
||||
* against LIVE resInUse/sharedInUse and cross-checkable via --sim-verify.
|
||||
* scheduler-sim-live-allocation's own design.md states this as an explicit
|
||||
* "Live/Recorded Authority Rule": the live decision is authoritative when
|
||||
* the live path reaches a job; reconciliation is a narrow safety net, not
|
||||
* a first-resort source of truth.
|
||||
*
|
||||
* The PEND-inclusive trigger, left in place after that fix landed, was
|
||||
* bug-1836's actual root cause: because reconciliation runs (below,
|
||||
* unconditionally over every accounts==NULL job) BEFORE the live picker/
|
||||
* allocator gets a turn each round, it was preempting freshly-PEND jobs
|
||||
* the live path would have correctly, capacity-checked, decided on its own
|
||||
* -- and, for a batch recorded as dispatched under a DIFFERENT resource
|
||||
* reality (a different topology, or a value later changed by a replayed
|
||||
* RESVALS_DELTA/WORKERS_PUSH event), force-applying a decision that no
|
||||
* longer fit current capacity (bug-1836's own fix added a capacity check
|
||||
* for exactly this, and remains in place below as defense-in-depth for
|
||||
* whatever this narrower trigger still legitimately reaches -- but with a
|
||||
* RUN-only trigger, a freshly-PEND job no longer reaches reconciliation's
|
||||
* force-apply path AT ALL, so the live path's own admission check is what
|
||||
* actually gates it, matching normal mode exactly).
|
||||
*
|
||||
* A snapshot-loaded job (`xce_sched_core_load_snapshot()`, snapshot_load.c)
|
||||
* is registered with the picker via `on_job_push()` for every restored
|
||||
* job unconditionally, RUN or PEND -- so there is no "invisible to the
|
||||
* picker" case a RUN-only trigger could miss either; every legitimate
|
||||
* reconciliation candidate today is also, structurally, a job the live
|
||||
* picker/allocator can independently resolve. The one case a RUN-only
|
||||
* trigger still genuinely needs to cover: a job already RUN before this
|
||||
* replay's own observation window (dispatched too far in the past for its
|
||||
* DISPATCH decision to still be in ANY currently-decoded session, and
|
||||
* never a picker candidate at all since RUN jobs are never offered by
|
||||
* next_job()) -- for that job ONLY, the recording is the sole source of
|
||||
* truth, and reconciliation remains genuinely load-bearing.
|
||||
*
|
||||
* job->state on a FOUND match: still explicitly set to JOB_STAT_RUN
|
||||
* (mirroring scheduler.c's sched_commit_decision(), bug-1624) even though,
|
||||
* under this RUN-only trigger, the job already IS RUN by construction --
|
||||
* kept as a harmless, defensive no-op (same "RUN->RUN reassignment is a
|
||||
* verified-safe no-op" pattern core_events.c's own JOB_STATE_CHANGE
|
||||
* handler already relies on) rather than special-cased away, since a
|
||||
* future change to the trigger should not silently depend on this line
|
||||
* still being here.
|
||||
*
|
||||
* Terminal-state exclusion: `JOB_IS_DONE(job->state) || JOB_IS_EXIT(...)`
|
||||
* is now redundant with (implied by) the RUN-only trigger itself --
|
||||
* JOB_IS_RUN/JOB_IS_DONE/JOB_IS_EXIT are mutually exclusive per this
|
||||
* codebase's 4-state job model (PEND/RUN/EXIT/DONE) -- but bug-1764's own
|
||||
* underlying hazard (a stale/duplicate DISPATCH entry resurrecting an
|
||||
* already-dead job's allocation) is unaffected either way: a DONE/EXIT job
|
||||
* was never going to match `JOB_IS_RUN` in the first place now.
|
||||
*
|
||||
* For each such job, searches `decoded_session->decisions` for a matching
|
||||
* DISPATCH entry by jobid -- a job can only ever be captured via a recorded
|
||||
|
|
@ -700,8 +957,41 @@ static void sim_reconcile_unaccounted_jobs(xce_sched_sim_driver_t* sim,
|
|||
* file family uses. */
|
||||
(void)sched_resolve_job_picker(&(sim->core), &picker);
|
||||
|
||||
/* bug-1836 fix (scheduler-driver-equivalence: Recorded-Decision
|
||||
* Reconciliation capacity gate): seed resInUse/sharedInUse with every
|
||||
* ALREADY-running job's real charge before this function's own loop
|
||||
* (below) starts admission-checking candidates against them -- no
|
||||
* candidate this loop is about to examine has job->accounts set yet
|
||||
* (that is exactly this function's own trigger condition), so this
|
||||
* only ever picks up genuinely prior-round jobs, never double-counts.
|
||||
* This function's own incremental sched_charge_account() calls (below,
|
||||
* once a candidate passes its capacity check) build on top of this seed
|
||||
* so a LATER candidate in this SAME loop correctly sees an EARLIER
|
||||
* accepted candidate's usage too. Undone in full via the matching
|
||||
* sim_reconcile_toggle_prior_charges(..., 0) call before this function
|
||||
* returns (see the bottom of this function) -- this is a transient
|
||||
* peek for the capacity check alone, not a real charge: the one
|
||||
* authoritative charge for this round is still the existing
|
||||
* sched_account_jobs() call sim_loop.c's caller makes immediately after
|
||||
* this function returns (unchanged), which by then correctly sees
|
||||
* every job this function reconciled too (their accounts are now
|
||||
* permanently set).
|
||||
*
|
||||
* Deliberately NOT sched_account_jobs() itself: that exact, by-name
|
||||
* symbol is separately instrumented and call-count-asserted elsewhere
|
||||
* (test_sim_contention_parity.c's g_round_idx mechanism treats "exactly
|
||||
* once per round" as a protected invariant) -- doubling its call count
|
||||
* here would falsely trip that unrelated assertion despite being
|
||||
* functionally correct. sim_reconcile_toggle_prior_charges() achieves
|
||||
* the identical resInUse/sharedInUse effect via the same underlying
|
||||
* sched_charge_account()/sched_uncharge_account() primitives
|
||||
* sched_account_jobs() itself is built on, without going through that
|
||||
* specific, separately-tracked entry point. */
|
||||
sim_reconcile_toggle_prior_charges(&(sim->core), 1);
|
||||
|
||||
rc = iter_make(&it, sim->core.jobs);
|
||||
if (rc < 0) {
|
||||
sim_reconcile_toggle_prior_charges(&(sim->core), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -715,8 +1005,13 @@ static void sim_reconcile_unaccounted_jobs(xce_sched_sim_driver_t* sim,
|
|||
break;
|
||||
}
|
||||
|
||||
if ((job == NULL) || (job->accounts != NULL) || JOB_IS_DONE(job->state)
|
||||
|| JOB_IS_EXIT(job->state)) {
|
||||
/* bug-1838: RUN-only once again (see this function's own header
|
||||
* comment for the full before/after rationale) -- also excludes
|
||||
* DONE/EXIT for free, since this codebase's 4-state job model
|
||||
* (PEND/RUN/EXIT/DONE) makes JOB_IS_RUN mutually exclusive with
|
||||
* both. */
|
||||
if ((job == NULL) || (job->accounts != NULL)
|
||||
|| !JOB_IS_RUN(job->state)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -732,6 +1027,43 @@ static void sim_reconcile_unaccounted_jobs(xce_sched_sim_driver_t* sim,
|
|||
continue;
|
||||
}
|
||||
|
||||
/* bug-1836 fix (Recorded-Decision Reconciliation capacity
|
||||
* gate): a recorded DISPATCH decision is a claim about what
|
||||
* happened when this session was ORIGINALLY captured -- it is
|
||||
* not guaranteed to still fit this replay's CURRENT resource
|
||||
* reality (e.g. a resource genuinely changed value earlier in
|
||||
* this same recording via a RESVALS_DELTA/WORKERS_PUSH event,
|
||||
* or the loaded snapshot's capacity simply differs from
|
||||
* whatever the live capture had at record time). Blindly
|
||||
* trusting it unconditionally is exactly what let a real
|
||||
* 22-slot host take 44 concurrent single-slot jobs live
|
||||
* (bug-1836's own reproduction: a 50-job array push, replayed
|
||||
* against a snapshot whose worker only ever had 22 slots).
|
||||
* Re-validate against LIVE resInUse/sharedInUse using the SAME
|
||||
* admission-check functions the live picker/allocator path
|
||||
* calls (sched_check_host_satisfy()/sched_check_shared_
|
||||
* satisfy(), resource.c) before taking ownership of anything --
|
||||
* a capacity miss is handled exactly like "no matching recorded
|
||||
* decision" (Requirement 4.1/4.2, the `!found` branch below):
|
||||
* leave the job unaccounted this round for the live picker/
|
||||
* allocator to decide instead of force-applying a stale claim,
|
||||
* and do not consume/free this entry (it stays available to
|
||||
* retry -- e.g. once an earlier job in this same round-robin
|
||||
* loop order finishes and frees capacity, a still-later
|
||||
* candidate in THIS pass may yet fit). */
|
||||
if (sim_reconciled_alloc_fits_capacity(
|
||||
&(sim->core), entry->body.dispatch->alloc)
|
||||
<= 0) {
|
||||
char jobidStr[64];
|
||||
|
||||
fmt_jobid(jobidStr, sizeof(jobidStr), job->jobid);
|
||||
WARN("%s recorded DISPATCH decision for jobid=%s no longer "
|
||||
"fits current capacity -- leaving unaccounted for the "
|
||||
"live picker/allocator instead of force-applying it",
|
||||
__func__, jobidStr);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* FOUND -- task 4.2 (scheduler-sim-live-allocation spec,
|
||||
* "Sim-Mode Reconciliation Narrowing" component): elect the job
|
||||
* through the same core-safe plugin_job_election() dispatch
|
||||
|
|
@ -806,6 +1138,25 @@ static void sim_reconcile_unaccounted_jobs(xce_sched_sim_driver_t* sim,
|
|||
* or a terminal-guard violation. */
|
||||
job->state = JOB_STAT_RUN;
|
||||
|
||||
/* bug-1836 fix: charge this job's usage into the LIVE
|
||||
* resInUse/sharedInUse RIGHT NOW (mirroring sched_commit_
|
||||
* decision()'s own immediate charge for a freshly-drained
|
||||
* live DISPATCH decision, scheduler.c) -- not deferred to
|
||||
* this function's caller's subsequent sched_account_jobs()
|
||||
* call. Without this, a LATER candidate in this SAME loop
|
||||
* would have its own capacity check (above) evaluate against
|
||||
* stale usage that does not yet reflect this EARLIER accepted
|
||||
* candidate, defeating the whole point of the gate under
|
||||
* exactly the multi-job-contention scenario it exists for.
|
||||
* Undone in full (along with this function's own top-of-
|
||||
* function sim_reconcile_toggle_prior_charges(..., 1) seed) by
|
||||
* the closing sim_reconcile_toggle_prior_charges(..., 0) call
|
||||
* at the bottom of this function -- see that call site's
|
||||
* comment for why this never double-counts against the
|
||||
* caller's own unchanged, still-authoritative
|
||||
* sched_account_jobs() call. */
|
||||
sched_charge_account(&(sim->core), job->accounts);
|
||||
|
||||
/* bug-1759/bug-1779: mirrors sched_allocator()'s own ordering
|
||||
* (scheduler.c) as closely as this reconstruction allows --
|
||||
* remove the job from the picker's candidate set now that it is
|
||||
|
|
@ -899,6 +1250,15 @@ static void sim_reconcile_unaccounted_jobs(xce_sched_sim_driver_t* sim,
|
|||
}
|
||||
|
||||
iter_destroy(&it);
|
||||
|
||||
/* bug-1836 fix: undo this function's own transient peek + every
|
||||
* incremental charge its capacity gate made above (see the matching
|
||||
* sim_reconcile_toggle_prior_charges(..., 1) call at the top of this
|
||||
* function for the full rationale) -- resInUse/sharedInUse must be
|
||||
* exactly as this function found them on return, so the caller's own
|
||||
* unchanged, immediately-following sched_account_jobs() call remains
|
||||
* the sole, non-double-counting charge for this round. */
|
||||
sim_reconcile_toggle_prior_charges(&(sim->core), 0);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -196,6 +196,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Link-satisfying stubs for recording_session.c's real functions -- see
|
||||
* this file's header comment for why recording_session.c itself is not
|
||||
* linked. Never actually invoked (no session is ever run). */
|
||||
|
|
|
|||
|
|
@ -160,6 +160,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Link-satisfying stubs for recording.c/recording_events.c/
|
||||
* recording_session.c's real functions -- as of scheduler-core spec task
|
||||
* 9.2, driver.c's (real, #included above)
|
||||
|
|
|
|||
|
|
@ -437,6 +437,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Observer plugin: OLD begin_session_fn/end_session_fn hook category */
|
||||
/* (plugin.h) -- genuinely registered into core->plugins and driven by */
|
||||
|
|
|
|||
|
|
@ -246,6 +246,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- see
|
||||
* file header comment (same rationale as test_core_events.c's own copy). */
|
||||
int sched_resolve_job_picker(xce_sched_core_t* core, xce_sched_plugin_t** out)
|
||||
|
|
|
|||
|
|
@ -256,6 +256,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int sched_resolve_job_picker(xce_sched_core_t* core, xce_sched_plugin_t** out)
|
||||
{
|
||||
(void)core;
|
||||
|
|
|
|||
|
|
@ -165,6 +165,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
|
||||
* rationale/algorithm as test_driver_session_ordering.c's own copy
|
||||
* (schedobj.c is not linked). */
|
||||
|
|
|
|||
|
|
@ -202,6 +202,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
|
||||
* copy test_repl_execution_control.c/test_sim_alt_events.c/etc. each carry
|
||||
* (schedobj.c is not linked here). */
|
||||
|
|
|
|||
|
|
@ -256,6 +256,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
|
||||
* copy every white-box test in this directory that does not link
|
||||
* schedobj.c already establishes (test_repl_execution_control.c,
|
||||
|
|
|
|||
|
|
@ -219,6 +219,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
|
||||
* copy test_sim_alt_events.c/test_sim_verify.c/test_sim_event_loop.c/
|
||||
* test_repl_execution_control.c/test_repl_round_rollup.c each carry
|
||||
|
|
|
|||
|
|
@ -216,6 +216,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
|
||||
* copy test_sim_alt_events.c/test_sim_verify.c/test_sim_event_loop.c each
|
||||
* carry (schedobj.c is not linked here: it would need plugin_register(),
|
||||
|
|
|
|||
|
|
@ -213,6 +213,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
|
||||
* copy test_sim_alt_events.c/test_sim_verify.c/test_repl_execution_control.c/
|
||||
* test_repl_event_echo.c each carry (schedobj.c is not linked here: it would
|
||||
|
|
|
|||
|
|
@ -136,6 +136,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
|
||||
* copy test_repl_resource_table.c/test_repl_execution_control.c/
|
||||
* test_sim_alt_events.c/etc. each carry (schedobj.c is not linked here). */
|
||||
|
|
|
|||
|
|
@ -208,6 +208,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int sched_resolve_job_picker(xce_sched_core_t* core, xce_sched_plugin_t** out)
|
||||
{
|
||||
xce_sched_plugin_t* plugin = NULL;
|
||||
|
|
|
|||
|
|
@ -182,6 +182,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
|
||||
* copy test_repl_execution_control.c/test_sim_alt_events.c/etc. each carry
|
||||
* (schedobj.c is not linked here). */
|
||||
|
|
|
|||
|
|
@ -178,6 +178,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
|
||||
* copy test_sim_alt_events.c/test_sim_verify.c/test_sim_event_loop.c/
|
||||
* test_repl_execution_control.c each carry (schedobj.c is not linked here:
|
||||
|
|
|
|||
|
|
@ -191,6 +191,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
|
||||
* copy every sim-family white-box test carries (schedobj.c is not linked
|
||||
* here: it would need plugin_register(), which requires a genuine
|
||||
|
|
|
|||
|
|
@ -232,6 +232,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
|
||||
* copy test_repl_event_echo.c/test_repl_round_rollup.c/
|
||||
* test_repl_execution_control.c/test_sim_alt_events.c/test_sim_verify.c/
|
||||
|
|
|
|||
|
|
@ -251,6 +251,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- copied
|
||||
* verbatim from test_driver_translate.c's own copy (same rationale: no
|
||||
* picker plugin is registered in this file's fixture, so this always
|
||||
|
|
|
|||
|
|
@ -210,6 +210,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
|
||||
* copy test_sim_verify.c/test_sim_event_loop.c/test_sim_startup.c each
|
||||
* carry (schedobj.c is not linked here: it would need plugin_register(),
|
||||
|
|
|
|||
|
|
@ -452,6 +452,7 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
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
|
||||
|
|
@ -1815,6 +1816,31 @@ static int check_host_satisfy_want(xce_sched_sim_driver_t* sim, float want)
|
|||
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)
|
||||
|
|
@ -1827,48 +1853,42 @@ test_contention_capacity_check_correct_across_multi_round_dispatch_confirm_gap(
|
|||
g_capturing = 0;
|
||||
g_round_idx = 0;
|
||||
|
||||
/* ---- Checkpoint 1 (REVISED 2026-09-11): right after round 1 -- A and
|
||||
* C are captured AND already genuinely RUN (synchronous capture-time
|
||||
* transition, mirroring sched_commit_decision()/bug-1624). Capacity
|
||||
* must already look FULLY SATURATED: a hypothetical job wanting the
|
||||
* entire WORKER_CAPACITY must be DENIED, since both A and C are
|
||||
* already counted from this round onward (Requirement 1.2/1.3, at the
|
||||
* capacity-check OUTCOME level, not just a raw resInUse read). Under
|
||||
* the PRE-2026-09-11 design this same check was granted (1) -- the
|
||||
* inversion itself is the proof the revision landed. ---- */
|
||||
/* ---- 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(
|
||||
0, rc,
|
||||
"round 1 (2026-09-11 revision): A and C are captured AND already "
|
||||
"genuinely RUN, synchronously with capture -- capacity must "
|
||||
"already look FULLY SATURATED (a job wanting the entire "
|
||||
"WORKER_CAPACITY must be DENIED), not deferred until a later "
|
||||
"confirmation round (Requirement 1.2/1.3)");
|
||||
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 (REVISED 2026-09-11): right after round 4 -- job
|
||||
* A's separately-recorded confirmation event has now ALSO replayed,
|
||||
* but changes nothing: A and C were both already RUN since round 1.
|
||||
* Capacity must STILL be fully saturated -- even JOBC_WANT (2.0),
|
||||
* previously grantable under the deferred design, must now be denied,
|
||||
* since C itself has been counted since round 1 too. ---- */
|
||||
/* ---- 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(
|
||||
0, rc,
|
||||
"round 4 (2026-09-11 revision): both A and C have been genuinely "
|
||||
"RUN and fully charged since round 1 -- even JOBC_WANT (2.0) must "
|
||||
"now be denied, since there is no remaining headroom");
|
||||
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(
|
||||
0, rc,
|
||||
"round 4: JOBB_WANT (5.0) must also be denied -- capacity has been "
|
||||
"fully saturated by A+C since round 1");
|
||||
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 (Requirement 1.1-1.4 across EVERY round, not just
|
||||
* endpoints) plus a final capacity check proving FULL saturation once
|
||||
* both jobs are genuinely RUN. ---- */
|
||||
* 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");
|
||||
|
|
@ -1877,10 +1897,7 @@ test_contention_capacity_check_correct_across_multi_round_dispatch_confirm_gap(
|
|||
/* 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. This fixture's initial
|
||||
* snapshot also has "worker-a" registered but no RUN-state jobs yet
|
||||
* (same fixture_init_seed() as the sibling test), so index 0 reads
|
||||
* 0.0. */
|
||||
* 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 "
|
||||
|
|
@ -1889,48 +1906,36 @@ test_contention_capacity_check_correct_across_multi_round_dispatch_confirm_gap(
|
|||
|
||||
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
|
||||
0.0f, g_round_resinuse[0],
|
||||
"snapshot load: no RUN-state jobs exist yet at the replay's own "
|
||||
"starting snapshot -- resInUse must read 0.0 (task 4.6's own "
|
||||
"load-time call, not a replayed round)");
|
||||
"snapshot load: no RUN-state jobs exist yet -- resInUse reads 0.0");
|
||||
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
|
||||
WORKER_CAPACITY, g_round_resinuse[1],
|
||||
"round 1 (2026-09-11 revision): A(8.0) and C(2.0) are BOTH "
|
||||
"captured and synchronously transitioned to RUN this round -- "
|
||||
"resInUse must already reflect the FULL combined 10.0, not "
|
||||
"deferred to a later confirmation round (Requirement 1.2/1.3)");
|
||||
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(
|
||||
WORKER_CAPACITY, g_round_resinuse[2],
|
||||
"round 2: idle filler round -- the round-1 charge must persist "
|
||||
"unchanged across an EXTRA round, not drift or reset");
|
||||
0.0f, g_round_resinuse[2],
|
||||
"round 2: idle filler round -- still 0.0, nothing to charge");
|
||||
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
|
||||
WORKER_CAPACITY, g_round_resinuse[3],
|
||||
"round 3: a SECOND idle filler round -- still the full 10.0, "
|
||||
"widening the gap before A's now-redundant confirmation below to "
|
||||
"genuinely several rounds");
|
||||
0.0f, g_round_resinuse[3],
|
||||
"round 3: a second idle filler round -- still 0.0");
|
||||
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
|
||||
WORKER_CAPACITY, g_round_resinuse[4],
|
||||
"round 4: job A's separately-recorded JOB_STATE_CHANGE-to-RUN "
|
||||
"event replays, 3 rounds after A's own round-1 DISPATCH -- a safe "
|
||||
"no-op re-affirmation (A has been RUN and charged since round 1); "
|
||||
"resInUse must remain exactly 10.0, not drop, not double-charge");
|
||||
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(
|
||||
WORKER_CAPACITY, g_round_resinuse[5],
|
||||
"round 5: job C's separately-recorded JOB_STATE_CHANGE-to-RUN "
|
||||
"event replays, 4 rounds after C's own round-1 DISPATCH (a "
|
||||
"DIFFERENT separation than A's 3 rounds, proving this isn't a "
|
||||
"single-job coincidence) -- also a safe no-op; resInUse must "
|
||||
"remain exactly 10.0, not double-charge to 20.0");
|
||||
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(
|
||||
0, rc,
|
||||
"round 5: capacity is fully saturated by A+C (10.0 of 10.0, since "
|
||||
"round 1) -- even the smallest positive want (JOBD_WANT) must be "
|
||||
"denied. Under the PRE-task-1.1 reconciliation gate, neither A nor "
|
||||
"C would EVER have been captured (their DISPATCH and RUN-"
|
||||
"confirmation rounds never coincide), so resInUse would read a "
|
||||
"PERMANENTLY-ZERO 0.0 here and this check would WRONGLY grant "
|
||||
"admission");
|
||||
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");
|
||||
|
|
|
|||
|
|
@ -404,6 +404,7 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
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
|
||||
|
|
|
|||
|
|
@ -315,6 +315,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* xce_sched_driver_on_decision()'s (driver.c) publish-side linker
|
||||
* dependencies -- never actually invoked at runtime: this test never
|
||||
* registers a decision callback (xce_core_set_decision_cb() is never
|
||||
|
|
|
|||
|
|
@ -231,6 +231,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
|
||||
* rationale/algorithm as test_sim_startup.c's/test_core_full_cycle.c's own
|
||||
* copies (schedobj.c is not linked: it would need plugin_register(), which
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@
|
|||
#include "resobj.h"
|
||||
#include "scheduler.h"
|
||||
#include "sim.h"
|
||||
#include "xcealloc.h"
|
||||
#include "xstring.h"
|
||||
|
||||
/* write_capnp_message (recording.c) has no public header declaration --
|
||||
|
|
@ -379,6 +380,95 @@ static void append_end_of_stream_marker(FILE* f)
|
|||
capn_free(&c);
|
||||
}
|
||||
|
||||
/* bug-1838 regression: same jobStateChange wire shape test_sim_contention_
|
||||
* parity.c's own append_job_state_change_event() establishes. */
|
||||
static void append_jobstatechange_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);
|
||||
}
|
||||
|
||||
/* run_sim_and_capture_stdout - same dup2(STDOUT_FILENO)-redirect technique
|
||||
* test_sim_verify.c's own copy establishes, to capture sim_log_mismatch()'s
|
||||
* ERROR()-logged "MISMATCH" text for a --sim-verify assertion. Takes the
|
||||
* caller's own static xce_sched_sim_driver_t* (never a stack-local one --
|
||||
* see this file's own header comment, "bug-1598/1632/1763 fixture-safety
|
||||
* requirement", for why: sched_sim_driver_run() spawns two real,
|
||||
* never-joined threads that keep dereferencing this pointer for the rest
|
||||
* of the process's life). */
|
||||
static char* run_sim_and_capture_stdout(xce_sched_sim_driver_t* sim,
|
||||
const char* path, bool verify,
|
||||
int* out_rc)
|
||||
{
|
||||
sched_sim_args_t args;
|
||||
char tmp[] = "/tmp/xce_simlivealloc_run_XXXXXX";
|
||||
int fd = mkstemp(tmp);
|
||||
int saved_stdout;
|
||||
off_t len;
|
||||
char* buf;
|
||||
|
||||
TEST_ASSERT_TRUE_MESSAGE(fd >= 0, "mkstemp for stdout capture must "
|
||||
"succeed");
|
||||
|
||||
fflush(stdout);
|
||||
saved_stdout = dup(STDOUT_FILENO);
|
||||
TEST_ASSERT_TRUE_MESSAGE(saved_stdout >= 0, "dup(STDOUT_FILENO) must "
|
||||
"succeed");
|
||||
TEST_ASSERT_TRUE_MESSAGE(dup2(fd, STDOUT_FILENO) >= 0,
|
||||
"dup2 redirect of STDOUT_FILENO must succeed");
|
||||
|
||||
memset(&args, 0, sizeof(args));
|
||||
args.replay_path = path;
|
||||
args.sim_start_ts = SCHED_SIM_START_TS_LATEST;
|
||||
args.sim_speed = 0;
|
||||
args.verify = verify;
|
||||
|
||||
*out_rc = sched_sim_driver_run(sim, &args);
|
||||
|
||||
fflush(stdout);
|
||||
dup2(saved_stdout, STDOUT_FILENO);
|
||||
close(saved_stdout);
|
||||
|
||||
len = lseek(fd, 0, SEEK_CUR);
|
||||
TEST_ASSERT_TRUE(len >= 0);
|
||||
|
||||
buf = (char*)calloc(1, (size_t)len + 1);
|
||||
TEST_ASSERT_NOT_NULL(buf);
|
||||
TEST_ASSERT_TRUE(lseek(fd, 0, SEEK_SET) == 0);
|
||||
if (len > 0) {
|
||||
ssize_t nread = read(fd, buf, (size_t)len);
|
||||
TEST_ASSERT_TRUE(nread == len);
|
||||
}
|
||||
close(fd);
|
||||
unlink(tmp);
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* write_header_and_snapshot - produces a valid ReplayHeader+SnapshotEntry
|
||||
* prefix at a fresh temp path via the real sched_record_start(), same
|
||||
* technique test_sim_event_loop.c's own write_header_and_snapshot()/
|
||||
|
|
@ -516,9 +606,200 @@ test_sim_driver_run_dispatches_a_job_with_available_resources(void)
|
|||
"state flip with no resource assignment behind it");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* bug-1838 regression: --sim-verify across a dispatch-then-later- */
|
||||
/* confirmation gap, via GENUINE live dispatch (not reconciliation). */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* test_sim_verify_no_false_mismatch_after_live_dispatch_and_later_
|
||||
* confirmation -- scheduler-sim-eager-reconciliation's original Requirement
|
||||
* 4.2 ("a dispatch decision and its RUN-state confirmation recorded several
|
||||
* rounds apart must not produce a false --sim-verify mismatch"), now proved
|
||||
* via the mechanism that actually handles this case in production since
|
||||
* bug-1838 (scheduler-driver-equivalence: sim_reconcile_unaccounted_jobs()
|
||||
* narrowed back to JOB_IS_RUN-only, undoing scheduler-sim-eager-
|
||||
* reconciliation's 2026-09-11 PEND-inclusive widening).
|
||||
*
|
||||
* The ORIGINAL version of this Observable lived in test_sim_verify.c, which
|
||||
* deliberately stubs sched_job_picker()/sched_allocator() to a PEND-only
|
||||
* pair (see that file's own header comment) -- so BEFORE bug-1838, that
|
||||
* file could only ever prove this Requirement via reconciliation itself
|
||||
* force-applying the recorded DISPATCH (the PEND-only stub could never
|
||||
* produce one). Under the new RUN-only trigger, job 601's fixture there
|
||||
* (freshly PEND at round 1) no longer reaches reconciliation at all -- and
|
||||
* that file's stub allocator has nothing else to offer, so the scenario is
|
||||
* no longer reachable there. This file links the REAL scheduler_srcs (see
|
||||
* this file's own header comment), so it is the correct home for a live
|
||||
* proof of the same property: job TEST_JOBID is pushed PEND and dispatched
|
||||
* by the REAL sched_job_picker()/sched_allocator() pair at round 1
|
||||
* (matching the fixture the file's own first test already establishes),
|
||||
* --sim-verify is enabled, and the job's SEPARATE, later-replayed
|
||||
* JOB_STATE_CHANGE-to-RUN confirmation (round 2) is a redundant, safe
|
||||
* no-op re-affirmation (core_events.c's own unconditional `job->state =
|
||||
* new_state` reassignment) -- neither round may report a MISMATCH.
|
||||
*
|
||||
* A second, independent `xce_sched_sim_driver_t` (g_sim2, NOT g_sim) is
|
||||
* required here even though this file's own single-RUN_TEST() convention
|
||||
* means only one of these two tests actually runs per process invocation
|
||||
* under normal `meson test` execution -- see this file's own header
|
||||
* comment ("bug-1598/1632/1763 fixture-safety requirement"): sharing g_sim
|
||||
* across two independently-invocable RUN_TEST() entries would leave a
|
||||
* SECOND call's sched_sim_driver_start() memset()ing state a FIRST call's
|
||||
* still-live, never-joined threads could concurrently be dereferencing, if
|
||||
* a future harness change ever runs both tests in the same process. Static
|
||||
* storage (not stack-local) for the identical reason g_sim already is.
|
||||
*/
|
||||
static xce_sched_sim_driver_t g_sim2;
|
||||
|
||||
static void
|
||||
test_sim_verify_no_false_mismatch_after_live_dispatch_and_later_confirmation(
|
||||
void)
|
||||
{
|
||||
char path[PATH_MAX];
|
||||
FILE* f;
|
||||
xce_resource_t* slots_res;
|
||||
xce_resource_t* res_ptrs[1];
|
||||
xce_worker_t* worker_ptrs[1];
|
||||
xce_queue_t* queue;
|
||||
xce_job_t* job_ptrs[1];
|
||||
xce_sched_job_t* sjob;
|
||||
char* captured;
|
||||
int rc;
|
||||
|
||||
f = write_header_and_snapshot(path, sizeof(path));
|
||||
|
||||
slots_res = (xce_resource_t*)calloc(1, sizeof(*slots_res));
|
||||
TEST_ASSERT_NOT_NULL(slots_res);
|
||||
slots_res->name = strdup("slots");
|
||||
res_ptrs[0] = slots_res;
|
||||
append_resourcespush_event(f, 1001ULL, res_ptrs, 1);
|
||||
|
||||
worker_ptrs[0] = make_wire_worker(WORKER_NAME, WORKER_SLOTS);
|
||||
append_workerspush_event(f, 1002ULL, worker_ptrs, 1);
|
||||
|
||||
queue = make_wire_queue(QUEUE_NAME, 1);
|
||||
append_queueupdate_event(f, 1003ULL, queue);
|
||||
|
||||
job_ptrs[0] = make_push_job(TEST_JOBID, TEST_RESREQ, (char*)"alice",
|
||||
(char*)QUEUE_NAME, (char*)"proj1", 1, 1);
|
||||
append_jobspush_event(f, 1004ULL, job_ptrs, 1);
|
||||
|
||||
/* Round 1: empty recorded decisions -- --sim-verify compares the live
|
||||
* picker/allocator's OWN freshly-drained DISPATCH decision against
|
||||
* this round's decoded_session, which carries nothing recorded at all.
|
||||
* A live decision with no corresponding recorded entry is ITSELF
|
||||
* reported as a mismatch by sim_loop.c's own drain loop (expected=NULL)
|
||||
* -- so, unlike test_sim_driver_run_dispatches_a_job_with_available_
|
||||
* resources above, THIS test must record the SAME DISPATCH the live
|
||||
* path is about to independently produce, proving they agree, not
|
||||
* merely that the live path ran. Built the same way test_sim_
|
||||
* contention_parity.c's own make_dispatch_alloc()-based fixtures do:
|
||||
* one host allocation naming WORKER_NAME, wanting exactly what a
|
||||
* 1-task job's TEST_RESREQ ("rusage[mem=1]", auto-injected 1 slot per
|
||||
* task) actually requests.
|
||||
*
|
||||
* sim_verify_alloc_eq() (sim_verify.c) compares xce_alloc_t
|
||||
* field-by-field, including instid and duration -- both must match
|
||||
* EXACTLY what the real seqjob_allocator()->sched_make_allocs()-
|
||||
* >make_alloc() chain (scheduler.c) will independently produce for
|
||||
* this exact fixture, not arbitrary placeholders:
|
||||
* - instid=1: sched_get_id() returns core->nextid++, and core.c
|
||||
* initializes nextid to 1 (core.c:127) -- this fixture's job is
|
||||
* the FIRST (and only) allocation ever made against this fresh
|
||||
* core, so it is deterministically the first ID issued.
|
||||
* - duration=-1: seqjob_allocator.c's own sched_make_allocs() call
|
||||
* passes a NULL duration array ("FIXME: no duration now"), so
|
||||
* sched_make_allocs() (scheduler.c) always defaults every entry's
|
||||
* duration to -1 today.
|
||||
* `from` (wall-clock time(NULL), set unconditionally by make_alloc()
|
||||
* itself) is NOT independently reproduced here -- it is expected to
|
||||
* agree with the live path's own call by virtue of both happening
|
||||
* within the same sub-second test execution window, not by
|
||||
* construction; this is a real, if practically negligible, source of
|
||||
* flakiness inherent to comparing a genuinely-live-dispatched
|
||||
* decision's wall-clock-stamped fields via --sim-verify, orthogonal
|
||||
* to anything bug-1838 changed. */
|
||||
{
|
||||
xce_alloc_t* a = make_alloc((char*)"slots", NULL, 1, 1.0,
|
||||
-1);
|
||||
xce_alloc_t** allocs = make_allocs(1, a);
|
||||
xce_host_alloc_t* h = make_host_alloc((char*)WORKER_NAME, 1, allocs);
|
||||
xce_host_alloc_t** hallocs = make_host_allocs(1, h);
|
||||
xce_job_alloc_t* alloc = make_job_alloc(TEST_JOBID, 1, hallocs);
|
||||
sched_dispatch_decision_t dispatch;
|
||||
replay_decision_t d_dispatch;
|
||||
replay_decision_t* decisions[1];
|
||||
replay_session_t session;
|
||||
struct capn c;
|
||||
struct capn_segment* cs;
|
||||
ReplaySession_ptr sp;
|
||||
|
||||
memset(&dispatch, 0, sizeof(dispatch));
|
||||
dispatch.alloc = alloc;
|
||||
|
||||
memset(&d_dispatch, 0, sizeof(d_dispatch));
|
||||
d_dispatch.body_which = ReplayDecision_body_dispatch;
|
||||
d_dispatch.body.dispatch = &dispatch;
|
||||
d_dispatch.ts = 2000ULL;
|
||||
|
||||
decisions[0] = &d_dispatch;
|
||||
|
||||
memset(&session, 0, sizeof(session));
|
||||
session.begin_round = 2000ULL;
|
||||
session.decisions = decisions;
|
||||
session.n_decisions = 1;
|
||||
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);
|
||||
}
|
||||
|
||||
/* Round 2: job's separately-recorded RUN confirmation, several rounds
|
||||
* (here: one) after its own round-1 DISPATCH -- a redundant, safe
|
||||
* no-op by the time it replays (the live path already made the job
|
||||
* RUN at round 1). Recorded decisions empty, matching what a genuinely
|
||||
* idle round produces. */
|
||||
append_jobstatechange_event(f, 2500ULL, TEST_JOBID, JOB_STAT_PEND,
|
||||
JOB_STAT_RUN);
|
||||
append_replay_session(f, 3000ULL);
|
||||
|
||||
append_end_of_stream_marker(f);
|
||||
fclose(f);
|
||||
|
||||
captured = run_sim_and_capture_stdout(&g_sim2, path, true, &rc);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
0, rc, "sched_sim_driver_run() must complete both rounds and reach "
|
||||
"its own end-of-stream marker");
|
||||
TEST_ASSERT_NULL_MESSAGE(
|
||||
strstr(captured, "MISMATCH"),
|
||||
"a dispatch decision and its RUN-state confirmation recorded in a "
|
||||
"later round must not produce a false --sim-verify mismatch "
|
||||
"(scheduler-sim-eager-reconciliation Requirement 4.2), now proved "
|
||||
"via genuine live dispatch (bug-1838) rather than reconciliation");
|
||||
|
||||
sjob = sched_get_job(&(g_sim2.core), TEST_JOBID);
|
||||
TEST_ASSERT_NOT_NULL(sjob);
|
||||
TEST_ASSERT_EQUAL_UINT_MESSAGE(
|
||||
JOB_STAT_RUN, sjob->state,
|
||||
"the job must genuinely be RUN via the live picker/allocator's own "
|
||||
"round-1 dispatch, independent of round 2's redundant confirmation "
|
||||
"event");
|
||||
TEST_ASSERT_NOT_NULL(sjob->allocs);
|
||||
|
||||
free(captured);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_sim_driver_run_dispatches_a_job_with_available_resources);
|
||||
RUN_TEST(
|
||||
test_sim_verify_no_false_mismatch_after_live_dispatch_and_later_confirmation);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,6 +237,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
|
||||
* copy test_sim_alt_events.c/test_sim_verify.c/test_sim_event_loop.c/
|
||||
* test_repl_execution_control.c/test_repl_round_rollup.c/
|
||||
|
|
|
|||
|
|
@ -261,6 +261,7 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return rc;
|
||||
}
|
||||
|
||||
|
||||
/* 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. */
|
||||
|
|
@ -838,31 +839,45 @@ static void test_reconcile_ignores_non_dispatch_entry_with_matching_jobid(
|
|||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Test 5 (scheduler-sim-eager-reconciliation task 1.1, replaces the old */
|
||||
/* test_reconcile_skips_non_run_state_job): the trigger is now */
|
||||
/* state-independent -- a job whose recorded DISPATCH decision is found */
|
||||
/* this round gets its allocation captured regardless of whether it has */
|
||||
/* reached RUN state yet. This is the inverse of the old (buggy) expected */
|
||||
/* behavior: previously a PEND job with a matching recorded DISPATCH entry */
|
||||
/* stayed uncaptured; it must now be captured just like an already-RUN */
|
||||
/* job (Requirement 1.1, 3.1). The dominant real-world case (dispatch and */
|
||||
/* its RUN-state confirmation recorded many rounds apart) depends on this. */
|
||||
/* Test 5 -- INVERTED (2026-09-21, bug-1838, scheduler-driver-equivalence). */
|
||||
/* Originally test_reconcile_captures_non_run_state_job_when_dispatch_ */
|
||||
/* recorded, from scheduler-sim-eager-reconciliation task 1.1: proved the */
|
||||
/* trigger was state-independent (a PEND job's recorded DISPATCH got */
|
||||
/* captured just like an already-RUN job's). bug-1838 narrowed the trigger */
|
||||
/* back to JOB_IS_RUN-only (see sim_reconcile_unaccounted_jobs()'s own */
|
||||
/* updated header comment in sim_loop.c for the full rationale: */
|
||||
/* scheduler-sim-live-allocation, completed after this spec, gave sim mode */
|
||||
/* a genuine capacity-checked live picker/allocator path, making the */
|
||||
/* PEND-inclusive widening redundant -- a freshly-PEND job now correctly */
|
||||
/* flows to the SAME live path normal mode uses instead). This test is */
|
||||
/* renamed and its assertions inverted to lock in the NEW behavior: a */
|
||||
/* PEND-state job's recorded DISPATCH decision is left UNTOUCHED by */
|
||||
/* reconciliation, even when a matching entry exists -- dispatching such a */
|
||||
/* job is now the live picker/allocator's job, not exercised by this */
|
||||
/* direct-call unit test (see test_sim_live_allocation.c for that proof). */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void
|
||||
test_reconcile_captures_non_run_state_job_when_dispatch_recorded(void)
|
||||
test_reconcile_ignores_pend_state_job_even_when_dispatch_recorded(void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
xce_sched_job_t* job;
|
||||
xce_job_alloc_t* jalloc;
|
||||
replay_session_t* decoded;
|
||||
int residx;
|
||||
|
||||
fixture_init(&sim);
|
||||
|
||||
/* PEND-state, no known allocation yet -- the realistic case: this
|
||||
* job's recorded DISPATCH decision has been found in this round's
|
||||
* replay stream, but its RUN-state confirmation has not been replayed
|
||||
* yet (may land many rounds later). */
|
||||
residx = register_slots_resource(&(sim.core));
|
||||
add_worker(&(sim.core), "worker1", residx, 1000.0f);
|
||||
|
||||
/* PEND-state, no known allocation yet, with a matching recorded
|
||||
* DISPATCH decision available this round -- under bug-1838's RUN-only
|
||||
* trigger, this must NOT be captured: reconciliation's remaining
|
||||
* scope is a job already RUN with no known allocation (structurally
|
||||
* invisible to the live picker, since next_job() never offers a RUN
|
||||
* job) -- a freshly-PEND job like this one is a perfectly ordinary
|
||||
* live-picker candidate instead. */
|
||||
job = add_job(&(sim.core), 7005ULL, JOB_STAT_PEND, NULL);
|
||||
TEST_ASSERT_NULL(job->accounts);
|
||||
TEST_ASSERT_NULL(job->allocs);
|
||||
|
|
@ -873,22 +888,27 @@ test_reconcile_captures_non_run_state_job_when_dispatch_recorded(void)
|
|||
|
||||
sim_reconcile_unaccounted_jobs(&sim, decoded, NULL);
|
||||
|
||||
TEST_ASSERT_EQUAL_PTR_MESSAGE(
|
||||
jalloc, job->allocs,
|
||||
"a PEND-state job's recorded DISPATCH decision must now be "
|
||||
"captured (ownership TAKEOVER, identical pointer) just like an "
|
||||
"already-RUN job -- capture eligibility no longer depends on "
|
||||
"job->state");
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
TEST_ASSERT_NULL_MESSAGE(
|
||||
job->allocs,
|
||||
"bug-1838: a PEND-state job must NOT be captured by reconciliation "
|
||||
"even when a matching recorded DISPATCH entry exists -- eligibility "
|
||||
"is RUN-only again; the live picker/allocator (not exercised by "
|
||||
"this direct-call unit test) is what decides a PEND job now");
|
||||
TEST_ASSERT_NULL_MESSAGE(
|
||||
job->accounts,
|
||||
"job->accounts must be built (make_job_account()) from the "
|
||||
"captured allocation even though the job has not reached RUN yet");
|
||||
"job->accounts must stay NULL -- nothing here should fabricate an "
|
||||
"allocation for a job reconciliation correctly left alone");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
0, decoded->n_decisions,
|
||||
"the now-fully-consumed recorded entry must be removed from "
|
||||
"decoded_session->decisions, exactly as for an already-RUN job");
|
||||
1, decoded->n_decisions,
|
||||
"the recorded entry must NOT be consumed -- it was never a "
|
||||
"candidate match in the first place, so it is left exactly as "
|
||||
"found for whatever (if anything) still wants to read it");
|
||||
TEST_ASSERT_EQUAL_UINT_MESSAGE(
|
||||
JOB_STAT_PEND, job->state,
|
||||
"job->state must remain untouched (still PEND) -- reconciliation "
|
||||
"never even attempted this job");
|
||||
|
||||
printf("test_reconcile_captures_non_run_state_job_when_dispatch_"
|
||||
printf("test_reconcile_ignores_pend_state_job_even_when_dispatch_"
|
||||
"recorded: PASS\n");
|
||||
}
|
||||
|
||||
|
|
@ -906,9 +926,16 @@ test_reconcile_reconstructs_allocs_and_accounts_from_matching_dispatch(void)
|
|||
xce_sched_job_t* job;
|
||||
xce_job_alloc_t* jalloc;
|
||||
replay_session_t* decoded;
|
||||
int residx;
|
||||
|
||||
fixture_init(&sim);
|
||||
|
||||
/* bug-1836 fix: see test_reconcile_captures_non_run_state_job_when_
|
||||
* dispatch_recorded's own comment above -- the capacity gate needs a
|
||||
* REAL registered "worker1". */
|
||||
residx = register_slots_resource(&(sim.core));
|
||||
add_worker(&(sim.core), "worker1", residx, 1000.0f);
|
||||
|
||||
job = add_job(&(sim.core), 7010ULL, JOB_STAT_RUN, NULL);
|
||||
TEST_ASSERT_NULL(job->accounts);
|
||||
TEST_ASSERT_NULL(job->allocs);
|
||||
|
|
@ -1017,9 +1044,14 @@ static void test_reconcile_then_release_via_job_state_change_is_asan_clean(void)
|
|||
replay_session_t* decoded;
|
||||
xce_core_event_t ev;
|
||||
int push_rc;
|
||||
int residx;
|
||||
|
||||
fixture_init(&sim);
|
||||
|
||||
/* bug-1836 fix: the capacity gate needs a REAL registered "worker1". */
|
||||
residx = register_slots_resource(&(sim.core));
|
||||
add_worker(&(sim.core), "worker1", residx, 1000.0f);
|
||||
|
||||
job = add_job(&(sim.core), 7012ULL, JOB_STAT_RUN, NULL);
|
||||
|
||||
decoded = make_decoded_session(
|
||||
|
|
@ -1094,9 +1126,14 @@ test_reconcile_reports_reconstructed_decision_through_existing_channel(void)
|
|||
int saved_stdout;
|
||||
off_t len;
|
||||
char* captured;
|
||||
int residx;
|
||||
|
||||
fixture_init(&sim);
|
||||
|
||||
/* bug-1836 fix: the capacity gate needs a REAL registered "worker9". */
|
||||
residx = register_slots_resource(&(sim.core));
|
||||
add_worker(&(sim.core), "worker9", residx, 1000.0f);
|
||||
|
||||
job = add_job(&(sim.core), 7020ULL, JOB_STAT_RUN, NULL);
|
||||
TEST_ASSERT_NULL(job->accounts);
|
||||
|
||||
|
|
@ -1208,6 +1245,16 @@ static void test_reconcile_null_args_are_a_safe_noop(void)
|
|||
/* contributed zero to resInUse until a SEPARATE, later job->state = RUN */
|
||||
/* write. See requirements.md's Requirement 1 2026-09-11 revision and */
|
||||
/* research.md's corresponding entry for the full rationale trail. */
|
||||
/* */
|
||||
/* REVISED AGAIN (2026-09-21, bug-1838): the job below now starts as */
|
||||
/* JOB_STAT_RUN, not PEND -- bug-1838 narrowed reconciliation's trigger */
|
||||
/* back to JOB_IS_RUN-only (a PEND job is no longer a candidate at all, */
|
||||
/* regardless of a matching recorded decision -- see sim_loop.c's own */
|
||||
/* updated header comment on sim_reconcile_unaccounted_jobs()). This */
|
||||
/* test's own point -- a captured job's resInUse charge lands the SAME */
|
||||
/* round as capture, not deferred -- is orthogonal to PEND-vs-RUN and */
|
||||
/* still fully exercised by a RUN-starting job (the remaining genuine */
|
||||
/* reconciliation case: already RUN, allocation still unknown). */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void
|
||||
|
|
@ -1229,9 +1276,11 @@ test_captured_job_included_in_resinuse_same_round_as_capture(void)
|
|||
residx = register_slots_resource(&(sim.core));
|
||||
w = add_worker(&(sim.core), "worker1", residx, 10.0f);
|
||||
|
||||
/* PEND-state job, no known allocation yet -- its recorded DISPATCH
|
||||
* decision is about to be found this round. */
|
||||
job = add_job(&(sim.core), 7030ULL, JOB_STAT_PEND, NULL);
|
||||
/* bug-1838: RUN-state job, no known allocation yet -- the remaining
|
||||
* genuine reconciliation case (already RUN before this replay's own
|
||||
* observation window, allocation never captured). Its recorded
|
||||
* DISPATCH decision is about to be found this round. */
|
||||
job = add_job(&(sim.core), 7030ULL, JOB_STAT_RUN, NULL);
|
||||
TEST_ASSERT_NULL(job->accounts);
|
||||
TEST_ASSERT_NULL(job->allocs);
|
||||
|
||||
|
|
@ -1327,20 +1376,137 @@ test_captured_job_included_in_resinuse_same_round_as_capture(void)
|
|||
"PASS\n");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* bug-1836 regression (scheduler-driver-equivalence): a resource- */
|
||||
/* contention fixture per .kiro/steering/scheduler-driver-equivalence.md's */
|
||||
/* own testing discipline ("multiple jobs competing for capacity */
|
||||
/* insufficient for all of them"). Live symptom this closes: a real */
|
||||
/* 22-slot host took 44 concurrent single-slot jobs during replay, because */
|
||||
/* sim_reconcile_unaccounted_jobs() force-applied every recorded DISPATCH */
|
||||
/* decision unconditionally, never checking whether it still fit CURRENT */
|
||||
/* capacity. Two RUN jobs (bug-1838: reconciliation's trigger is RUN-only */
|
||||
/* again -- these model jobs already confirmed RUN via a replayed */
|
||||
/* JOB_STATE_CHANGE event, awaiting their allocation data), each with a */
|
||||
/* recorded DISPATCH decision wanting the SAME host's only unit of */
|
||||
/* "slots" (capacity 1) -- normal mode would dispatch exactly one and PEND */
|
||||
/* the other; this proves sim mode's reconciliation backstop now does the */
|
||||
/* capacity check instead of force-applying both unconditionally. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_reconcile_respects_host_capacity_under_contention(void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
xce_sched_job_t* jobA;
|
||||
xce_sched_job_t* jobB;
|
||||
xce_sched_worker_t* w;
|
||||
xce_job_alloc_t* allocA;
|
||||
xce_job_alloc_t* allocB;
|
||||
replay_session_t* decoded;
|
||||
int residx;
|
||||
float got;
|
||||
int rc;
|
||||
int n_admitted;
|
||||
int n_unaccounted;
|
||||
|
||||
fixture_init(&sim);
|
||||
|
||||
residx = register_slots_resource(&(sim.core));
|
||||
w = add_worker(&(sim.core), "worker1", residx, 1.0f);
|
||||
|
||||
jobA = add_job(&(sim.core), 7040ULL, JOB_STAT_RUN, NULL);
|
||||
jobB = add_job(&(sim.core), 7041ULL, JOB_STAT_RUN, NULL);
|
||||
|
||||
allocA = make_test_job_alloc_with_host(7040ULL, "worker1", "slots", 1.0f);
|
||||
allocB = make_test_job_alloc_with_host(7041ULL, "worker1", "slots", 1.0f);
|
||||
|
||||
decoded = (replay_session_t*)calloc(1, sizeof(*decoded));
|
||||
decoded->begin_round = 1000ULL;
|
||||
decoded->summary = NULL;
|
||||
decoded->decisions
|
||||
= (replay_decision_t**)calloc(2, sizeof(*(decoded->decisions)));
|
||||
decoded->decisions[0] = make_dispatch_entry_from_alloc(allocA);
|
||||
decoded->decisions[1] = make_dispatch_entry_from_alloc(allocB);
|
||||
decoded->n_decisions = 2;
|
||||
|
||||
sim_reconcile_unaccounted_jobs(&sim, decoded, NULL);
|
||||
|
||||
n_admitted = ((jobA->accounts != NULL) ? 1 : 0)
|
||||
+ ((jobB->accounts != NULL) ? 1 : 0);
|
||||
n_unaccounted = ((jobA->accounts == NULL) ? 1 : 0)
|
||||
+ ((jobB->accounts == NULL) ? 1 : 0);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
1, n_admitted,
|
||||
"worker1 has capacity for exactly ONE of these two 1-slot jobs -- "
|
||||
"reconciliation must admit exactly one, not both (the bug-1836 "
|
||||
"over-commit) and not zero");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
1, n_unaccounted,
|
||||
"the job that does not fit must be left unaccounted (accounts == "
|
||||
"NULL) to retry on a later round, not force-dispatched");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
1, decoded->n_decisions,
|
||||
"the entry for the job that did not fit must NOT be consumed -- it "
|
||||
"stays available to retry a later round");
|
||||
|
||||
/* Whichever job was admitted must genuinely be RUN with accounts
|
||||
* populated; the other must ALSO still be RUN (bug-1838: it entered
|
||||
* this call already RUN, and a failed capacity gate never reverts
|
||||
* state -- it is simply left RUN-but-unaccounted, exactly like a job
|
||||
* whose confirming JOB_STATE_CHANGE arrived before its DISPATCH could
|
||||
* be reconciled) -- distinguished from the admitted job only by
|
||||
* accounts/allocs, never by job->state. */
|
||||
if (jobA->accounts != NULL) {
|
||||
TEST_ASSERT_TRUE(JOB_STAT_RUN == jobA->state);
|
||||
TEST_ASSERT_TRUE(JOB_STAT_RUN == jobB->state);
|
||||
} else {
|
||||
TEST_ASSERT_TRUE(JOB_STAT_RUN == jobB->state);
|
||||
TEST_ASSERT_TRUE(JOB_STAT_RUN == jobA->state);
|
||||
}
|
||||
|
||||
/* The core correctness bar: resInUse (via the real, unchanged
|
||||
* sched_account_jobs() call this function's own caller always makes
|
||||
* immediately afterward in production) must reflect EXACTLY the one
|
||||
* admitted job's charge -- 1.0, never 2.0 (the bug-1836 over-commit) --
|
||||
* proving this function's internal peek+incremental-charge+undo left no
|
||||
* residue AND that the one real charge landed correctly. */
|
||||
rc = sched_account_jobs(&(sim.core));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
got = -1.0f;
|
||||
rc = get_resvals(&(w->resInUse), 1, &residx, &got);
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
|
||||
1.0f, got,
|
||||
"bug-1836: resInUse must reflect exactly the one admitted job's "
|
||||
"charge (1.0), never both (2.0, the over-commit this fix closes) "
|
||||
"and never neither (0.0, which would mean the fitting job was "
|
||||
"wrongly rejected too)");
|
||||
|
||||
printf("test_reconcile_respects_host_capacity_under_contention: PASS\n");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Test 13 (scheduler-sim-eager-reconciliation task 1.2, Requirement 3.2): */
|
||||
/* a job with job->accounts ALREADY set is never re-searched or */
|
||||
/* re-captured on a later round's call, regardless of its state (PEND */
|
||||
/* here, deliberately different from test_reconcile_skips_job_that_ */
|
||||
/* already_has_accounts' RUN-state fixture above -- the state genuinely */
|
||||
/* does not matter, only job->accounts != NULL does) -- even when a */
|
||||
/* decoded session that round happens to also contain a matching-jobid */
|
||||
/* DISPATCH entry. Proves the state-widened gate (task 1.1) did not */
|
||||
/* weaken the pre-existing, untouched job->accounts != NULL guard */
|
||||
/* (research.md's "Does removing the RUN-state gate risk fabricating or */
|
||||
/* double-capturing?" log entry: this guard was never touched by task */
|
||||
/* 1.1, so this is regression-proof coverage for an existing guarantee, */
|
||||
/* not new-behavior coverage). */
|
||||
/* re-captured on a later round's call, deliberately fixtured PEND here */
|
||||
/* (different from test_reconcile_skips_job_that_already_has_accounts' */
|
||||
/* RUN-state fixture above) -- even when a decoded session that round */
|
||||
/* happens to also contain a matching-jobid DISPATCH entry. Proves the */
|
||||
/* job->accounts != NULL guard alone is enough to block re-capture, */
|
||||
/* independent of whatever the RUN-only trigger condition (bug-1838) */
|
||||
/* would otherwise say about this job's state. */
|
||||
/* */
|
||||
/* UPDATED (2026-09-21, bug-1838): this test predates the RUN-only */
|
||||
/* narrowing and originally demonstrated the accounts-guard held even */
|
||||
/* under the (now-superseded) state-independent trigger. Since a PEND job */
|
||||
/* is skipped by the state check alone today, this fixture no longer */
|
||||
/* isolates the accounts guard specifically -- it still correctly proves */
|
||||
/* "PEND + already captured" is never re-captured, just via a condition */
|
||||
/* that is now doubly-guarded (state AND accounts) rather than singly. */
|
||||
/* Kept as-is (not deleted) since it remains a true, valid regression */
|
||||
/* lock; see test_reconcile_skips_job_that_already_has_accounts() for the */
|
||||
/* RUN-state fixture that DOES still isolate the accounts guard alone. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void
|
||||
|
|
@ -1384,8 +1550,9 @@ test_reconcile_still_skips_already_captured_job_regardless_of_state(void)
|
|||
accounts_before, job->accounts,
|
||||
"an already-captured PEND-state job must never be re-captured on a "
|
||||
"later round, even with a matching-jobid recorded DISPATCH entry "
|
||||
"present -- job->accounts != NULL remains the sole gate under the "
|
||||
"widened, state-independent trigger");
|
||||
"present -- job->accounts != NULL alone is sufficient to block "
|
||||
"re-capture, regardless of what the RUN-only state check would "
|
||||
"separately say about this job");
|
||||
TEST_ASSERT_EQUAL_PTR_MESSAGE(
|
||||
allocs_before, job->allocs,
|
||||
"job->allocs likewise must stay exactly what it already was -- "
|
||||
|
|
@ -1400,16 +1567,22 @@ test_reconcile_still_skips_already_captured_job_regardless_of_state(void)
|
|||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Test 14 (scheduler-sim-eager-reconciliation task 1.2, Requirement 4.4): */
|
||||
/* a JOB_STAT_PEND job with NO matching recorded decision this round stays */
|
||||
/* uncaptured (job->accounts stays NULL) after one call, and remains */
|
||||
/* eligible -- demonstrated (per this task's own design.md guidance) by */
|
||||
/* calling again with a NOW-matching decoded session and confirming */
|
||||
/* capture succeeds then. Mirrors test_reconcile_is_safe_to_recheck_on_a_ */
|
||||
/* later_round()'s "safe to retry" idiom above, but (unlike that sibling */
|
||||
/* test, whose second round also has no match) this is the genuinely new */
|
||||
/* behavior task 1.1 enables: a PEND job now gets multiple real chances */
|
||||
/* across rounds and can actually succeed on a later one. */
|
||||
/* Test 14 (scheduler-sim-eager-reconciliation task 1.2, Requirement 4.4). */
|
||||
/* REVISED (2026-09-21, bug-1838): originally a JOB_STAT_PEND job (the */
|
||||
/* "in-flight, no match yet" case task 1.1's now-superseded state- */
|
||||
/* independent widening existed for). bug-1838 narrowed reconciliation */
|
||||
/* back to JOB_IS_RUN-only, so a PEND job is never a candidate regardless */
|
||||
/* of whether a match ever appears -- that scenario is now covered by */
|
||||
/* test_reconcile_ignores_pend_state_job_even_when_dispatch_recorded */
|
||||
/* instead. This test's job is changed to JOB_STAT_RUN so its OWN real */
|
||||
/* point survives: a RUN job with no matching recorded decision stays */
|
||||
/* uncaptured (job->accounts stays NULL) after one call, remains fully */
|
||||
/* eligible, and succeeds once a matching entry appears on a later round -- */
|
||||
/* still the remaining, genuine reconciliation case (already RUN, */
|
||||
/* allocation unknown, recorded decision not yet found). Mirrors */
|
||||
/* test_reconcile_is_safe_to_recheck_on_a_later_round()'s "safe to retry" */
|
||||
/* idiom above, but (unlike that sibling test, whose second round also has */
|
||||
/* no match) proves the retry can actually succeed on a later one. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void
|
||||
|
|
@ -1420,12 +1593,18 @@ test_reconcile_no_match_leaves_pend_job_uncaptured_and_retries(void)
|
|||
xce_job_alloc_t* jalloc;
|
||||
replay_session_t* decoded_round1;
|
||||
replay_session_t* decoded_round2;
|
||||
int residx;
|
||||
|
||||
fixture_init(&sim);
|
||||
|
||||
/* PEND-state, no known allocation, no recorded decision available yet
|
||||
* -- the realistic in-flight case task 1.1's widened gate exists for. */
|
||||
job = add_job(&(sim.core), 7041ULL, JOB_STAT_PEND, NULL);
|
||||
/* bug-1836 fix: the capacity gate needs a REAL registered "workerB". */
|
||||
residx = register_slots_resource(&(sim.core));
|
||||
add_worker(&(sim.core), "workerB", residx, 1000.0f);
|
||||
|
||||
/* bug-1838: RUN-state, no known allocation, no recorded decision
|
||||
* available yet -- the realistic in-flight case reconciliation's
|
||||
* RUN-only trigger still genuinely needs to cover. */
|
||||
job = add_job(&(sim.core), 7041ULL, JOB_STAT_RUN, NULL);
|
||||
TEST_ASSERT_NULL(job->accounts);
|
||||
TEST_ASSERT_NULL(job->allocs);
|
||||
|
||||
|
|
@ -1434,8 +1613,9 @@ test_reconcile_no_match_leaves_pend_job_uncaptured_and_retries(void)
|
|||
|
||||
TEST_ASSERT_NULL_MESSAGE(
|
||||
job->accounts,
|
||||
"no matching recorded decision this round -- a PEND job must stay "
|
||||
"uncaptured (usage unavailable), never fabricated");
|
||||
"no matching recorded decision this round -- a RUN job with an "
|
||||
"unknown allocation must stay uncaptured (usage unavailable), "
|
||||
"never fabricated");
|
||||
TEST_ASSERT_NULL_MESSAGE(job->allocs, "job->allocs likewise stays NULL");
|
||||
|
||||
/* Round 2: the job's recorded DISPATCH decision now shows up. The job
|
||||
|
|
@ -1469,8 +1649,8 @@ test_reconcile_no_match_leaves_pend_job_uncaptured_and_retries(void)
|
|||
/* 3.1 under concurrent load): several simultaneously outstanding jobs, */
|
||||
/* each with a DIFFERENT jobid, spread across three scenarios a single */
|
||||
/* reconcile call must resolve with zero cross-job interference: */
|
||||
/* - job_matched: PEND, its own DISPATCH entry IS this round. */
|
||||
/* - job_no_match: PEND, no recorded entry at all this round. */
|
||||
/* - job_matched: RUN, its own DISPATCH entry IS this round. */
|
||||
/* - job_no_match: RUN, no recorded entry at all this round. */
|
||||
/* - job_already: captured on a hypothetical EARLIER round already */
|
||||
/* (job->accounts/job->allocs preset directly via add_job()'s own */
|
||||
/* "already accounted" shape, same technique */
|
||||
|
|
@ -1482,6 +1662,15 @@ test_reconcile_no_match_leaves_pend_job_uncaptured_and_retries(void)
|
|||
/* recorded decision, with no cross-job data or overwrite (the single-job */
|
||||
/* tests above cannot distinguish "correct" from "correct by coincidence, */
|
||||
/* only one candidate available", per design.md's own Testing Strategy). */
|
||||
/* */
|
||||
/* bug-1838: job_matched and job_no_match are both RUN (not PEND) here -- */
|
||||
/* reconciliation's trigger is RUN-only again, so a PEND job is now always */
|
||||
/* skipped regardless of whether a matching entry exists (that claim is */
|
||||
/* covered on its own by */
|
||||
/* test_reconcile_ignores_pend_state_job_even_when_dispatch_recorded). */
|
||||
/* Keeping job_no_match RUN here preserves this test's original, distinct */
|
||||
/* claim: among multiple RUN candidates, reconciliation matches strictly */
|
||||
/* by jobid rather than capturing the first/any available entry. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void
|
||||
|
|
@ -1497,11 +1686,21 @@ test_reconcile_multiple_jobs_captured_independently_across_different_rounds(
|
|||
xce_job_alloc_t* jalloc_matched;
|
||||
xce_job_alloc_t* jalloc_already_new;
|
||||
replay_session_t* decoded;
|
||||
int residx;
|
||||
|
||||
fixture_init(&sim);
|
||||
|
||||
job_matched = add_job(&(sim.core), 7050ULL, JOB_STAT_PEND, NULL);
|
||||
job_no_match = add_job(&(sim.core), 7051ULL, JOB_STAT_PEND, NULL);
|
||||
/* bug-1836 fix: the capacity gate needs a REAL registered "workerA" --
|
||||
* "worker-should-not-apply" (job_already's entry) is deliberately never
|
||||
* registered: that entry is never even searched for (job_already's
|
||||
* accounts != NULL guard skips it), so it must never reach the
|
||||
* capacity gate at all -- if it does, this test's own assertions below
|
||||
* already catch that regression independently. */
|
||||
residx = register_slots_resource(&(sim.core));
|
||||
add_worker(&(sim.core), "workerA", residx, 1000.0f);
|
||||
|
||||
job_matched = add_job(&(sim.core), 7050ULL, JOB_STAT_RUN, NULL);
|
||||
job_no_match = add_job(&(sim.core), 7051ULL, JOB_STAT_RUN, NULL);
|
||||
job_already = add_job(&(sim.core), 7052ULL, JOB_STAT_RUN,
|
||||
make_job_alloc(7052ULL, 0, NULL));
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
|
|
@ -1618,9 +1817,14 @@ test_reconcile_continue_mode_with_repl_state_suppresses_bare_decision_line(
|
|||
int saved_stdout;
|
||||
off_t len;
|
||||
char* captured;
|
||||
int residx;
|
||||
|
||||
fixture_init(&sim);
|
||||
|
||||
/* bug-1836 fix: the capacity gate needs a REAL registered "worker9". */
|
||||
residx = register_slots_resource(&(sim.core));
|
||||
add_worker(&(sim.core), "worker9", residx, 1000.0f);
|
||||
|
||||
job = add_job(&(sim.core), 7060ULL, JOB_STAT_RUN, NULL);
|
||||
TEST_ASSERT_NULL(job->accounts);
|
||||
|
||||
|
|
@ -1716,9 +1920,14 @@ test_reconcile_step_mode_with_repl_state_still_prints_bare_decision_line(
|
|||
int saved_stdout;
|
||||
off_t len;
|
||||
char* captured;
|
||||
int residx;
|
||||
|
||||
fixture_init(&sim);
|
||||
|
||||
/* bug-1836 fix: the capacity gate needs a REAL registered "worker9". */
|
||||
residx = register_slots_resource(&(sim.core));
|
||||
add_worker(&(sim.core), "worker9", residx, 1000.0f);
|
||||
|
||||
job = add_job(&(sim.core), 7061ULL, JOB_STAT_RUN, NULL);
|
||||
|
||||
jalloc = make_test_job_alloc_with_host(7061ULL, "worker9", "slots", 3.0f);
|
||||
|
|
@ -2220,6 +2429,31 @@ test_reconcile_info_worker_and_summary_hide_completed_job_after_stray_dispatch(
|
|||
* picker candidates, so `job` genuinely sits in the picker's pendJobs
|
||||
* exactly as it would after a live JOBS_PUSH, without needing a full
|
||||
* xce_core_push_events() event round trip.
|
||||
*
|
||||
* bug-1838: reconciliation's trigger is RUN-only again, so `job` must
|
||||
* already be RUN by the time sim_reconcile_unaccounted_jobs() looks at it.
|
||||
* But builtin_fcfs_on_job_push() (the picker hook sim_bootstrap_picker()
|
||||
* calls) only ever inserts a PEND job into pendJobs -- a job constructed
|
||||
* directly as RUN would never become a picker candidate in the first
|
||||
* place, which would make this test's own "fixture sanity" check below
|
||||
* fail for an unrelated reason (never a candidate to begin with) instead
|
||||
* of proving the thing this test exists to prove.
|
||||
*
|
||||
* So the fixture builds the job PEND (so sim_bootstrap_picker() legitimately
|
||||
* seats it in pendJobs, exactly like a live JOBS_PUSH would), then forces
|
||||
* job->state to RUN directly afterward -- WITHOUT going through
|
||||
* on_job_state_change(), unlike every real production path that reaches
|
||||
* RUN (XCE_CORE_EVENT_JOB_STATE_CHANGE, core_events.c, always calls
|
||||
* on_job_state_change() in the same breath it sets job->state, which
|
||||
* already retires a no-longer-pending job from pendJobs on its own; the
|
||||
* live sched_allocator() path, sched_election.c, always calls on_dispatch()
|
||||
* BEFORE sched_commit_decision() sets job->state = RUN). This deliberately
|
||||
* reproduces the pathological state those two invariants are supposed to
|
||||
* make unreachable in production -- a RUN job the picker was never told
|
||||
* about -- so this test still proves sim_reconcile_unaccounted_jobs()'s own
|
||||
* on_dispatch() call (bug-1759/1779) is a genuine, working safety net, not
|
||||
* dead code, even though bug-1838 closed off the one call path
|
||||
* (PEND-state reconciliation) that used to reach it in practice.
|
||||
*/
|
||||
static void
|
||||
test_reconcile_removes_reconciled_job_from_picker_candidates(void)
|
||||
|
|
@ -2251,10 +2485,18 @@ test_reconcile_removes_reconciled_job_from_picker_candidates(void)
|
|||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
offered, "fixture sanity: job must be a picker candidate before "
|
||||
"reconciliation runs");
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
|
||||
9101ULL, offered->jobid,
|
||||
"fixture sanity: the offered candidate must be this test's own job");
|
||||
|
||||
/* bug-1838: force RUN directly (bypassing on_job_state_change) -- see
|
||||
* this test's own header comment for why this, and not constructing
|
||||
* the job as RUN from the start, is required to both (a) make it a
|
||||
* genuine reconciliation candidate under the new RUN-only trigger and
|
||||
* (b) keep it (pathologically) still present in pendJobs going in. */
|
||||
job->state = JOB_STAT_RUN;
|
||||
|
||||
dispatch_entry = make_dispatch_entry(9101ULL);
|
||||
decoded_session = make_decoded_session(0, dispatch_entry);
|
||||
|
||||
|
|
@ -2403,11 +2645,26 @@ test_reconcile_registers_global_limit_usage_for_reconciled_job(void)
|
|||
iterator_t it;
|
||||
void* item = NULL;
|
||||
int rc;
|
||||
xce_resource_t* mem_res;
|
||||
|
||||
fixture_init(&sim);
|
||||
|
||||
bootstrap_global_limit_plugin(&sim, "sim_reconcile_global_limit");
|
||||
|
||||
/* bug-1836 fix: the capacity gate needs a REAL registered "worker1"
|
||||
* owning the "mem" resource. bootstrap_global_limit_plugin() above
|
||||
* already registered "mem" itself (via its own register_mem_resource()
|
||||
* call, for limit-matching purposes) -- calling register_mem_resource()
|
||||
* a second time here would fail (makeResource() rejects a duplicate
|
||||
* name), so look the already-registered resource up instead; only the
|
||||
* WORKER instance owning it is still missing (the capacity gate's
|
||||
* per-host check needs sched_get_worker("worker1") to resolve, which
|
||||
* bootstrap_global_limit_plugin() never adds). */
|
||||
TEST_ASSERT_TRUE(
|
||||
key_find(sim.core.resmgr, (void*)"mem", (void**)&mem_res) >= 0);
|
||||
TEST_ASSERT_NOT_NULL(mem_res);
|
||||
add_worker(&(sim.core), "worker1", mem_res->idx, 1000.0f);
|
||||
|
||||
job = add_job(&(sim.core), 7090ULL, JOB_STAT_RUN, NULL);
|
||||
TEST_ASSERT_NULL(job->accounts);
|
||||
/* match_job_global_limit_() (plugins/limit.c) matches against
|
||||
|
|
@ -2534,7 +2791,7 @@ int main(void)
|
|||
RUN_TEST(test_reconcile_skips_job_that_already_has_accounts);
|
||||
RUN_TEST(test_reconcile_ignores_non_dispatch_entry_with_matching_jobid);
|
||||
RUN_TEST(
|
||||
test_reconcile_captures_non_run_state_job_when_dispatch_recorded);
|
||||
test_reconcile_ignores_pend_state_job_even_when_dispatch_recorded);
|
||||
RUN_TEST(
|
||||
test_reconcile_reconstructs_allocs_and_accounts_from_matching_dispatch);
|
||||
RUN_TEST(test_reconcile_compacts_via_shift_down_preserving_order);
|
||||
|
|
|
|||
|
|
@ -196,6 +196,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int sched_resolve_job_picker(xce_sched_core_t* core, xce_sched_plugin_t** out)
|
||||
{
|
||||
xce_sched_plugin_t* plugin = NULL;
|
||||
|
|
|
|||
|
|
@ -236,6 +236,7 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* xce_sched_driver_on_decision()'s (driver.c) publish-side linker
|
||||
* dependencies -- never actually invoked: this test never registers a
|
||||
* decision callback, so every decision is simply recorded and dropped.
|
||||
|
|
|
|||
|
|
@ -296,6 +296,7 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
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
|
||||
|
|
|
|||
|
|
@ -376,6 +376,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static void reset_spy(void)
|
||||
{
|
||||
atomic_store(&g_spy_calls, 0);
|
||||
|
|
|
|||
|
|
@ -225,6 +225,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
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. */
|
||||
|
|
|
|||
|
|
@ -95,6 +95,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
|
||||
* rationale/algorithm as test_record_lifecycle.c's own copy (schedobj.c is
|
||||
* not linked). Both the FIXTURE core (built via a real xce_scheduler_t,
|
||||
|
|
|
|||
|
|
@ -222,6 +222,55 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
||||
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
||||
* file's existing sched_account_jobs() no-op precedent immediately above --
|
||||
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
||||
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
||||
* re-validate a recorded decision against live capacity, and
|
||||
* sched_charge_account() (resource.c) to keep that check's own view of
|
||||
* capacity up to date across multiple candidates in the same round. This
|
||||
* file never exercises real resource contention (same rationale as the
|
||||
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
||||
* the satisfy checks trivially always succeed and the charge call is a
|
||||
* no-op. */
|
||||
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
||||
xvec_t* uncomitted, xvec_idval_t* wanted,
|
||||
xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)w;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
||||
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
||||
{
|
||||
(void)core;
|
||||
(void)uncomitted;
|
||||
(void)wanted;
|
||||
(void)unmeet;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
||||
{
|
||||
(void)core;
|
||||
(void)acct;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
|
||||
* copy test_sim_event_loop.c/test_sim_startup.c/test_core_full_cycle.c each
|
||||
* carry (schedobj.c is not linked here: it would need plugin_register(),
|
||||
|
|
@ -750,6 +799,29 @@ static void make_tmp_dir(char* out, size_t out_len, const char* suffix)
|
|||
* require a resolvable picker (Req 5 AC 21: "Picker state is NOT stored in
|
||||
* snapshot").
|
||||
*/
|
||||
/* bug-1836 fix: sim_reconcile_unaccounted_jobs()'s new capacity gate calls
|
||||
* sched_get_worker() for every host named in a reconciled job's recorded
|
||||
* allocation, and treats "host not found" as a capacity miss -- this file's
|
||||
* own sched_check_host_satisfy() stub (link-satisfying only, resource.c is
|
||||
* not white-box #included here) always reports "satisfied" once reached,
|
||||
* but a bare, unregistered host name is now rejected BEFORE that stub is
|
||||
* even called. This helper registers a bare worker (no real resource
|
||||
* values needed -- the stub never reads them) so tests whose fixtures name
|
||||
* "hostA"/"hostC" in a recorded DISPATCH decision resolve correctly. */
|
||||
static void add_bare_worker(xce_sched_core_t* core, const char* name)
|
||||
{
|
||||
xce_sched_worker_t* w = (xce_sched_worker_t*)calloc(1, sizeof(*w));
|
||||
|
||||
TEST_ASSERT_NOT_NULL(w);
|
||||
w->name = strdup(name);
|
||||
TEST_ASSERT_NOT_NULL(w->name);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, zarray_init(&(w->resvals), 8, NULL));
|
||||
TEST_ASSERT_EQUAL_INT(0, zarray_init(&(w->resInUse), 8, NULL));
|
||||
|
||||
TEST_ASSERT_TRUE(sched_add_worker(core, w) >= 0);
|
||||
}
|
||||
|
||||
static void build_fixture_scheduler(xce_scheduler_t* sched, char* record_dir)
|
||||
{
|
||||
int rc;
|
||||
|
|
@ -762,6 +834,13 @@ static void build_fixture_scheduler(xce_scheduler_t* sched, char* record_dir)
|
|||
|
||||
sched->core.job_pick_plugin = (char*)"builtin_fcfs";
|
||||
sched->record_dir = record_dir;
|
||||
|
||||
/* bug-1836 fix: registered once here (captured into every fixture's
|
||||
* initial snapshot) rather than per-test, since several tests in this
|
||||
* file build a recorded DISPATCH decision naming "hostA"/"hostC" --
|
||||
* harmless for tests that never reference these hosts. */
|
||||
add_bare_worker(&(sched->core), "hostA");
|
||||
add_bare_worker(&(sched->core), "hostC");
|
||||
}
|
||||
|
||||
/* Same "leave the file open, skip sched_record_stop()'s own endSession
|
||||
|
|
@ -1543,74 +1622,45 @@ test_verify_with_multiple_already_resolved_jobs_across_rounds_reports_no_mismatc
|
|||
}
|
||||
|
||||
/*
|
||||
* test_sim_verify_no_false_mismatch_across_round_separated_dispatch --
|
||||
* scheduler-sim-eager-reconciliation spec, task 3.2 (Req 2.1, 2.2, 2.3,
|
||||
* 4.2): --sim-verify against a replay where a job's recorded DISPATCH
|
||||
* decision (round 1) and its recorded JOB_STATE_CHANGE-to-RUN confirmation
|
||||
* (round 4, three rounds later) are genuinely separated -- the multi-round
|
||||
* shape task 1.1 (this spec) fixed sim_reconcile_unaccounted_jobs() to
|
||||
* capture correctly. Under the OLD gate (`!JOB_IS_RUN(job->state)` in
|
||||
* addition to `job->accounts != NULL`), job 601 was still PEND at round 1,
|
||||
* so reconciliation skipped it entirely -- decoded_session's own
|
||||
* DISPATCH(601) entry was NEVER consumed, left over for --sim-verify's
|
||||
* positional walk to compare against a live stream that never produces a
|
||||
* DISPATCH at all (sim mode's own picker is PEND-only) -- a false
|
||||
* MISMATCH. research.md's own "Interaction with --sim-verify" research log
|
||||
* entry documents this exact latent risk and why widening the gate to
|
||||
* `job->accounts == NULL` (state-independent) removes it rather than
|
||||
* introducing a new one.
|
||||
* test_reconciliation_no_longer_masks_a_pend_jobs_dispatch_mismatch --
|
||||
* RENAMED and INVERTED (2026-09-21, bug-1838, scheduler-driver-equivalence):
|
||||
* this test used to be test_sim_verify_no_false_mismatch_across_round_
|
||||
* separated_dispatch, proving scheduler-sim-eager-reconciliation's Req 4.2
|
||||
* ("a dispatch decision and its RUN-state confirmation recorded several
|
||||
* rounds apart must not produce a false --sim-verify mismatch") via
|
||||
* sim_reconcile_unaccounted_jobs() force-applying job 601's recorded round-1
|
||||
* DISPATCH decision while it was still PEND (the gate was state-independent
|
||||
* at the time).
|
||||
*
|
||||
* **Correction (2026-09-07, bug-1759/bug-1779, scheduler-sim-decision-
|
||||
* parity):** this fixture originally recorded a PEND(601) entry alongside
|
||||
* round 1's DISPATCH(601), and again for rounds 2-3, reasoning that
|
||||
* sim_reconcile_unaccounted_jobs() never called picker_iface->on_dispatch()
|
||||
* to retire job 601 from the live picker's own pendJobs -- so the live
|
||||
* picker allegedly HAD to keep re-offering and re-PENDing it every round
|
||||
* until the round-4 RUN confirmation removed it. That turned out to be a
|
||||
* genuine, live-observed bug (bug-1759: a 50-job batch where 22
|
||||
* already-dispatched jobs also received a PEND in the same round;
|
||||
* bug-1779: a 10-element array job, 5 already-dispatched elements same
|
||||
* shape), not an unavoidable consequence of the design -- it directly
|
||||
* contradicts this same spec's own design.md "Integration with
|
||||
* --sim-verify" section, which already documented the correct assumption
|
||||
* ("the picker never re-decides an already-resolved job"). Fixed:
|
||||
* sim_reconcile_unaccounted_jobs() now calls on_dispatch() the moment it
|
||||
* reconstructs a job's allocation, mirroring sched_allocator()'s own live
|
||||
* dispatch ordering (scheduler.c) exactly. The fixture below reflects the
|
||||
* corrected shape.
|
||||
* bug-1838 narrowed that gate back to JOB_IS_RUN-only (see sim_loop.c's own
|
||||
* updated header comment on sim_reconcile_unaccounted_jobs() for the full
|
||||
* rationale: scheduler-sim-live-allocation, completed after this spec, gave
|
||||
* sim mode a genuine capacity-checked live picker/allocator path, making
|
||||
* reconciliation's PEND-inclusive widening redundant for an ordinary
|
||||
* cross-round gap -- the live path now independently re-derives the SAME
|
||||
* decision the recording made). Job 601 here is freshly PEND at round 1, so
|
||||
* it is no longer a reconciliation candidate at all -- but THIS FILE's own
|
||||
* sched_job_picker()/sched_allocator() pair (see this file's header comment,
|
||||
* "Genuine resource-based dispatch... is proved separately by
|
||||
* test_sim_live_allocation.c") is a deliberate PEND-only stub, incapable of
|
||||
* ever producing a live DISPATCH. With neither mechanism able to dispatch
|
||||
* job 601 anymore, comparing the live stream's PEND(601) against the
|
||||
* recorded DISPATCH(601) now correctly reports a MISMATCH -- not a false
|
||||
* one: this fixture's own environment genuinely cannot reproduce what the
|
||||
* recording claims happened, and --sim-verify is supposed to say so.
|
||||
*
|
||||
* Round shape (mirrors test_sim_contention_parity.c's own task 3.1 fixture
|
||||
* numbers -- job A: dispatch round 1, RUN-confirmed round 4, a 3-round
|
||||
* gap):
|
||||
* Round 1 (begin_round=2000): job 601 pushed PEND; recorded decisions =
|
||||
* [DISPATCH(601)]. sim_reconcile_unaccounted_jobs() retires job 601
|
||||
* from the live picker's own pendJobs (on_dispatch()) BEFORE this
|
||||
* round's picker session runs (sim_loop.c's own fixed ordering:
|
||||
* reconcile, then xce_core_signal_go(), then drain/compare), so the
|
||||
* live picker has nothing left to offer this round either -- no
|
||||
* same-round PEND echo to record.
|
||||
* Round 2, 3 (begin_round=3000/4000): job 601 is retired from the live
|
||||
* picker's pendJobs (round 1's on_dispatch() above) -- nothing left for
|
||||
* the real picker to offer or re-PEND. Recorded decisions = [] (empty
|
||||
* ReplaySessions), the same shape round 4 below already uses for the
|
||||
* identical reason. Still genuinely widens the dispatch-to-confirmation
|
||||
* GAP to "several rounds" (Requirement 4.2) -- only the picker-echo
|
||||
* noise in between changed, not the gap itself.
|
||||
* Round 4 (begin_round=5000): job 601's JOB_STATE_CHANGE to RUN is
|
||||
* applied as part of this round's own pushed event batch. Recorded
|
||||
* decisions = [] (an empty ReplaySession, the same shape a genuinely
|
||||
* idle round produces in a real recording).
|
||||
*
|
||||
* Zero mismatches across all four rounds is the Observable (Requirement
|
||||
* 4.2). It is also consistent evidence for 2.1 (the DISPATCH decision IS
|
||||
* reported, via reconciliation's own three-call sequence, at round 1 --
|
||||
* not silently dropped or deferred to round 4) and 2.3 (no duplicate
|
||||
* report: the live stream never independently emits a DISPATCH for job
|
||||
* 601 at all, so there is nothing for reconciliation's own report to
|
||||
* collide with).
|
||||
* The genuine, mismatch-free proof of Req 4.2 (a real dispatch-then-later-
|
||||
* confirmation gap causing no false mismatch) now lives in
|
||||
* 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 job 601 the way this file's stub never could
|
||||
* -- see that test's own header comment for the full mirrored rationale.
|
||||
* This test is kept, inverted, as a regression lock on the NEW behavior:
|
||||
* reconciliation must not "help" a PEND job here anymore, even when a
|
||||
* matching recorded DISPATCH entry exists.
|
||||
*/
|
||||
static void
|
||||
test_sim_verify_no_false_mismatch_across_round_separated_dispatch(void)
|
||||
test_reconciliation_no_longer_masks_a_pend_jobs_dispatch_mismatch(void)
|
||||
{
|
||||
char path[PATH_MAX];
|
||||
FILE* f;
|
||||
|
|
@ -1630,31 +1680,22 @@ test_sim_verify_no_false_mismatch_across_round_separated_dispatch(void)
|
|||
host[0] = make_test_host_alloc("hostA", res, 1);
|
||||
alloc = make_test_job_alloc(601ULL, host, 1);
|
||||
|
||||
/* Round 1: DISPATCH recorded. bug-1759/bug-1779 correction:
|
||||
* sim_reconcile_unaccounted_jobs() now calls the picker's on_dispatch()
|
||||
* the moment it reconstructs job 601's allocation this round, retiring
|
||||
* it from the live picker's own pendJobs BEFORE that round's picker
|
||||
* session runs (sim_loop.c's fixed ordering: reconcile, then
|
||||
* xce_core_signal_go()) -- so there is no longer a same-round PEND
|
||||
* echo to record. */
|
||||
/* Round 1: job 601 pushed PEND this same round; recorded decisions =
|
||||
* [DISPATCH(601)]. bug-1838: sim_reconcile_unaccounted_jobs() is now
|
||||
* JOB_IS_RUN-only, so it does not touch job 601 (still PEND) at all --
|
||||
* this file's own PEND-only sched_allocator() stub (see this file's
|
||||
* header comment) is the only thing that CAN decide it, and it always
|
||||
* PENDs. The live stream therefore produces PEND(601) against a
|
||||
* recorded DISPATCH(601) -- a genuine, correctly-reported mismatch. */
|
||||
append_replay_session_with_dispatch(f, 2000ULL, alloc);
|
||||
|
||||
/* Round 2, 3: job 601 is now retired from the live picker's pendJobs
|
||||
* (bug-1759/bug-1779 correction, same reasoning as round 4's own
|
||||
* comment below) -- nothing left for the live picker to offer, so these
|
||||
* rounds are recorded empty, the same shape a genuinely idle round
|
||||
* produces. Still genuinely widens the dispatch-to-confirmation gap to
|
||||
* "several rounds" (Requirement 4.2) -- only the picker-echo noise in
|
||||
* between changed, not the gap itself. */
|
||||
/* Round 2: job 601 is STILL a picker candidate (nothing retired it --
|
||||
* reconciliation never touched it, and the stub allocator's on_pend()
|
||||
* path doesn't retire candidates either), so the live stream produces
|
||||
* ANOTHER PEND(601) here against this round's empty recorded
|
||||
* decisions -- also a genuine mismatch, not asserted on individually
|
||||
* (this test only checks that at least one mismatch is reported). */
|
||||
append_empty_replay_session(f, 3000ULL);
|
||||
append_empty_replay_session(f, 4000ULL);
|
||||
|
||||
/* Round 4: job 601's RUN confirmation, 3 rounds after its own round-1
|
||||
* DISPATCH -- applied before this round's picker session runs, so the
|
||||
* live picker has nothing left to offer. */
|
||||
append_job_state_change_event(f, 4500ULL, 601ULL, JOB_STAT_PEND,
|
||||
JOB_STAT_RUN);
|
||||
append_empty_replay_session(f, 5000ULL);
|
||||
|
||||
append_end_of_stream_marker(f);
|
||||
fclose(f);
|
||||
|
|
@ -1662,15 +1703,17 @@ test_sim_verify_no_false_mismatch_across_round_separated_dispatch(void)
|
|||
captured = run_sim_and_capture_stdout(path, true, &rc);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_NULL_MESSAGE(
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
strstr(captured, "MISMATCH"),
|
||||
"a dispatch decision and its RUN-state confirmation recorded "
|
||||
"several rounds apart must not produce a false --sim-verify "
|
||||
"mismatch (Requirement 4.2)");
|
||||
"a PEND-state job's recorded DISPATCH decision must no longer be "
|
||||
"force-applied by reconciliation (bug-1838) -- this file's "
|
||||
"PEND-only live stub cannot reproduce it either, so --sim-verify "
|
||||
"must correctly report a real mismatch here, not silently agree "
|
||||
"with a decision nothing in this environment actually made");
|
||||
|
||||
free(captured);
|
||||
printf("test_sim_verify_no_false_mismatch_across_round_separated_"
|
||||
"dispatch: PASS\n");
|
||||
printf("test_reconciliation_no_longer_masks_a_pend_jobs_dispatch_"
|
||||
"mismatch: PASS\n");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
|
|
@ -1695,7 +1738,7 @@ int main(void)
|
|||
RUN_TEST(
|
||||
test_verify_with_multiple_already_resolved_jobs_across_rounds_reports_no_mismatch);
|
||||
RUN_TEST(
|
||||
test_sim_verify_no_false_mismatch_across_round_separated_dispatch);
|
||||
test_reconciliation_no_longer_masks_a_pend_jobs_dispatch_mismatch);
|
||||
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1785,6 +1785,7 @@ int sched_account_jobs(xce_sched_core_t* core)
|
|||
return rc;
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
test_snapshot_round_trip_mixed_jobs_shared_pool_resinuse_and_teardown_independence(
|
||||
void)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue