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:
parent
75edb7a329
commit
1264409478
5 changed files with 947 additions and 11 deletions
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue