scheduler tests: UT cover for E2 Phase 3 increments (adoption shared allocs + per-queue instances)
Two gaps in the Phase 3 regression net:
1. test_core_events.c: test_job_update_run_adoption_carries_shared_allocs
-- scheduler-side counterpart of bug-1976. A JOB_UPDATE marking a job
RUN whose jalloc carries shared-pool records (the per-project limit
charge + the external resource itself) must rebuild job->accounts
with accounts_shared mirroring them, or the next sched_account_jobs
pass never charges the limit vres and every global limit admits
forever. Also pins the payload ownership move (updated.allocs == NULL).
2. test_limit_project_scope.c: test_queue_scope_per_queue_instance_names
-- the per_queue shape of limits.yaml's extlic_qdist: queues:-scoped
limit slices its usage instance per queue ("limit=<name>/q=<queue>")
exactly as per_project slices by project ("/p="); cap lands at the
instance's own vres idx; out-of-scope queues match nothing. Uses the
same heap group-config lifetime contract as the project shape, so
free_limit_config() gained queues handling (and NULL-guarded the
projects group -- per_queue configs carry projects == NULL, which the
unconditional dereference crashed on; deterministic under
MALLOC_PERTURB_, every run).
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
13a28a387a
commit
700f3d4e1a
2 changed files with 196 additions and 2 deletions
|
|
@ -900,6 +900,99 @@ static void test_job_update_run_adopts_controller_allocation(void)
|
||||||
"ownership of the allocation moves to the scheduler's job");
|
"ownership of the allocation moves to the scheduler's job");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* bug-1976 companion (external-license-resources Phase 3): the controller's
|
||||||
|
* RUN adoption is the scheduler's ONLY view of a dispatch (the controller is
|
||||||
|
* ground truth), so the adopted wire alloc must carry the shared-pool
|
||||||
|
* records too -- the limit vres charge among them (allocs_shared; the
|
||||||
|
* controller-side drop was bug-1976 itself, fixed in dispatch_a_job_()).
|
||||||
|
* Without them make_job_account() builds accounts_shared == NULL and
|
||||||
|
* sched_account_jobs() never charges the vres: sharedInUse stays 0.0 and
|
||||||
|
* unsatisfy_'s non-EXTERNAL branch (total >= inuse + uncomit + wanted)
|
||||||
|
* admits past every global limit forever. This pins the scheduler side of
|
||||||
|
* the round trip: an adoption whose wire alloc HAS shared records must
|
||||||
|
* produce accounts whose accounts_shared mirror them. */
|
||||||
|
static void test_job_update_run_adoption_carries_shared_allocs(void)
|
||||||
|
{
|
||||||
|
xce_sched_core_t core;
|
||||||
|
xce_core_event_t event;
|
||||||
|
xce_job_t* job1;
|
||||||
|
xce_job_t updated;
|
||||||
|
xce_sched_job_t* job;
|
||||||
|
xce_alloc_t* a_slots;
|
||||||
|
xce_host_alloc_t* h0;
|
||||||
|
xce_host_alloc_t* hallocs[1];
|
||||||
|
xce_job_alloc_t* jalloc;
|
||||||
|
xce_alloc_t** shared;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
fixture_init(&core, &g_full_plugin);
|
||||||
|
|
||||||
|
job1 = make_job(71, JOB_STAT_PEND, "normal", "alice", "projA");
|
||||||
|
|
||||||
|
memset(&event, 0, sizeof(event));
|
||||||
|
event.type = XCE_CORE_EVENT_JOBS_PUSH;
|
||||||
|
event.u.jobs_push.njobs = 1;
|
||||||
|
event.u.jobs_push.jobs = &job1;
|
||||||
|
rc = xce_core_push_events(&core, &event, 1);
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||||
|
|
||||||
|
job = sched_get_job(&core, 71);
|
||||||
|
TEST_ASSERT_NOT_NULL(job);
|
||||||
|
|
||||||
|
/* the exact shape the controller re-sends post-fix: per-host slots +
|
||||||
|
* two shared records -- the limit vres instance charge and the
|
||||||
|
* underlying EXTERNAL pool draw. */
|
||||||
|
a_slots = make_alloc((char*)"slots", NULL, 0, 1.0f, 0);
|
||||||
|
h0 = make_host_alloc((char*)"hostC", 1, make_allocs(1, a_slots));
|
||||||
|
hallocs[0] = h0;
|
||||||
|
jalloc = make_job_alloc(71, 1, make_host_allocs(1, hallocs[0]));
|
||||||
|
|
||||||
|
shared = make_allocs(2,
|
||||||
|
make_alloc((char*)"limit=extlic_share/p=projA", NULL, 0, 1.0f, 0),
|
||||||
|
make_alloc((char*)"extlic", NULL, 0, 1.0f, 0));
|
||||||
|
TEST_ASSERT_NOT_NULL(shared);
|
||||||
|
jalloc->allocs_shared = shared;
|
||||||
|
jalloc->nres_shared = 2;
|
||||||
|
|
||||||
|
memset(&updated, 0, sizeof(updated));
|
||||||
|
updated.jobid = 71;
|
||||||
|
updated.state = JOB_STAT_RUN;
|
||||||
|
updated.allocs = jalloc;
|
||||||
|
|
||||||
|
memset(&event, 0, sizeof(event));
|
||||||
|
event.type = XCE_CORE_EVENT_JOB_UPDATE;
|
||||||
|
event.u.job_update.jobid = 71;
|
||||||
|
event.u.job_update.job = &updated;
|
||||||
|
rc = xce_core_push_events(&core, &event, 1);
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(JOB_STAT_RUN, job->state);
|
||||||
|
TEST_ASSERT_NOT_NULL(job->allocs);
|
||||||
|
|
||||||
|
/* the accounts the charge loop walks must mirror the shared records --
|
||||||
|
* this is the exact property bug-1976's controller drop destroyed */
|
||||||
|
TEST_ASSERT_NOT_NULL(job->accounts);
|
||||||
|
TEST_ASSERT_EQUAL_INT_MESSAGE(2, job->accounts->nres_shared,
|
||||||
|
"adoption must rebuild accounts WITH the shared records -- without "
|
||||||
|
"them sched_account_jobs() never charges the vres and every global "
|
||||||
|
"limit admits forever (bug-1976)");
|
||||||
|
TEST_ASSERT_NOT_NULL(job->accounts->accounts_shared);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("limit=extlic_share/p=projA",
|
||||||
|
job->accounts->accounts_shared[0]->resname);
|
||||||
|
TEST_ASSERT_EQUAL_FLOAT(1.0f,
|
||||||
|
job->accounts->accounts_shared[0]->reserved);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("extlic",
|
||||||
|
job->accounts->accounts_shared[1]->resname);
|
||||||
|
TEST_ASSERT_EQUAL_FLOAT(1.0f,
|
||||||
|
job->accounts->accounts_shared[1]->reserved);
|
||||||
|
|
||||||
|
/* allocation ownership moves; the job's own records survive intact */
|
||||||
|
TEST_ASSERT_NULL(updated.allocs);
|
||||||
|
TEST_ASSERT_EQUAL_INT(2, job->allocs->nres_shared);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("limit=extlic_share/p=projA",
|
||||||
|
job->allocs->allocs_shared[0]->resname);
|
||||||
|
}
|
||||||
|
|
||||||
/* Companion: a resync claiming a genuinely TERMINAL state (DONE/EXIT) for a
|
/* Companion: a resync claiming a genuinely TERMINAL state (DONE/EXIT) for a
|
||||||
* RUN-with-real-allocation job must still apply -- this is the legitimate
|
* RUN-with-real-allocation job must still apply -- this is the legitimate
|
||||||
* "job finished while this scheduler was disconnected" case bug-1734's own
|
* "job finished while this scheduler was disconnected" case bug-1734's own
|
||||||
|
|
@ -2147,6 +2240,7 @@ int main(void)
|
||||||
RUN_TEST(test_jobs_push_duplicate_jobid_run_state_transfers_allocation);
|
RUN_TEST(test_jobs_push_duplicate_jobid_run_state_transfers_allocation);
|
||||||
RUN_TEST(test_jobs_push_duplicate_jobid_controller_pend_wins_over_local_run);
|
RUN_TEST(test_jobs_push_duplicate_jobid_controller_pend_wins_over_local_run);
|
||||||
RUN_TEST(test_job_update_run_adopts_controller_allocation);
|
RUN_TEST(test_job_update_run_adopts_controller_allocation);
|
||||||
|
RUN_TEST(test_job_update_run_adoption_carries_shared_allocs);
|
||||||
RUN_TEST(test_jobs_push_duplicate_jobid_terminal_resync_releases_real_allocation);
|
RUN_TEST(test_jobs_push_duplicate_jobid_terminal_resync_releases_real_allocation);
|
||||||
RUN_TEST(test_jobs_push_duplicate_jobid_run_state_without_allocation_is_refused);
|
RUN_TEST(test_jobs_push_duplicate_jobid_run_state_without_allocation_is_refused);
|
||||||
RUN_TEST(test_jobs_push_duplicate_jobid_does_not_regress_terminal_state);
|
RUN_TEST(test_jobs_push_duplicate_jobid_does_not_regress_terminal_state);
|
||||||
|
|
|
||||||
|
|
@ -270,8 +270,16 @@ static xce_limit_config_t* make_project_limit(const char* name,
|
||||||
static void free_limit_config(xce_limit_config_t* cfg)
|
static void free_limit_config(xce_limit_config_t* cfg)
|
||||||
{
|
{
|
||||||
FREEUP(cfg->val);
|
FREEUP(cfg->val);
|
||||||
FREEUP(cfg->projects->members.data);
|
/* per_queue configs carry the scope in queues and NULL projects (the
|
||||||
FREEUP(cfg->projects);
|
* per_project shape is the mirror image) -- either group is optional */
|
||||||
|
if (cfg->projects != NULL) {
|
||||||
|
FREEUP(cfg->projects->members.data);
|
||||||
|
FREEUP(cfg->projects);
|
||||||
|
}
|
||||||
|
if (cfg->queues != NULL) {
|
||||||
|
FREEUP(cfg->queues->members.data);
|
||||||
|
FREEUP(cfg->queues);
|
||||||
|
}
|
||||||
FREEUP(cfg);
|
FREEUP(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -462,6 +470,97 @@ static void test_cap_value_and_per_project_instance_names(void)
|
||||||
free_limit_config(limits[1]);
|
free_limit_config(limits[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* 2b. The per-queue distribution shape (limits.yaml's extlic_qdist): a
|
||||||
|
* queues:-scoped limit with per_queue slices its usage instance by queue
|
||||||
|
* ("limit=<name>/q=<queue>") exactly as the projects shape slices by
|
||||||
|
* project ("/p=") -- how a site partitions one license pool across
|
||||||
|
* queues. Also pins that the queue scope does NOT match a project-only
|
||||||
|
* identity: the consumer matched is the obj's queue, so a different
|
||||||
|
* queue outside the members list matches nothing. */
|
||||||
|
static void test_queue_scope_per_queue_instance_names(void)
|
||||||
|
{
|
||||||
|
xce_scheduler_t sched;
|
||||||
|
xce_sched_plugin_t plugin;
|
||||||
|
xce_sched_obj_t obj;
|
||||||
|
xce_limit_config_t* limits[1];
|
||||||
|
xce_limit_config_t* cfg;
|
||||||
|
xce_group_config_t* qg;
|
||||||
|
xce_vres_t* vres;
|
||||||
|
const char* const scope_a[] = { "projA" };
|
||||||
|
const char* const queues[] = { "normal", "night" };
|
||||||
|
char** qmembers;
|
||||||
|
char name[64];
|
||||||
|
int rc;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
fixture_init(&sched, &plugin);
|
||||||
|
|
||||||
|
register_resource(&(sched.core), "extlic",
|
||||||
|
XCE_RESF_SHARED | XCE_RESF_GLOBAL | XCE_RESF_EXTERNAL);
|
||||||
|
|
||||||
|
limits[0] = make_project_limit("extlic_qdist", "extlic", 4.0f, scope_a,
|
||||||
|
1);
|
||||||
|
cfg = limits[0];
|
||||||
|
FREEUP(cfg->projects->members.data);
|
||||||
|
FREEUP(cfg->projects);
|
||||||
|
cfg->projects = NULL;
|
||||||
|
cfg->per_queue = 1;
|
||||||
|
|
||||||
|
/* queue scope: heap group config, same lifetime contract as the
|
||||||
|
* project one make_project_limit() builds */
|
||||||
|
qg = (xce_group_config_t*)calloc(1, sizeof(*qg));
|
||||||
|
qmembers = (char**)calloc(2, sizeof(char*));
|
||||||
|
TEST_ASSERT_NOT_NULL(qg);
|
||||||
|
TEST_ASSERT_NOT_NULL(qmembers);
|
||||||
|
for (i = 0; i < 2; i++) {
|
||||||
|
qmembers[i] = (char*)queues[i];
|
||||||
|
}
|
||||||
|
qg->members.count = 2;
|
||||||
|
qg->members.data = qmembers;
|
||||||
|
cfg->queues = qg;
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, push_limits(&plugin, limits, 1));
|
||||||
|
|
||||||
|
/* each in-scope queue gets its OWN /q= instance */
|
||||||
|
make_obj(&obj, "projA");
|
||||||
|
obj.queue = makeString("normal");
|
||||||
|
rc = match_global(&plugin, &obj, find_limit(&plugin, "extlic_qdist"));
|
||||||
|
TEST_ASSERT_GREATER_OR_EQUAL_INT(0, rc);
|
||||||
|
|
||||||
|
snprintf(name, sizeof(name), "limit=extlic_qdist/q=normal");
|
||||||
|
vres = find_instance(&plugin, name);
|
||||||
|
TEST_ASSERT_NOT_NULL_MESSAGE(vres,
|
||||||
|
"per_queue must slice the usage instance by queue (/q= segment), "
|
||||||
|
"the queue analogue of the /p= project shape");
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, obj.glimits.count);
|
||||||
|
TEST_ASSERT_EQUAL_PTR(vres, xlist_get(&(obj.glimits), 0));
|
||||||
|
|
||||||
|
/* cap 4.0 written at this instance's own vres idx */
|
||||||
|
{
|
||||||
|
xce_val_t* v = (xce_val_t*)zarray_get(&(sched.core.sharedInst),
|
||||||
|
vres->vres->idx);
|
||||||
|
TEST_ASSERT_NOT_NULL(v);
|
||||||
|
TEST_ASSERT_EQUAL_FLOAT(4.0f, v->u.fvalue);
|
||||||
|
}
|
||||||
|
|
||||||
|
make_obj(&obj, "projA");
|
||||||
|
obj.queue = makeString("night");
|
||||||
|
rc = match_global(&plugin, &obj, find_limit(&plugin, "extlic_qdist"));
|
||||||
|
TEST_ASSERT_GREATER_OR_EQUAL_INT(0, rc);
|
||||||
|
snprintf(name, sizeof(name), "limit=extlic_qdist/q=night");
|
||||||
|
TEST_ASSERT_NOT_NULL(find_instance(&plugin, name));
|
||||||
|
|
||||||
|
/* out-of-scope queue matches nothing */
|
||||||
|
make_obj(&obj, "projA");
|
||||||
|
obj.queue = makeString("short");
|
||||||
|
rc = match_global(&plugin, &obj, find_limit(&plugin, "extlic_qdist"));
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, obj.glimits.count);
|
||||||
|
|
||||||
|
free_limit_config(limits[0]);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------- */
|
/* ---------------------------------------------------------------------- */
|
||||||
/* 3. The unknown-underlying-resource failure is a clean -ENOENT: a limit
|
/* 3. The unknown-underlying-resource failure is a clean -ENOENT: a limit
|
||||||
* naming a resource the core never registered must not crash and must
|
* naming a resource the core never registered must not crash and must
|
||||||
|
|
@ -502,6 +601,7 @@ int main(void)
|
||||||
UNITY_BEGIN();
|
UNITY_BEGIN();
|
||||||
RUN_TEST(test_project_scope_match_and_instantiate);
|
RUN_TEST(test_project_scope_match_and_instantiate);
|
||||||
RUN_TEST(test_cap_value_and_per_project_instance_names);
|
RUN_TEST(test_cap_value_and_per_project_instance_names);
|
||||||
|
RUN_TEST(test_queue_scope_per_queue_instance_names);
|
||||||
RUN_TEST(test_unknown_underlying_resource_fails_clean);
|
RUN_TEST(test_unknown_underlying_resource_fails_clean);
|
||||||
rc = UNITY_END();
|
rc = UNITY_END();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue