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");
|
||||
}
|
||||
|
||||
/* 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
|
||||
* RUN-with-real-allocation job must still apply -- this is the legitimate
|
||||
* "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_controller_pend_wins_over_local_run);
|
||||
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_run_state_without_allocation_is_refused);
|
||||
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)
|
||||
{
|
||||
FREEUP(cfg->val);
|
||||
FREEUP(cfg->projects->members.data);
|
||||
FREEUP(cfg->projects);
|
||||
/* per_queue configs carry the scope in queues and NULL projects (the
|
||||
* 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);
|
||||
}
|
||||
|
||||
|
|
@ -462,6 +470,97 @@ static void test_cap_value_and_per_project_instance_names(void)
|
|||
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
|
||||
* naming a resource the core never registered must not crash and must
|
||||
|
|
@ -502,6 +601,7 @@ int main(void)
|
|||
UNITY_BEGIN();
|
||||
RUN_TEST(test_project_scope_match_and_instantiate);
|
||||
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);
|
||||
rc = UNITY_END();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue