feat(scheduler-allocation): implement parjob's per-bucket allocation loop
Replace parjob's return-0 stub with a real multi-host allocator: for each candidate plan, walk buckets in the given (descending) order, claiming a distinct host per bucket via a fresh host_iter excluding hosts already claimed elsewhere in the same plan attempt. On a bucket failure, reverse every earlier successful claim's provisional debit via sched_ctx_uncommit_host() before advancing to the next plan (Req 4.2, 4.4, 4.5, 4.6, 4.7). Fix parjob_prepare_allocation() to compute each bucket's want from that bucket's own slot count (ctx->nslots) instead of a fixed value, and reset ctx->alloc_req.host at the top of each bucket's scan so host_iter's pre-filter can't inherit a stale want from a differently-sized prior bucket (safe: sched_try_alloc_on_host_() always repopulates it from the real per-bucket want before its own authoritative check runs). Add a maxTasks<=1 guard matching Req 4 AC1's precondition, needed because a snapshot-restored job with unpopulated maxTasks==0 (pre-existing, tracked separately as bug-1767) would otherwise reach parjob's now-real allocator instead of being silently ignored as before. On full success, merges every claimed bucket into one xce_job_alloc_t via make_job_alloc()'s existing runtime-sized host array, satisfying task 4.3's multi-host commit scope as well. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
This commit is contained in:
parent
6ca717fb86
commit
3d5e1c32c0
3 changed files with 1109 additions and 46 deletions
|
|
@ -1,26 +1,33 @@
|
|||
/*
|
||||
* test_parjob_combinatorial.c - task 4.1 (scheduler-allocation spec):
|
||||
* unit tests for parjob_allocator.c's REAL combinatorial
|
||||
* xce_alloc_plan_iter_t implementation (Req 2 AC 1-6, Req 4 AC 1).
|
||||
* test_parjob_combinatorial.c - tasks 4.1 + 4.2 (scheduler-allocation
|
||||
* spec): unit tests for parjob_allocator.c's REAL combinatorial
|
||||
* xce_alloc_plan_iter_t implementation (Req 2 AC 1-6, Req 4 AC 1) AND
|
||||
* (task 4.2, this pass) the real per-bucket allocation loop
|
||||
* (my_alloc()/parjob_try_plan_()/parjob_prepare_allocation(), Req 4 AC
|
||||
* 2/4/5/6/7).
|
||||
*
|
||||
* Task 4.1's boundary is the plan-ITERATOR only -- parjob_allocator.c's
|
||||
* my_alloc()/parjob_prepare_allocation() bodies are untouched stubs,
|
||||
* still returning 0 unconditionally (task 4.2's job). So this file never
|
||||
* calls my_alloc(); it drives parjob_plan_iter_init()/_next()/_fini()
|
||||
* (static functions, hence the white-box #include below) directly,
|
||||
* exactly the way a future my_alloc() will.
|
||||
* Task 4.1's own tests below never call my_alloc() -- they drive
|
||||
* parjob_plan_iter_init()/_next()/_fini() (static, hence the white-box
|
||||
* #include) directly, and needed no fixture beyond a bare
|
||||
* xce_primary_resreq_t, since the pre-4.2 my_alloc()/parjob_prepare_
|
||||
* allocation() stub bodies made no calls into resource.c/schedobj.c/
|
||||
* core.c/host_iter.c at all. Left completely unmodified below.
|
||||
*
|
||||
* White-box #include convention: same as test_seqjob_allocator.c
|
||||
* (task 3.1), but simpler -- unlike seqjob_allocator.c's my_alloc(),
|
||||
* parjob_allocator.c's current my_alloc()/parjob_prepare_allocation()
|
||||
* stub bodies make no calls into resource.c/schedobj.c/core.c/host_iter.c
|
||||
* at all (confirmed by reading the file: both just touch struct fields
|
||||
* and return 0), so only ../src/plugins/parjob_allocator.c itself needs
|
||||
* to be pulled in, plus a link-satisfying stub for plugin_register()/
|
||||
* plugin_unregister() (referenced by register_parjob_allocator(), never
|
||||
* itself invoked by any test below).
|
||||
* Task 4.2 replaces that stub with a real implementation that DOES call
|
||||
* into those files (sched_try_alloc_on_host_(), sched_ctx_uncommit_host(),
|
||||
* sched_host_iter_init/next/fini(), sched_ctx_get_uncomit_shared(),
|
||||
* sched_check_shared_satisfy(), plugin_prepare_allocation()'s fan-out),
|
||||
* so this pass adds the SAME white-box #include combination
|
||||
* test_seqjob_allocator.c (task 3.1)/test_seqjob_rewrap_equivalence.c
|
||||
* (task 3.2) already established (core.c + resource.c + schedobj.c +
|
||||
* host_iter.c), plus the matching link-satisfying stubs (a
|
||||
* plugin_prepare_allocation() that delegates to the real
|
||||
* parjob_prepare_allocation() under test, and a sched_make_allocs()
|
||||
* stand-in that avoids white-box-including the rest of the 1382-line
|
||||
* scheduler.c) -- and a real fixture (registered "slots" resource, real
|
||||
* sched_add_worker() hosts) for the new bucket-loop tests.
|
||||
*
|
||||
* Covers:
|
||||
* Covers (task 4.1, unchanged):
|
||||
* - one-plan-per-call generation (Req 2 AC 2)
|
||||
* - most-preferred-first ordering: ascending host-count k = 1..N
|
||||
* (Req 2 AC 3, Req 4 AC 1)
|
||||
|
|
@ -40,32 +47,62 @@
|
|||
* - Req 2 AC 6: next() calls pthread_testcancel() (or an equivalent
|
||||
* real cancellation point) -- proved behaviorally, same technique
|
||||
* test_host_iter.c already established for host_iter_next().
|
||||
*
|
||||
* Covers (task 4.2, new):
|
||||
* - multi-bucket success through the real my_alloc() end to end (Req
|
||||
* 4 AC 2/AC 3), including per-bucket resource want scaling with that
|
||||
* bucket's own slot count, not a fixed value (Req 4 AC 5/AC 6)
|
||||
* - the descending-vs-ascending worked example from requirements.md's
|
||||
* Composition Algorithm, driven directly through parjob_try_plan_()
|
||||
* with a hand-built plan for each order against the identical host
|
||||
* pool (Req 4 AC 2)
|
||||
* - a mid-plan bucket failure correctly reversing the earlier
|
||||
* successful bucket's provisional debit before the next candidate
|
||||
* plan is tried, proved by that same host being successfully
|
||||
* reclaimed by the later, winning plan (Req 4 AC 2, "Rollback scope")
|
||||
* - Req 4 AC 4: the same not-satisfied signal (0) once every plan is
|
||||
* exhausted
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <rtti.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "resobj.h"
|
||||
#include "resreq.h"
|
||||
#include "sched/alloc_iter.h"
|
||||
#include "scheduler.h"
|
||||
#include "xcealloc.h"
|
||||
|
||||
/* Pull in parjob_allocator.c itself (the file under test) -- the plan
|
||||
* iterator functions (parjob_plan_iter_init/next/fini) are `static`, so
|
||||
* this white-box #include is the only way to reach them directly,
|
||||
* matching test_seqjob_allocator.c's own established convention. */
|
||||
/* Pull in the real xce_sched_core_init()/sched_add_worker() (core.c),
|
||||
* sched_meet_host_selection()/sched_check_host_satisfy()/
|
||||
* sched_try_alloc_on_host_()/sched_ctx_uncommit_host()/put_resvals()
|
||||
* (resource.c), the real sched_ctx_init()/sched_ctx_get_uncomit_*()
|
||||
* (schedobj.c), the shared host_iter.c implementation, and
|
||||
* parjob_allocator.c itself (the file under test) into this TU -- same
|
||||
* combination test_seqjob_allocator.c (task 3.1) established for its own
|
||||
* sibling plugin. The plan-iterator and (new, task 4.2) bucket-loop
|
||||
* functions are all `static`, so this white-box #include is the only way
|
||||
* to reach them directly.
|
||||
*/
|
||||
#include "../src/core.c"
|
||||
#include "../src/host_iter.c"
|
||||
#include "../src/plugins/parjob_allocator.c"
|
||||
#include "../src/resource.c"
|
||||
#include "../src/schedobj.c"
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Link-satisfying stubs */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/* plugin_register()/plugin_unregister() are referenced by
|
||||
* register_parjob_allocator() (parjob_allocator.c), but no test below
|
||||
* calls register_parjob_allocator() -- the plan iterator is exercised
|
||||
* register_parjob_allocator() (parjob_allocator.c) and core.c
|
||||
* respectively, but no test below calls register_parjob_allocator() --
|
||||
* the plan iterator and my_alloc()/parjob_try_plan_() are exercised
|
||||
* directly. Never actually invoked by anything this file's tests
|
||||
* exercise; provided only so the TU links. */
|
||||
int plugin_register(void* sched, void* plugin)
|
||||
|
|
@ -77,10 +114,187 @@ int plugin_register(void* sched, void* plugin)
|
|||
|
||||
void plugin_unregister(void* arg) { (void)arg; }
|
||||
|
||||
/* 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
|
||||
* fixtures. Mirrors plugin.c:591's own reset-before-fan-out step, same
|
||||
* rationale as test_seqjob_allocator.c's identical stub. */
|
||||
int plugin_prepare_allocation(void* sched, xce_sched_ctx_t* ctx)
|
||||
{
|
||||
xce_sched_plugin_t fake_plugin;
|
||||
|
||||
if (ctx->worker == NULL) {
|
||||
xvec_idval_fini(&(ctx->alloc_req.shared));
|
||||
xvec_idval_init(&(ctx->alloc_req.shared), 8);
|
||||
} else {
|
||||
xvec_idval_fini(&(ctx->alloc_req.host));
|
||||
xvec_idval_init(&(ctx->alloc_req.host), 8);
|
||||
}
|
||||
|
||||
memset(&fake_plugin, 0, sizeof(fake_plugin));
|
||||
fake_plugin.sched = sched;
|
||||
|
||||
return parjob_prepare_allocation(&fake_plugin, ctx);
|
||||
}
|
||||
|
||||
int plugin_job_update(void* s, xce_sched_job_t* j)
|
||||
{
|
||||
(void)s;
|
||||
(void)j;
|
||||
return 0;
|
||||
}
|
||||
int plugin_queue_update(void* s, xce_sched_queue_t* q)
|
||||
{
|
||||
(void)s;
|
||||
(void)q;
|
||||
return 0;
|
||||
}
|
||||
int plugin_job_rusage_update(void* s, xce_sched_job_t* j, xce_job_rusages_t* r)
|
||||
{
|
||||
(void)s;
|
||||
(void)j;
|
||||
(void)r;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Avoids white-box-including the rest of scheduler.c just to build the
|
||||
* leaf xce_alloc_t records my_alloc()'s success path constructs -- same
|
||||
* rationale/shape as test_seqjob_allocator.c's identical stub. This
|
||||
* file's own quantitative per-host assertions go through
|
||||
* assert_uncommitted_host_debit() (real ctx->uncomitted state, untouched
|
||||
* by this stub) rather than these fake xce_alloc_t's fields, exactly like
|
||||
* the seqjob equivalence tests already do. */
|
||||
xce_alloc_t** sched_make_allocs(xce_scheduler_t* sched, char* reserve, int nres,
|
||||
int* idxs, float* vals, int* duration)
|
||||
{
|
||||
xce_alloc_t** allocs;
|
||||
int i;
|
||||
|
||||
(void)sched;
|
||||
(void)reserve;
|
||||
(void)idxs;
|
||||
(void)vals;
|
||||
(void)duration;
|
||||
|
||||
if (nres <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
allocs = (xce_alloc_t**)calloc(nres, sizeof(*allocs));
|
||||
if (allocs == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (i = 0; i < nres; i++) {
|
||||
allocs[i] = (xce_alloc_t*)calloc(1, sizeof(xce_alloc_t));
|
||||
}
|
||||
|
||||
return allocs;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Helpers */
|
||||
/* Fixture helpers */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/* register_slots_resource - the real makeResource() (resobj.c), giving
|
||||
* "slots" a resmgr-assigned idx -- identical to test_seqjob_allocator.c's
|
||||
* own helper; parjob_prepare_allocation()'s key_find(resmgr, "slots",...)
|
||||
* call needs this to be a genuinely registered resource. */
|
||||
static xce_resource_t* register_slots_resource(var resmgr)
|
||||
{
|
||||
xce_resource_t res;
|
||||
|
||||
memset(&res, 0, sizeof(res));
|
||||
res.name = strdup("slots");
|
||||
|
||||
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. */
|
||||
static xce_sched_worker_t* add_worker_with_slots(xce_sched_core_t* core,
|
||||
const char* name,
|
||||
int slots_idx,
|
||||
float slots_total)
|
||||
{
|
||||
xce_sched_worker_t* w;
|
||||
int idx[1];
|
||||
float val[1];
|
||||
int rc;
|
||||
|
||||
w = (xce_sched_worker_t*)calloc(1, sizeof(*w));
|
||||
TEST_ASSERT_NOT_NULL(w);
|
||||
|
||||
w->name = strdup(name);
|
||||
TEST_ASSERT_NOT_NULL(w->name);
|
||||
|
||||
rc = zarray_init(&(w->resvals), 8, NULL);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
rc = zarray_init(&(w->resInUse), 8, NULL);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
idx[0] = slots_idx;
|
||||
val[0] = slots_total;
|
||||
put_resvals(&(w->resvals), 1, idx, val);
|
||||
val[0] = 0.0f;
|
||||
put_resvals(&(w->resInUse), 1, idx, val);
|
||||
|
||||
rc = sched_add_worker(core, w);
|
||||
TEST_ASSERT_TRUE(rc >= 0);
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
static void init_primary_obj(xce_sched_obj_t* obj,
|
||||
xce_primary_schedobj_t* primary_obj,
|
||||
xce_primary_resreq_t* primary)
|
||||
{
|
||||
memset(primary_obj, 0, sizeof(*primary_obj));
|
||||
primary_obj->resreq = primary;
|
||||
|
||||
memset(obj, 0, sizeof(*obj));
|
||||
obj->tag = XCE_RESREQ_PRIMARY;
|
||||
obj->u.primary = primary_obj;
|
||||
}
|
||||
|
||||
/* drive_my_alloc - the exact production sequence a job's allocation
|
||||
* attempt runs: sched_ctx_init() -> sched_ctx_set_primary() (the real
|
||||
* call that stages ctx->alloc_req.shared via plugin_prepare_allocation()'s
|
||||
* ctx->worker==NULL branch, exactly as sched_make_primary_allocation()
|
||||
* does today) -> my_alloc(). Returns my_alloc()'s return code; ctx is left
|
||||
* initialized (caller must sched_ctx_fini()). Identical to
|
||||
* test_seqjob_rewrap_equivalence.c's own helper. */
|
||||
static int drive_my_alloc(xce_scheduler_t* sched, xce_sched_obj_t* obj,
|
||||
xce_primary_resreq_t* primary, xce_sched_ctx_t* ctx)
|
||||
{
|
||||
xce_sched_plugin_t plugin;
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, sched_ctx_init(ctx, sched, obj));
|
||||
sched_ctx_set_primary(ctx, primary);
|
||||
|
||||
memset(&plugin, 0, sizeof(plugin));
|
||||
plugin.sched = sched;
|
||||
|
||||
return my_alloc(&plugin, ctx);
|
||||
}
|
||||
|
||||
/* assert_uncommitted_host_debit - reads ctx->uncomitted.hosts[w->idx] for
|
||||
* resource residx and asserts it equals `expected`. Identical to
|
||||
* test_seqjob_rewrap_equivalence.c's own helper. */
|
||||
static void assert_uncommitted_host_debit(xce_sched_ctx_t* ctx,
|
||||
xce_sched_worker_t* w, int residx,
|
||||
float expected)
|
||||
{
|
||||
xvec_t* uncomit = NULL;
|
||||
int rc;
|
||||
|
||||
rc = sched_ctx_get_uncomit_host(&uncomit, ctx, w->idx);
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
TEST_ASSERT_NOT_NULL(uncomit);
|
||||
TEST_ASSERT_EQUAL_FLOAT(expected, ((float*)uncomit->data)[residx]);
|
||||
}
|
||||
|
||||
static void bind_plan_iter(xce_alloc_plan_iter_t* iter)
|
||||
{
|
||||
memset(iter, 0, sizeof(*iter));
|
||||
|
|
@ -386,6 +600,369 @@ static void test_next_has_a_real_cancellation_point(void)
|
|||
iter->fini(iter);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Task 4.2 tests: the real per-bucket allocation loop */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* Req 4 AC 2/AC 3/AC 5/AC 6: a real multi-bucket plan, driven end to end
|
||||
* through the real my_alloc() (not parjob_try_plan_() directly). Two
|
||||
* hosts are engineered so the outcome is deterministic REGARDLESS of
|
||||
* sched->core.workers' own hash-based iteration order: host-A has exactly
|
||||
* 2 free slots (only it can satisfy a "need 2" bucket -- host-B's 1 free
|
||||
* slot never qualifies) and host-B has exactly 1 free slot. maxTasks=3
|
||||
* forces k=1 ([3]) to fail first (no single host has >=3), so k=2's
|
||||
* [2,1] plan is the one that actually succeeds -- proving the bucket loop
|
||||
* itself works, not just the trivial single-bucket/single-plan case.
|
||||
* Also proves Req 4 AC 5/AC 6: parjob_prepare_allocation() must have
|
||||
* staged a "slots" want of exactly 2 (not a fixed 1, and not maxTasks=3)
|
||||
* for host-A's bucket, and exactly 1 for host-B's -- an unfixed stub or a
|
||||
* fixed-value bug would either allocate nothing or debit the wrong
|
||||
* amount, and assert_uncommitted_host_debit() below would catch either.
|
||||
*/
|
||||
static void test_multi_bucket_plan_succeeds_through_real_my_alloc(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_resource_t* slots_res;
|
||||
xce_sched_worker_t* host_a;
|
||||
xce_sched_worker_t* host_b;
|
||||
xce_sched_obj_t obj;
|
||||
xce_primary_schedobj_t primary_obj;
|
||||
xce_primary_resreq_t primary;
|
||||
xce_sched_ctx_t ctx;
|
||||
int rc;
|
||||
int i;
|
||||
int seen_a = 0;
|
||||
int seen_b = 0;
|
||||
|
||||
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);
|
||||
|
||||
host_a
|
||||
= add_worker_with_slots(&(sched.core), "host-a", slots_res->idx, 2.0f);
|
||||
host_b
|
||||
= add_worker_with_slots(&(sched.core), "host-b", slots_res->idx, 1.0f);
|
||||
|
||||
memset(&primary, 0, sizeof(primary));
|
||||
primary.minTasks = 1;
|
||||
primary.maxTasks = 3;
|
||||
primary.select = NULL;
|
||||
|
||||
init_primary_obj(&obj, &primary_obj, &primary);
|
||||
|
||||
rc = drive_my_alloc(&sched, &obj, &primary, &ctx);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
TEST_ASSERT_NOT_NULL(ctx.alloc);
|
||||
TEST_ASSERT_EQUAL_INT(2, 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 */
|
||||
if (strcmp(ha->host, "host-a") == 0) {
|
||||
seen_a = 1;
|
||||
} else if (strcmp(ha->host, "host-b") == 0) {
|
||||
seen_b = 1;
|
||||
}
|
||||
}
|
||||
TEST_ASSERT_TRUE(seen_a);
|
||||
TEST_ASSERT_TRUE(seen_b);
|
||||
|
||||
/* host-a's own bucket needed 2 slots, host-b's needed 1 -- the exact
|
||||
* per-bucket want Req 4 AC 5/AC 6 requires, not a fixed 1 (seqjob's
|
||||
* old hardcode) or maxTasks=3 (a different, equally wrong fixed
|
||||
* value). */
|
||||
assert_uncommitted_host_debit(&ctx, host_a, slots_res->idx, 2.0f);
|
||||
assert_uncommitted_host_debit(&ctx, host_b, slots_res->idx, 1.0f);
|
||||
|
||||
sched_ctx_fini(&ctx);
|
||||
}
|
||||
|
||||
/*
|
||||
* Req 4 AC 2: the exact descending-vs-ascending worked example from
|
||||
* requirements.md's Composition Algorithm. Two hosts, capacities 1 and 2;
|
||||
* WHICH physical host holds which capacity is assigned by a real,
|
||||
* independent iter_make()/iter_next() scan (the identical primitive
|
||||
* host_iter.c itself uses over the same sched.core.workers) so the test
|
||||
* does not depend on assuming insertion order matches hash-table
|
||||
* iteration order -- the same technique
|
||||
* test_seqjob_rewrap_equivalence.c's own iteration-order fixture uses.
|
||||
* The 2-free-slot capacity goes to whichever host is visited FIRST,
|
||||
* matching requirements.md's own worked example precisely (a fresh scan
|
||||
* for a small bucket greedily claims whatever it encounters first that
|
||||
* already satisfies it).
|
||||
*
|
||||
* Driven directly through parjob_try_plan_() (not the real plan
|
||||
* iterator, which only ever generates descending order) with a
|
||||
* hand-built xce_alloc_plan_t for each order, against the SAME
|
||||
* unmodified host pool (the descending attempt's claims are rolled back
|
||||
* before the ascending attempt runs) -- this is the seam task 4.2's own
|
||||
* bucket-loop factoring (out of my_alloc()) exists to make testable
|
||||
* without reimplementing the loop in the test.
|
||||
*/
|
||||
static void test_descending_bucket_order_succeeds_where_ascending_fails(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_resource_t* slots_res;
|
||||
xce_sched_worker_t* host_first;
|
||||
xce_sched_worker_t* host_second;
|
||||
iterator_t it;
|
||||
xce_sched_obj_t obj;
|
||||
xce_primary_schedobj_t primary_obj;
|
||||
xce_primary_resreq_t primary;
|
||||
xce_sched_ctx_t ctx;
|
||||
xce_alloc_plan_t plan;
|
||||
int bucket_slots_desc[2];
|
||||
int bucket_slots_asc[2];
|
||||
parjob_claim_t claimed[2];
|
||||
int nclaimed;
|
||||
int rc;
|
||||
int idx[1];
|
||||
float val[1];
|
||||
|
||||
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);
|
||||
|
||||
/* Added with a placeholder capacity -- real capacities are assigned
|
||||
* below, once the shared iteration order is known. */
|
||||
add_worker_with_slots(&(sched.core), "host-1", slots_res->idx, 0.0f);
|
||||
add_worker_with_slots(&(sched.core), "host-2", slots_res->idx, 0.0f);
|
||||
|
||||
rc = iter_make(&it, sched.core.workers);
|
||||
TEST_ASSERT_TRUE(rc >= 0);
|
||||
rc = iter_next(&it, (void**)&host_first);
|
||||
TEST_ASSERT_TRUE(rc > 0);
|
||||
rc = iter_next(&it, (void**)&host_second);
|
||||
TEST_ASSERT_TRUE(rc > 0);
|
||||
iter_destroy(&it);
|
||||
|
||||
idx[0] = slots_res->idx;
|
||||
val[0] = 2.0f;
|
||||
put_resvals(&(host_first->resvals), 1, idx, val);
|
||||
val[0] = 1.0f;
|
||||
put_resvals(&(host_second->resvals), 1, idx, val);
|
||||
|
||||
memset(&primary, 0, sizeof(primary));
|
||||
primary.minTasks = 1;
|
||||
primary.maxTasks = 3;
|
||||
primary.select = NULL;
|
||||
|
||||
init_primary_obj(&obj, &primary_obj, &primary);
|
||||
TEST_ASSERT_EQUAL_INT(0, sched_ctx_init(&ctx, &sched, &obj));
|
||||
sched_ctx_set_primary(&ctx, &primary);
|
||||
|
||||
/* my_alloc() always lazily creates ctx->reason before ever reaching
|
||||
* parjob_try_plan_() (see my_alloc()'s own top-of-function guard);
|
||||
* this test calls parjob_try_plan_() directly, bypassing that step,
|
||||
* so it must set one up itself -- sched_try_alloc_on_host_()
|
||||
* (resource.c) unconditionally records a pend reason into
|
||||
* ctx->reason->reasons on a resource-unmet candidate, which the
|
||||
* ascending attempt below deliberately exercises (bucket1 failing
|
||||
* "need 2" against host_second's 1 free slot). */
|
||||
ctx.reason = make_pendreason(0);
|
||||
TEST_ASSERT_NOT_NULL(ctx.reason);
|
||||
|
||||
/* Descending [2,1] -- the plan iterator's own v1 default order --
|
||||
* succeeds: bucket0 ("need 2") can only be satisfied by host_first
|
||||
* (2 free); bucket1 ("need 1") then claims host_second (1 free),
|
||||
* the only host left. */
|
||||
bucket_slots_desc[0] = 2;
|
||||
bucket_slots_desc[1] = 1;
|
||||
memset(&plan, 0, sizeof(plan));
|
||||
plan.nbuckets = 2;
|
||||
plan.bucket_slots = bucket_slots_desc;
|
||||
|
||||
rc = parjob_try_plan_(&sched, &ctx, &plan, claimed, &nclaimed);
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
TEST_ASSERT_EQUAL_INT(2, nclaimed);
|
||||
TEST_ASSERT_TRUE(claimed[0].w == host_first);
|
||||
TEST_ASSERT_TRUE(claimed[1].w == host_second);
|
||||
|
||||
/* Roll back before trying the ascending variant against the exact
|
||||
* same, otherwise-untouched host pool. */
|
||||
parjob_rollback_claims_(&ctx, claimed, nclaimed);
|
||||
assert_uncommitted_host_debit(&ctx, host_first, slots_res->idx, 0.0f);
|
||||
assert_uncommitted_host_debit(&ctx, host_second, slots_res->idx, 0.0f);
|
||||
|
||||
/* Ascending [1,2] -- the SAME host pool -- fails: bucket0 ("need 1")
|
||||
* greedily claims host_first (2 free, visited first, trivially
|
||||
* satisfies "need 1" even though it is overqualified), leaving only
|
||||
* host_second (1 free) for bucket1 ("need 2"), which cannot satisfy
|
||||
* it. */
|
||||
bucket_slots_asc[0] = 1;
|
||||
bucket_slots_asc[1] = 2;
|
||||
memset(&plan, 0, sizeof(plan));
|
||||
plan.nbuckets = 2;
|
||||
plan.bucket_slots = bucket_slots_asc;
|
||||
|
||||
rc = parjob_try_plan_(&sched, &ctx, &plan, claimed, &nclaimed);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(0, nclaimed);
|
||||
|
||||
/* No phantom debit left behind by the failed ascending attempt. */
|
||||
assert_uncommitted_host_debit(&ctx, host_first, slots_res->idx, 0.0f);
|
||||
assert_uncommitted_host_debit(&ctx, host_second, slots_res->idx, 0.0f);
|
||||
|
||||
sched_ctx_fini(&ctx);
|
||||
}
|
||||
|
||||
/*
|
||||
* Req 4 AC 2 ("Rollback scope"): a mid-plan bucket failure must reverse
|
||||
* every earlier bucket's provisional debit in that SAME plan attempt
|
||||
* before the next candidate plan is tried. Proved the strong way -- not
|
||||
* just "the debit reads back as zero" but "the SAME host is successfully
|
||||
* reclaimed by a LATER plan attempt for a full, real allocation," which
|
||||
* could only happen if the earlier phantom debit was genuinely reversed
|
||||
* (sched_check_host_satisfy() would otherwise reject the later attempt's
|
||||
* own fresh want against a host that still looked partially consumed).
|
||||
*
|
||||
* Fixture: h1 (2 free), h2/h3/h4 (1 free each), maxTasks=4.
|
||||
* k=1 ([4]): fails (no single host has >=4).
|
||||
* k=2 ([2,2]): bucket0 ("need 2") can only be satisfied by h1 (the only
|
||||
* host with >=2 free) -- claims h1. bucket1 ("need 2"), excluding h1,
|
||||
* has only h2/h3/h4 left, each with just 1 free -- ALL fail. The
|
||||
* whole k=2 plan fails, and h1's provisional debit from bucket0 must
|
||||
* be rolled back here.
|
||||
* k=3 ([2,1,1]): a fresh, empty claimed[] (independent of k=2's
|
||||
* rolled-back attempt) -- bucket0 ("need 2") again only h1 qualifies;
|
||||
* if k=2's debit on h1 had NOT been reversed, h1 would appear to
|
||||
* already carry a 2.0 phantom debit and sched_check_host_satisfy()
|
||||
* would reject this fresh "need 2" want against it (2 total < 2
|
||||
* phantom + 2 new) -- the whole allocation would fail outright
|
||||
* instead of succeeding. bucket1/bucket2 ("need 1" each) then claim
|
||||
* two of the three remaining 1-free-slot hosts. SUCCEEDS with exactly
|
||||
* 3 claimed hosts.
|
||||
*/
|
||||
static void
|
||||
test_mid_plan_bucket_failure_rolls_back_before_later_plan_succeeds(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_resource_t* slots_res;
|
||||
xce_sched_worker_t* h1;
|
||||
xce_sched_worker_t* h2;
|
||||
xce_sched_worker_t* h3;
|
||||
xce_sched_worker_t* h4;
|
||||
xce_sched_obj_t obj;
|
||||
xce_primary_schedobj_t primary_obj;
|
||||
xce_primary_resreq_t primary;
|
||||
xce_sched_ctx_t ctx;
|
||||
int rc;
|
||||
int i;
|
||||
int seen_h1 = 0;
|
||||
int nother = 0;
|
||||
xvec_t* u2 = NULL;
|
||||
xvec_t* u3 = NULL;
|
||||
xvec_t* u4 = NULL;
|
||||
float other_sum;
|
||||
|
||||
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);
|
||||
|
||||
h1 = add_worker_with_slots(&(sched.core), "h1", slots_res->idx, 2.0f);
|
||||
h2 = add_worker_with_slots(&(sched.core), "h2", slots_res->idx, 1.0f);
|
||||
h3 = add_worker_with_slots(&(sched.core), "h3", slots_res->idx, 1.0f);
|
||||
h4 = add_worker_with_slots(&(sched.core), "h4", slots_res->idx, 1.0f);
|
||||
|
||||
memset(&primary, 0, sizeof(primary));
|
||||
primary.minTasks = 1;
|
||||
primary.maxTasks = 4;
|
||||
primary.select = NULL;
|
||||
|
||||
init_primary_obj(&obj, &primary_obj, &primary);
|
||||
|
||||
rc = drive_my_alloc(&sched, &obj, &primary, &ctx);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
TEST_ASSERT_NOT_NULL(ctx.alloc);
|
||||
TEST_ASSERT_EQUAL_INT(3, 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);
|
||||
if (strcmp(ha->host, "h1") == 0) {
|
||||
seen_h1++;
|
||||
} else {
|
||||
nother++;
|
||||
}
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT(1, seen_h1);
|
||||
TEST_ASSERT_EQUAL_INT(2, nother);
|
||||
|
||||
/* THE regression assertion: h1 carries exactly its own bucket's 2.0
|
||||
* want in the final, committed state -- not 4.0 (which a missing
|
||||
* rollback would leave: 2.0 phantom from the failed k=2 attempt plus
|
||||
* 2.0 genuine from k=3's own successful claim). */
|
||||
assert_uncommitted_host_debit(&ctx, h1, slots_res->idx, 2.0f);
|
||||
|
||||
/* Exactly two of h2/h3/h4 were claimed at 1.0 each (the third stayed
|
||||
* at 0.0, host_iter's own internal candidate order decides which) --
|
||||
* proves neither a double-debit nor a leaked phantom debit from the
|
||||
* rolled-back k=2 attempt. */
|
||||
TEST_ASSERT_EQUAL_INT(1, sched_ctx_get_uncomit_host(&u2, &ctx, h2->idx));
|
||||
TEST_ASSERT_EQUAL_INT(1, sched_ctx_get_uncomit_host(&u3, &ctx, h3->idx));
|
||||
TEST_ASSERT_EQUAL_INT(1, sched_ctx_get_uncomit_host(&u4, &ctx, h4->idx));
|
||||
other_sum = ((float*)u2->data)[slots_res->idx]
|
||||
+ ((float*)u3->data)[slots_res->idx]
|
||||
+ ((float*)u4->data)[slots_res->idx];
|
||||
TEST_ASSERT_EQUAL_FLOAT(2.0f, other_sum);
|
||||
|
||||
sched_ctx_fini(&ctx);
|
||||
}
|
||||
|
||||
/*
|
||||
* Req 4 AC 4: when every candidate plan is exhausted without a full
|
||||
* success, my_alloc() returns the same not-satisfied signal (0) the old
|
||||
* stub always returned, unconditionally -- pend-reason handling
|
||||
* downstream is unaffected by this task's change.
|
||||
*/
|
||||
static void test_returns_zero_when_every_plan_is_exhausted(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_resource_t* slots_res;
|
||||
xce_sched_obj_t obj;
|
||||
xce_primary_schedobj_t primary_obj;
|
||||
xce_primary_resreq_t primary;
|
||||
xce_sched_ctx_t ctx;
|
||||
int rc;
|
||||
|
||||
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);
|
||||
|
||||
/* A single host with only 1 free slot can never satisfy ANY plan for
|
||||
* a 3-task job (k=1 needs 3 on one host; k=2/k=3 each need a SECOND
|
||||
* distinct host that does not exist). */
|
||||
add_worker_with_slots(&(sched.core), "lonely-host", slots_res->idx, 1.0f);
|
||||
|
||||
memset(&primary, 0, sizeof(primary));
|
||||
primary.minTasks = 1;
|
||||
primary.maxTasks = 3;
|
||||
primary.select = NULL;
|
||||
|
||||
init_primary_obj(&obj, &primary_obj, &primary);
|
||||
|
||||
rc = drive_my_alloc(&sched, &obj, &primary, &ctx);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_NULL(ctx.alloc);
|
||||
|
||||
sched_ctx_fini(&ctx);
|
||||
}
|
||||
|
||||
void setUp(void) { return; }
|
||||
|
||||
void tearDown(void) { return; }
|
||||
|
|
@ -399,5 +976,10 @@ int main(void)
|
|||
RUN_TEST(test_plan_generation_ignores_scheduler_state);
|
||||
RUN_TEST(test_init_rejects_invalid_input);
|
||||
RUN_TEST(test_next_has_a_real_cancellation_point);
|
||||
RUN_TEST(test_multi_bucket_plan_succeeds_through_real_my_alloc);
|
||||
RUN_TEST(test_descending_bucket_order_succeeds_where_ascending_fails);
|
||||
RUN_TEST(
|
||||
test_mid_plan_bucket_failure_rolls_back_before_later_plan_succeeds);
|
||||
RUN_TEST(test_returns_zero_when_every_plan_is_exhausted);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue