feat(scheduler-sim-decision-parity): reconstruct job allocation with safe ownership (task 2.3)
Extends sim_reconcile_unaccounted_jobs()'s FOUND branch: takes ownership of a matched recorded DISPATCH decision's allocation (job->allocs = entry's alloc, not a copy -- mirrors scheduler.c's sched_commit_decision() precedent exactly), builds job->accounts via make_job_account(), then safely consumes the recorded entry -- nulling the source alloc pointer BEFORE freeing the entry (preventing a double-free through job->allocs), then compacting decoded_session->decisions via shift-down (memmove, never swap-with-last, which would reorder the remaining entries and break the positional --sim-verify comparison a later task depends on). Fixed a real correctness bug found while implementing: the "not found" check (i == n_decisions) goes stale once n_decisions can be decremented mid-loop by compaction -- replaced with an explicit found flag. Primary correctness bar: reconcile-then-release-through-normal-job-cleanup is ASAN-clean. Verified with ASAN_OPTIONS=detect_leaks=1 (beyond this suite's registered detect_leaks=0): zero double-free/UAF/overflow. Found and logged (not fixed, out of boundary) a real pre-existing leak in src/lib/misc.c's free_host_account() -- equally present on the live dispatch path, bug-1621. 46/47 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
c38207458f
commit
eccba84112
3 changed files with 306 additions and 26 deletions
|
|
@ -266,6 +266,42 @@ static replay_session_t* make_decoded_session(uint64_t begin_round,
|
|||
return s;
|
||||
}
|
||||
|
||||
/* 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
|
||||
* 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. */
|
||||
static xce_job_alloc_t* make_test_job_alloc_with_host(uint64_t jobid,
|
||||
const char* host,
|
||||
const char* resname,
|
||||
float value)
|
||||
{
|
||||
xce_alloc_t* a = make_alloc((char*)resname, NULL, 0, value, 0);
|
||||
xce_alloc_t** allocs = make_allocs(1, a);
|
||||
xce_host_alloc_t* h = make_host_alloc((char*)host, 1, allocs);
|
||||
xce_host_alloc_t** hallocs = make_host_allocs(1, h);
|
||||
|
||||
return make_job_alloc(jobid, 1, hallocs);
|
||||
}
|
||||
|
||||
/* task 2.3 fixture helper: wraps an already-built xce_job_alloc_t (as
|
||||
* opposed to make_dispatch_entry() above, which builds its own empty
|
||||
* (nhosts=0) alloc) into a DISPATCH-shaped replay_decision_t. */
|
||||
static replay_decision_t* make_dispatch_entry_from_alloc(xce_job_alloc_t* alloc)
|
||||
{
|
||||
sched_dispatch_decision_t* dispatch
|
||||
= (sched_dispatch_decision_t*)calloc(1, sizeof(*dispatch));
|
||||
replay_decision_t* rd = (replay_decision_t*)calloc(1, sizeof(*rd));
|
||||
|
||||
dispatch->alloc = alloc;
|
||||
|
||||
rd->body_which = ReplayDecision_body_dispatch;
|
||||
rd->body.dispatch = dispatch;
|
||||
|
||||
return rd;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Test 1 (the task's own primary Observable): a RUN-state job with no */
|
||||
/* known allocation and no matching recorded decision in this round's */
|
||||
|
|
@ -446,6 +482,175 @@ static void test_reconcile_skips_non_run_state_job(void)
|
|||
printf("test_reconcile_skips_non_run_state_job: PASS\n");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Test 7 (task 2.3): a matching recorded DISPATCH entry is reconstructed */
|
||||
/* into job->allocs (ownership TAKEOVER -- same pointer, not a copy) and */
|
||||
/* job->accounts (built from it), and the consumed entry is removed from */
|
||||
/* decoded_session (Requirement 1.1, 1.2, 3.2). */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void
|
||||
test_reconcile_reconstructs_allocs_and_accounts_from_matching_dispatch(void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
xce_sched_job_t* job;
|
||||
xce_job_alloc_t* jalloc;
|
||||
replay_session_t* decoded;
|
||||
|
||||
fixture_init(&sim);
|
||||
|
||||
job = add_job(&(sim.core), 7010ULL, JOB_STAT_RUN, NULL);
|
||||
TEST_ASSERT_NULL(job->accounts);
|
||||
TEST_ASSERT_NULL(job->allocs);
|
||||
|
||||
jalloc = make_test_job_alloc_with_host(7010ULL, "worker1", "slots", 2.0f);
|
||||
decoded
|
||||
= make_decoded_session(1000ULL, make_dispatch_entry_from_alloc(jalloc));
|
||||
|
||||
sim_reconcile_unaccounted_jobs(&sim, decoded);
|
||||
|
||||
TEST_ASSERT_EQUAL_PTR_MESSAGE(
|
||||
jalloc, job->allocs,
|
||||
"job->allocs must take OWNERSHIP of the recorded entry's alloc "
|
||||
"(identical pointer), not a duplicate copy -- same pattern "
|
||||
"scheduler.c's sched_commit_decision() uses");
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
job->accounts,
|
||||
"job->accounts must be built (make_job_account()) from the "
|
||||
"reconstructed job->allocs");
|
||||
TEST_ASSERT_EQUAL_INT(1, job->accounts->nhosts);
|
||||
TEST_ASSERT_EQUAL_STRING("worker1", job->accounts->accounts[0]->host);
|
||||
TEST_ASSERT_EQUAL_FLOAT(2.0f,
|
||||
job->accounts->accounts[0]->accounts[0]->reserved);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
0, decoded->n_decisions,
|
||||
"the now-fully-consumed recorded entry must be removed from "
|
||||
"decoded_session->decisions");
|
||||
|
||||
printf("test_reconcile_reconstructs_allocs_and_accounts_from_matching_"
|
||||
"dispatch: PASS\n");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Test 8 (task 2.3): compaction is SHIFT-DOWN, not swap-with-last -- the */
|
||||
/* remaining recorded entries must keep their ORIGINAL RELATIVE ORDER, per */
|
||||
/* design.md's Implementation Notes (a later --sim-verify positional walk */
|
||||
/* depends on this). A 4-entry fixture with the match in the middle */
|
||||
/* distinguishes the two strategies: swap-with-last would relocate the */
|
||||
/* last entry (C) into the removed slot, producing [A, C, B] instead of the */
|
||||
/* correct [A, B, C]. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_reconcile_compacts_via_shift_down_preserving_order(void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
xce_sched_job_t* job;
|
||||
replay_session_t* decoded;
|
||||
|
||||
fixture_init(&sim);
|
||||
|
||||
job = add_job(&(sim.core), 7011ULL, JOB_STAT_RUN, NULL);
|
||||
|
||||
decoded = (replay_session_t*)calloc(1, sizeof(*decoded));
|
||||
decoded->begin_round = 1000ULL;
|
||||
decoded->summary = NULL;
|
||||
decoded->decisions
|
||||
= (replay_decision_t**)calloc(4, sizeof(*(decoded->decisions)));
|
||||
decoded->decisions[0] = make_pend_entry(9001ULL); /* A */
|
||||
decoded->decisions[1] = make_dispatch_entry(7011ULL); /* matched */
|
||||
decoded->decisions[2] = make_pend_entry(9002ULL); /* B */
|
||||
decoded->decisions[3] = make_pend_entry(9003ULL); /* C */
|
||||
decoded->n_decisions = 4;
|
||||
|
||||
sim_reconcile_unaccounted_jobs(&sim, decoded);
|
||||
|
||||
TEST_ASSERT_NOT_NULL(job->accounts);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
3, decoded->n_decisions,
|
||||
"exactly one entry (the matched DISPATCH) must be consumed");
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE(9001ULL,
|
||||
decoded->decisions[0]->body.pend->jobid,
|
||||
"entry A must stay first");
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
|
||||
9002ULL, decoded->decisions[1]->body.pend->jobid,
|
||||
"entry B must be second (shift-down) -- swap-with-last would put C "
|
||||
"here instead");
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE(9003ULL,
|
||||
decoded->decisions[2]->body.pend->jobid,
|
||||
"entry C must be third, untouched");
|
||||
|
||||
printf("test_reconcile_compacts_via_shift_down_preserving_order: "
|
||||
"PASS\n");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Test 9 (task 2.3, the task's own PRIMARY correctness bar): reconciling a */
|
||||
/* job's allocation from a recorded DISPATCH entry, then releasing that */
|
||||
/* job's allocation through its NORMAL end-of-life path -- */
|
||||
/* XCE_CORE_EVENT_JOB_STATE_CHANGE (core_events.c) -- is clean: no */
|
||||
/* double-free, no use-after-free. This directly exercises the */
|
||||
/* move-then-null-then-free ownership handling design.md's Implementation */
|
||||
/* Notes require: `entry->body.dispatch->alloc` must be nulled out BEFORE */
|
||||
/* free_ReplayDecision(entry) frees the rest of the entry, or that free */
|
||||
/* would free the exact memory job->allocs now owns, and this exact */
|
||||
/* end-of-life release would then double-free it. Meaningful evidence for */
|
||||
/* this only under the project's ASAN build (see meson.build/tasks.md), */
|
||||
/* not as a plain functional assertion. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_reconcile_then_release_via_job_state_change_is_asan_clean(void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
xce_sched_job_t* job;
|
||||
replay_session_t* decoded;
|
||||
xce_core_event_t ev;
|
||||
int push_rc;
|
||||
|
||||
fixture_init(&sim);
|
||||
|
||||
job = add_job(&(sim.core), 7012ULL, JOB_STAT_RUN, NULL);
|
||||
|
||||
decoded = make_decoded_session(
|
||||
1000ULL, make_dispatch_entry_from_alloc(make_test_job_alloc_with_host(
|
||||
7012ULL, "worker1", "slots", 4.0f)));
|
||||
|
||||
sim_reconcile_unaccounted_jobs(&sim, decoded);
|
||||
|
||||
TEST_ASSERT_NOT_NULL(job->accounts);
|
||||
TEST_ASSERT_NOT_NULL(job->allocs);
|
||||
TEST_ASSERT_EQUAL_INT(0, decoded->n_decisions);
|
||||
|
||||
memset(&ev, 0, sizeof(ev));
|
||||
ev.type = XCE_CORE_EVENT_JOB_STATE_CHANGE;
|
||||
ev.u.job_state_change.jobid = 7012ULL;
|
||||
ev.u.job_state_change.old_state = JOB_STAT_RUN;
|
||||
ev.u.job_state_change.new_state = JOB_STAT_DONE;
|
||||
|
||||
push_rc = xce_core_push_events(&(sim.core), &ev, 1);
|
||||
TEST_ASSERT_EQUAL_INT(0, push_rc);
|
||||
|
||||
TEST_ASSERT_NULL_MESSAGE(
|
||||
job->accounts, "released cleanly via the normal end-of-life path");
|
||||
TEST_ASSERT_NULL_MESSAGE(
|
||||
job->allocs, "released cleanly via the normal end-of-life path");
|
||||
|
||||
/* Also exercise decoded_session's own round-end cleanup path
|
||||
* (free_ReplaySession_ptr(), the same call sched_sim_driver_run() makes
|
||||
* every round) now that n_decisions correctly reflects only the
|
||||
* remaining, still-owned entries (here: none) -- ASAN-clean under a
|
||||
* leak-detecting run (this suite's registered meson test uses
|
||||
* detect_leaks=0 per its own established fixture-teardown convention;
|
||||
* this call is exercised here regardless, and was additionally verified
|
||||
* with ASAN_OPTIONS=detect_leaks=1 against the standalone binary as
|
||||
* part of this task's own validation). */
|
||||
free_ReplaySession_ptr(&decoded);
|
||||
|
||||
printf("test_reconcile_then_release_via_job_state_change_is_asan_clean: "
|
||||
"PASS\n");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Test 6: NULL `sim`/`decoded_session` is a safe no-op (design.md's */
|
||||
/* Service Interface preconditions -- "never load-bearing" convention). */
|
||||
|
|
@ -478,6 +683,10 @@ 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_skips_non_run_state_job);
|
||||
RUN_TEST(
|
||||
test_reconcile_reconstructs_allocs_and_accounts_from_matching_dispatch);
|
||||
RUN_TEST(test_reconcile_compacts_via_shift_down_preserving_order);
|
||||
RUN_TEST(test_reconcile_then_release_via_job_state_change_is_asan_clean);
|
||||
RUN_TEST(test_reconcile_null_args_are_a_safe_noop);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue