diff --git a/src/scheduler/src/plugin.c b/src/scheduler/src/plugin.c index 3d17be39..f9c2721b 100644 --- a/src/scheduler/src/plugin.c +++ b/src/scheduler/src/plugin.c @@ -615,8 +615,15 @@ int plugin_prepare_allocation(xce_sched_core_t* core, xce_sched_ctx_t* ctx) } 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_init(&(ctx->alloc_req.shared), 8); + xvec_idval_fini(&(ctx->alloc_req.host)); + xvec_idval_init(&(ctx->alloc_req.host), 8); } else { xvec_idval_fini(&(ctx->alloc_req.host)); xvec_idval_init(&(ctx->alloc_req.host), 8); diff --git a/src/scheduler/testsuite/test_parjob_e2e_multislot.c b/src/scheduler/testsuite/test_parjob_e2e_multislot.c index d438d84b..1c2c49ea 100644 --- a/src/scheduler/testsuite/test_parjob_e2e_multislot.c +++ b/src/scheduler/testsuite/test_parjob_e2e_multislot.c @@ -875,6 +875,56 @@ static void test_failed_compound_leaves_no_partial_allocation(void) 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) { UNITY_BEGIN(); @@ -887,5 +937,6 @@ int main(void) RUN_TEST(test_compound_allocation_includes_every_primary); RUN_TEST(test_compound_primaries_on_same_host_merge); RUN_TEST(test_failed_compound_leaves_no_partial_allocation); + RUN_TEST(test_compound_later_seqjob_primary_ignores_previous_want); return UNITY_END(); }