test(scheduler-allocation): prove seqjob rewrap preserves host selection
Add a dedicated equivalence suite (test_seqjob_rewrap_equivalence.c) covering multi-host fixtures the trivial single-host test doesn't reach: select-filter-only rejection, resource-only rejection, a combined select-then-resource-then-success ordering, and per-host debit correctness. The iteration-order test is grounded in the deleted pre-rewrap sched_collect_workers() code path (verified via git show against the parent commit) and the unmodified xlist.c primitive, not merely in host_iter.c's own traversal, so it proves equivalence to the old behavior rather than internal self-consistency. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
This commit is contained in:
parent
b18c5f780d
commit
9d64ba0d69
3 changed files with 621 additions and 1 deletions
|
|
@ -44,7 +44,7 @@
|
|||
- Add a guard so seqjob's resource-request preparation only ever acts on single-task jobs, closing the fan-out gap this creates for the parallel allocator's later fix
|
||||
- Observable: seqjob's allocation still runs end to end using the shared iterators, with no allocator-specific worker-collection logic left in seqjob's own code
|
||||
- _Requirements: 9.1, 4.6_
|
||||
- [ ] 3.2 Prove seqjob's rewrap preserves today's host-selection behavior
|
||||
- [x] 3.2 Prove seqjob's rewrap preserves today's host-selection behavior
|
||||
- Write a test suite constructing fixture states with multiple candidate hosts, including at least one host failing the select filter, asserting identical host selection to today's pre-rewrap behavior given identical worker/resource state
|
||||
- Observable: a passing equivalence test suite demonstrates no behavior change across fixture states beyond the single-obvious-host trivial case
|
||||
- _Requirements: 9.1_
|
||||
|
|
@ -113,3 +113,5 @@
|
|||
- (1.2) `resreq_fp`'s ingest-time population lives in `core_build_primary_schedobj()` (`src/scheduler/src/core_events.c`), right after `primary->resreq = resreq;` — confirmed wired (min_tasks/max_tasks/select_node_id/rusage_hash all set from the parsed `xce_primary_resreq_t`) as of task 1.2 round 2, regression-covered by `test_core_build_primary_schedobj_populates_resreq_fp_from_parsed_resreq` in `src/scheduler/testsuite/test_core_events.c`. Round 1 added the type/field/hash machinery but never wired this call site, leaving `resreq_fp` permanently zeroed — do not rediscover this gap in a later task.
|
||||
- (3.1) The per-host try-alloc leaf (`sched_try_alloc_on_host_(xce_sched_worker_t* w, xce_scheduler_t* sched, xce_sched_ctx_t* ctx)` — select check → `sched_ctx_get_uncomit_host` → `plugin_prepare_allocation` → `sched_check_host_satisfy` → `xvec_idval(..., XVEC_ADD_FLOAT)`) now lives in `resource.c`, exported (non-static, non-variadic) and declared in `scheduler.h`. **Task 4.2 (parjob's per-bucket allocation loop) must call this exact shared function from its own bucket/host_iter loop, not write a second copy.** Round 1 of this task wrote a private ~70-line duplicate (`seqjob_try_alloc_on_host_`) because the original was `static`/`va_list`-based; round 2 fixed this by exporting the real function and deleting both the duplicate and the now-dead `sched_try_alloc_on_hosts()` variadic wrapper. This is the same reimplementation-vs-share risk task 1.4 hit in test code — here it recurred in production code, one task later.
|
||||
- (3.1) `src/scheduler/meson.build`'s production `scheduler_srcs` list was missing `host_iter.c` since task 2.1 only wired that file into `testsuite/meson.build` — invisible until task 3.1 became the first *production* caller of `host_iter.c`'s functions (a shared .c file with zero production callers builds/links fine without being in `scheduler_srcs` at all, so task 2.1's own green build/test run couldn't have caught this). Any future task adding a new shared scheduler `.c` file should double check it lands in `scheduler_srcs`, not only `testsuite/meson.build`, even if nothing calls it yet.
|
||||
- (3.2) `host_iter.c`'s pre-filter select check and `sched_try_alloc_on_host_()`'s (`resource.c`) own independent `sched_meet_host_selection` re-check are pre-existing, intentional defense-in-depth: both run the identical check against the same live state, so a regression in either one alone is invisible at the observable-pick level — only a regression in *both simultaneously* changes behavior. This is benign and predates this feature; task 4.2 (parjob) will inherit the same double-check when it reuses `host_iter.c`/`sched_try_alloc_on_host_()`, and should not "fix" the redundancy as part of that task.
|
||||
- (3.2) `sched_collect_workers()`'s deleted (task 3.1) iteration and `host_iter.c`'s `iter_make()/iter_next()` iteration are structurally identical, not coincidentally similar: both called the exact same third-party `iter_make()/iter_next()` (RTTI lib) over the literal same `sched->core.workers` var, and the old `xlist_find()` walk was forward/insertion-order — confirmed via `git show b18c5f78^:src/scheduler/src/scheduler.c` and `xlist.c` (unmodified across this feature). Useful precedent if task 4.1/4.2's parjob equivalence claims ever need the same grounding.
|
||||
|
|
|
|||
|
|
@ -1542,6 +1542,23 @@ sched_seqjob_allocator_test = executable(
|
|||
dependencies: [scheduler_test_deps],
|
||||
install: false)
|
||||
|
||||
# Task 3.2 (scheduler-allocation spec, Req 9.1): the dedicated pre/post-
|
||||
# rewrap host-selection EQUIVALENCE proof for seqjob_allocator.c's
|
||||
# my_alloc() -- fixture states task 3.1's own smoke test does not cover
|
||||
# (multiple candidate hosts with only one resource-satisfying, a host
|
||||
# failing the select filter specifically despite abundant resources,
|
||||
# iteration-order sensitivity across two equally-valid hosts, and a
|
||||
# select-fail-then-resource-fail-then-success sequence). Same white-box
|
||||
# #include pattern as sched_seqjob_allocator_test above, plus <rtti.h>
|
||||
# directly for this file's own iter_make()/iter_next() grounding of
|
||||
# "iteration order".
|
||||
sched_seqjob_rewrap_equivalence_test = executable(
|
||||
'sched_seqjob_rewrap_equivalence_test',
|
||||
['test_seqjob_rewrap_equivalence.c'],
|
||||
include_directories:[incdir],
|
||||
dependencies: [scheduler_test_deps],
|
||||
install: false)
|
||||
|
||||
# Register as meson tests so `meson test --suite scheduler` runs them all.
|
||||
# detect_leaks=0 matches the suite convention (fixtures are not torn down).
|
||||
test('scheduler test', scheduler_test, suite: 'scheduler',
|
||||
|
|
@ -1736,3 +1753,6 @@ test('scheduler alloc cache test',
|
|||
test('scheduler seqjob allocator test',
|
||||
sched_seqjob_allocator_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
test('scheduler seqjob rewrap equivalence test',
|
||||
sched_seqjob_rewrap_equivalence_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
|
|
|
|||
598
src/scheduler/testsuite/test_seqjob_rewrap_equivalence.c
Normal file
598
src/scheduler/testsuite/test_seqjob_rewrap_equivalence.c
Normal file
|
|
@ -0,0 +1,598 @@
|
|||
/*
|
||||
* test_seqjob_rewrap_equivalence.c - task 3.2 (scheduler-allocation spec):
|
||||
* the dedicated pre/post-rewrap host-selection EQUIVALENCE proof for
|
||||
* seqjob_allocator.c's my_alloc() (Req 9.1).
|
||||
*
|
||||
* task 3.1's own test_seqjob_allocator.c only proves the rewrapped
|
||||
* my_alloc() still runs end to end through the shared plan_iter/host_iter
|
||||
* composition (a single-obvious-host smoke case) and that the new
|
||||
* maxTasks==1 guard (Req 4.6) works. Req 9.1's actual claim is narrower
|
||||
* and harder to prove: "which host a job lands on, given the same
|
||||
* worker/resource state" must be UNCHANGED by the rewrap. A test that only
|
||||
* exercises the single-obvious-host case cannot distinguish "the new
|
||||
* composition happens to work" from "the new composition picks the same
|
||||
* host the old sched_collect_workers()+xlist_find() loop would have" --
|
||||
* this file's whole job is to close that gap with fixture states rich
|
||||
* enough that a plausible regression in the rewrap would actually show up.
|
||||
*
|
||||
* How equivalence is established (see this task's own status report for
|
||||
* the full rationale): rather than literally resurrecting the deleted
|
||||
* pre-rewrap sched_collect_workers()/sched_try_alloc_on_hosts() code (git
|
||||
* history: commit b18c5f78^) and running it side by side, this file hand-
|
||||
* derives the expected pick from the two well-defined, already-unit-tested
|
||||
* primitives the composition is built from -- sched_meet_host_selection()
|
||||
* and sched_check_host_satisfy() (both resource.c, unmodified by the
|
||||
* rewrap) -- and asserts my_alloc()'s real behavior against that
|
||||
* derivation. This is valid because both the old and new code paths are
|
||||
* confirmed (by reading resource.c) to funnel every host decision through
|
||||
* the exact same leaf, sched_try_alloc_on_host_(): pre-rewrap it was
|
||||
* `static`/va_list-based and reached via sched_try_alloc_on_hosts(); post-
|
||||
* rewrap (task 3.1) it is the same logic, exported with real parameters.
|
||||
* Byte-for-byte identical body (confirmed via `git show b18c5f78^:
|
||||
* src/scheduler/src/resource.c` against the current file) -- the ONLY
|
||||
* thing that changed is how candidate hosts are fed into it: an inline
|
||||
* sched_collect_workers()+xlist_find() scan before, xce_host_iter_t's
|
||||
* lazy iter_make()/iter_next()-driven scan (the SAME underlying RTTI
|
||||
* primitive over the SAME sched->core.workers var -- see host_iter.c's own
|
||||
* file header) after. So "identical host selection" reduces exactly to
|
||||
* "the new host_iter-driven loop still visits candidates in
|
||||
* iter_make()/iter_next() order and still picks the first one
|
||||
* sched_try_alloc_on_host_() accepts" -- which is what each test below
|
||||
* independently drives and checks, without assuming it.
|
||||
*
|
||||
* Covers (see design.md's Testing Strategy for
|
||||
* test_seqjob_rewrap_equivalence.c):
|
||||
* 1. Multiple candidate hosts, only one resource-satisfies -- the
|
||||
* correct one is chosen, and ONLY it carries a provisional debit.
|
||||
* 2. A host that fails the SELECT filter specifically, despite having
|
||||
* abundant free resources -- must still be skipped.
|
||||
* 3. Iteration-order sensitivity: with two equally-valid hosts, the pick
|
||||
* must match whichever host sched->core.workers' own
|
||||
* iter_make()/iter_next() (the identical primitive both the deleted
|
||||
* pre-rewrap loop and today's host_iter.c use) visits first --
|
||||
* derived directly from that primitive in the test itself, not
|
||||
* assumed.
|
||||
* 4. A select-failing host, then a resource-failing host, then a
|
||||
* succeeding host, in that order -- proves the loop doesn't
|
||||
* short-circuit or mis-skip when back-to-back failures of two
|
||||
* DIFFERENT kinds precede the eventual success.
|
||||
*
|
||||
* White-box #include convention: identical combination
|
||||
* test_seqjob_allocator.c (task 3.1) already established (core.c +
|
||||
* resource.c + schedobj.c + host_iter.c + plugins/seqjob_allocator.c),
|
||||
* plus <rtti.h> directly for iter_make()/iter_next()/iter_destroy() (test
|
||||
* 3's own grounding of "iteration order"), and test_host_iter.c's
|
||||
* make_rack_eq_select() helper (adapted for a resmgr-assigned residx
|
||||
* rather than a hardcoded one, since this file -- like
|
||||
* test_seqjob_allocator.c, unlike test_host_iter.c -- runs the REAL
|
||||
* seqjob_prepare_allocation(), whose key_find(resmgr, "slots", ...) call
|
||||
* needs a genuinely registered "slots" resource).
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <rtti.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "resobj.h"
|
||||
#include "resreq.h"
|
||||
#include "sched/alloc_iter.h"
|
||||
#include "scheduler.h"
|
||||
#include "xcealloc.h"
|
||||
|
||||
/* Pull in the real xce_sched_core_init()/sched_add_worker() (core.c),
|
||||
* sched_meet_host_selection()/sched_check_host_satisfy()/put_resvals()
|
||||
* (resource.c), the real sched_ctx_init()/sched_ctx_get_uncomit_*()
|
||||
* (schedobj.c), the shared host_iter.c implementation, and
|
||||
* seqjob_allocator.c itself (the file under test) into this TU -- same
|
||||
* combination test_seqjob_allocator.c (task 3.1) already established. */
|
||||
#include "../src/core.c"
|
||||
#include "../src/resource.c"
|
||||
#include "../src/schedobj.c"
|
||||
#include "../src/host_iter.c"
|
||||
#include "../src/plugins/seqjob_allocator.c"
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Link-satisfying stubs (identical set/rationale to test_seqjob_allocator.c) */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
int plugin_register(void* sched, void* plugin)
|
||||
{
|
||||
(void)sched;
|
||||
(void)plugin;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void plugin_unregister(void* arg) { (void)arg; }
|
||||
|
||||
/* Delegates to the real seqjob_prepare_allocation() under test, standing
|
||||
* in for plugin_prepare_allocation()'s (plugin.c) real fan-out across
|
||||
* every registered plugin -- exactly one (seqjob's own) in this file's
|
||||
* fixtures. See test_seqjob_allocator.c's identical stub for the full
|
||||
* xvec_idval_fini()/init() reset-before-fan-out rationale. */
|
||||
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 seqjob_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;
|
||||
}
|
||||
|
||||
/* Same rationale as test_seqjob_allocator.c's identical stub: avoids
|
||||
* white-box-including the rest of scheduler.c just to build the leaf
|
||||
* xce_alloc_t records my_alloc()'s success path constructs. */
|
||||
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;
|
||||
}
|
||||
|
||||
void setUp(void) { return; }
|
||||
void tearDown(void) { return; }
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* 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. */
|
||||
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;
|
||||
}
|
||||
|
||||
/* add_worker_with_slots_and_rack - same as above, plus a second, numeric
|
||||
* "rack" resource value (never staged as a want -- consumed only by the
|
||||
* select expression, matching test_host_iter.c's own add_worker() shape)
|
||||
* so select-filter tests can distinguish hosts by something other than
|
||||
* resource availability. */
|
||||
static xce_sched_worker_t*
|
||||
add_worker_with_slots_and_rack(xce_sched_core_t* core, const char* name,
|
||||
int slots_idx, float slots_total, int rack_idx,
|
||||
double rack)
|
||||
{
|
||||
xce_sched_worker_t* w = add_worker_with_slots(core, name, slots_idx, slots_total);
|
||||
int idx[1];
|
||||
float val[1];
|
||||
|
||||
idx[0] = rack_idx;
|
||||
val[0] = (float)rack;
|
||||
put_resvals(&(w->resvals), 1, idx, val);
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
/* make_rack_eq_select - builds `rack == literal` (SEL_EQ over a numeric
|
||||
* SEL_RESNAME leaf and a SEL_NUM literal), the same hand-built tree shape
|
||||
* test_host_iter.c's own helper uses, generalized to take a resmgr-
|
||||
* assigned rack_idx instead of a hardcoded one. */
|
||||
static void make_rack_eq_select(xce_select_node_t* resname,
|
||||
xce_select_node_t* num, xce_select_node_t* eq,
|
||||
xce_select_t* select, double literal,
|
||||
int rack_idx)
|
||||
{
|
||||
memset(resname, 0, sizeof(*resname));
|
||||
resname->type = SEL_RESNAME;
|
||||
resname->u.resname.res_idx = rack_idx;
|
||||
resname->u.resname.res_type = RES_TYPE_NUMERIC;
|
||||
|
||||
memset(num, 0, sizeof(*num));
|
||||
num->type = SEL_NUM;
|
||||
num->u.numval = literal;
|
||||
|
||||
memset(eq, 0, sizeof(*eq));
|
||||
eq->type = SEL_EQ;
|
||||
eq->u.binop.left = resname;
|
||||
eq->u.binop.right = num;
|
||||
|
||||
select->root = eq;
|
||||
}
|
||||
|
||||
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()). */
|
||||
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`. Same accessor pattern
|
||||
* test_resource_accounting.c's own debit test uses. */
|
||||
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]);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Tests */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* Req 9.1, fixture 1: three candidate hosts, two of which fail the
|
||||
* resource-availability check and one of which satisfies -- the
|
||||
* satisfying host must be chosen, and it alone carries the provisional
|
||||
* debit. Hosts are inserted in an order that requires the loop to try (and
|
||||
* correctly reject) two failing candidates before reaching the one that
|
||||
* works -- a loop that stopped after the first failure, or that debited
|
||||
* the wrong host, would fail this test.
|
||||
*/
|
||||
static void
|
||||
test_three_hosts_only_resource_satisfying_host_is_chosen_and_debited(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_resource_t* slots_res;
|
||||
xce_sched_worker_t* host_a;
|
||||
xce_sched_worker_t* host_b;
|
||||
xce_sched_worker_t* host_c;
|
||||
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);
|
||||
|
||||
host_a = add_worker_with_slots(&(sched.core), "host-a", slots_res->idx,
|
||||
0.0f); /* no free slots -- resource-fails */
|
||||
host_b = add_worker_with_slots(&(sched.core), "host-b", slots_res->idx,
|
||||
0.0f); /* no free slots -- resource-fails */
|
||||
host_c = add_worker_with_slots(&(sched.core), "host-c", slots_res->idx,
|
||||
4.0f); /* plenty -- satisfies */
|
||||
|
||||
memset(&primary, 0, sizeof(primary));
|
||||
primary.minTasks = 1;
|
||||
primary.maxTasks = 1;
|
||||
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(1, ctx.alloc->nhosts);
|
||||
TEST_ASSERT_NOT_NULL(ctx.alloc->allocs[0]);
|
||||
TEST_ASSERT_EQUAL_STRING("host-c", ctx.alloc->allocs[0]->host);
|
||||
|
||||
assert_uncommitted_host_debit(&ctx, host_c, slots_res->idx, 1.0f);
|
||||
assert_uncommitted_host_debit(&ctx, host_a, slots_res->idx, 0.0f);
|
||||
assert_uncommitted_host_debit(&ctx, host_b, slots_res->idx, 0.0f);
|
||||
|
||||
sched_ctx_fini(&ctx);
|
||||
}
|
||||
|
||||
/*
|
||||
* Req 9.1, fixture 2: a host that fails the SELECT filter specifically
|
||||
* (not resource availability -- it has abundant free slots) must still be
|
||||
* skipped in favor of a host that both matches the select expression and
|
||||
* satisfies resources. This is the exact case task 3.2's own task text
|
||||
* and design.md's Testing Strategy call out by name: a test that never
|
||||
* exercises a select-filter failure would not actually prove equivalence.
|
||||
*/
|
||||
static void
|
||||
test_host_failing_select_filter_is_skipped_despite_plenty_of_resources(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_resource_t* slots_res;
|
||||
int rack_idx;
|
||||
xce_sched_worker_t* rack_nomatch;
|
||||
xce_sched_worker_t* rack_match;
|
||||
xce_sched_obj_t obj;
|
||||
xce_primary_schedobj_t primary_obj;
|
||||
xce_primary_resreq_t primary;
|
||||
xce_select_node_t resname, num, eq;
|
||||
xce_select_t select;
|
||||
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);
|
||||
rack_idx = slots_res->idx + 1;
|
||||
|
||||
/* rack-nomatch: 8 free slots (far more than the 1-slot want a
|
||||
* maxTasks==1 job stages) but the wrong rack -- inserted FIRST so a
|
||||
* regression that checked resource availability before (or instead
|
||||
* of) the select expression would wrongly pick it. */
|
||||
rack_nomatch = add_worker_with_slots_and_rack(
|
||||
&(sched.core), "rack-nomatch", slots_res->idx, 8.0f, rack_idx, 2.0);
|
||||
rack_match = add_worker_with_slots_and_rack(
|
||||
&(sched.core), "rack-match", slots_res->idx, 4.0f, rack_idx, 1.0);
|
||||
|
||||
make_rack_eq_select(&resname, &num, &eq, &select, 1.0, rack_idx);
|
||||
|
||||
memset(&primary, 0, sizeof(primary));
|
||||
primary.minTasks = 1;
|
||||
primary.maxTasks = 1;
|
||||
primary.select = &select;
|
||||
|
||||
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_STRING("rack-match", ctx.alloc->allocs[0]->host);
|
||||
|
||||
assert_uncommitted_host_debit(&ctx, rack_match, slots_res->idx, 1.0f);
|
||||
assert_uncommitted_host_debit(&ctx, rack_nomatch, slots_res->idx, 0.0f);
|
||||
|
||||
sched_ctx_fini(&ctx);
|
||||
}
|
||||
|
||||
/*
|
||||
* Req 9.1, fixture 3 (iteration-order sensitivity): two hosts that are
|
||||
* BOTH select-matching and resource-satisfying -- my_alloc() must pick
|
||||
* whichever one sched->core.workers' own iteration mechanism visits
|
||||
* FIRST. That mechanism -- iter_make()/iter_next() over the RTTI `var`
|
||||
* sched->core.workers -- is the exact same primitive the deleted
|
||||
* pre-rewrap sched_collect_workers() used (confirmed via `git show
|
||||
* b18c5f78^:src/scheduler/src/scheduler.c`) and the one host_iter.c uses
|
||||
* today (host_iter.c's own file header comment): same underlying object,
|
||||
* same traversal call, so "first in that order" is definitionally
|
||||
* identical pre- and post-rewrap. This test derives that order directly
|
||||
* (an independent iter_make()/iter_next() scan below) rather than
|
||||
* assuming worker-insertion order happens to match it, then asserts
|
||||
* my_alloc()'s pick against the derived value.
|
||||
*/
|
||||
static void
|
||||
test_pick_matches_first_host_in_the_shared_workers_iteration_order(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;
|
||||
iterator_t it;
|
||||
xce_sched_worker_t* first_in_order = NULL;
|
||||
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);
|
||||
|
||||
/* Two equally-valid hosts -- neither the select expression (unset)
|
||||
* nor resource availability (both have plenty) distinguishes them. */
|
||||
add_worker_with_slots(&(sched.core), "host-1", slots_res->idx, 4.0f);
|
||||
add_worker_with_slots(&(sched.core), "host-2", slots_res->idx, 4.0f);
|
||||
|
||||
/* Ground truth: the same RTTI iteration primitive host_iter.c (and,
|
||||
* pre-rewrap, sched_collect_workers()) uses over the same
|
||||
* sched.core.workers var. */
|
||||
rc = iter_make(&it, sched.core.workers);
|
||||
TEST_ASSERT_TRUE(rc >= 0);
|
||||
rc = iter_next(&it, (void**)&first_in_order);
|
||||
TEST_ASSERT_TRUE(rc > 0);
|
||||
TEST_ASSERT_NOT_NULL(first_in_order);
|
||||
iter_destroy(&it);
|
||||
|
||||
memset(&primary, 0, sizeof(primary));
|
||||
primary.minTasks = 1;
|
||||
primary.maxTasks = 1;
|
||||
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_STRING(first_in_order->name, ctx.alloc->allocs[0]->host);
|
||||
|
||||
sched_ctx_fini(&ctx);
|
||||
}
|
||||
|
||||
/*
|
||||
* Req 9.1, fixture 4: a select-failing host, THEN a resource-failing
|
||||
* host, THEN a succeeding host, in that exact order -- proves the
|
||||
* composition correctly skips two DIFFERENT kinds of failure back to back
|
||||
* without short-circuiting (stopping early) or mis-skipping (e.g.
|
||||
* skipping past the eventual success too).
|
||||
*/
|
||||
static void
|
||||
test_select_fail_then_resource_fail_then_success_does_not_short_circuit(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_resource_t* slots_res;
|
||||
int rack_idx;
|
||||
xce_sched_worker_t* sel_fail;
|
||||
xce_sched_worker_t* res_fail;
|
||||
xce_sched_worker_t* success;
|
||||
xce_sched_obj_t obj;
|
||||
xce_primary_schedobj_t primary_obj;
|
||||
xce_primary_resreq_t primary;
|
||||
xce_select_node_t resname, num, eq;
|
||||
xce_select_t select;
|
||||
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);
|
||||
rack_idx = slots_res->idx + 1;
|
||||
|
||||
sel_fail = add_worker_with_slots_and_rack(&(sched.core), "sel-fail",
|
||||
slots_res->idx, 8.0f, rack_idx,
|
||||
2.0 /* wrong rack */);
|
||||
res_fail = add_worker_with_slots_and_rack(&(sched.core), "res-fail",
|
||||
slots_res->idx, 0.0f, rack_idx,
|
||||
1.0 /* right rack, no slots */);
|
||||
success = add_worker_with_slots_and_rack(&(sched.core), "success",
|
||||
slots_res->idx, 4.0f, rack_idx,
|
||||
1.0 /* right rack, plenty */);
|
||||
|
||||
make_rack_eq_select(&resname, &num, &eq, &select, 1.0, rack_idx);
|
||||
|
||||
memset(&primary, 0, sizeof(primary));
|
||||
primary.minTasks = 1;
|
||||
primary.maxTasks = 1;
|
||||
primary.select = &select;
|
||||
|
||||
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_STRING("success", ctx.alloc->allocs[0]->host);
|
||||
|
||||
assert_uncommitted_host_debit(&ctx, success, slots_res->idx, 1.0f);
|
||||
assert_uncommitted_host_debit(&ctx, sel_fail, slots_res->idx, 0.0f);
|
||||
assert_uncommitted_host_debit(&ctx, res_fail, slots_res->idx, 0.0f);
|
||||
|
||||
sched_ctx_fini(&ctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(
|
||||
test_three_hosts_only_resource_satisfying_host_is_chosen_and_debited);
|
||||
RUN_TEST(
|
||||
test_host_failing_select_filter_is_skipped_despite_plenty_of_resources);
|
||||
RUN_TEST(
|
||||
test_pick_matches_first_host_in_the_shared_workers_iteration_order);
|
||||
RUN_TEST(
|
||||
test_select_fail_then_resource_fail_then_success_does_not_short_circuit);
|
||||
return UNITY_END();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue