diff --git a/.kiro/specs/scheduler-snapshot-job-allocation/tasks.md b/.kiro/specs/scheduler-snapshot-job-allocation/tasks.md index 03a20cec..9827adc1 100644 --- a/.kiro/specs/scheduler-snapshot-job-allocation/tasks.md +++ b/.kiro/specs/scheduler-snapshot-job-allocation/tasks.md @@ -45,7 +45,7 @@ - _Requirements: 1.1, 1.2, 1.3, 1.4, 2.1, 2.3, 2.4_ - _Depends: 1.2, 2.1, 2.2_ -- [ ] 3.2 Verify a snapshot with no allocation data loads cleanly +- [x] 3.2 Verify a snapshot with no allocation data loads cleanly - Extend the round-trip coverage with a saved state containing no allocation data for its jobs (the natural case for a job that was never dispatched, or a save taken before this feature existed). - Assert the load succeeds with no error, and every such job's resource contribution reads as zero/unavailable, identical to today's behavior. - Observable completion: the backward-compatibility test passes without any special-casing visible to the caller. diff --git a/src/scheduler/testsuite/test_snapshot_save_load.c b/src/scheduler/testsuite/test_snapshot_save_load.c index 3d3ee7dd..4b3e3274 100644 --- a/src/scheduler/testsuite/test_snapshot_save_load.c +++ b/src/scheduler/testsuite/test_snapshot_save_load.c @@ -2124,6 +2124,167 @@ 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 3.2 (Req 3.5): the backward- */ +/* compatibility case made explicit for the scenario Req 3.5's own text and */ +/* the original bug report actually describe -- a job that was ALREADY */ +/* RUNNING as of a PRE-feature build's snapshot save point, so its */ +/* dispatch-time allocation was never captured onto the wire at all. This */ +/* is distinct from task 2.2's own load-side companion test above */ +/* (test_snapshot_load_leaves_allocs_and_accounts_null_for_job_without_ */ +/* allocation), which only covers a PENDING job -- the "never dispatched" */ +/* case, not the "dispatched before a pre-feature build could capture it" */ +/* case Req 3.5 is actually about. */ +/* */ +/* Since the wire job's `allocs` field is a plain optional pointer, not a */ +/* schema-versioned/gated blob (brief.md's "Backward compatibility note */ +/* (pre-release product)"), a pre-feature build's snapshot and "this */ +/* RUNNING job's allocs happens to be NULL today" are the literal same wire */ +/* shape -- so this test builds the wire job directly with `allocs == NULL` */ +/* and `state == JOB_STAT_RUN`, exactly what a pre-feature build would have */ +/* produced for a job that was running when it saved. */ +/* */ +/* Asserts, all against a fresh core with NO special-casing visible to the */ +/* caller: */ +/* (a) load succeeds (rc == 0); */ +/* (b) job->state correctly restores as RUNNING -- state round-trips */ +/* independently of allocs, this feature does not touch it; */ +/* (c) job->schedobj is non-NULL (task 1.2's fix -- independent of */ +/* allocation data, so a RUNNING job from a legacy snapshot is just */ +/* as safe to hand to the allocator/picker as any other restored */ +/* job); */ +/* (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 */ +/* 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 */ +/* NULL in isolation. */ +/* ---------------------------------------------------------------------- */ + +static void +test_snapshot_load_leaves_running_job_without_allocation_at_zero_resinuse(void) +{ + xce_sched_core_t core; + sched_snapshot_data_t in; + xce_jobs_data_t jobs_data; + xce_resource_data_t resources_data; + xce_resource_t res_slots; + xce_resource_t* resources_arr[1]; + xce_workers_data_t workers_data; + xce_worker_t wire_worker; + xce_worker_t* workers_arr[1]; + xce_val_t worker_slots_val; + xce_name_val_t worker_slots_nv; + xce_name_val_t* worker_resvals[1]; + xce_queue_t wire_queue; + xce_queue_t* queues[1]; + xce_job_t* wire_job; + xce_job_t* jobs[1]; + xce_sched_job_t* restored; + 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); + + /* one real resource ("slots") and one real worker advertising it -- so + * assertion (e) below is a genuine "worker present, zero charged" proof, + * not vacuously true because no worker/resource exists at all. */ + memset(&res_slots, 0, sizeof(res_slots)); + res_slots.name = "slots"; + resources_arr[0] = &res_slots; + memset(&resources_data, 0, sizeof(resources_data)); + resources_data.count = 1; + resources_data.resources = resources_arr; + + 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_arr[0] = &wire_worker; + memset(&workers_data, 0, sizeof(workers_data)); + workers_data.count = 1; + workers_data.workers = workers_arr; + + 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; + + /* the pre-feature wire shape: a job already RUNNING as of the snapshot's + * save point, whose allocs field was never populated -- make_job() + * never sets ->allocs (calloc()-zeroed NULL default, same helper task + * 2.2's own PENDING-job companion test uses above), but here with state + * forced to JOB_STAT_RUN to simulate "already running when a + * pre-feature build took the snapshot" (Req 3.5's actual described + * scenario), not "pending / never dispatched". */ + wire_job = make_job(801, JOB_STAT_RUN, "normal", "alice", "proj1"); + jobs[0] = wire_job; + + memset(&jobs_data, 0, sizeof(jobs_data)); + jobs_data.count = 1; + jobs_data.jobs = jobs; + + memset(&in, 0, sizeof(in)); + in.jobs = &jobs_data; + in.queues = queues; + in.n_queues = 1; + in.resources = &resources_data; + in.workers = &workers_data; + + rc = xce_sched_core_load_snapshot(&core, &in); + TEST_ASSERT_EQUAL_INT(0, rc); + + /* (a)+(b): load succeeded, state correctly restored as RUNNING. */ + restored = sched_get_job(&core, 801); + TEST_ASSERT_NOT_NULL(restored); + TEST_ASSERT_EQUAL_UINT32(JOB_STAT_RUN, restored->state); + + /* (c): task 1.2's fix -- schedobj construction is independent of + * allocation data, so a RUNNING job with no allocs still gets a valid, + * non-NULL scheduling representation. */ + TEST_ASSERT_NOT_NULL(restored->schedobj); + TEST_ASSERT_EQUAL_INT(XCE_RESREQ_PRIMARY, restored->schedobj->tag); + TEST_ASSERT_NOT_NULL(restored->schedobj->u.primary); + + /* (d): clean degrade, not a fabricated allocation. */ + 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. */ + worker = sched_get_worker(&core, "hostA"); + TEST_ASSERT_NOT_NULL(worker); + + rc = key_find(core.resmgr, (void*)"slots", (void**)&slots_res); + 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); +} + int main(void) { UNITY_BEGIN(); @@ -2146,5 +2307,7 @@ 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_leaves_running_job_without_allocation_at_zero_resinuse); return UNITY_END(); }