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:
Rongsong Shen 2026-08-27 03:45:08 +00:00
parent 3891bd909a
commit e2bb98c87e
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
6 changed files with 419 additions and 35 deletions

View file

@ -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);
}

View file

@ -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);