feat(scheduler-sim-eager-reconciliation): task 1.1 state-independent dispatch capture
Widens sim_reconcile_unaccounted_jobs()'s trigger gate in sim_loop.c: removes !JOB_IS_RUN(job->state) so a job's recorded DISPATCH decision is captured as soon as the replay reaches the round it occurred in, regardless of the job's current state -- closing bug-1611/bug-1613's residual (a job whose RUN confirmation lands in a later round than its dispatch decision, the realistic case under any real dispatch-to-confirmation latency, was never reconciled). sched_account_jobs()'s own unmodified JOB_IS_RUN charging gate continues to defer actual resource-in-use accounting until the job genuinely transitions to RUN -- no other line in the match/capture/report path changes. Replaces test_reconcile_skips_non_run_state_job (which asserted the bug as correct behavior) with test_reconcile_captures_non_run_state_job_when_dispatch_recorded, proving the opposite for a PEND-state job. Adds the xce-test-rc-file-style spec docs (requirements/design/research/tasks) for this feature. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
This commit is contained in:
parent
3891bd909a
commit
e2bb98c87e
6 changed files with 419 additions and 35 deletions
|
|
@ -502,21 +502,29 @@ static void* sim_picker_thread_fn(void* arg)
|
|||
* site's own comment below. Still directly unit-testable standalone too
|
||||
* (test_sim_reconcile_unaccounted.c), per design.md's own Testing Strategy.
|
||||
*
|
||||
* Trigger condition: mirrors sched_account_jobs()'s own `job->accounts ==
|
||||
* NULL` guard (scheduler.c) -- reused here as both the trigger AND (now that
|
||||
* the FOUND branch below populates job->accounts) the double-application
|
||||
* guard, per design.md's Implementation Notes. A RUN-state job in
|
||||
* `sim->core.jobs` with `job->accounts == NULL` is the signature of a job
|
||||
* already resolved earlier in the original recording, before this replay
|
||||
* run's own live decision-making ever considered it. 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()
|
||||
* Trigger condition (scheduler-sim-eager-reconciliation spec, task 1.1):
|
||||
* `job->accounts == NULL` is now the SOLE capture-eligibility gate --
|
||||
* state-independent, reused here as both the trigger AND (now that the
|
||||
* FOUND branch below populates job->accounts) the double-application guard.
|
||||
* Any job in `sim->core.jobs` with `job->accounts == NULL` is eligible to be
|
||||
* searched for a matching recorded DISPATCH decision this round, regardless
|
||||
* of its current `job->state` -- a job's recorded DISPATCH decision and its
|
||||
* RUN-state confirmation can legitimately land many rounds apart in the
|
||||
* original recording (the dominant real-world case), and this function is
|
||||
* called unconditionally every round, so a not-yet-running job is simply
|
||||
* re-checked round after round until its originating decision is found (or
|
||||
* the replay ends). Actual resource charging remains fully deferred to
|
||||
* sched_account_jobs()'s own unchanged `JOB_IS_RUN` gate (scheduler.c) --
|
||||
* capturing a job's allocation here while it is not yet RUN is inert until
|
||||
* that gate later includes it; this function performs no charging itself.
|
||||
* 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.
|
||||
*
|
||||
* For each such job, searches `decoded_session->decisions` for a matching
|
||||
* DISPATCH entry by jobid -- a job can only reach RUN-state via a recorded
|
||||
* DISPATCH entry by jobid -- a job can only ever be captured via a recorded
|
||||
* DISPATCH decision, so a PEND/PREEMPT entry (even one that happens to
|
||||
* carry the same jobid) is never treated as a match (see design.md's
|
||||
* Implementation Notes for why PEND/PREEMPT are structurally impossible to
|
||||
|
|
@ -589,8 +597,7 @@ static void sim_reconcile_unaccounted_jobs(xce_sched_sim_driver_t* sim,
|
|||
break;
|
||||
}
|
||||
|
||||
if ((job == NULL) || !JOB_IS_RUN(job->state)
|
||||
|| (job->accounts != NULL)) {
|
||||
if ((job == NULL) || (job->accounts != NULL)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -699,7 +706,7 @@ static void sim_reconcile_unaccounted_jobs(xce_sched_sim_driver_t* sim,
|
|||
* -- not an error, nothing fabricated, keep going. */
|
||||
fmt_jobid(jobidStr, sizeof(jobidStr), job->jobid);
|
||||
TRACE("%s no matching recorded DISPATCH decision this round for "
|
||||
"RUN-state jobid=%s -- accounts left unknown, usage "
|
||||
"jobid=%s -- accounts left unknown, usage "
|
||||
"unavailable, will re-check next round",
|
||||
__func__, jobidStr);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -498,36 +498,58 @@ static void test_reconcile_ignores_non_dispatch_entry_with_matching_jobid(
|
|||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Test 5: the trigger only fires for a RUN-state job -- a PEND-state job */
|
||||
/* with no accounts must never be touched here (the live picker re-derives */
|
||||
/* PEND decisions fresh every round, per design.md's Revalidation */
|
||||
/* Triggers section). */
|
||||
/* 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. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_reconcile_skips_non_run_state_job(void)
|
||||
static void
|
||||
test_reconcile_captures_non_run_state_job_when_dispatch_recorded(void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
xce_sched_job_t* job;
|
||||
xce_job_alloc_t* jalloc;
|
||||
replay_session_t* decoded;
|
||||
|
||||
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). */
|
||||
job = add_job(&(sim.core), 7005ULL, JOB_STAT_PEND, NULL);
|
||||
TEST_ASSERT_NULL(job->accounts);
|
||||
TEST_ASSERT_NULL(job->allocs);
|
||||
|
||||
/* A matching recorded DISPATCH entry is present -- but must not be
|
||||
* consulted at all, since the trigger's RUN-state gate must reject this
|
||||
* job before any lookup happens. */
|
||||
decoded = make_decoded_session(1000ULL, make_dispatch_entry(7005ULL));
|
||||
jalloc = make_test_job_alloc_with_host(7005ULL, "worker1", "slots", 2.0f);
|
||||
decoded
|
||||
= make_decoded_session(1000ULL, make_dispatch_entry_from_alloc(jalloc));
|
||||
|
||||
sim_reconcile_unaccounted_jobs(&sim, decoded, NULL);
|
||||
|
||||
TEST_ASSERT_NULL_MESSAGE(
|
||||
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(
|
||||
job->accounts,
|
||||
"a non-RUN-state job must never be reconciled here, regardless of "
|
||||
"what recorded data exists for it this round");
|
||||
"job->accounts must be built (make_job_account()) from the "
|
||||
"captured allocation even though the job has not reached RUN yet");
|
||||
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");
|
||||
|
||||
printf("test_reconcile_skips_non_run_state_job: PASS\n");
|
||||
printf("test_reconcile_captures_non_run_state_job_when_dispatch_"
|
||||
"recorded: PASS\n");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
|
@ -839,7 +861,8 @@ int main(void)
|
|||
RUN_TEST(test_reconcile_is_safe_to_recheck_on_a_later_round);
|
||||
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_captures_non_run_state_job_when_dispatch_recorded);
|
||||
RUN_TEST(
|
||||
test_reconcile_reconstructs_allocs_and_accounts_from_matching_dispatch);
|
||||
RUN_TEST(test_reconcile_compacts_via_shift_down_preserving_order);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue