diff --git a/src/scheduler/src/snapshot_load.c b/src/scheduler/src/snapshot_load.c index 34a59fcd..f2e77924 100644 --- a/src/scheduler/src/snapshot_load.c +++ b/src/scheduler/src/snapshot_load.c @@ -490,6 +490,26 @@ int xce_sched_core_load_snapshot(xce_sched_core_t* core, } } + /* worker/shared resource "used" totals (Req 1.2, 1.3): every restored + * worker's resInUse (and core->sharedInUse) was left freshly zeroed by + * the zarray_init() calls in the workers-restoration block above, and + * job->accounts was just rebuilt from the restored job->allocs -- so + * charging them here, ONCE, is a correct, idempotent-from-zero + * re-derivation, not a fabrication (same source data and same + * sched_charge_account() chain the next real session's own + * sched_account_jobs() call would use). Without this, a caller + * inspecting worker/resource state between a snapshot load and the + * first scheduling session (e.g. the interactive REPL's `info worker` + * before the first `step`) would see a misleadingly empty "used" total + * for jobs already known to be RUNNING, even though no scheduling + * decision is ever actually made against that stale state (every + * driver's own round-boundary sequence resets-then-re-sums before + * unblocking the picker regardless, core.h's xce_core_begin_session()/ + * xce_core_signal_go() doc comment). Must not be called again without + * an intervening resInUse/sharedInUse reset -- sched_charge_account() + * accumulates (+=), not assigns. */ + sched_account_jobs(core); + /* prng_seed -- see snapshot.c header "PRNG seed resolution". Genuine * gap found while writing this task's own cross-core equivalence test * (test_snapshot_save_load.c): save_snapshot() already writes diff --git a/src/scheduler/testsuite/test_sim_contention_parity.c b/src/scheduler/testsuite/test_sim_contention_parity.c index ed49830c..49b05c56 100644 --- a/src/scheduler/testsuite/test_sim_contention_parity.c +++ b/src/scheduler/testsuite/test_sim_contention_parity.c @@ -127,9 +127,12 @@ * Discriminating-power note (validated live, not merely asserted): this * test was run against a deliberately-reverted tree (task 3's * sim_reconcile_unaccounted_jobs() call site temporarily removed from - * sim_loop.c) and confirmed to fail -- specifically, g_round_resinuse[0] - * read back 0.0 instead of 10.0 (jobs A/C never reconciled, so - * sched_account_jobs()'s JOB_IS_RUN()+accounts!=NULL guard skipped them), + * sim_loop.c) and confirmed to fail -- specifically, the array slot for + * round 1's own resInUse snapshot (g_round_resinuse[1] as of task 4.6's + * added load-time slot at index 0; g_round_resinuse[0] at the time of this + * historical run, before that slot existed) read back 0.0 instead of 10.0 + * (jobs A/C never reconciled, so sched_account_jobs()'s + * JOB_IS_RUN()+accounts!=NULL guard skipped them), * and the direct sched_check_host_satisfy() capacity-awareness proof below * WRONGLY granted a hypothetical job wanting JOBB_WANT (5.0) against sim's * post-replay state -- then confirmed to pass again once the call site was @@ -1211,12 +1214,23 @@ test_contention_parity_matches_real_normal_mode_recording_round_by_round( g_capturing = 0; TEST_ASSERT_EQUAL_INT(0, rc); + /* 3, not 2: scheduler-snapshot-job-allocation task 4.6 added ONE extra + * sched_account_jobs() call, inside xce_sched_core_load_snapshot() + * itself (snapshot_load.c), which runs during sched_sim_driver_start() + * -- strictly before either replayed round -- so it lands at + * g_round_resinuse[0], pushing round 1/round 2's own snapshots to + * indices 1/2 (see the shifted indices below). This fixture's initial + * snapshot has no RUN-state jobs yet (A/B/C/D are all dispatched via + * recorded round decisions, never present at the replay's own starting + * snapshot), so this extra call charges nothing -- g_round_resinuse[0] + * is expected to read 0.0, not a real round observation. */ TEST_ASSERT_EQUAL_INT_MESSAGE( - 2, g_round_idx, + 3, g_round_idx, "sched_account_jobs() must have been called exactly once per " - "replayed round (2 rounds in this fixture) -- if this isn't 2, " - "the round-boundary snapshot mechanism itself is miscounting, " - "independent of the parity assertions below"); + "replayed round (2 rounds in this fixture) PLUS once at snapshot- " + "load time (task 4.6) -- if this isn't 3, either the round-boundary " + "snapshot mechanism itself is miscounting, or the load-time call " + "is missing/duplicated, independent of the parity assertions below"); /* ================================================================== * Part C: the actual parity assertions @@ -1224,15 +1238,16 @@ test_contention_parity_matches_real_normal_mode_recording_round_by_round( /* Requirement 1.4: sim mode's round-by-round resInUse must match what * the real normal-mode recording observed at the SAME point -- not - * just at the final round. */ + * just at the final round. Indices start at 1, not 0: index 0 is the + * load-time call above, not a round. */ TEST_ASSERT_EQUAL_FLOAT_MESSAGE( - normal_resinuse_after_round1, g_round_resinuse[0], + normal_resinuse_after_round1, g_round_resinuse[1], "round 1: simulation mode's reconciled resInUse must byte-for-byte " "match what the real normal-mode recording actually observed after " "its own round 1 (jobs A+C dispatched, consuming 10.0 of 10.0 " "total capacity)"); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( - normal_resinuse_after_round2, g_round_resinuse[1], + normal_resinuse_after_round2, g_round_resinuse[2], "round 2: simulation mode's resInUse must still match normal " "mode's -- the already-consumed capacity must PERSIST correctly " "into a SECOND round, not just the first"); @@ -1625,30 +1640,43 @@ test_contention_capacity_check_correct_across_multi_round_dispatch_confirm_gap( run_multiround_contention_fixture(&sim5, 5, "_mr5"); g_capturing = 0; + /* 6, not 5: scheduler-snapshot-job-allocation task 4.6's extra + * snapshot-load-time sched_account_jobs() call (see the sibling test + * above for the full rationale) lands at g_round_resinuse[0], pushing + * every round's own snapshot to index+1 below. This fixture's initial + * snapshot also has "worker-a" registered but no RUN-state jobs yet + * (same fixture_init_seed() as the sibling test), so index 0 reads + * 0.0. */ TEST_ASSERT_EQUAL_INT_MESSAGE( - 5, g_round_idx, + 6, g_round_idx, "sched_account_jobs() must have been called exactly once per " - "replayed round (5 rounds in this fixture)"); + "replayed round (5 rounds in this fixture) PLUS once at " + "snapshot-load time (task 4.6)"); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( 0.0f, g_round_resinuse[0], + "snapshot load: no RUN-state jobs exist yet at the replay's own " + "starting snapshot -- resInUse must read 0.0 (task 4.6's own " + "load-time call, not a replayed round)"); + TEST_ASSERT_EQUAL_FLOAT_MESSAGE( + 0.0f, g_round_resinuse[1], "round 1: A/C dispatched but still PEND -- resInUse must read 0.0, " "not fabricated or prematurely charged (Requirement 1.2)"); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( - 0.0f, g_round_resinuse[1], + 0.0f, g_round_resinuse[2], "round 2: idle filler round -- the deferred-zero state from round " "1 must persist, not drift, across an EXTRA round"); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( - 0.0f, g_round_resinuse[2], + 0.0f, g_round_resinuse[3], "round 3: a SECOND idle filler round -- still 0.0, widening the " "gap before A's confirmation below to genuinely several rounds"); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( - JOBA_WANT, g_round_resinuse[3], + JOBA_WANT, g_round_resinuse[4], "round 4: job A's JOB_STATE_CHANGE to RUN just replayed, 3 rounds " "after A's own round-1 DISPATCH -- resInUse must now reflect A's " "8.0 usage (Requirement 1.3), and ONLY A's -- job C is still PEND"); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( - WORKER_CAPACITY, g_round_resinuse[4], + WORKER_CAPACITY, g_round_resinuse[5], "round 5: job C's JOB_STATE_CHANGE to RUN just replayed, 4 rounds " "after C's own round-1 DISPATCH (a DIFFERENT separation than A's 3 " "rounds, proving this isn't a single-job coincidence) -- resInUse " diff --git a/src/scheduler/testsuite/test_sim_session_ordering.c b/src/scheduler/testsuite/test_sim_session_ordering.c index a96d9e66..1861bb73 100644 --- a/src/scheduler/testsuite/test_sim_session_ordering.c +++ b/src/scheduler/testsuite/test_sim_session_ordering.c @@ -644,11 +644,27 @@ test_sim_replay_calls_account_jobs_once_per_round_within_session_window( /* THE central regression proof (task 2.1's own scope, Req 1.3/1.4): * before this task, this count is 0 (sim_loop.c never called - * sched_account_jobs() at all, bug-1613) -- after, it is 2, one per - * replayed round, not merely once ever. */ - TEST_ASSERT_EQUAL_INT(2, atomic_load(&g_spy_calls)); + * sched_account_jobs() at all, bug-1613) -- after, it is 2 per-round + * calls plus ONE load-time call (task 4.6: + * xce_sched_core_load_snapshot(), called from sched_sim_driver_start() + * at the very top of sched_sim_driver_run(), now also charges + * sched_account_jobs() once for whatever RUN-state jobs the loaded + * snapshot already has -- see snapshot_load.c's own doc comment on + * that call). Total 3, not merely once ever and not merely once per + * round. */ + TEST_ASSERT_EQUAL_INT(3, atomic_load(&g_spy_calls)); - for (i = 0; i < 2; i++) { + /* Index 0 is task 4.6's load-time call -- by design it runs BEFORE any + * session begins (core->session_active == 0 at that point is correct, + * not a violation of the round-boundary contract below, which only + * governs calls made ONCE A ROUND IS UNDERWAY). */ + TEST_ASSERT_FALSE_MESSAGE( + g_spy_session_active_ok[0], + "the load-time sched_account_jobs() call (task 4.6) runs before " + "any session begins -- core->session_active must be 0 here, not " + "the round-boundary contract's session_active==1"); + + for (i = 1; i < 3; i++) { TEST_ASSERT_TRUE_MESSAGE( g_spy_session_active_ok[i], "sched_account_jobs() must run with core->session_active==1 -- " @@ -737,8 +753,12 @@ static void test_sim_replay_batch_mode_uses_the_same_round_boundary(void) rc = sched_sim_driver_run(&sim, &sim_args); TEST_ASSERT_EQUAL_INT(0, rc); - TEST_ASSERT_EQUAL_INT(1, atomic_load(&g_spy_calls)); - TEST_ASSERT_TRUE(g_spy_session_active_ok[0]); + /* 2, not 1: task 4.6's load-time call (index 0, session_active==0 by + * design -- see the two-round test's own comment above) plus this + * batch run's one replayed round (index 1, session_active==1). */ + TEST_ASSERT_EQUAL_INT(2, atomic_load(&g_spy_calls)); + TEST_ASSERT_FALSE(g_spy_session_active_ok[0]); + TEST_ASSERT_TRUE(g_spy_session_active_ok[1]); TEST_ASSERT_EQUAL_INT(1, atomic_load(&g_begin_session_spy_calls)); TEST_ASSERT_TRUE(g_begin_session_spy_active_ok[0]); diff --git a/src/scheduler/testsuite/test_sim_startup.c b/src/scheduler/testsuite/test_sim_startup.c index 1e2dfcfc..940750e2 100644 --- a/src/scheduler/testsuite/test_sim_startup.c +++ b/src/scheduler/testsuite/test_sim_startup.c @@ -81,6 +81,20 @@ void kill_sched_resval(xce_val_t* val) { (void)val; } * xce_sched_core_init()'s kvmgr datakiller. */ void plugin_unregister(void* arg) { (void)arg; } +/* xce_sched_core_load_snapshot()'s (snapshot_load.c) linker dependency as + * of scheduler-snapshot-job-allocation task 4.6 -- scheduler.c is + * deliberately not linked here (this file never drives a real scheduling + * session, see this file's own header comment), so a trivial no-op + * satisfies the link. Behaviorally exact, not just link-satisfying: every + * fixture in this file (write_valid_fixture()) snapshots a scheduler with + * zero jobs ever pushed, so the real sched_account_jobs() would iterate + * zero RUN-state jobs and charge nothing here regardless. */ +int sched_account_jobs(xce_sched_core_t* core) +{ + (void)core; + return 0; +} + /* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same * rationale/algorithm as test_record_lifecycle.c's own copy (schedobj.c is * not linked). Both the FIXTURE core (built via a real xce_scheduler_t, diff --git a/src/scheduler/testsuite/test_snapshot_save_load.c b/src/scheduler/testsuite/test_snapshot_save_load.c index 4b3e3274..2e8b40db 100644 --- a/src/scheduler/testsuite/test_snapshot_save_load.c +++ b/src/scheduler/testsuite/test_snapshot_save_load.c @@ -1666,8 +1666,20 @@ test_snapshot_load_leaves_allocs_and_accounts_null_for_job_without_allocation( * #include scheduler.c itself). Calls the REAL sched_charge_account() * (resource.c, #include'd above), so this genuinely exercises the same * accounting chain live dispatch and sim_reconcile_unaccounted_jobs() both - * rely on downstream. */ -static int test_sched_account_jobs(xce_sched_core_t* core) + * rely on downstream. + * + * Named (and exported, not `static`) as the REAL symbol -- not + * `test_sched_account_jobs` -- because xce_sched_core_load_snapshot() + * (snapshot_load.c, #include'd above) now calls sched_account_jobs() + * itself (scheduler-snapshot-job-allocation task 4.6, Req 1.2/1.3: worker/ + * shared resource "used" totals correct immediately on load, not only + * after a caller manually re-derives them). This TU's own white-box + * #include of snapshot_load.c means that call resolves to THIS + * definition, exactly like every other real/stub function this file + * already provides for a function snapshot_load.c or core.c reference + * (kill_sched_*, sched_resolve_job_picker, etc.) -- same convention, not a + * new one. */ +int sched_account_jobs(xce_sched_core_t* core) { iterator_t it; int rc = 0; @@ -1914,12 +1926,16 @@ test_snapshot_round_trip_mixed_jobs_shared_pool_resinuse_and_teardown_independen /* =================================================================== * Phase 3: the actual proof point (Req 1.2/1.3) -- call the REAL - * sched_account_jobs() chain (test_sched_account_jobs() above, which - * calls the REAL sched_charge_account(), resource.c) on core_a BEFORE - * saving, capturing what "resInUse/sharedInUse, reported the normal - * way" looks like for the source instance. + * sched_account_jobs() chain (this file's own real reimplementation + * above, which calls the REAL sched_charge_account(), resource.c) on + * core_a BEFORE saving, capturing what "resInUse/sharedInUse, reported + * the normal way" looks like for the source instance. Manual here + * because core_a is the SOURCE core, built via direct field pokes + * (never loaded from a snapshot), so nothing calls this for it + * automatically the way xce_sched_core_load_snapshot() now does for + * core_b below (Phase 5/8). * =================================================================== */ - rc = test_sched_account_jobs(&core_a); + rc = sched_account_jobs(&core_a); TEST_ASSERT_EQUAL_INT(0, rc); worker_a_a = sched_get_worker(&core_a, "hostA"); @@ -2030,16 +2046,19 @@ test_snapshot_round_trip_mixed_jobs_shared_pool_resinuse_and_teardown_independen TEST_ASSERT_TRUE(restored_running->allocs != snap.jobs->jobs[0]->allocs); /* =================================================================== - * Phase 8: the actual proof (Req 1.2/1.3) -- call the SAME real - * accounting chain on core_b (the RESTORED instance) and confirm its - * resInUse/sharedInUse totals EXACTLY match what the identical chain - * produced on core_a (Phase 3) before saving -- proving the restored - * allocation is "included in in-use totals immediately" and "reported - * identically to ... normal dispatch" (Req 1.2, 1.3), not merely that - * job->accounts' own fields happen to look right. + * Phase 8: the actual proof (Req 1.2/1.3) -- confirm core_b's + * resInUse/sharedInUse totals EXACTLY match what the identical + * accounting chain produced on core_a (Phase 3) before saving -- + * proving the restored allocation is "included in in-use totals + * immediately" and "reported identically to ... normal dispatch" (Req + * 1.2, 1.3), not merely that job->accounts' own fields happen to look + * right. NO manual sched_account_jobs(&core_b) call here (task 4.6, + * unlike this test's pre-4.6 form): xce_sched_core_load_snapshot() + * itself (Phase 5 above) now calls it internally, exactly once, right + * after restoring job->accounts -- calling it again here would + * double-charge (sched_charge_account() accumulates, it does not + * assign) and silently corrupt every assertion below. * =================================================================== */ - rc = test_sched_account_jobs(&core_b); - TEST_ASSERT_EQUAL_INT(0, rc); worker_a_b = sched_get_worker(&core_b, "hostA"); worker_b_b = sched_get_worker(&core_b, "hostB"); @@ -2124,6 +2143,160 @@ test_snapshot_round_trip_mixed_jobs_shared_pool_resinuse_and_teardown_independen TEST_ASSERT_EQUAL_FLOAT(1.0f, restored_shared_lic); } +/* ---------------------------------------------------------------------- */ +/* scheduler-snapshot-job-allocation task 4.6 (Req 1.2/1.3, "correct */ +/* immediately on load"): the direct, minimal proof that */ +/* xce_sched_core_load_snapshot() itself -- with NO further call from the */ +/* caller -- leaves a restored RUNNING job's worker resInUse already */ +/* correct. Found live (2026-08-28): an operator running */ +/* `xce_scheduler -i -s ` saw `info worker all` report `used=0.00` */ +/* for a worker with jobs already RUNNING in the loaded snapshot, correct */ +/* only after the first `step` -- because, before this task, ONLY the */ +/* round-boundary sched_account_jobs() call (scheduler-sim-decision-parity */ +/* spec, bug-1613's fix, sim_loop.c) ever re-derived resInUse; nothing ran */ +/* at load time itself. The round-trip test above already covers this */ +/* incidentally (its Phase 8 no longer needs a manual sched_account_jobs() */ +/* call and still passes) -- this test names the specific fix directly, */ +/* with the leanest fixture that can prove it (one resource, one worker, */ +/* one running job), rather than relying on a much larger test's side */ +/* effect. */ +/* ---------------------------------------------------------------------- */ +static void +test_snapshot_load_populates_worker_resinuse_immediately_without_a_session( + void) +{ + xce_sched_core_t core; + sched_snapshot_data_t snap; + xce_core_event_t events[3]; + xce_conf4sched_t config; + xce_resource_t res_slots; + xce_resource_t* resources[1]; + xce_queue_t wire_queue; + xce_queue_t* queues[1]; + xce_worker_t wire_worker; + xce_worker_t* workers[1]; + xce_val_t worker_slots_val; + xce_name_val_t worker_slots_nv; + xce_name_val_t* worker_resvals[1]; + xce_job_t* wire_job; + xce_job_t* jobs[1]; + xce_sched_job_t* job; + xce_alloc_t* a_slots; + xce_host_alloc_t* h0; + xce_host_alloc_t** hallocs; + xce_job_alloc_t* jalloc; + xce_sched_worker_t* worker; + xce_resource_t* slots_res; + float resinuse_slots = -1.0f; + int rc; + + memset(&g_stub_state_a, 0, sizeof(g_stub_state_a)); + fixture_init(&core, &g_stub_plugin_a); + + memset(&res_slots, 0, sizeof(res_slots)); + res_slots.name = "slots"; + resources[0] = &res_slots; + + memset(&wire_queue, 0, sizeof(wire_queue)); + wire_queue.queue = "normal"; + wire_queue.priority = 1; + wire_queue.status = QUEUE_STAT_OPEN; + queues[0] = &wire_queue; + + memset(&config, 0, sizeof(config)); + config.n_resources = 1; + config.resources = resources; + config.n_queues = 1; + config.queues = queues; + + memset(&worker_slots_val, 0, sizeof(worker_slots_val)); + worker_slots_val.which = XCE_VAL_NUMERIC; + worker_slots_val.u.ivalue32 = 16; + memset(&worker_slots_nv, 0, sizeof(worker_slots_nv)); + worker_slots_nv.name = "slots"; + worker_slots_nv.val = &worker_slots_val; + worker_resvals[0] = &worker_slots_nv; + + memset(&wire_worker, 0, sizeof(wire_worker)); + wire_worker.name = "hostA"; + wire_worker.status = 0; /* available */ + wire_worker.nresvals_to_sched = 1; + wire_worker.resvals_to_sched = worker_resvals; + workers[0] = &wire_worker; + + wire_job = make_job(901, JOB_STAT_PEND, "normal", "alice", "proj1"); + jobs[0] = wire_job; + + memset(events, 0, sizeof(events)); + events[0].type = XCE_CORE_EVENT_CONFIG_PUSH; + events[0].u.config_push.config = &config; + events[1].type = XCE_CORE_EVENT_WORKERS_PUSH; + events[1].u.workers_push.nworkers = 1; + events[1].u.workers_push.workers = workers; + events[2].type = XCE_CORE_EVENT_JOBS_PUSH; + events[2].u.jobs_push.njobs = 1; + events[2].u.jobs_push.jobs = jobs; + + rc = xce_core_push_events(&core, events, 3); + TEST_ASSERT_EQUAL_INT(0, rc); + + job = sched_get_job(&core, 901); + TEST_ASSERT_NOT_NULL(job); + + a_slots = make_alloc((char*)"slots", NULL, 0, 4.0f, 0); + TEST_ASSERT_NOT_NULL(a_slots); + + h0 = make_host_alloc((char*)"hostA", 1, make_allocs(1, a_slots)); + TEST_ASSERT_NOT_NULL(h0); + + hallocs = make_host_allocs(1, h0); + TEST_ASSERT_NOT_NULL(hallocs); + + jalloc = make_job_alloc(901, 1, hallocs); + TEST_ASSERT_NOT_NULL(jalloc); + + job->state = JOB_STAT_RUN; + job->allocs = jalloc; + job->accounts = make_job_account(job->allocs); + TEST_ASSERT_NOT_NULL(job->accounts); + + memset(&snap, 0, sizeof(snap)); + rc = xce_sched_core_save_snapshot(&core, &snap); + TEST_ASSERT_EQUAL_INT(0, rc); + + /* Reload into the SAME core -- xce_sched_core_load_snapshot() only + * ADDS state (sched_add_job/sched_add_worker on fresh kvmgr entries), + * it does not clear what is already there first, so a second load onto + * an already-populated core would double the very jobs/workers this + * test needs to stay singular; a fresh core (matching every other test + * in this file's own "load into a separate instance" convention) keeps + * the fixture unambiguous. */ + { + xce_sched_core_t core_b; + + memset(&g_stub_state_b, 0, sizeof(g_stub_state_b)); + fixture_init(&core_b, &g_stub_plugin_b); + + rc = xce_sched_core_load_snapshot(&core_b, &snap); + TEST_ASSERT_EQUAL_INT(0, rc); + + /* The actual proof: NO sched_account_jobs()/manual accounting call + * anywhere in this test -- xce_sched_core_load_snapshot() alone + * must already have charged this restored RUNNING job's + * allocation onto its worker's resInUse. */ + worker = sched_get_worker(&core_b, "hostA"); + TEST_ASSERT_NOT_NULL(worker); + + rc = key_find(core_b.resmgr, (void*)"slots", (void**)&slots_res); + TEST_ASSERT_GREATER_THAN_INT(0, rc); + TEST_ASSERT_NOT_NULL(slots_res); + + get_resvals(&(worker->resInUse), 1, &(slots_res->idx), + &resinuse_slots); + TEST_ASSERT_EQUAL_FLOAT(4.0f, resinuse_slots); + } +} + /* ---------------------------------------------------------------------- */ /* scheduler-snapshot-job-allocation task 3.2 (Req 3.5): the backward- */ /* compatibility case made explicit for the scenario Req 3.5's own text and */ @@ -2156,8 +2329,9 @@ test_snapshot_round_trip_mixed_jobs_shared_pool_resinuse_and_teardown_independen /* (d) job->allocs and job->accounts are both NULL -- clean degrade, not */ /* a fabricated allocation or a crash; */ /* (e) with a REAL worker present, the REAL sched_account_jobs() chain */ -/* (test_sched_account_jobs() above, task 3.1's own real */ -/* reimplementation) correctly contributes ZERO from this job to the */ +/* (this file's own reimplementation above, task 3.1, now called */ +/* automatically by xce_sched_core_load_snapshot() itself per task */ +/* 4.6) correctly contributes ZERO from this job to the */ /* worker's resInUse -- the concrete "used=0, identical to today's */ /* behavior" claim Req 3.5 and the originally-reported "info worker" */ /* symptom are both actually about, not merely that job->accounts is */ @@ -2266,11 +2440,17 @@ test_snapshot_load_leaves_running_job_without_allocation_at_zero_resinuse(void) TEST_ASSERT_NULL(restored->allocs); TEST_ASSERT_NULL(restored->accounts); - /* (e): the REAL accounting chain, run against the restored core, charges - * NOTHING for this job (job->accounts == NULL skips it, same guard live - * dispatch's own sched_account_jobs() uses) -- the worker's resInUse for - * "slots" reads back as exactly 0, matching today's pre-feature - * behavior, not an uninitialized/undefined value. */ + /* (e): the REAL accounting chain -- xce_sched_core_load_snapshot() + * itself already ran it once above (task 4.6) -- charges NOTHING for + * this job (job->accounts == NULL skips it, same guard live dispatch's + * own sched_account_jobs() uses) -- the worker's resInUse for "slots" + * reads back as exactly 0, matching today's pre-feature behavior, not + * an uninitialized/undefined value. No second manual call here: with + * nothing charged either way for this NULL-accounts job, a redundant + * call would be harmless for THIS assertion specifically, but is + * omitted anyway to match this file's one-call-per-load convention + * (see Phase 8 of the round-trip test above, where a second call would + * NOT be harmless). */ worker = sched_get_worker(&core, "hostA"); TEST_ASSERT_NOT_NULL(worker); @@ -2278,9 +2458,6 @@ test_snapshot_load_leaves_running_job_without_allocation_at_zero_resinuse(void) TEST_ASSERT_GREATER_THAN_INT(0, rc); TEST_ASSERT_NOT_NULL(slots_res); - rc = test_sched_account_jobs(&core); - TEST_ASSERT_EQUAL_INT(0, rc); - get_resvals(&(worker->resInUse), 1, &(slots_res->idx), &resinuse_slots); TEST_ASSERT_EQUAL_FLOAT(0.0f, resinuse_slots); } @@ -2307,6 +2484,8 @@ int main(void) test_snapshot_load_leaves_allocs_and_accounts_null_for_job_without_allocation); RUN_TEST( test_snapshot_round_trip_mixed_jobs_shared_pool_resinuse_and_teardown_independence); + RUN_TEST( + test_snapshot_load_populates_worker_resinuse_immediately_without_a_session); RUN_TEST( test_snapshot_load_leaves_running_job_without_allocation_at_zero_resinuse); return UNITY_END();