fix(scheduler): sim resource over-commit, byte-unit display, sync job-state-at-capture
- bug-1799/1801: fix resInUse over-commit in sim replay -- sched_account_jobs's per-round re-derive pass now skips RECONCILING/UNAVAIL workers the same way core_reset_workers already does (bug-1751), closing the gap that let a worker's used slots exceed its own capacity across a resync replay - bug-1804: revise scheduler-sim-eager-reconciliation spec (user-directed) so a captured job's job->state is set to RUN synchronously at dispatch capture, mirroring sched_commit_decision's normal-mode semantics (bug-1624), instead of deferring until its separately-recorded JOB_STATE_CHANGE event replays - bug-1800: render bytes-valued CONSUMABLE resources (mem/swp/tmp) with the same human-scaled unit formatting already used for maxmem/maxswap/maxtmp - bug-1802/1803: add then revert a break-on-ANY-event breakpoint kind after live feedback that it over-fires inside busy sessions; replace it with a prescan-based "next non-dummy session" hint in info summary instead Full scheduler suite: 77/77 tests pass (asan profile). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
This commit is contained in:
parent
ffca959ed9
commit
25fb5c115a
22 changed files with 1827 additions and 303 deletions
|
|
@ -1902,6 +1902,99 @@ static void test_event_breakpoint_pauses_repl_at_matching_event(void)
|
|||
printf("test_event_breakpoint_pauses_repl_at_matching_event: PASS\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* test_info_summary_reports_next_nondummy_session_when_present -
|
||||
* user-requested (2026-09-11, replacing a reverted `break ANY` breakpoint
|
||||
* kind -- see repl_internal.h's own doc comment on xce_repl_bp_kind_t):
|
||||
* `info summary`, called at the very start (before any `step`/`continue`,
|
||||
* i.e. session_count==0), must report the next session that decodes at
|
||||
* least one real event, skipping two fully-empty dummy sessions (rounds
|
||||
* 1000 and 2000), plus the exact `step` count to reach it.
|
||||
*/
|
||||
static void
|
||||
test_info_summary_reports_next_nondummy_session_when_present(void)
|
||||
{
|
||||
char replay_path[PATH_MAX];
|
||||
char aux_dir[PATH_MAX];
|
||||
char script_path[PATH_MAX];
|
||||
char capture_path[PATH_MAX];
|
||||
FILE* f;
|
||||
char* captured;
|
||||
|
||||
f = write_header_and_snapshot(replay_path, sizeof(replay_path),
|
||||
"_nextnd");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
|
||||
append_replay_session(f, 1000ULL); /* session 1: dummy */
|
||||
append_replay_session(f, 2000ULL); /* session 2: dummy */
|
||||
append_jobstatechange_event(f, 2500ULL, 444ULL); /* session 3's event */
|
||||
append_replay_session(f, 3000ULL); /* session 3: real */
|
||||
append_end_of_stream_marker(f);
|
||||
fclose(f);
|
||||
|
||||
make_tmp_dir(aux_dir, sizeof(aux_dir), "_nextndaux");
|
||||
snprintf(script_path, sizeof(script_path), "%s/script.txt", aux_dir);
|
||||
snprintf(capture_path, sizeof(capture_path), "%s/capture.out", aux_dir);
|
||||
write_text_file(script_path, "info summary\nquit\n");
|
||||
|
||||
captured = run_group_d_and_capture(replay_path, script_path, capture_path);
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
strstr(captured, "next non-dummy session: 3 -- run 'step 3' to "
|
||||
"reach it"),
|
||||
"info summary must report session 3 (skipping the two dummy "
|
||||
"sessions 1 and 2) and the exact step count (3) to reach it from "
|
||||
"the very start (session_count==0)");
|
||||
|
||||
free(captured);
|
||||
|
||||
printf("test_info_summary_reports_next_nondummy_session_when_present: "
|
||||
"PASS\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* test_info_summary_reports_none_remaining_when_all_dummy - the file has
|
||||
* exactly two sessions, BOTH dummy, and nothing after them -- `info
|
||||
* summary` must say so plainly rather than reporting a bogus session
|
||||
* number or silently omitting the line.
|
||||
*/
|
||||
static void
|
||||
test_info_summary_reports_none_remaining_when_all_dummy(void)
|
||||
{
|
||||
char replay_path[PATH_MAX];
|
||||
char aux_dir[PATH_MAX];
|
||||
char script_path[PATH_MAX];
|
||||
char capture_path[PATH_MAX];
|
||||
FILE* f;
|
||||
char* captured;
|
||||
|
||||
f = write_header_and_snapshot(replay_path, sizeof(replay_path),
|
||||
"_nonend");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
|
||||
append_replay_session(f, 1000ULL); /* session 1: dummy */
|
||||
append_replay_session(f, 2000ULL); /* session 2: dummy */
|
||||
append_end_of_stream_marker(f);
|
||||
fclose(f);
|
||||
|
||||
make_tmp_dir(aux_dir, sizeof(aux_dir), "_nonendaux");
|
||||
snprintf(script_path, sizeof(script_path), "%s/script.txt", aux_dir);
|
||||
snprintf(capture_path, sizeof(capture_path), "%s/capture.out", aux_dir);
|
||||
write_text_file(script_path, "info summary\nquit\n");
|
||||
|
||||
captured = run_group_d_and_capture(replay_path, script_path, capture_path);
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
strstr(captured, "next non-dummy session: none remaining"),
|
||||
"info summary must plainly report that no non-dummy session "
|
||||
"remains, not a bogus number or silence");
|
||||
|
||||
free(captured);
|
||||
|
||||
printf("test_info_summary_reports_none_remaining_when_all_dummy: "
|
||||
"PASS\n");
|
||||
}
|
||||
|
||||
static void test_decision_breakpoint_pauses_after_decision_output(void)
|
||||
{
|
||||
char replay_path[PATH_MAX];
|
||||
|
|
@ -2149,6 +2242,8 @@ int main(void)
|
|||
RUN_TEST(test_info_resource_all_reports_when_none_are_global_shared);
|
||||
RUN_TEST(test_info_job_accepts_quoted_jobid_argument);
|
||||
RUN_TEST(test_info_summary_reports_counts);
|
||||
RUN_TEST(test_info_summary_reports_next_nondummy_session_when_present);
|
||||
RUN_TEST(test_info_summary_reports_none_remaining_when_all_dummy);
|
||||
RUN_TEST(test_info_session_reports_position_after_synthetic_events);
|
||||
|
||||
RUN_TEST(test_event_breakpoint_pauses_repl_at_matching_event);
|
||||
|
|
|
|||
|
|
@ -1714,6 +1714,404 @@ test_info_resource_units_mode_both_renders_raw_and_human_for_bytes_resource(
|
|||
"bytes_resource: PASS\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* User-reported (2026-09-11, live replay session): a CONSUMABLE byte-valued
|
||||
* per-host resource (mem/tmp/swp) rendered its value=/used= pair as a raw
|
||||
* `%.2f` byte count (e.g. "used=87960930222080.00"), unlike every other
|
||||
* bytes-valued field in this same listing (maxmem/maxswap/maxtmp, and mem/
|
||||
* tmp/swp themselves when NOT flagged consumable, via the Load Metrics
|
||||
* path) which already render human-scaled. repl_info_worker_one()'s own
|
||||
* pre-fix header comment explicitly scoped this out ("no bytes-valued
|
||||
* CONSUMABLE resource is exercised anywhere in this codebase's tests
|
||||
* today") -- this test (and its "raw"/swp/info-resource siblings below)
|
||||
* closes that gap. A non-bytes-valued consumable (slots/licenses) must keep
|
||||
* its existing pinned "%.2f"/"%.2f" shape unchanged -- see
|
||||
* test_info_worker_reports_consumable_value_and_used (bug-1727,
|
||||
* test_repl_breakpoints_inspection.c), left untouched by this fix.
|
||||
*/
|
||||
static void
|
||||
test_info_worker_units_mode_both_renders_raw_and_human_for_consumable_bytes_resource(
|
||||
void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
xce_core_event_t events[1];
|
||||
xce_resource_t wire_resource;
|
||||
xce_resource_t* r;
|
||||
xce_worker_t wire_worker;
|
||||
xce_worker_t* worker_ptrs[1];
|
||||
xce_val_t val;
|
||||
xce_name_val_t nv;
|
||||
xce_name_val_t* nv_ptrs[1];
|
||||
xce_val_t used_val;
|
||||
xce_sched_worker_t* sw;
|
||||
char* captured;
|
||||
char expected_value[64];
|
||||
char expected_used[64];
|
||||
char expected_line[160];
|
||||
double avail;
|
||||
double used;
|
||||
int rc;
|
||||
|
||||
avail = 8589934592.0; /* 8 GB */
|
||||
used = 4294967296.0; /* 4 GB */
|
||||
|
||||
memset(&sim, 0, sizeof(sim));
|
||||
rc = xce_sched_core_init(&(sim.core));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
memset(&wire_resource, 0, sizeof(wire_resource));
|
||||
wire_resource.name = "mem";
|
||||
wire_resource.valtype = XCE_VAL_NUMERIC;
|
||||
wire_resource.flags = XCE_RESF_CONSUMABLE;
|
||||
r = makeResource(sim.core.resmgr, &wire_resource);
|
||||
TEST_ASSERT_NOT_NULL(r);
|
||||
|
||||
memset(&val, 0, sizeof(val));
|
||||
val.which = Value_u_fvalue;
|
||||
val.u.fvalue = avail;
|
||||
memset(&nv, 0, sizeof(nv));
|
||||
nv.name = "mem";
|
||||
nv.val = &val;
|
||||
nv_ptrs[0] = &nv;
|
||||
|
||||
memset(&wire_worker, 0, sizeof(wire_worker));
|
||||
wire_worker.name = "hostA";
|
||||
wire_worker.status = 0;
|
||||
wire_worker.nresvals_to_sched = 1;
|
||||
wire_worker.resvals_to_sched = nv_ptrs;
|
||||
worker_ptrs[0] = &wire_worker;
|
||||
|
||||
memset(events, 0, sizeof(events));
|
||||
events[0].type = XCE_CORE_EVENT_WORKERS_PUSH;
|
||||
events[0].u.workers_push.nworkers = 1;
|
||||
events[0].u.workers_push.workers = worker_ptrs;
|
||||
|
||||
rc = xce_core_push_events(&(sim.core), events, 1);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
sw = sched_get_worker(&(sim.core), "hostA");
|
||||
TEST_ASSERT_NOT_NULL(sw);
|
||||
|
||||
memset(&used_val, 0, sizeof(used_val));
|
||||
used_val.which = Value_u_fvalue;
|
||||
used_val.u.fvalue = used;
|
||||
zarray_put(&(sw->resInUse), r->idx, dup_val(&used_val));
|
||||
|
||||
sim.repl_state = repl_state_ensure(&sim);
|
||||
TEST_ASSERT_NOT_NULL(sim.repl_state);
|
||||
sim.repl_state->units_mode = XCE_REPL_UNITS_BOTH;
|
||||
|
||||
captured = capture_dispatch(&sim, "info worker hostA");
|
||||
|
||||
repl_format_bytes_human(avail, expected_value, sizeof(expected_value));
|
||||
repl_format_bytes_human(used, expected_used, sizeof(expected_used));
|
||||
snprintf(expected_line, sizeof(expected_line), "value=%f (%s) used=%f (%s)",
|
||||
avail, expected_value, used, expected_used);
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
strstr(captured, expected_line),
|
||||
"a CONSUMABLE byte-valued resource (mem/tmp/swp) must render both "
|
||||
"value= and used= human-scaled under units_mode=both, the same "
|
||||
"as every other bytes-valued field in this listing");
|
||||
|
||||
free(captured);
|
||||
|
||||
printf(
|
||||
"test_info_worker_units_mode_both_renders_raw_and_human_for_"
|
||||
"consumable_bytes_resource: PASS\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* AC 6.2 'raw' counterpart for a CONSUMABLE byte-valued resource: under
|
||||
* units_mode=raw, value=/used= must show ONLY the bare wire numbers, no
|
||||
* "GB"/human suffix -- mirrors
|
||||
* test_info_worker_units_mode_raw_renders_bare_wire_value_without_mutating_stored_value's
|
||||
* contract, applied to the consumable value=/used= pair instead of a
|
||||
* single value= line.
|
||||
*/
|
||||
static void
|
||||
test_info_worker_units_mode_raw_renders_bare_values_for_consumable_bytes_resource(
|
||||
void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
xce_core_event_t events[1];
|
||||
xce_resource_t wire_resource;
|
||||
xce_resource_t* r;
|
||||
xce_worker_t wire_worker;
|
||||
xce_worker_t* worker_ptrs[1];
|
||||
xce_val_t val;
|
||||
xce_name_val_t nv;
|
||||
xce_name_val_t* nv_ptrs[1];
|
||||
xce_val_t used_val;
|
||||
xce_sched_worker_t* sw;
|
||||
char* captured;
|
||||
char expected_line[160];
|
||||
double avail;
|
||||
double used;
|
||||
int rc;
|
||||
|
||||
avail = 8589934592.0;
|
||||
used = 4294967296.0;
|
||||
|
||||
memset(&sim, 0, sizeof(sim));
|
||||
rc = xce_sched_core_init(&(sim.core));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
memset(&wire_resource, 0, sizeof(wire_resource));
|
||||
wire_resource.name = "mem";
|
||||
wire_resource.valtype = XCE_VAL_NUMERIC;
|
||||
wire_resource.flags = XCE_RESF_CONSUMABLE;
|
||||
r = makeResource(sim.core.resmgr, &wire_resource);
|
||||
TEST_ASSERT_NOT_NULL(r);
|
||||
|
||||
memset(&val, 0, sizeof(val));
|
||||
val.which = Value_u_fvalue;
|
||||
val.u.fvalue = avail;
|
||||
memset(&nv, 0, sizeof(nv));
|
||||
nv.name = "mem";
|
||||
nv.val = &val;
|
||||
nv_ptrs[0] = &nv;
|
||||
|
||||
memset(&wire_worker, 0, sizeof(wire_worker));
|
||||
wire_worker.name = "hostA";
|
||||
wire_worker.status = 0;
|
||||
wire_worker.nresvals_to_sched = 1;
|
||||
wire_worker.resvals_to_sched = nv_ptrs;
|
||||
worker_ptrs[0] = &wire_worker;
|
||||
|
||||
memset(events, 0, sizeof(events));
|
||||
events[0].type = XCE_CORE_EVENT_WORKERS_PUSH;
|
||||
events[0].u.workers_push.nworkers = 1;
|
||||
events[0].u.workers_push.workers = worker_ptrs;
|
||||
|
||||
rc = xce_core_push_events(&(sim.core), events, 1);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
sw = sched_get_worker(&(sim.core), "hostA");
|
||||
TEST_ASSERT_NOT_NULL(sw);
|
||||
|
||||
memset(&used_val, 0, sizeof(used_val));
|
||||
used_val.which = Value_u_fvalue;
|
||||
used_val.u.fvalue = used;
|
||||
zarray_put(&(sw->resInUse), r->idx, dup_val(&used_val));
|
||||
|
||||
sim.repl_state = repl_state_ensure(&sim);
|
||||
TEST_ASSERT_NOT_NULL(sim.repl_state);
|
||||
sim.repl_state->units_mode = XCE_REPL_UNITS_RAW;
|
||||
|
||||
captured = capture_dispatch(&sim, "info worker hostA");
|
||||
|
||||
snprintf(expected_line, sizeof(expected_line), "value=%f used=%f", avail,
|
||||
used);
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
strstr(captured, expected_line),
|
||||
"AC 6.2 'raw': a CONSUMABLE byte-valued resource must render bare "
|
||||
"wire values for both value= and used= under units_mode=raw");
|
||||
TEST_ASSERT_NULL_MESSAGE(
|
||||
strstr(captured, "GB"),
|
||||
"AC 6.2 'raw': must show no human-readable unit suffix");
|
||||
|
||||
free(captured);
|
||||
|
||||
printf(
|
||||
"test_info_worker_units_mode_raw_renders_bare_values_for_"
|
||||
"consumable_bytes_resource: PASS\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* AC 6.1's swp-is-megabytes special case, applied to the CONSUMABLE
|
||||
* value=/used= pair: both fields must be converted MB->bytes before
|
||||
* human-scaling, mirroring
|
||||
* test_info_worker_units_mode_converts_swp_megabytes_before_human_scaling's
|
||||
* contract for the Load Metrics (non-consumable) path.
|
||||
*/
|
||||
static void
|
||||
test_info_worker_units_mode_converts_swp_megabytes_for_consumable_resource(
|
||||
void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
xce_core_event_t events[1];
|
||||
xce_resource_t wire_resource;
|
||||
xce_resource_t* r;
|
||||
xce_worker_t wire_worker;
|
||||
xce_worker_t* worker_ptrs[1];
|
||||
xce_val_t val;
|
||||
xce_name_val_t nv;
|
||||
xce_name_val_t* nv_ptrs[1];
|
||||
xce_val_t used_val;
|
||||
xce_sched_worker_t* sw;
|
||||
char* captured;
|
||||
char expected_value_human[64];
|
||||
char expected_used_human[64];
|
||||
double avail_mb;
|
||||
double used_mb;
|
||||
int rc;
|
||||
|
||||
avail_mb = 16384.0; /* 16384 MB -> 16 GB */
|
||||
used_mb = 4096.0; /* 4096 MB -> 4 GB */
|
||||
|
||||
memset(&sim, 0, sizeof(sim));
|
||||
rc = xce_sched_core_init(&(sim.core));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
memset(&wire_resource, 0, sizeof(wire_resource));
|
||||
wire_resource.name = "swp";
|
||||
wire_resource.valtype = XCE_VAL_NUMERIC;
|
||||
wire_resource.flags = XCE_RESF_CONSUMABLE;
|
||||
r = makeResource(sim.core.resmgr, &wire_resource);
|
||||
TEST_ASSERT_NOT_NULL(r);
|
||||
|
||||
memset(&val, 0, sizeof(val));
|
||||
val.which = Value_u_fvalue;
|
||||
val.u.fvalue = avail_mb;
|
||||
memset(&nv, 0, sizeof(nv));
|
||||
nv.name = "swp";
|
||||
nv.val = &val;
|
||||
nv_ptrs[0] = &nv;
|
||||
|
||||
memset(&wire_worker, 0, sizeof(wire_worker));
|
||||
wire_worker.name = "hostA";
|
||||
wire_worker.status = 0;
|
||||
wire_worker.nresvals_to_sched = 1;
|
||||
wire_worker.resvals_to_sched = nv_ptrs;
|
||||
worker_ptrs[0] = &wire_worker;
|
||||
|
||||
memset(events, 0, sizeof(events));
|
||||
events[0].type = XCE_CORE_EVENT_WORKERS_PUSH;
|
||||
events[0].u.workers_push.nworkers = 1;
|
||||
events[0].u.workers_push.workers = worker_ptrs;
|
||||
|
||||
rc = xce_core_push_events(&(sim.core), events, 1);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
sw = sched_get_worker(&(sim.core), "hostA");
|
||||
TEST_ASSERT_NOT_NULL(sw);
|
||||
|
||||
memset(&used_val, 0, sizeof(used_val));
|
||||
used_val.which = Value_u_fvalue;
|
||||
used_val.u.fvalue = used_mb;
|
||||
zarray_put(&(sw->resInUse), r->idx, dup_val(&used_val));
|
||||
|
||||
sim.repl_state = repl_state_ensure(&sim);
|
||||
TEST_ASSERT_NOT_NULL(sim.repl_state);
|
||||
sim.repl_state->units_mode = XCE_REPL_UNITS_HUMAN;
|
||||
|
||||
captured = capture_dispatch(&sim, "info worker hostA");
|
||||
|
||||
repl_format_bytes_human(avail_mb * 1024.0 * 1024.0, expected_value_human,
|
||||
sizeof(expected_value_human));
|
||||
repl_format_bytes_human(used_mb * 1024.0 * 1024.0, expected_used_human,
|
||||
sizeof(expected_used_human));
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
strstr(captured, expected_value_human),
|
||||
"swp's value= must be converted MB->bytes before human-scaling, "
|
||||
"same as the Load Metrics path");
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
strstr(captured, expected_used_human),
|
||||
"swp's used= must be converted MB->bytes before human-scaling too");
|
||||
|
||||
free(captured);
|
||||
|
||||
printf(
|
||||
"test_info_worker_units_mode_converts_swp_megabytes_for_"
|
||||
"consumable_resource: PASS\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* info resource's counterpart: a GLOBAL/SHARED CONSUMABLE byte-valued
|
||||
* resource must render its value=/used= pair human-scaled too, mirroring
|
||||
* test_info_resource_units_mode_both_renders_raw_and_human_for_bytes_resource
|
||||
* (the non-consumable case) and this file's own info-worker tests above.
|
||||
*/
|
||||
static void
|
||||
test_info_resource_units_mode_both_renders_raw_and_human_for_consumable_bytes_resource(
|
||||
void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
xce_core_event_t events[1];
|
||||
xce_resource_t wire_mem;
|
||||
xce_resource_t* r;
|
||||
xce_val_t val_mem;
|
||||
xce_resval_t rv_mem;
|
||||
xce_resval_t* rv_ptrs[1];
|
||||
xce_val_t* stored;
|
||||
xce_val_t used_val;
|
||||
char* captured;
|
||||
char expected_value[64];
|
||||
char expected_used[64];
|
||||
char expected_line[160];
|
||||
double avail;
|
||||
double used;
|
||||
int rc;
|
||||
|
||||
avail = 8589934592.0; /* 8 GB */
|
||||
used = 2147483648.0; /* 2 GB */
|
||||
|
||||
memset(&sim, 0, sizeof(sim));
|
||||
rc = xce_sched_core_init(&(sim.core));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
memset(&wire_mem, 0, sizeof(wire_mem));
|
||||
wire_mem.name = "mem";
|
||||
wire_mem.valtype = XCE_VAL_NUMERIC;
|
||||
wire_mem.flags = XCE_RESF_SHARED | XCE_RESF_CONSUMABLE;
|
||||
r = makeResource(sim.core.resmgr, &wire_mem);
|
||||
TEST_ASSERT_NOT_NULL(r);
|
||||
|
||||
memset(&val_mem, 0, sizeof(val_mem));
|
||||
val_mem.which = Value_u_fvalue;
|
||||
val_mem.u.fvalue = avail;
|
||||
memset(&rv_mem, 0, sizeof(rv_mem));
|
||||
rv_mem.name = "mem";
|
||||
rv_mem.val = &val_mem;
|
||||
rv_ptrs[0] = &rv_mem;
|
||||
|
||||
memset(events, 0, sizeof(events));
|
||||
events[0].type = XCE_CORE_EVENT_RESVALS_FULL;
|
||||
events[0].u.resvals_full.n = 1;
|
||||
events[0].u.resvals_full.resvals = rv_ptrs;
|
||||
|
||||
rc = xce_core_push_events(&(sim.core), events, 1);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
memset(&used_val, 0, sizeof(used_val));
|
||||
used_val.which = Value_u_fvalue;
|
||||
used_val.u.fvalue = used;
|
||||
zarray_put(&(sim.core.sharedInUse), r->idx, dup_val(&used_val));
|
||||
|
||||
sim.repl_state = repl_state_ensure(&sim);
|
||||
TEST_ASSERT_NOT_NULL(sim.repl_state);
|
||||
sim.repl_state->units_mode = XCE_REPL_UNITS_BOTH;
|
||||
|
||||
captured = capture_dispatch(&sim, "info resource mem");
|
||||
|
||||
repl_format_bytes_human(avail, expected_value, sizeof(expected_value));
|
||||
repl_format_bytes_human(used, expected_used, sizeof(expected_used));
|
||||
snprintf(expected_line, sizeof(expected_line), "value=%f (%s) used=%f (%s)",
|
||||
avail, expected_value, used, expected_used);
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
strstr(captured, expected_line),
|
||||
"a GLOBAL/SHARED CONSUMABLE byte-valued resource must render both "
|
||||
"value= and used= human-scaled under units_mode=both");
|
||||
|
||||
stored = (xce_val_t*)zarray_get(&(sim.core.sharedInst), r->idx);
|
||||
TEST_ASSERT_NOT_NULL(stored);
|
||||
TEST_ASSERT_EQUAL_INT(Value_u_fvalue, stored->which);
|
||||
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
|
||||
(float)avail, (float)stored->u.fvalue,
|
||||
"AC 6.4: displaying a resource under any units_mode must never "
|
||||
"mutate the underlying stored/wire value");
|
||||
|
||||
free(captured);
|
||||
|
||||
printf(
|
||||
"test_info_resource_units_mode_both_renders_raw_and_human_for_"
|
||||
"consumable_bytes_resource: PASS\n");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
|
|
@ -1724,6 +2122,9 @@ int main(void)
|
|||
RUN_TEST(test_info_worker_units_mode_human_renders_scaled_value_without_mutating_stored_value);
|
||||
RUN_TEST(test_info_worker_units_mode_both_renders_raw_and_human_without_mutating_stored_value);
|
||||
RUN_TEST(test_info_worker_units_mode_converts_swp_megabytes_before_human_scaling);
|
||||
RUN_TEST(test_info_worker_units_mode_both_renders_raw_and_human_for_consumable_bytes_resource);
|
||||
RUN_TEST(test_info_worker_units_mode_raw_renders_bare_values_for_consumable_bytes_resource);
|
||||
RUN_TEST(test_info_worker_units_mode_converts_swp_megabytes_for_consumable_resource);
|
||||
RUN_TEST(test_info_worker_assigned_job_listing_compresses_contiguous_array_indices);
|
||||
RUN_TEST(test_info_worker_assigned_job_listing_appends_slot_count_when_uniform);
|
||||
RUN_TEST(test_info_worker_assigned_job_listing_excludes_terminal_job_despite_stale_allocs);
|
||||
|
|
@ -1731,6 +2132,7 @@ int main(void)
|
|||
RUN_TEST(test_info_resource_groups_resources_into_labeled_table_sections_in_order);
|
||||
RUN_TEST(test_info_resource_load_metric_whole_number_omits_trailing_zeros);
|
||||
RUN_TEST(test_info_resource_units_mode_both_renders_raw_and_human_for_bytes_resource);
|
||||
RUN_TEST(test_info_resource_units_mode_both_renders_raw_and_human_for_consumable_bytes_resource);
|
||||
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -369,6 +369,86 @@ static void test_commit_job_alloc_identical_across_core_containers(void)
|
|||
TEST_ASSERT_EQUAL_FLOAT(shared_sched[0], shared_sim[0]);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_charge_account_skips_reconciling_or_unavail_worker_host_charge -
|
||||
* bug-1799 fix: sched_charge_account()'s per-host resInUse charge must be
|
||||
* skipped for a worker currently IS_WORKER_RECONCILING or IS_WORKER_UNAVAIL.
|
||||
* core_reset_workers() (core_session.c, bug-1751) already skips RESETTING
|
||||
* such a worker's resInUse every round; sched_account_jobs()'s own
|
||||
* per-round re-derive pass reaches resInUse only through this same
|
||||
* sched_charge_account()->sched_update_account() path, so without a
|
||||
* matching skip here it re-added a full charge ON TOP of the not-reset
|
||||
* total every round the worker stayed RECONCILING/UNAVAIL -- resInUse grew
|
||||
* unboundedly and could exceed the resource's real capacity (live-observed:
|
||||
* a 22-slot worker showing used=26.00). core->sharedInUse has no
|
||||
* per-worker gating at all (its own reset, core_reset_shared_resval(), is
|
||||
* unconditional) and must keep charging normally regardless of any one
|
||||
* host's status.
|
||||
*/
|
||||
static void
|
||||
test_charge_account_skips_reconciling_or_unavail_worker_host_charge(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_job_account_t* acct;
|
||||
xce_sched_worker_t* w;
|
||||
float shared_val[1];
|
||||
float host_val[1];
|
||||
int shared_idx[1] = { SHARED_RESIDX };
|
||||
int host_idx[1] = { HOST_RESIDX };
|
||||
int rc;
|
||||
|
||||
memset(&sched, 0, sizeof(sched));
|
||||
setup_core_with_worker(&(sched.core), WORKER_NAME);
|
||||
|
||||
w = sched_get_worker(&(sched.core), WORKER_NAME);
|
||||
TEST_ASSERT_NOT_NULL(w);
|
||||
|
||||
acct = make_test_account(WORKER_NAME, SHARED_RESIDX, SHARED_USED,
|
||||
SHARED_RESERVED, HOST_RESIDX, HOST_USED,
|
||||
HOST_RESERVED);
|
||||
|
||||
/* --- RECONCILING: shared charges normally, per-host charge skipped --- */
|
||||
w->status = XCE_WORKER_RECONCILING;
|
||||
rc = sched_charge_account(&(sched.core), acct);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
get_resvals(&(sched.core.sharedInUse), 1, shared_idx, shared_val);
|
||||
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
|
||||
EXPECTED_SHARED_CHARGE, shared_val[0],
|
||||
"shared resources have no per-worker gating and must still charge "
|
||||
"while the host is RECONCILING");
|
||||
|
||||
get_resvals(&(w->resInUse), 1, host_idx, host_val);
|
||||
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
|
||||
0.0f, host_val[0],
|
||||
"bug-1799: a RECONCILING worker's per-host resInUse must NOT be "
|
||||
"charged -- core_reset_workers() already skips resetting it, so "
|
||||
"charging on top would grow it every round it stays RECONCILING");
|
||||
|
||||
/* --- UNAVAIL: same per-host skip --- */
|
||||
w->status = XCE_WORKER_UNAVAIL;
|
||||
rc = sched_charge_account(&(sched.core), acct);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
get_resvals(&(w->resInUse), 1, host_idx, host_val);
|
||||
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
|
||||
0.0f, host_val[0],
|
||||
"bug-1799: an UNAVAIL worker's per-host resInUse must NOT be "
|
||||
"charged either -- the same reset-skip condition covers both bits");
|
||||
|
||||
/* --- status clears: normal charging resumes on the very next call --- */
|
||||
w->status = 0;
|
||||
rc = sched_charge_account(&(sched.core), acct);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
get_resvals(&(w->resInUse), 1, host_idx, host_val);
|
||||
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
|
||||
EXPECTED_HOST_CHARGE, host_val[0],
|
||||
"once the worker's status bit clears, charging must resume "
|
||||
"normally on the very next call -- this is a per-call skip, not a "
|
||||
"permanent block");
|
||||
}
|
||||
|
||||
/*
|
||||
* resource-accounting.md's reservation-vs-actual-usage reconciliation for
|
||||
* a REAL (gauge-style) resource, e.g. "mem" -- sched_reconcile_host_
|
||||
|
|
@ -1142,6 +1222,7 @@ int main(void)
|
|||
UNITY_BEGIN();
|
||||
RUN_TEST(test_charge_account_identical_across_core_containers);
|
||||
RUN_TEST(test_commit_job_alloc_identical_across_core_containers);
|
||||
RUN_TEST(test_charge_account_skips_reconciling_or_unavail_worker_host_charge);
|
||||
RUN_TEST(
|
||||
test_reconcile_host_resource_usage_below_reservation_keeps_reservation);
|
||||
RUN_TEST(
|
||||
|
|
|
|||
|
|
@ -1380,49 +1380,70 @@ test_contention_parity_matches_real_normal_mode_recording_round_by_round(
|
|||
}
|
||||
|
||||
/* ======================================================================
|
||||
* scheduler-sim-eager-reconciliation task 3.1 (Req 1.1-1.4): a resource-
|
||||
* contention fixture where a job's recorded DISPATCH decision and its
|
||||
* JOB_STATE_CHANGE-to-RUN confirmation are separated by SEVERAL rounds --
|
||||
* not the same-round shape the test above already covers (there,
|
||||
* scheduler-sim-eager-reconciliation task 3.1 (Req 1.1-1.4), REVISED
|
||||
* 2026-09-11 (user-directed, requirements.md Requirement 1's revision):
|
||||
* a resource-contention fixture where a job's recorded DISPATCH decision
|
||||
* and its JOB_STATE_CHANGE-to-RUN confirmation are separated by SEVERAL
|
||||
* rounds -- not the same-round shape the test above already covers (there,
|
||||
* JOB_STATE_CHANGE lands in the SAME round window as the matching DISPATCH
|
||||
* decision, which is exactly the shape the OLD, pre-task-1.1 trigger gate
|
||||
* (`!JOB_IS_RUN(job->state)` in sim_reconcile_unaccounted_jobs(), removed
|
||||
* by task 1.1) already handled correctly). This is the genuinely NEW
|
||||
* scenario the OLD gate could never reconcile: jobs A and C are PEND at
|
||||
* the exact round their own DISPATCH decision is recorded (round 1), and
|
||||
* only reach RUN several rounds later (rounds 4 and 5 respectively) via a
|
||||
* bare JOB_STATE_CHANGE event with no DISPATCH decision of its own -- by
|
||||
* which point round 1's own decoded_session (the only place their
|
||||
* DISPATCH ever existed) is long freed. Under the OLD gate, job->accounts
|
||||
* would stay NULL forever for both jobs, and sched_account_jobs()'s own
|
||||
* JOB_IS_RUN + accounts!=NULL charging gate would therefore never charge
|
||||
* them -- resInUse would read a PERMANENTLY-ZERO 0.0 for the rest of the
|
||||
* replay, wrongly making the worker's full WORKER_CAPACITY look available
|
||||
* even once both jobs are genuinely running. See this task's own Status
|
||||
* Report for the live revert-rebuild confirmation that this exact fixture
|
||||
* reproduces that failure against a reverted tree.
|
||||
* by task 1.1) already handled correctly). Jobs A and C are PEND at the
|
||||
* exact round their own DISPATCH decision is recorded (round 1), and only
|
||||
* reach a SEPARATELY-RECORDED RUN confirmation several rounds later
|
||||
* (rounds 4 and 5 respectively) via a bare JOB_STATE_CHANGE event with no
|
||||
* DISPATCH decision of its own -- by which point round 1's own
|
||||
* decoded_session (the only place their DISPATCH ever existed) is long
|
||||
* freed.
|
||||
*
|
||||
* Pre-task-1.1: job->accounts stayed NULL forever for both jobs (the OLD
|
||||
* `!JOB_IS_RUN` gate never re-searched a PEND job past its own round), so
|
||||
* resInUse read a PERMANENTLY-ZERO 0.0 for the rest of the replay.
|
||||
*
|
||||
* Task 1.1 through 2026-09-07: `sim_reconcile_unaccounted_jobs()` captured
|
||||
* job->allocs/job->accounts at round 1 regardless of state, but LEFT
|
||||
* job->state untouched -- charging stayed deferred to sched_account_jobs()'s
|
||||
* unchanged JOB_IS_RUN gate until the SEPARATELY-recorded JOB_STATE_CHANGE
|
||||
* event replayed at round 4/5, several rounds after capture.
|
||||
*
|
||||
* 2026-09-11 revision (current, this fixture): capture now ALSO sets
|
||||
* job->state = JOB_STAT_RUN synchronously (mirroring scheduler.c's
|
||||
* sched_commit_decision(), bug-1624) -- so BOTH jobs A and C are
|
||||
* genuinely RUN and charged starting round 1 itself, the SAME round their
|
||||
* DISPATCH decision is captured, not deferred to rounds 4/5. The later,
|
||||
* separately-recorded JOB_STATE_CHANGE-to-RUN events at rounds 4/5 are
|
||||
* still replayed (this fixture keeps them, unchanged, to prove they
|
||||
* remain safe/idempotent) but no longer change resInUse at all -- they
|
||||
* re-affirm an already-RUN job. This test's core value survives
|
||||
* unchanged: prove capacity accounting is CORRECT across a realistic
|
||||
* multi-round dispatch-to-confirmation gap, not permanently wrong for the
|
||||
* rest of the replay (the pre-task-1.1 bug) -- but "correct" now means
|
||||
* "already fully accounted from round 1", not "deferred until
|
||||
* confirmation."
|
||||
*
|
||||
* Fixture shape (reuses this file's own established constants verbatim --
|
||||
* WORKER_CAPACITY=10.0, JOBA_WANT=8.0, JOBB_WANT=5.0, JOBC_WANT=2.0,
|
||||
* JOBD_WANT=1.0 -- never re-typed):
|
||||
* Round 1 (begin_round=2000): A/B/C pushed PEND; A(8.0) and C(2.0)
|
||||
* dispatched (A+C=10.0, exactly saturating capacity), B genuinely
|
||||
* denied. NEITHER A NOR C's JOB_STATE_CHANGE appears anywhere near
|
||||
* this round.
|
||||
* dispatched (A+C=10.0, exactly saturating capacity) -- BOTH
|
||||
* genuinely RUN and charged from this round onward (2026-09-11
|
||||
* revision), B genuinely denied. NEITHER A NOR C's JOB_STATE_CHANGE
|
||||
* appears anywhere near this round.
|
||||
* Round 2, 3 (begin_round=3000/4000): pure filler -- B is genuinely
|
||||
* re-offered and re-denied each round (builtin_fcfs's own documented
|
||||
* "no-op, retried next round" contract), widening the gap before A/C's
|
||||
* eventual confirmations below to genuinely "several rounds", and
|
||||
* proving the deferred-zero state from round 1 persists correctly
|
||||
* across MULTIPLE idle rounds, not just one.
|
||||
* Round 4 (begin_round=5000): job A's JOB_STATE_CHANGE to RUN -- 3
|
||||
* rounds after its own round-1 DISPATCH. Job C is still PEND -- only
|
||||
* PARTIAL usage (8.0 of 10.0) must be reflected from this round on.
|
||||
* Round 5 (begin_round=6000): job C's JOB_STATE_CHANGE to RUN -- 4
|
||||
* rounds after ITS OWN round-1 DISPATCH (a deliberately DIFFERENT
|
||||
* separation than job A's 3 rounds, proving this isn't a single-job
|
||||
* coincidence). Both jobs now genuinely RUN -- FULL usage (10.0 of
|
||||
* 10.0) must be reflected from this round on.
|
||||
* eventual (now redundant) confirmations below to genuinely "several
|
||||
* rounds", and proving the round-1 charge persists correctly across
|
||||
* MULTIPLE idle rounds, not just one.
|
||||
* Round 4 (begin_round=5000): job A's SEPARATELY-recorded JOB_STATE_
|
||||
* CHANGE to RUN replays -- 3 rounds after its own round-1 DISPATCH.
|
||||
* A safe no-op re-affirmation (already RUN since round 1); resInUse
|
||||
* stays at the SAME full 10.0 it already reached at round 1.
|
||||
* Round 5 (begin_round=6000): job C's SEPARATELY-recorded JOB_STATE_
|
||||
* CHANGE to RUN replays -- 4 rounds after ITS OWN round-1 DISPATCH (a
|
||||
* deliberately DIFFERENT separation than job A's 3 rounds, proving
|
||||
* this isn't a single-job coincidence). Also a safe no-op; resInUse
|
||||
* is unchanged, still 10.0.
|
||||
*
|
||||
* The single test function below drives this SAME fixture shape three
|
||||
* times, truncated after round 1, round 4, and round 5 respectively (via
|
||||
|
|
@ -1597,41 +1618,43 @@ test_contention_capacity_check_correct_across_multi_round_dispatch_confirm_gap(
|
|||
g_capturing = 0;
|
||||
g_round_idx = 0;
|
||||
|
||||
/* ---- Checkpoint 1: right after round 1 -- A and C are dispatched but
|
||||
* still PEND (neither has reached RUN yet). Capacity must look FULLY
|
||||
* free: a hypothetical job wanting the entire WORKER_CAPACITY must be
|
||||
* granted, since nothing is genuinely running yet (Requirement 1.2, at
|
||||
* the capacity-check OUTCOME level, not just a raw resInUse read). ---- */
|
||||
/* ---- Checkpoint 1 (REVISED 2026-09-11): right after round 1 -- A and
|
||||
* C are captured AND already genuinely RUN (synchronous capture-time
|
||||
* transition, mirroring sched_commit_decision()/bug-1624). Capacity
|
||||
* must already look FULLY SATURATED: a hypothetical job wanting the
|
||||
* entire WORKER_CAPACITY must be DENIED, since both A and C are
|
||||
* already counted from this round onward (Requirement 1.2/1.3, at the
|
||||
* capacity-check OUTCOME level, not just a raw resInUse read). Under
|
||||
* the PRE-2026-09-11 design this same check was granted (1) -- the
|
||||
* inversion itself is the proof the revision landed. ---- */
|
||||
run_multiround_contention_fixture(&sim1, 1, "_mr1");
|
||||
rc = check_host_satisfy_want(&sim1, WORKER_CAPACITY);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
1, rc,
|
||||
"round 1: A and C are dispatched-but-still-PEND -- capacity must "
|
||||
"look FULLY free (a job wanting the entire WORKER_CAPACITY must be "
|
||||
"granted), since neither job has genuinely reached RUN yet "
|
||||
"(Requirement 1.2)");
|
||||
0, rc,
|
||||
"round 1 (2026-09-11 revision): A and C are captured AND already "
|
||||
"genuinely RUN, synchronously with capture -- capacity must "
|
||||
"already look FULLY SATURATED (a job wanting the entire "
|
||||
"WORKER_CAPACITY must be DENIED), not deferred until a later "
|
||||
"confirmation round (Requirement 1.2/1.3)");
|
||||
|
||||
/* ---- Checkpoint 2: right after round 4 -- job A confirmed RUN, 3
|
||||
* rounds after its own round-1 DISPATCH; job C still PEND. Capacity
|
||||
* must reflect PARTIAL usage: exactly JOBC_WANT (2.0) of headroom
|
||||
* remains -- a job wanting exactly that must be granted, one wanting
|
||||
* more (JOBB_WANT, 5.0) must be denied. This is the strongest single
|
||||
* proof that combined accounting is correct, not just "some job got
|
||||
* counted": an implementation that dropped job A's contribution (the
|
||||
* OLD same-round-only bug) would wrongly grant JOBB_WANT here too. ---- */
|
||||
/* ---- Checkpoint 2 (REVISED 2026-09-11): right after round 4 -- job
|
||||
* A's separately-recorded confirmation event has now ALSO replayed,
|
||||
* but changes nothing: A and C were both already RUN since round 1.
|
||||
* Capacity must STILL be fully saturated -- even JOBC_WANT (2.0),
|
||||
* previously grantable under the deferred design, must now be denied,
|
||||
* since C itself has been counted since round 1 too. ---- */
|
||||
run_multiround_contention_fixture(&sim4, 4, "_mr4");
|
||||
rc = check_host_satisfy_want(&sim4, JOBC_WANT);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
1, rc,
|
||||
"round 4: only job A (8.0) is genuinely RUN -- exactly JOBC_WANT "
|
||||
"(2.0) of headroom must remain and must be granted");
|
||||
0, rc,
|
||||
"round 4 (2026-09-11 revision): both A and C have been genuinely "
|
||||
"RUN and fully charged since round 1 -- even JOBC_WANT (2.0) must "
|
||||
"now be denied, since there is no remaining headroom");
|
||||
rc = check_host_satisfy_want(&sim4, JOBB_WANT);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
0, rc,
|
||||
"round 4: JOBB_WANT (5.0) exceeds the 2.0 remaining headroom -- "
|
||||
"must be denied. A capacity check that wrongly dropped job A's "
|
||||
"already-RUN 8.0 usage (the OLD same-round-only bug) would wrongly "
|
||||
"grant this");
|
||||
"round 4: JOBB_WANT (5.0) must also be denied -- capacity has been "
|
||||
"fully saturated by A+C since round 1");
|
||||
|
||||
/* ---- Checkpoint 3: the full 5-round replay -- per-round resInUse
|
||||
* snapshots (Requirement 1.1-1.4 across EVERY round, not just
|
||||
|
|
@ -1661,37 +1684,41 @@ test_contention_capacity_check_correct_across_multi_round_dispatch_confirm_gap(
|
|||
"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)");
|
||||
WORKER_CAPACITY, g_round_resinuse[1],
|
||||
"round 1 (2026-09-11 revision): A(8.0) and C(2.0) are BOTH "
|
||||
"captured and synchronously transitioned to RUN this round -- "
|
||||
"resInUse must already reflect the FULL combined 10.0, not "
|
||||
"deferred to a later confirmation round (Requirement 1.2/1.3)");
|
||||
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
|
||||
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");
|
||||
WORKER_CAPACITY, g_round_resinuse[2],
|
||||
"round 2: idle filler round -- the round-1 charge must persist "
|
||||
"unchanged across an EXTRA round, not drift or reset");
|
||||
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
|
||||
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");
|
||||
WORKER_CAPACITY, g_round_resinuse[3],
|
||||
"round 3: a SECOND idle filler round -- still the full 10.0, "
|
||||
"widening the gap before A's now-redundant confirmation below to "
|
||||
"genuinely several rounds");
|
||||
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
|
||||
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");
|
||||
WORKER_CAPACITY, g_round_resinuse[4],
|
||||
"round 4: job A's separately-recorded JOB_STATE_CHANGE-to-RUN "
|
||||
"event replays, 3 rounds after A's own round-1 DISPATCH -- a safe "
|
||||
"no-op re-affirmation (A has been RUN and charged since round 1); "
|
||||
"resInUse must remain exactly 10.0, not drop, not double-charge");
|
||||
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
|
||||
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 "
|
||||
"must now reflect the FULL combined 10.0 usage of both A and C "
|
||||
"together");
|
||||
"round 5: job C's separately-recorded JOB_STATE_CHANGE-to-RUN "
|
||||
"event replays, 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) -- also a safe no-op; resInUse must "
|
||||
"remain exactly 10.0, not double-charge to 20.0");
|
||||
|
||||
rc = check_host_satisfy_want(&sim5, JOBD_WANT);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
0, rc,
|
||||
"round 5: capacity is fully saturated by A+C (10.0 of 10.0) -- "
|
||||
"even the smallest positive want (JOBD_WANT) must be denied. Under "
|
||||
"the OLD same-round-only reconciliation gate, neither A nor C "
|
||||
"would EVER have been captured (their DISPATCH and RUN-"
|
||||
"round 5: capacity is fully saturated by A+C (10.0 of 10.0, since "
|
||||
"round 1) -- even the smallest positive want (JOBD_WANT) must be "
|
||||
"denied. Under the PRE-task-1.1 reconciliation gate, neither A nor "
|
||||
"C would EVER have been captured (their DISPATCH and RUN-"
|
||||
"confirmation rounds never coincide), so resInUse would read a "
|
||||
"PERMANENTLY-ZERO 0.0 here and this check would WRONGLY grant "
|
||||
"admission");
|
||||
|
|
|
|||
|
|
@ -1036,46 +1036,43 @@ static void test_reconcile_null_args_are_a_safe_noop(void)
|
|||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Test 12 (scheduler-sim-eager-reconciliation task 2.1, Req 1.2, 1.3, */
|
||||
/* 1.4): resource-in-use accounting must defer to the job's REAL running */
|
||||
/* state, not its capture time. task 1.1 widened sim_reconcile_unaccounted_ */
|
||||
/* jobs()'s trigger so a job's allocation is captured (job->allocs/ */
|
||||
/* job->accounts set) as soon as its recorded DISPATCH decision is found, */
|
||||
/* regardless of job->state -- this test proves the OTHER half of that */
|
||||
/* design's correctness: sched_account_jobs() (scheduler.c, UNCHANGED, */
|
||||
/* real body pulled in above) still gates charging on JOB_IS_RUN(state), */
|
||||
/* so a captured-but-still-PEND job contributes nothing to w->resInUse */
|
||||
/* (Req 1.2) until it genuinely transitions to RUN, at which point the */
|
||||
/* SAME captured allocation (never re-captured, never re-typed) is charged */
|
||||
/* (Req 1.3). Req 1.4 ("matches normal-mode's equivalent-point reporting") */
|
||||
/* is satisfied by construction: this test drives the exact same */
|
||||
/* sched_account_jobs() normal mode's own driver.c calls at the identical */
|
||||
/* round-boundary position (design.md's Architecture Integration note) -- */
|
||||
/* no separate normal-mode comparison fixture is needed to prove a shared */
|
||||
/* function behaves consistently with itself. */
|
||||
/* Test 12 (scheduler-sim-eager-reconciliation task 4.1/4.2, Req 1.2, 1.3, */
|
||||
/* 1.4, REVISED 2026-09-11, user-directed): a captured job's resource */
|
||||
/* consumption must be reflected in resInUse starting the SAME round its */
|
||||
/* dispatch decision is captured, not deferred to a later round's */
|
||||
/* separately-recorded RUN-state confirmation event. sim_reconcile_ */
|
||||
/* unaccounted_jobs() now sets job->state = JOB_STAT_RUN synchronously, */
|
||||
/* in the same operation as the allocation capture -- mirroring */
|
||||
/* scheduler.c's sched_commit_decision() (bug-1624) exactly. This */
|
||||
/* INVERTS the pre-2026-09-11 version of this test (formerly */
|
||||
/* test_captured_pend_job_excluded_from_resinuse_until_run), which */
|
||||
/* asserted the OPPOSITE: that a captured-but-PEND job stayed PEND and */
|
||||
/* contributed zero to resInUse until a SEPARATE, later job->state = RUN */
|
||||
/* write. See requirements.md's Requirement 1 2026-09-11 revision and */
|
||||
/* research.md's corresponding entry for the full rationale trail. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void
|
||||
test_captured_pend_job_excluded_from_resinuse_until_run(void)
|
||||
test_captured_job_included_in_resinuse_same_round_as_capture(void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
xce_sched_job_t* job;
|
||||
xce_sched_worker_t* w;
|
||||
xce_job_alloc_t* jalloc;
|
||||
replay_session_t* decoded;
|
||||
xce_core_event_t ev;
|
||||
int residx;
|
||||
float got;
|
||||
int rc;
|
||||
int push_rc;
|
||||
|
||||
fixture_init(&sim);
|
||||
|
||||
residx = register_slots_resource(&(sim.core));
|
||||
w = add_worker(&(sim.core), "worker1", residx, 10.0f);
|
||||
|
||||
/* PEND-state job, no known allocation yet -- exactly task 1.1's own
|
||||
* widened trigger shape: its recorded DISPATCH decision has been found
|
||||
* this round, but its RUN-state confirmation has not been replayed
|
||||
* yet. */
|
||||
/* PEND-state job, no known allocation yet -- its recorded DISPATCH
|
||||
* decision is about to be found this round. */
|
||||
job = add_job(&(sim.core), 7030ULL, JOB_STAT_PEND, NULL);
|
||||
TEST_ASSERT_NULL(job->accounts);
|
||||
TEST_ASSERT_NULL(job->allocs);
|
||||
|
|
@ -1088,49 +1085,21 @@ test_captured_pend_job_excluded_from_resinuse_until_run(void)
|
|||
|
||||
TEST_ASSERT_EQUAL_PTR_MESSAGE(
|
||||
jalloc, job->allocs,
|
||||
"sanity: the PEND job's recorded DISPATCH decision must be captured "
|
||||
"(task 1.1's own widened gate) before this test's own checkpoints "
|
||||
"mean anything");
|
||||
"the job's recorded DISPATCH decision must be captured before this "
|
||||
"test's own checkpoints mean anything");
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
job->accounts, "sanity: job->accounts must be built from the "
|
||||
"captured allocation even though the job is still "
|
||||
"PEND");
|
||||
job->accounts,
|
||||
"job->accounts must be built from the captured allocation");
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
JOB_STAT_PEND == job->state,
|
||||
"sanity: the job must still genuinely be PEND at this checkpoint -- "
|
||||
"otherwise the next assertion (resInUse == 0) would be trivially "
|
||||
"true for the wrong reason");
|
||||
JOB_STAT_RUN == job->state,
|
||||
"Requirement 1.2 (2026-09-11 revision): capturing a job's dispatch "
|
||||
"decision must ALSO transition it to RUN synchronously, mirroring "
|
||||
"scheduler.c's sched_commit_decision() (bug-1624) -- no separate "
|
||||
"job->state write should be needed here");
|
||||
|
||||
/* Checkpoint 1 (Requirement 1.2): recompute resInUse from scratch via
|
||||
* the REAL sched_account_jobs() -- a captured-but-PEND job must
|
||||
* contribute nothing. */
|
||||
rc = sched_account_jobs(&(sim.core));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
got = -1.0f;
|
||||
rc = get_resvals(&(w->resInUse), 1, &residx, &got);
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
|
||||
0.0f, got,
|
||||
"Requirement 1.2: a job whose allocation was captured early (while "
|
||||
"still PEND) must contribute ZERO to its worker's resInUse until it "
|
||||
"genuinely transitions to RUN -- sched_account_jobs()'s own "
|
||||
"JOB_IS_RUN gate (scheduler.c, unchanged by this feature) is the "
|
||||
"sole mechanism deferring this, and must still be effective now "
|
||||
"that capture happens earlier than before");
|
||||
|
||||
/* Transition the SAME job to RUN -- direct field write, same white-box
|
||||
* technique test_sim_contention_parity.c's own capacity_aware_
|
||||
* allocator_thread_fn() uses to model a worker-confirmed state change
|
||||
* without driving the full plugin_job_state_change() machinery (out of
|
||||
* this file's own boundary -- scheduler.c is not white-box included). */
|
||||
job->state = JOB_STAT_RUN;
|
||||
|
||||
/* Checkpoint 2 (Requirement 1.3): recompute resInUse again -- the SAME
|
||||
* captured allocation (jalloc, never re-captured -- sim_reconcile_
|
||||
* unaccounted_jobs() is not called a second time here, proving this
|
||||
* checkpoint's nonzero value comes from the EARLIER capture, not a
|
||||
* fresh one) must now be charged. */
|
||||
/* Checkpoint 1 (Requirement 1.3, 2026-09-11 revision): recompute
|
||||
* resInUse via the REAL sched_account_jobs() -- the SAME round as
|
||||
* capture must ALREADY reflect the charge, not a later one. */
|
||||
rc = sched_account_jobs(&(sim.core));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
|
|
@ -1139,14 +1108,65 @@ test_captured_pend_job_excluded_from_resinuse_until_run(void)
|
|||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
|
||||
4.0f, got,
|
||||
"Requirement 1.3: once the captured-early job genuinely transitions "
|
||||
"to RUN, its worker's resInUse must reflect its allocation from "
|
||||
"that point forward -- using the EXACT allocation amount (4.0) "
|
||||
"captured earlier by sim_reconcile_unaccounted_jobs(), not a "
|
||||
"freshly re-captured or re-typed value");
|
||||
"Requirement 1.3 (2026-09-11 revision): a captured job's resInUse "
|
||||
"charge must appear the SAME round its dispatch decision is "
|
||||
"captured -- sched_account_jobs()'s unchanged JOB_IS_RUN gate now "
|
||||
"sees the job as RUN immediately, since capture itself sets "
|
||||
"job->state = JOB_STAT_RUN synchronously");
|
||||
|
||||
/* Checkpoint 2 (Requirement 1.4, new 2026-09-11): a LATER, separately-
|
||||
* recorded JOB_STATE_CHANGE-to-RUN event for the SAME job (the
|
||||
* historical worker-confirmation signal, still present in a real
|
||||
* recording and still replayed) must be a safe, idempotent
|
||||
* re-affirmation -- no double-charge, no crash, allocs/accounts
|
||||
* untouched (since !JOB_IS_RUN(new_state) is false for RUN, the
|
||||
* handler's cleanup branch is skipped). Drives the REAL
|
||||
* XCE_CORE_EVENT_JOB_STATE_CHANGE handler (core_events.c), same
|
||||
* technique test_reconcile_then_release_via_job_state_change_is_asan_
|
||||
* clean() above already establishes. */
|
||||
memset(&ev, 0, sizeof(ev));
|
||||
ev.type = XCE_CORE_EVENT_JOB_STATE_CHANGE;
|
||||
ev.u.job_state_change.jobid = 7030ULL;
|
||||
ev.u.job_state_change.old_state = JOB_STAT_RUN;
|
||||
ev.u.job_state_change.new_state = JOB_STAT_RUN;
|
||||
|
||||
push_rc = xce_core_push_events(&(sim.core), &ev, 1);
|
||||
TEST_ASSERT_EQUAL_INT(0, push_rc);
|
||||
|
||||
TEST_ASSERT_EQUAL_PTR_MESSAGE(
|
||||
jalloc, job->allocs,
|
||||
"Requirement 1.4: a redundant RUN->RUN confirmation event must not "
|
||||
"touch the already-captured allocation");
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
job->accounts,
|
||||
"Requirement 1.4: a redundant RUN->RUN confirmation event must not "
|
||||
"free the already-captured accounts");
|
||||
|
||||
/* core_reset_workers() (core_session.c, white-box #include'd above)
|
||||
* mirrors the REAL per-round sequence sched_sim_driver_run() always
|
||||
* uses (xce_core_begin_session() zeroes resInUse -> sched_account_jobs()
|
||||
* re-sums it from scratch) -- production NEVER calls sched_account_jobs()
|
||||
* a second time without this reset in between, so this call is what
|
||||
* makes the following recompute a faithful round-4-boundary proof
|
||||
* instead of an artificial double-charge test-methodology artifact. */
|
||||
core_reset_workers(&(sim.core));
|
||||
|
||||
rc = sched_account_jobs(&(sim.core));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
got = -1.0f;
|
||||
rc = get_resvals(&(w->resInUse), 1, &residx, &got);
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
|
||||
4.0f, got,
|
||||
"Requirement 1.4: after a proper reset-then-resum round boundary, "
|
||||
"the redundant RUN->RUN confirmation event must NOT have changed "
|
||||
"what gets charged -- still exactly 4.0, the same single "
|
||||
"allocation, not fabricated or duplicated");
|
||||
|
||||
printf(
|
||||
"test_captured_pend_job_excluded_from_resinuse_until_run: PASS\n");
|
||||
"test_captured_job_included_in_resinuse_same_round_as_capture: "
|
||||
"PASS\n");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
|
@ -2121,7 +2141,7 @@ int main(void)
|
|||
RUN_TEST(
|
||||
test_reconcile_reports_reconstructed_decision_through_existing_channel);
|
||||
RUN_TEST(test_reconcile_null_args_are_a_safe_noop);
|
||||
RUN_TEST(test_captured_pend_job_excluded_from_resinuse_until_run);
|
||||
RUN_TEST(test_captured_job_included_in_resinuse_same_round_as_capture);
|
||||
RUN_TEST(
|
||||
test_reconcile_still_skips_already_captured_job_regardless_of_state);
|
||||
RUN_TEST(test_reconcile_no_match_leaves_pend_job_uncaptured_and_retries);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue