fix(scheduler): a failed COMPOUND leaves no partial allocation (bug-1766)

When a later primary of a COMPOUND failed, sched_make_compound_alloc_
still moved the earlier primaries' merged allocation onto the schedobj,
so a job decided PEND carried a partial allocation that the next
successful round overwrote and leaked. The merged allocation is now
published only when every primary succeeds and freed otherwise. No
resource-state rollback is needed: the primaries' uncommitted debits live
in ctx.uncomitted, per-attempt scratch freed by sched_ctx_fini(), and
schedobj->alloc's only reader is the dispatch path. The ALTER loop's
per-alternative cleanup (added with bug-1769) is now redundant and removed.

Test: "1*{..} + 5*{..}" on 2x2 slots PENDs with schedobj->alloc NULL
(held the first primary's allocation before). Scheduler suite 87/87.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-09-25 13:27:08 +00:00
parent b63bb581d4
commit 601b245e5a
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
2 changed files with 39 additions and 7 deletions

View file

@ -315,8 +315,17 @@ static int sched_make_compound_alloc_(xce_sched_core_t* core,
}
}
/* bug-1766: a compound is allocated only if EVERY primary is -- when a
* later primary fails, drop the earlier primaries' allocation instead
* of leaving a partial one on obj. Their uncommitted debits live in
* ctx.uncomitted, per-attempt scratch freed by sched_ctx_fini(), so
* nothing else needs undoing. */
if (merged != NULL) {
MOVE_PTR(obj->alloc, merged);
if (rc > 0) {
MOVE_PTR(obj->alloc, merged);
} else {
free_JobAlloc_ptr(&merged);
}
}
if (ctx.reason != NULL) {
@ -356,12 +365,6 @@ static int sched_make_alter_allocation(xce_sched_core_t* core,
if (rc > 0) {
return rc;
}
/* a failed alternative may leave a partial allocation (no
* rollback yet, bug-1766) -- drop it before trying the next one */
if (obj->alloc != NULL) {
free_JobAlloc_ptr(&(obj->alloc));
}
}
/* FIXME: merge pend reason from all children */

View file

@ -847,6 +847,34 @@ static void test_compound_primaries_on_same_host_merge(void)
"entry");
}
/*
* bug-1766: when a later primary of a COMPOUND fails, the earlier
* primaries' (merged) allocation was still moved onto the schedobj, so a
* job reported PEND carried a partial allocation that the next successful
* round overwrote and leaked. A failed compound must leave no allocation
* (its uncommitted debits are per-attempt scratch, freed with the ctx).
*/
static void test_failed_compound_leaves_no_partial_allocation(void)
{
xce_scheduler_t* sched = start_sched();
xce_decision_t* decision;
xce_sched_job_t* sjob;
e2e_push_queue_and_workers(sched, "cmp4_queue");
decision = e2e_dispatch_resreq(sched, JOB_ID + 7, "cmp4_queue",
"1*{rusage[mem=1]} + 5*{rusage[mem=1]}");
TEST_ASSERT_NOT_NULL(decision);
TEST_ASSERT_EQUAL_INT_MESSAGE(XCE_DECISION_PEND, decision->op,
"the 5-task primary cannot fit on 4 slots");
sjob = sched_get_job(&(sched->core), JOB_ID + 7);
TEST_ASSERT_NOT_NULL(sjob);
TEST_ASSERT_NULL_MESSAGE(sjob->schedobj->alloc,
"a failed compound must not keep the first primary's partial "
"allocation");
TEST_ASSERT_NULL(sjob->allocs);
}
int main(void)
{
UNITY_BEGIN();
@ -858,5 +886,6 @@ int main(void)
RUN_TEST(test_alter_falls_back_to_alternative_that_fits);
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);
return UNITY_END();
}