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:
parent
3d5e1c32c0
commit
9d23060b76
2 changed files with 173 additions and 1 deletions
|
|
@ -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_
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue