diff --git a/.kiro/specs/libxce-alloc-preason-equality/tasks.md b/.kiro/specs/libxce-alloc-preason-equality/tasks.md index fe857d00..203a958e 100644 --- a/.kiro/specs/libxce-alloc-preason-equality/tasks.md +++ b/.kiro/specs/libxce-alloc-preason-equality/tasks.md @@ -53,7 +53,7 @@ Test-first throughout (red → green → refactor). Every sub-task leaves the bu - Done when the three tests pass unchanged in outcome under the stricter checks; a failure is investigated as a real divergence, not loosened. - _Requirements: 6.1, 6.2, 6.3, 6.7_ - _Depends: 2.1, 2.2, 3.1, 3.2_ (3.1 as well as 3.2: a normal-mode recording's begin time and its allocations' start time must come from one clock reading, or a recorded-vs-replayed comparison can differ by one second) -- [ ] 4.2 Bring hand-built recorded dispatch fixtures in line with the session start time +- [x] 4.2 Bring hand-built recorded dispatch fixtures in line with the session start time - In every test that hand-builds a recorded dispatch later compared against a live replay, set the recorded allocation's reservation start time to the fixture session's recorded start in whole seconds (design.md, File Structure Plan: the six listed test files). - Update the comments in those files that describe the reservation start time as excluded or name the private comparators. - Harmless before 4.3, because the simulator still ignores the field; required after it. @@ -89,3 +89,4 @@ Test-first throughout (red → green → refactor). Every sub-task leaves the bu - `xce-build.sh` resolves a relative build dir under `src/`: `bash src/tools/xce-build.sh lib asan xce.build.alloceq` builds into `src/xce.build.alloceq`, so iterate with `meson test -C src/xce.build.alloceq`. - `xce-build.sh lib` runs only the `libxce test` binary; run `meson test -C src/xce.build.alloceq "libxce alloc OOM test"` (or plain `meson test`) for the allocation OOM binary, which also wraps `vasprintf`/`__vasprintf_chk` since 2.1. - Simulation mode (`sim_loop.c`) still takes its own session-begin clock reading before `xce_core_begin_session()`; by design that is left to spec scheduler-sim-discrete-clock (3.1 fixed only the normal-mode recorder). +- 4.2 left the `from`-excluded comments at `test_sim_verify.c` ~639-681 for 4.3, which inverts the test they document. diff --git a/src/scheduler/testsuite/test_sim_contention_parity.c b/src/scheduler/testsuite/test_sim_contention_parity.c index 4b002fb1..6c2bdc58 100644 --- a/src/scheduler/testsuite/test_sim_contention_parity.c +++ b/src/scheduler/testsuite/test_sim_contention_parity.c @@ -842,13 +842,26 @@ static float lookup_want(uint64_t jobid) return 0.0f; } +/* SESSION_FROM - the `from` an allocation decided in a session that began at + * `begin_ns` (epoch nanoseconds) carries: the session start in whole + * seconds, floor division (libxce-alloc-preason-equality Decision 8; + * scheduler.c's sched_make_primary_alloc_() stamps it, and in simulation + * mode the session starts at the recorded beginRound). Hand-built recorded + * DISPATCH fixtures derive their `from` from the begin_round they are + * recorded under, so a live replay of the same round reproduces it. */ +#define SESSION_FROM(begin_ns) ((uint64_t)(begin_ns) / 1000000000ULL) + +/* The recorded round-1 beginRound the hand-built DISPATCH fixtures below + * are appended under (logical nanoseconds). */ +#define ROUND1_BEGIN_NS 2000ULL + /* Builds the wire-shaped xce_job_alloc_t a DISPATCH decision carries -- * shared by BOTH Part A's real dispatch construction below AND Part B's * hand-crafted decisions further down -- same field shape * test_sim_verify.c's own make_test_alloc()/make_test_host_alloc()/ * make_test_job_alloc() establish. */ static xce_job_alloc_t* make_dispatch_alloc(uint64_t jobid, const char* host, - float value) + float value, uint64_t from) { xce_alloc_t* res = (xce_alloc_t*)calloc(1, sizeof(*res)); xce_host_alloc_t* h = (xce_host_alloc_t*)calloc(1, sizeof(*h)); @@ -866,7 +879,7 @@ static xce_job_alloc_t* make_dispatch_alloc(uint64_t jobid, const char* host, res->reserve = strdup("run"); res->instid = 1; res->value = value; - res->from = 0; + res->from = from; res->duration = 0; allocs[0] = res; @@ -996,7 +1009,12 @@ static void* capacity_aware_allocator_thread_fn(void* arg) g_slots_residx_normal, want); sched_commit_job_alloc(core, job); - job->allocs = make_dispatch_alloc(jobid, WORKER_NAME, want); + /* Stamp `from` exactly as sched_make_primary_alloc_() + * (scheduler.c) stamps a real allocation: the deciding + * session's start in whole seconds. */ + job->allocs = make_dispatch_alloc( + jobid, WORKER_NAME, want, + SESSION_FROM(core->session_start)); /* This file's own addition -- see this function's header * comment for why this is required. */ @@ -1416,13 +1434,15 @@ test_contention_parity_matches_real_normal_mode_recording_round_by_round( append_job_state_change_event(f, 1800ULL, JOBC_ID, JOB_STAT_PEND, JOB_STAT_RUN); - allocA = make_dispatch_alloc(JOBA_ID, WORKER_NAME, JOBA_WANT); - allocC = make_dispatch_alloc(JOBC_ID, WORKER_NAME, JOBC_WANT); + allocA = make_dispatch_alloc(JOBA_ID, WORKER_NAME, JOBA_WANT, + SESSION_FROM(ROUND1_BEGIN_NS)); + allocC = make_dispatch_alloc(JOBC_ID, WORKER_NAME, JOBC_WANT, + SESSION_FROM(ROUND1_BEGIN_NS)); - round1_decisions[0] = make_decision_dispatch(2000ULL, allocA); - round1_decisions[1] = make_decision_dispatch(2000ULL, allocC); - round1_decisions[2] = make_decision_pend(2000ULL, JOBB_ID, 0); - append_replay_session(f, 2000ULL, round1_decisions, 3); + round1_decisions[0] = make_decision_dispatch(ROUND1_BEGIN_NS, allocA); + round1_decisions[1] = make_decision_dispatch(ROUND1_BEGIN_NS, allocC); + round1_decisions[2] = make_decision_pend(ROUND1_BEGIN_NS, JOBB_ID, 0); + append_replay_session(f, ROUND1_BEGIN_NS, round1_decisions, 3); /* Round 2: job D pushed fresh -- no reconciliation involved (D was * never dispatched anywhere); sim's own live picker independently @@ -1707,12 +1727,14 @@ static void write_multiround_contention_rounds(FILE* f, int upto_round) append_jobs_push_event(f, 1500ULL, JOBB_ID, "normal"); append_jobs_push_event(f, 1500ULL, JOBC_ID, "normal"); - allocA = make_dispatch_alloc(JOBA_ID, WORKER_NAME, JOBA_WANT); - allocC = make_dispatch_alloc(JOBC_ID, WORKER_NAME, JOBC_WANT); - round1_decisions[0] = make_decision_dispatch(2000ULL, allocA); - round1_decisions[1] = make_decision_dispatch(2000ULL, allocC); - round1_decisions[2] = make_decision_pend(2000ULL, JOBB_ID, 0); - append_replay_session(f, 2000ULL, round1_decisions, 3); + allocA = make_dispatch_alloc(JOBA_ID, WORKER_NAME, JOBA_WANT, + SESSION_FROM(ROUND1_BEGIN_NS)); + allocC = make_dispatch_alloc(JOBC_ID, WORKER_NAME, JOBC_WANT, + SESSION_FROM(ROUND1_BEGIN_NS)); + round1_decisions[0] = make_decision_dispatch(ROUND1_BEGIN_NS, allocA); + round1_decisions[1] = make_decision_dispatch(ROUND1_BEGIN_NS, allocC); + round1_decisions[2] = make_decision_pend(ROUND1_BEGIN_NS, JOBB_ID, 0); + append_replay_session(f, ROUND1_BEGIN_NS, round1_decisions, 3); if (upto_round < 2) { return; diff --git a/src/scheduler/testsuite/test_sim_live_allocation.c b/src/scheduler/testsuite/test_sim_live_allocation.c index ebac750f..1862cae5 100644 --- a/src/scheduler/testsuite/test_sim_live_allocation.c +++ b/src/scheduler/testsuite/test_sim_live_allocation.c @@ -699,36 +699,32 @@ test_sim_verify_no_false_mismatch_after_live_dispatch_and_later_confirmation( * 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. + * job_alloc_eq() (xcealloc.h, via sim_verify.c's sim_decision_eq()) + * compares xce_alloc_t field-by-field, excluding only instid -- so + * duration and from must match EXACTLY what the real + * seqjob_allocator()->sched_make_allocs()->make_alloc() chain plus + * sched_make_primary_alloc_()'s session stamp (scheduler.c) will + * independently produce for this exact fixture, not placeholders: * - 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 left at whatever value make_alloc() happens to stamp it - * with here -- it no longer needs to agree with the live path's own - * call. bug-1838-followup removed `from` from sim_verify_alloc_eq()'s - * comparison entirely (sim_verify.c): it is a real-wall-clock - * reservation-start stamp, not part of a decision's semantic identity, - * and comparing it made --sim-verify false-flag every genuine - * live-dispatched decision replayed more than a wall-clock second away - * from its own recording. What used to be accepted same-second-window - * flakiness here is now a non-issue by construction. */ + * - from=ROUND1_BEGIN_NS / 10^9: every allocation is stamped with + * its deciding session's start in whole seconds (libxce-alloc- + * preason-equality Decision 8), and in simulation mode the session + * starts at the recorded beginRound. make_alloc()'s own time(NULL) + * default is therefore overwritten below. + * instid=1 is kept realistic (sched_get_id() returns core->nextid++, + * and this is the first allocation made against a fresh core) even + * though the comparison ignores it. */ { + const uint64_t ROUND1_BEGIN_NS = 2000ULL; 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); + xce_alloc_t** allocs; + xce_host_alloc_t* h; + xce_host_alloc_t** hallocs; + xce_job_alloc_t* alloc; sched_dispatch_decision_t dispatch; replay_decision_t d_dispatch; replay_decision_t* decisions[1]; @@ -737,18 +733,24 @@ test_sim_verify_no_false_mismatch_after_live_dispatch_and_later_confirmation( struct capn_segment* cs; ReplaySession_ptr sp; + a->from = ROUND1_BEGIN_NS / 1000000000ULL; + allocs = make_allocs(1, a); + h = make_host_alloc((char*)WORKER_NAME, 1, allocs); + hallocs = make_host_allocs(1, h); + alloc = make_job_alloc(TEST_JOBID, 1, hallocs); + 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; + d_dispatch.ts = ROUND1_BEGIN_NS; decisions[0] = &d_dispatch; memset(&session, 0, sizeof(session)); - session.begin_round = 2000ULL; + session.begin_round = ROUND1_BEGIN_NS; session.decisions = decisions; session.n_decisions = 1; session.summary = NULL; diff --git a/src/scheduler/testsuite/test_sim_reconcile_unaccounted.c b/src/scheduler/testsuite/test_sim_reconcile_unaccounted.c index eb6b3a53..4bfa71d8 100644 --- a/src/scheduler/testsuite/test_sim_reconcile_unaccounted.c +++ b/src/scheduler/testsuite/test_sim_reconcile_unaccounted.c @@ -547,11 +547,16 @@ static replay_session_t* make_decoded_session(uint64_t begin_round, } /* task 2.3 fixture helper: a real, one-host/one-resource xce_job_alloc_t -- - * same field shape sim_verify_job_alloc_eq() already validates (design.md's + * same field shape job_alloc_eq() (xcealloc.h) already validates (design.md's * Allowed Dependencies), built via the real xcealloc.c production helpers * (make_alloc/make_allocs/make_host_alloc/make_host_allocs/make_job_alloc), * not a hand-rolled struct literal -- so a reconciled job's `job->accounts` - * shape can be checked against real, non-trivial resource values. */ + * shape can be checked against real, non-trivial resource values. + * `from` stays at make_alloc()'s time(NULL) default: these recorded + * DISPATCH entries only feed sim_reconcile_unaccounted_jobs() and are never + * compared against a live replay's allocation (whose `from` would be the + * session start in whole seconds, libxce-alloc-preason-equality + * Decision 8). */ static xce_job_alloc_t* make_test_job_alloc_with_host(uint64_t jobid, const char* host, const char* resname, diff --git a/src/scheduler/testsuite/test_sim_resource_contention.c b/src/scheduler/testsuite/test_sim_resource_contention.c index 9f2a1715..a20f2314 100644 --- a/src/scheduler/testsuite/test_sim_resource_contention.c +++ b/src/scheduler/testsuite/test_sim_resource_contention.c @@ -648,11 +648,21 @@ static float lookup_want(uint64_t jobid) return 0.0f; } +/* SESSION_FROM - the `from` an allocation decided in a session that began at + * `begin_ns` (epoch nanoseconds) carries: the session start in whole + * seconds, floor division (libxce-alloc-preason-equality Decision 8; + * scheduler.c's sched_make_primary_alloc_() stamps it, and in simulation + * mode the session starts at the recorded beginRound). This file's + * capacity-aware stand-in allocator stamps its DISPATCH allocations the + * same way, so the recording it produces carries the value a live replay + * of that round reproduces. */ +#define SESSION_FROM(begin_ns) ((uint64_t)(begin_ns) / 1000000000ULL) + /* Builds the wire-shaped xce_job_alloc_t a DISPATCH decision carries -- same * field shape test_sim_verify.c's own make_test_alloc()/make_test_host_alloc()/ * make_test_job_alloc() establish. */ static xce_job_alloc_t* make_dispatch_alloc(uint64_t jobid, const char* host, - float value) + float value, uint64_t from) { xce_alloc_t* res = (xce_alloc_t*)calloc(1, sizeof(*res)); xce_host_alloc_t* h = (xce_host_alloc_t*)calloc(1, sizeof(*h)); @@ -670,7 +680,7 @@ static xce_job_alloc_t* make_dispatch_alloc(uint64_t jobid, const char* host, res->reserve = strdup("run"); res->instid = 1; res->value = value; - res->from = 0; + res->from = from; res->duration = 0; allocs[0] = res; @@ -798,7 +808,12 @@ static void* capacity_aware_allocator_thread_fn(void* arg) job->accounts = make_charge_account(WORKER_NAME, want); sched_commit_job_alloc(core, job); - job->allocs = make_dispatch_alloc(jobid, WORKER_NAME, want); + /* Stamp `from` exactly as sched_make_primary_alloc_() + * (scheduler.c) stamps a real allocation: the deciding + * session's start in whole seconds. */ + job->allocs = make_dispatch_alloc( + jobid, WORKER_NAME, want, + SESSION_FROM(core->session_start)); picker->picker_iface->on_dispatch(picker, jobid); decision = make_dispatch_decision(job->allocs); diff --git a/src/scheduler/testsuite/test_sim_round_boundary_reconciliation.c b/src/scheduler/testsuite/test_sim_round_boundary_reconciliation.c index d2ae4a1d..d345dbb1 100644 --- a/src/scheduler/testsuite/test_sim_round_boundary_reconciliation.c +++ b/src/scheduler/testsuite/test_sim_round_boundary_reconciliation.c @@ -675,11 +675,24 @@ static void append_end_of_stream_marker(FILE* f) capn_free(&c); } +/* SESSION_FROM - the `from` an allocation decided in a session that began at + * `begin_ns` (epoch nanoseconds) carries: the session start in whole + * seconds, floor division (libxce-alloc-preason-equality Decision 8; + * scheduler.c's sched_make_primary_alloc_() stamps it, and in simulation + * mode the session starts at the recorded beginRound). Hand-built recorded + * DISPATCH fixtures derive their `from` from the begin_round they are + * recorded under, so a live replay of the same round reproduces it. */ +#define SESSION_FROM(begin_ns) ((uint64_t)(begin_ns) / 1000000000ULL) + +/* The recorded round-1 beginRound the hand-built DISPATCH fixtures below + * are appended under (logical nanoseconds). */ +#define ROUND1_BEGIN_NS 2000ULL + /* Builds the wire-shaped xce_job_alloc_t a DISPATCH decision carries -- * same field shape test_sim_verify.c's own make_test_alloc()/ * make_test_host_alloc()/make_test_job_alloc() establish. */ static xce_job_alloc_t* make_dispatch_alloc(uint64_t jobid, const char* host, - float value) + float value, uint64_t from) { xce_alloc_t* res = (xce_alloc_t*)calloc(1, sizeof(*res)); xce_host_alloc_t* h = (xce_host_alloc_t*)calloc(1, sizeof(*h)); @@ -697,7 +710,7 @@ static xce_job_alloc_t* make_dispatch_alloc(uint64_t jobid, const char* host, res->reserve = strdup("run"); res->instid = 1; res->value = value; - res->from = 0; + res->from = from; res->duration = 0; allocs[0] = res; @@ -843,9 +856,10 @@ test_reconciled_job_charges_resinuse_before_live_capacity_check(void) append_job_state_change_event(f, 1800ULL, JOBA_ID, JOB_STAT_PEND, JOB_STAT_RUN); - alloc = make_dispatch_alloc(JOBA_ID, WORKER_NAME, JOBA_WANT); - append_replay_session_with_dispatch_and_pend(f, 2000ULL, alloc, JOBB_ID, - 0); + alloc = make_dispatch_alloc(JOBA_ID, WORKER_NAME, JOBA_WANT, + SESSION_FROM(ROUND1_BEGIN_NS)); + append_replay_session_with_dispatch_and_pend(f, ROUND1_BEGIN_NS, alloc, + JOBB_ID, 0); append_end_of_stream_marker(f); fclose(f); diff --git a/src/scheduler/testsuite/test_sim_verify.c b/src/scheduler/testsuite/test_sim_verify.c index c3fe7b43..77ceeb76 100644 --- a/src/scheduler/testsuite/test_sim_verify.c +++ b/src/scheduler/testsuite/test_sim_verify.c @@ -460,6 +460,19 @@ void tearDown(void) { return; } /* Unit-level fixture helpers: hand-built xce_job_alloc_t trees. */ /* ---------------------------------------------------------------------- */ +/* SESSION_FROM - the `from` a DISPATCH allocation decided in a session that + * began at `begin_ns` (epoch nanoseconds) carries: the session start in + * whole seconds, floor division (libxce-alloc-preason-equality Decision 8; + * scheduler.c's sched_make_primary_alloc_() stamps it, and in simulation + * mode the session starts at the recorded beginRound). Hand-built recorded + * DISPATCH fixtures replayed through sched_sim_driver_run() below derive + * their `from` from the same begin_round they are recorded under. */ +#define SESSION_FROM(begin_ns) ((uint64_t)(begin_ns) / 1000000000ULL) + +/* The recorded round-1 beginRound those DISPATCH fixtures are appended + * under (logical nanoseconds). */ +#define ROUND1_BEGIN_NS 2000ULL + static xce_alloc_t* make_test_alloc(const char* resname, const char* reserve, uint64_t instid, double value, uint64_t from, int64_t duration) @@ -1645,12 +1658,13 @@ static void test_verify_with_already_resolved_job_reports_no_mismatch(void) 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); + res[0] = make_test_alloc("slots", "run", 1, 4.0, + SESSION_FROM(ROUND1_BEGIN_NS), 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_replay_session_with_dispatch_and_pend(f, ROUND1_BEGIN_NS, alloc, + 502ULL, 0); append_end_of_stream_marker(f); fclose(f); @@ -1722,18 +1736,21 @@ test_verify_with_multiple_already_resolved_jobs_across_rounds_reports_no_mismatc append_job_state_change_event(f, 1801ULL, 503ULL, JOB_STAT_PEND, JOB_STAT_RUN); - res_a[0] = make_test_alloc("slots", "run", 1, 4.0, 0, 0); + res_a[0] = make_test_alloc("slots", "run", 1, 4.0, + SESSION_FROM(ROUND1_BEGIN_NS), 0); host_a[0] = make_test_host_alloc("hostA", res_a, 1); alloc_a = make_test_job_alloc(501ULL, host_a, 1); - res_c[0] = make_test_alloc("slots", "run", 1, 2.0, 0, 0); + res_c[0] = make_test_alloc("slots", "run", 1, 2.0, + SESSION_FROM(ROUND1_BEGIN_NS), 0); host_c[0] = make_test_host_alloc("hostC", res_c, 1); alloc_c = make_test_job_alloc(503ULL, host_c, 1); /* Recorded order: DISPATCH(A), PEND(B), DISPATCH(C) -- B's entry is * sandwiched between the two entries reconciliation consumes. */ - append_replay_session_with_two_dispatches_and_pend(f, 2000ULL, alloc_a, - 502ULL, 0, alloc_c); + append_replay_session_with_two_dispatches_and_pend(f, ROUND1_BEGIN_NS, + alloc_a, 502ULL, 0, + alloc_c); /* Round 2: job B (502) is still genuinely PEND (never dispatched, never * state-changed) so builtin_fcfs's pendJobs discipline re-offers it to @@ -1821,7 +1838,8 @@ test_reconciliation_no_longer_masks_a_pend_jobs_dispatch_mismatch(void) append_queue_update_event(f, 1000ULL, "normal"); append_jobs_push_event(f, 1500ULL, 601ULL, "normal"); - res[0] = make_test_alloc("slots", "run", 1, 4.0, 0, 0); + res[0] = make_test_alloc("slots", "run", 1, 4.0, + SESSION_FROM(ROUND1_BEGIN_NS), 0); host[0] = make_test_host_alloc("hostA", res, 1); alloc = make_test_job_alloc(601ULL, host, 1); @@ -1832,7 +1850,7 @@ test_reconciliation_no_longer_masks_a_pend_jobs_dispatch_mismatch(void) * 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); + append_replay_session_with_dispatch(f, ROUND1_BEGIN_NS, alloc); /* Round 2: job 601 is STILL a picker candidate (nothing retired it -- * reconciliation never touched it, and the stub allocator's on_pend()