feat(scheduler-sim-decision-parity): wire reconciliation into the round boundary (task 3)

This is the integration point that makes the whole feature real
end-to-end. Calls sim_reconcile_unaccounted_jobs() (tasks 2.2-2.4) at
exactly the position design.md's mermaid diagram requires: after
xce_core_begin_session()'s reset, strictly before sched_account_jobs()
(so the same round's re-charge picks up every newly-reconstructed job,
Req 1.3/1.4) and before xce_core_signal_go() unblocks the live picker (so
still-pending jobs' capacity checks this round see correct resInUse, Req
2.1/2.2). decoded_session and record_ctx are the same per-round locals
sched_sim_driver_run() already declares -- no new plumbing.

sim_verify.c needs zero changes: reconciliation consumes/compacts
decoded_session->decisions before the verify drain loop runs each round,
which is what keeps the existing positional comparison correct (Req 5.2)
-- confirmed by extending test_sim_verify.c with a no-false-mismatch test
against a recording containing already-resolved jobs.

New end-to-end test proves the ordering matters for real: a reconciled
job's resInUse is charged before a live sched_check_host_satisfy() call
for a different job in the same round correctly denies it (5.0 wanted vs
2.0 remaining) and grants an exact-fit request (2.0) -- using the real
scheduler.c/resource.c accounting and capacity-check code, not stubs.

Found and fixed a test-authoring bug (get_resvals() returns nres on
success, not 0) during GREEN verification -- logged as bug-1622, a test
bug, not production.

47/48 scheduler tests pass; the 1 failure is pre-existing bug-1560
(unrelated).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-08-22 10:00:06 +00:00
parent 75edb7a329
commit 1264409478
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
5 changed files with 947 additions and 11 deletions

View file

@ -748,6 +748,89 @@ static void append_replay_session_with_pend(FILE* f, uint64_t begin_round,
capn_free(&c);
}
/* Appends one ReplayEvent{jobStateChange} message -- scheduler-sim-
* decision-parity spec, task 3 (Req 1.3/1.4, 2.1/2.2, 5.2): this is the
* mechanism (core_events.c's XCE_CORE_EVENT_JOB_STATE_CHANGE handling)
* that flips a job to RUN state during replay independently of any
* decision -- it does NOT set job->accounts/job->allocs (core_events.c
* only clears them when leaving RUN state), which is exactly what leaves
* a RUN-state job with job->accounts == NULL for Recorded-Decision
* Reconciliation (sim_reconcile_unaccounted_jobs(), sim_loop.c) to pick
* up. Same encode/write technique append_queue_update_event() above
* establishes. */
static void append_job_state_change_event(FILE* f, uint64_t ts,
uint64_t jobid, uint32_t old_state,
uint32_t new_state)
{
job_state_change_event_t jsc;
replay_event_t src;
struct capn c;
struct capn_segment* cs;
ReplayEvent_ptr p;
memset(&jsc, 0, sizeof(jsc));
jsc.jobid = jobid;
jsc.old_state = old_state;
jsc.new_state = new_state;
jsc.exit_code = 0;
memset(&src, 0, sizeof(src));
src.ts = ts;
src.body_which = ReplayEvent_body_jobStateChange;
src.body.job_state_change = &jsc;
capn_init_malloc(&c);
cs = capn_root(&c).seg;
encode_ReplayEvent_ptr(cs, &p, &src);
capn_setp(capn_root(&c), 0, p.p);
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
capn_free(&c);
}
/* Appends one ReplaySession carrying TWO recorded decisions -- a DISPATCH
* for `dispatch_alloc`'s jobid and a PEND for `pend_jobid` -- scheduler-
* sim-decision-parity spec, task 3 (Req 5.2). Reuses
* make_replay_dispatch_decision()/make_replay_pend_decision() (this file's
* own unit-level fixture helpers, above) rather than hand-rolling the
* decision shape a second time. Mirrors append_replay_session_with_pend()'s
* own encode_ReplaySession_ptr() technique, just with two decisions
* instead of one. */
static void append_replay_session_with_dispatch_and_pend(
FILE* f, uint64_t begin_round, xce_job_alloc_t* dispatch_alloc,
uint64_t pend_jobid, int pend_main_reason)
{
replay_decision_t d_dispatch;
replay_decision_t d_pend;
replay_decision_t* decisions[2];
replay_session_t session;
struct capn c;
struct capn_segment* cs;
ReplaySession_ptr sp;
d_dispatch = make_replay_dispatch_decision(dispatch_alloc);
d_dispatch.ts = begin_round;
d_pend = make_replay_pend_decision(pend_jobid, pend_main_reason);
d_pend.ts = begin_round;
decisions[0] = &d_dispatch;
decisions[1] = &d_pend;
memset(&session, 0, sizeof(session));
session.begin_round = begin_round;
session.decisions = decisions;
session.n_decisions = 2;
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);
}
/* Runs sched_sim_driver_run() against `path` with --sim-verify set to
* `verify`, capturing everything written to stdout during the call (the
* same dup()/dup2() technique capture_stdout_around() above uses, inlined
@ -922,6 +1005,77 @@ static void test_verify_disabled_reports_no_mismatches_even_when_altered(void)
"PASS\n");
}
/*
* test_verify_with_already_resolved_job_reports_no_mismatch -- scheduler-
* sim-decision-parity spec, task 3 (Req 5.2): --sim-verify against a
* recording containing an already-resolved job (job A: pushed PEND, then
* flipped to RUN by a JOB_STATE_CHANGE event in the SAME round's event
* batch, with a matching recorded DISPATCH decision this round -- the
* Recorded-Decision Reconciliation trigger shape) must report ZERO
* mismatches, alongside a genuinely-live-pended job (job B) whose own
* recorded PEND decision the real picker independently reproduces.
*
* Before task 3's round-boundary wiring, sim_reconcile_unaccounted_jobs()
* was never called, so job A's RUN state was never reconciled: the live
* picker never independently re-decides an already-RUN job (it only ever
* offers PEND-state jobs), so the live-drained decision stream has only
* job B's PEND -- one entry -- while decoded_session->decisions still
* carries BOTH the DISPATCH(A) and PEND(B) entries (neither ever
* consumed). The positional verify walk then finds job A's recorded
* DISPATCH left over with no corresponding live decision once the live
* stream ends, and reports it as a mismatch (expected=DISPATCH(A),
* actual=NULL) -- a false positive entirely attributable to this
* feature's own gap, not a real behavioral divergence. After task 3's
* fix, reconciliation consumes and removes job A's DISPATCH entry from
* decoded_session BEFORE this verify loop ever runs, so only job B's PEND
* remains to compare against the live stream's own single PEND -- zero
* mismatches.
*/
static void test_verify_with_already_resolved_job_reports_no_mismatch(void)
{
char path[PATH_MAX];
FILE* f;
int rc;
char* captured;
xce_alloc_t* res[1];
xce_host_alloc_t* host[1];
xce_job_alloc_t* alloc;
f = write_header_and_snapshot(path, sizeof(path), "_reconciled");
TEST_ASSERT_NOT_NULL(f);
append_queue_update_event(f, 1000ULL, "normal");
/* Job A (501): already resolved before this round's live picker ever
* considers it -- pushed PEND, then flipped to RUN in the SAME event
* batch, so it never reaches next_job() this round. Job B (502):
* genuinely still PEND -- the real picker independently re-pends it. */
append_jobs_push_event(f, 1500ULL, 501ULL, "normal");
append_jobs_push_event(f, 1500ULL, 502ULL, "normal");
append_job_state_change_event(f, 1800ULL, 501ULL, JOB_STAT_PEND,
JOB_STAT_RUN);
res[0] = make_test_alloc("slots", "run", 1, 4.0, 0, 0);
host[0] = make_test_host_alloc("hostA", res, 1);
alloc = make_test_job_alloc(501ULL, host, 1);
append_replay_session_with_dispatch_and_pend(f, 2000ULL, alloc, 502ULL,
0);
append_end_of_stream_marker(f);
fclose(f);
captured = run_sim_and_capture_stdout(path, true, &rc);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_NULL_MESSAGE(
strstr(captured, "MISMATCH"),
"reconciling an already-resolved job's recorded DISPATCH decision "
"must not produce a false --sim-verify mismatch (Requirement 5.2)");
free(captured);
printf("test_verify_with_already_resolved_job_reports_no_mismatch: "
"PASS\n");
}
int main(void)
{
UNITY_BEGIN();
@ -940,6 +1094,7 @@ int main(void)
RUN_TEST(test_verify_against_self_reports_zero_mismatches);
RUN_TEST(test_verify_against_altered_copy_reports_exact_mismatch);
RUN_TEST(test_verify_disabled_reports_no_mismatches_even_when_altered);
RUN_TEST(test_verify_with_already_resolved_job_reports_no_mismatch);
return UNITY_END();
}