test(scheduler-allocation): prove parjob's shared resource commits once

Task 4.3's multi-host merge itself was already implemented and verified
by task 4.2. The one unproven clause -- job-level shared-resource
allocation computed once regardless of host count -- is now covered by
test_shared_resource_debited_once_regardless_of_host_count, using a
PER_JOB (not PER_TASK) global resource so a per-bucket-repeated
computation can't coincidentally sum to the correct total. Mutation
testing (a deliberately injected duplicate per-host shared debit)
confirms the test fails exactly as expected; production code required
no change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
This commit is contained in:
Rongsong Shen 2026-09-04 21:53:12 +00:00
parent 3d5e1c32c0
commit 9d23060b76
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
2 changed files with 173 additions and 1 deletions

View file

@ -66,7 +66,7 @@
- Observable: a passing test suite covers multi-bucket success, the descending-vs-ascending worked example, and partial-failure-then-reversal
- _Requirements: 4.2, 4.4, 4.5, 4.6, 4.7_
- _Depends: 3.1_
- [ ] 4.3 Implement parjob's multi-host allocation commit
- [x] 4.3 Implement parjob's multi-host allocation commit
- Merge every claimed bucket's host allocation into a single job allocation record spanning all claimed hosts, leaving job-level shared-resource allocation computed once and unaffected by how many hosts were used
- Observable: a successfully allocated multi-host job's allocation record correctly lists every claimed host with its own slot count
- _Requirements: 4.3_

View file

@ -114,6 +114,17 @@ int plugin_register(void* sched, void* plugin)
void plugin_unregister(void* arg) { (void)arg; }
/* g_job_level_prepare_calls - task 4.3 (Req 4 AC 7): counts how many times
* the fan-out stub immediately below is invoked with ctx->worker == NULL --
* the job-level shared/global-resource prepare pass. requirements.md's Req
* 4 AC 7 requires this pass to fire exactly once per primary resreq,
* independent of which plan/bucket-shape is later tried -- this counter
* lets test_shared_resource_debited_once_regardless_of_host_count() below
* confirm that dispatch-count claim dynamically (not just by inspection)
* across a real multi-bucket success. Each test that reads it resets it
* first, so counts never leak across tests. */
static int g_job_level_prepare_calls = 0;
/* Delegates to the real parjob_prepare_allocation() under test, standing
* in for plugin_prepare_allocation()'s (plugin.c) real fan-out across
* every registered plugin -- exactly one (parjob's own) in this file's
@ -124,6 +135,7 @@ int plugin_prepare_allocation(void* sched, xce_sched_ctx_t* ctx)
xce_sched_plugin_t fake_plugin;
if (ctx->worker == NULL) {
g_job_level_prepare_calls++;
xvec_idval_fini(&(ctx->alloc_req.shared));
xvec_idval_init(&(ctx->alloc_req.shared), 8);
} else {
@ -210,6 +222,28 @@ static xce_resource_t* register_slots_resource(var resmgr)
return makeResource(resmgr, &res);
}
/* register_global_resource - task 4.3 (Req 4 AC 7): like
* register_slots_resource() above, but flagged XCE_RESF_GLOBAL (xce/
* types.h) -- the scheduler's job-level/shared-resource pool
* (sched->core.sharedInst/sharedInUse), distinct from a per-host resource
* like "slots". resreq.c's calc_wanted_global_resvals()/
* calc_wanted_host_resvals() dispatch on exactly this flag: a GLOBAL
* resource is only ever computed by the ctx->worker == NULL job-level
* prepare pass, never by the per-host pass. makeResource()'s own
* makeInstance()->resobj_init() memcpy()s the whole xce_resource_t
* (including .flags) into the resmgr-owned copy, so setting it here is
* sufficient. */
static xce_resource_t* register_global_resource(var resmgr, const char* name)
{
xce_resource_t res;
memset(&res, 0, sizeof(res));
res.name = strdup(name);
res.flags = XCE_RESF_GLOBAL;
return makeResource(resmgr, &res);
}
/* add_worker_with_slots - a real sched_add_worker() (core.c) worker
* carrying `slots_total` free "slots" (idx == the resmgr-assigned index),
* identical to test_seqjob_allocator.c's own helper. */
@ -963,6 +997,143 @@ static void test_returns_zero_when_every_plan_is_exhausted(void)
sched_ctx_fini(&ctx);
}
/*
* Task 4.3, Req 4 AC 7's own previously-unverified clause: "leaving
* job-level shared-resource allocation computed once and unaffected by how
* many hosts were used." Task 4.2's fixtures only ever used a per-host
* ("slots") resource -- this test drives a job carrying BOTH a per-host
* resource AND a genuinely job-level/shared (XCE_RESF_GLOBAL) resource
* through a real multi-host my_alloc() success and asserts the shared
* resource is debited exactly ONCE at the job level, not once per claimed
* host/bucket.
*
* The shared rusage uses PER_JOB semantics deliberately, not PER_TASK:
* calc_wanted_global_resvals()'s PER_TASK case scales by ctx->nslots, and
* since a plan's bucket_slots[] always sum to maxTasks by construction, a
* (hypothetical, buggy) per-bucket-repeated PER_TASK computation would sum
* to the SAME total as a correct once-only computation -- masking exactly
* the defect this test exists to catch. PER_JOB's flat value has no such
* coincidence: calc_wanted_global_resvals()'s `case PER_JOB: val =
* v->value;` ignores nslots/nhosts entirely, so a mistaken per-bucket (or
* per-claimed-host) repeat of the job-level pass would total value*nbuckets
* instead of value.
*
* Fixture: 4 hosts, each with exactly 1 free "slots" -- forces k=1 ([4]),
* k=2 ([2,2]), and k=3 ([2,1,1]) to all fail (no single host, or any pair/
* triple, can supply a bucket needing >=2 slots) and k=4 ([1,1,1,1]) to be
* the only plan that succeeds, claiming ALL 4 hosts. This is a wider
* multi-host spread than task 4.2's existing 2-3 host tests, so this test
* also independently re-proves this task's OTHER Observable clause ("every
* claimed host with its own slot count") without relying on 4.2's own
* tests remaining unchanged.
*
* g_job_level_prepare_calls (the fan-out stub above) additionally proves,
* by direct dynamic count rather than code inspection alone, that the
* job-level (ctx->worker == NULL) prepare pass fires exactly once for this
* whole 4-host allocation attempt -- confirming sched_ctx_set_primary()'s
* single call site (schedobj.c), not my_alloc()'s plan/bucket loop, is what
* drives it.
*/
static void test_shared_resource_debited_once_regardless_of_host_count(void)
{
xce_scheduler_t sched;
xce_resource_t* slots_res;
xce_resource_t* license_res;
xce_sched_worker_t* hosts[4];
xce_sched_obj_t obj;
xce_primary_schedobj_t primary_obj;
xce_primary_resreq_t primary;
rusage_value_t license_rusage;
xce_sched_ctx_t ctx;
xvec_t* shared_uncomit = NULL;
int rc;
int i;
int idx[1];
float val[1];
char name[16];
memset(&sched, 0, sizeof(sched));
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core)));
slots_res = register_slots_resource(sched.core.resmgr);
TEST_ASSERT_NOT_NULL(slots_res);
license_res = register_global_resource(sched.core.resmgr, "licenses");
TEST_ASSERT_NOT_NULL(license_res);
/* Shared-pool total capacity for "licenses" -- far more than the 5.0
* PER_JOB want below, so the shared-satisfy check trivially passes and
* the only thing under test is HOW MANY TIMES / HOW MUCH gets debited,
* not whether capacity exists. sched->core.sharedInUse is left at its
* zarray_init() default (nothing put), i.e. 0 currently in use. */
idx[0] = license_res->idx;
val[0] = 100.0f;
put_resvals(&(sched.core.sharedInst), 1, idx, val);
for (i = 0; i < 4; i++) {
snprintf(name, sizeof(name), "host-%d", i);
hosts[i]
= add_worker_with_slots(&(sched.core), name, slots_res->idx, 1.0f);
}
memset(&primary, 0, sizeof(primary));
primary.minTasks = 1;
primary.maxTasks = 4;
primary.select = NULL;
memset(&license_rusage, 0, sizeof(license_rusage));
license_rusage.res = strdup("licenses");
license_rusage.value = 5.0f;
license_rusage.per_flag = PER_JOB;
xlist_init(&(primary.rusages), 4);
xlist_store(&(primary.rusages), &license_rusage);
init_primary_obj(&obj, &primary_obj, &primary);
g_job_level_prepare_calls = 0;
rc = drive_my_alloc(&sched, &obj, &primary, &ctx);
TEST_ASSERT_EQUAL_INT(1, rc);
TEST_ASSERT_NOT_NULL(ctx.alloc);
/* This task's OTHER Observable clause, re-proved independently of
* task 4.2's own tests: every claimed host with its own slot count. */
TEST_ASSERT_EQUAL_INT(4, ctx.alloc->nhosts);
for (i = 0; i < ctx.alloc->nhosts; i++) {
xce_host_alloc_t* ha = ctx.alloc->allocs[i];
TEST_ASSERT_NOT_NULL(ha);
TEST_ASSERT_EQUAL_INT(1, ha->nallocs); /* "slots" only */
}
for (i = 0; i < 4; i++) {
assert_uncommitted_host_debit(&ctx, hosts[i], slots_res->idx, 1.0f);
}
/* THE dispatch-count claim, confirmed dynamically: the job-level
* shared/global-resource prepare pass fired exactly once for this
* whole 4-host allocation attempt -- not once per claimed bucket/host. */
TEST_ASSERT_EQUAL_INT(1, g_job_level_prepare_calls);
/* THE property under test: the committed shared debit is the flat
* PER_JOB value exactly once (5.0) -- not multiplied by nhosts/
* nbuckets (which a per-bucket-repeated commit would make 20.0). */
rc = sched_ctx_get_uncomit_shared(&shared_uncomit, &ctx);
TEST_ASSERT_EQUAL_INT(1, rc);
TEST_ASSERT_NOT_NULL(shared_uncomit);
TEST_ASSERT_EQUAL_FLOAT(5.0f,
((float*)shared_uncomit->data)[license_res->idx]);
/* The final committed allocation record's own shared-resource fields
* mirror the same once-only amount, unaffected by nhosts. */
TEST_ASSERT_EQUAL_INT(1, ctx.alloc->nres_shared);
TEST_ASSERT_NOT_NULL(ctx.alloc->allocs_shared);
xlist_fini(&(primary.rusages));
FREEUP(license_rusage.res);
sched_ctx_fini(&ctx);
}
void setUp(void) { return; }
void tearDown(void) { return; }
@ -981,5 +1152,6 @@ int main(void)
RUN_TEST(
test_mid_plan_bucket_failure_rolls_back_before_later_plan_succeeds);
RUN_TEST(test_returns_zero_when_every_plan_is_exhausted);
RUN_TEST(test_shared_resource_debited_once_regardless_of_host_count);
return UNITY_END();
}