fix(scheduler): reset the per-host want at each primary's job-level prepare pass (bug-1883)

A COMPOUND/ALTER job's primaries share one ctx. The job-level prepare pass
reset only alloc_req.shared, and the allocators append their slots want
into alloc_req.host, so a later primary's host pre-filter (and a global
limit on slots) read the previous primary's last per-host want: a 1-task
primary after a 4-task one was filtered on slots:4 and pended although a
host with its one free slot existed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-09-29 08:09:27 +00:00
parent 61f948e601
commit c10f32ccd8
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
2 changed files with 58 additions and 0 deletions

View file

@ -615,8 +615,15 @@ int plugin_prepare_allocation(xce_sched_core_t* core, xce_sched_ctx_t* ctx)
} }
if (ctx->worker == NULL) { if (ctx->worker == NULL) {
/* bug-1883: the job-level pass also resets .host. The allocators'
* hooks store this primary's job-level slots want there (the host
* pre-filter and a global limit on slots read it); a COMPOUND's
* previous primary left its last per-host want in .host, which the
* append-only store would otherwise extend, not replace. */
xvec_idval_fini(&(ctx->alloc_req.shared)); xvec_idval_fini(&(ctx->alloc_req.shared));
xvec_idval_init(&(ctx->alloc_req.shared), 8); xvec_idval_init(&(ctx->alloc_req.shared), 8);
xvec_idval_fini(&(ctx->alloc_req.host));
xvec_idval_init(&(ctx->alloc_req.host), 8);
} else { } else {
xvec_idval_fini(&(ctx->alloc_req.host)); xvec_idval_fini(&(ctx->alloc_req.host));
xvec_idval_init(&(ctx->alloc_req.host), 8); xvec_idval_init(&(ctx->alloc_req.host), 8);

View file

@ -875,6 +875,56 @@ static void test_failed_compound_leaves_no_partial_allocation(void)
TEST_ASSERT_NULL(sjob->allocs); TEST_ASSERT_NULL(sjob->allocs);
} }
/*
* bug-1883: a COMPOUND's primaries share one ctx, and the job-level prepare
* pass reset only alloc_req.shared, so a later 1-task (seqjob) primary's
* host pre-filter ran against the previous primary's per-host want with
* slots:1 appended. Host A (4 slots) takes the 4-task primary, leaving
* slots:4 behind; host B (1 slot) fits the 1-task primary, but the stale
* slots:4 rejected it and the job pended.
*/
static void test_compound_later_seqjob_primary_ignores_previous_want(void)
{
xce_scheduler_t* sched = start_sched();
xce_core_event_t ev;
xce_worker_t* worker_ptrs[2];
xce_decision_t* decision;
int i;
memset(&ev, 0, sizeof(ev));
ev.type = XCE_CORE_EVENT_QUEUE_UPDATE;
ev.u.queue_update.queue = make_wire_queue("cmp5_queue", 1);
TEST_ASSERT_TRUE(xce_core_push_events(&(sched->core), &ev, 1) >= 0);
worker_ptrs[0] = make_wire_worker(WORKER_A_NAME, 4.0f);
worker_ptrs[1] = make_wire_worker(WORKER_B_NAME, 1.0f);
memset(&ev, 0, sizeof(ev));
ev.type = XCE_CORE_EVENT_WORKERS_PUSH;
ev.u.workers_push.nworkers = 2;
ev.u.workers_push.workers = worker_ptrs;
TEST_ASSERT_TRUE(xce_core_push_events(&(sched->core), &ev, 1) >= 0);
decision = e2e_dispatch_resreq(sched, JOB_ID + 8, "cmp5_queue",
"4*{rusage[mem=1]} + 1*{rusage[mem=1]}");
TEST_ASSERT_NOT_NULL(decision);
TEST_ASSERT_EQUAL_INT_MESSAGE(XCE_DECISION_DISPATCH, decision->op,
"the 1-task primary fits host B's free slot -- it must not be "
"filtered on the 4-task primary's per-host want");
TEST_ASSERT_EQUAL_FLOAT(5.0f,
(float)e2e_alloc_total_slots(decision->u.alloc));
TEST_ASSERT_EQUAL_INT(2, decision->u.alloc->nhosts);
for (i = 0; i < decision->u.alloc->nhosts; i++) {
xce_job_alloc_t one = *(decision->u.alloc);
one.nhosts = 1;
one.allocs = &(decision->u.alloc->allocs[i]);
TEST_ASSERT_EQUAL_FLOAT(
(strcmp(decision->u.alloc->allocs[i]->host, WORKER_A_NAME) == 0)
? 4.0f : 1.0f,
(float)e2e_alloc_total_slots(&one));
}
}
int main(void) int main(void)
{ {
UNITY_BEGIN(); UNITY_BEGIN();
@ -887,5 +937,6 @@ int main(void)
RUN_TEST(test_compound_allocation_includes_every_primary); RUN_TEST(test_compound_allocation_includes_every_primary);
RUN_TEST(test_compound_primaries_on_same_host_merge); RUN_TEST(test_compound_primaries_on_same_host_merge);
RUN_TEST(test_failed_compound_leaves_no_partial_allocation); RUN_TEST(test_failed_compound_leaves_no_partial_allocation);
RUN_TEST(test_compound_later_seqjob_primary_ignores_previous_want);
return UNITY_END(); return UNITY_END();
} }