test(scheduler-allocator-runner): characterize the parjob allocator (task 2.2)
15 tests drive the real parjob plugin through job_alloc_fn: success, short shared, pending on hosts, mid-plan rollback, task counts 0/1, missing inputs, six OOM steps with their debits, COMPOUND. Values the runner changes on purpose sit in *_today tests. Spec: the task showed that parjob records host reasons only for a bucket search's first rejected candidate (the rejected host's request stays in alloc_req.host and the host iterator pre-filters later hosts against it). Decision: the runner clears alloc_req.host before every candidate, for both allocators, so every short host gets its reasons (Req 3.6/3.7); publishing reasons only when they change is a follow-up. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
91a8580948
commit
b4470b2b94
6 changed files with 874 additions and 56 deletions
|
|
@ -1729,6 +1729,20 @@ sched_alloc_equivalence_seqjob_test = executable(
|
|||
'-Wl,--wrap=make_job_alloc'],
|
||||
install: false)
|
||||
|
||||
# scheduler-allocator-runner task 2.2 (Req 4.5, 5.1, 5.2, 5.4, 5.8, 7.2):
|
||||
# parjob characterization through the plugin's registered job_alloc_fn,
|
||||
# against today's allocator and (from task 4.1) the framework runner. Same
|
||||
# white-box stack and make_host_alloc/make_job_alloc wraps as
|
||||
# sched_alloc_equivalence_seqjob_test.
|
||||
sched_alloc_equivalence_parjob_test = executable(
|
||||
'sched_alloc_equivalence_parjob_test',
|
||||
['test_alloc_equivalence_parjob.c'],
|
||||
include_directories:[incdir],
|
||||
dependencies: [scheduler_test_deps],
|
||||
link_args: ['-Wl,--wrap=make_host_alloc',
|
||||
'-Wl,--wrap=make_job_alloc'],
|
||||
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
|
||||
|
|
@ -1998,6 +2012,9 @@ test('scheduler seqjob allocator test',
|
|||
# Leak detection stays on: every fixture tears down its ctx, core and plugin.
|
||||
test('scheduler alloc equivalence seqjob test',
|
||||
sched_alloc_equivalence_seqjob_test, suite: 'scheduler')
|
||||
# Leak detection stays on (as for the seqjob characterization above).
|
||||
test('scheduler alloc equivalence parjob test',
|
||||
sched_alloc_equivalence_parjob_test, suite: 'scheduler')
|
||||
test('scheduler seqjob rewrap equivalence test',
|
||||
sched_seqjob_rewrap_equivalence_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
|
|
|
|||
799
src/scheduler/testsuite/test_alloc_equivalence_parjob.c
Normal file
799
src/scheduler/testsuite/test_alloc_equivalence_parjob.c
Normal file
|
|
@ -0,0 +1,799 @@
|
|||
/*
|
||||
* test_alloc_equivalence_parjob.c - scheduler-allocator-runner task 2.2:
|
||||
* characterization of the parallel-job allocator (Req 4.5, 5.1, 5.2, 5.4,
|
||||
* 5.8, 7.2) through the registered plugin->job_alloc_fn, never the static
|
||||
* routine by name; must still hold once task 4.1 switches parjob to the
|
||||
* framework runner. Stubs and OOM injection are those of
|
||||
* test_alloc_equivalence_seqjob.c: sched_make_allocs() failing on call N
|
||||
* (claimed hosts' records in claim order, then shared records) and
|
||||
* --wrap'ed make_host_alloc/make_job_alloc failing on call N; the
|
||||
* host-array allocation is not injected (task 3.3).
|
||||
*
|
||||
* Values the switch changes on purpose sit in `_today` tests: bug-1893
|
||||
* (Req 2.2) and host reasons (Req 3.6/3.7). Today the host request is
|
||||
* reset once per bucket, so later short candidates are pre-filtered
|
||||
* (host_iter.c) against the request the first one left and get no reason;
|
||||
* the runner clears it before every candidate. Stable tests assert only
|
||||
* reason entries both rules give. bug-1892 layouts are kept out.
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "preason.h"
|
||||
#include "resobj.h"
|
||||
#include "resreq.h"
|
||||
#include "sched/alloc_iter.h"
|
||||
#include "scheduler.h"
|
||||
#include "xcealloc.h"
|
||||
#include "xstring.h"
|
||||
|
||||
/* White-box sources under test. Task 4.1 adds the framework runner
|
||||
* ("../src/alloc_runner.c") as one more line of this list. */
|
||||
#include "../src/core.c"
|
||||
#include "../src/resource.c"
|
||||
#include "../src/schedobj.c"
|
||||
#include "../src/host_iter.c"
|
||||
#include "../src/plugins/parjob_allocator.c"
|
||||
|
||||
/* Link-satisfying stubs (same convention as test_seqjob_allocator.c). */
|
||||
int plugin_register(void* s, void* p) { return (void)s, (void)p, 0; }
|
||||
void plugin_unregister(void* arg) { (void)arg; }
|
||||
|
||||
/* plugin.c's fan-out with parjob's hook only: reset the request vectors as
|
||||
* plugin.c does (job-level pass: .shared and .host, bug-1883; per-host
|
||||
* pass: .host), then call the hook. */
|
||||
int plugin_prepare_allocation(xce_sched_core_t* 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);
|
||||
}
|
||||
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;
|
||||
fake_plugin.core = sched;
|
||||
|
||||
return parjob_prepare_allocation(&fake_plugin, ctx);
|
||||
}
|
||||
|
||||
int plugin_job_update(void* s, xce_sched_job_t* j)
|
||||
{
|
||||
return (void)s, (void)j, 0;
|
||||
}
|
||||
|
||||
int plugin_queue_update(void* s, xce_sched_queue_t* q)
|
||||
{
|
||||
return (void)s, (void)q, 0;
|
||||
}
|
||||
|
||||
int plugin_job_rusage_update(void* s, xce_sched_job_t* j, xce_job_rusages_t* r)
|
||||
{
|
||||
return (void)s, (void)j, (void)r, 0;
|
||||
}
|
||||
|
||||
/* Counters count every call; fail_on N > 0 fails call N (NULL, ENOMEM). */
|
||||
static struct {
|
||||
int make_allocs_calls, make_allocs_fail_on;
|
||||
int host_alloc_calls, host_alloc_fail_on;
|
||||
int job_alloc_calls, job_alloc_fail_on;
|
||||
} g;
|
||||
|
||||
static void oom_injection_reset(void) { memset(&g, 0, sizeof(g)); }
|
||||
|
||||
/* Stub for scheduler.c's sched_make_allocs(): real make_alloc() records
|
||||
* (resmgr name, requested value, empty reservation, duration -1 for a NULL
|
||||
* duration array, instid 0); NULL for nres <= 0 as the real one. */
|
||||
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;
|
||||
|
||||
g.make_allocs_calls++;
|
||||
if ((g.make_allocs_fail_on > 0)
|
||||
&& (g.make_allocs_calls == g.make_allocs_fail_on)) {
|
||||
errno = ENOMEM;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if ((sched == NULL) || (nres <= 0) || (idxs == NULL) || (vals == NULL)) {
|
||||
errno = EINVAL;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
allocs = (xce_alloc_t**)calloc(nres, sizeof(*allocs));
|
||||
if (allocs == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (i = 0; i < nres; i++) {
|
||||
xce_resource_t* r = NULL;
|
||||
|
||||
vget(sched->core.resmgr, idxs[i], (void**)&r);
|
||||
if (r == NULL) {
|
||||
continue;
|
||||
}
|
||||
allocs[i] = make_alloc(r->name, reserve, 0, vals[i],
|
||||
(duration != NULL) ? duration[i] : -1);
|
||||
if (allocs[i] == NULL) {
|
||||
free_ResAlloc_list(nres, allocs);
|
||||
errno = ENOMEM;
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
return allocs;
|
||||
}
|
||||
|
||||
xce_host_alloc_t* __real_make_host_alloc(char*, int, xce_alloc_t**);
|
||||
xce_job_alloc_t* __real_make_job_alloc(uint64_t, int, xce_host_alloc_t**);
|
||||
|
||||
xce_host_alloc_t* __wrap_make_host_alloc(char* host, int nalloc,
|
||||
xce_alloc_t** allocs)
|
||||
{
|
||||
g.host_alloc_calls++;
|
||||
if ((g.host_alloc_fail_on > 0)
|
||||
&& (g.host_alloc_calls == g.host_alloc_fail_on)) {
|
||||
errno = ENOMEM;
|
||||
return NULL;
|
||||
}
|
||||
return __real_make_host_alloc(host, nalloc, allocs);
|
||||
}
|
||||
|
||||
xce_job_alloc_t* __wrap_make_job_alloc(uint64_t jobid, int nhost,
|
||||
xce_host_alloc_t** hallocs)
|
||||
{
|
||||
g.job_alloc_calls++;
|
||||
if ((g.job_alloc_fail_on > 0)
|
||||
&& (g.job_alloc_calls == g.job_alloc_fail_on)) {
|
||||
errno = ENOMEM;
|
||||
return NULL;
|
||||
}
|
||||
return __real_make_job_alloc(jobid, nhost, hallocs);
|
||||
}
|
||||
|
||||
void setUp(void) { oom_injection_reset(); }
|
||||
void tearDown(void) { oom_injection_reset(); }
|
||||
|
||||
#define HOST_A "pq-host-a"
|
||||
#define HOST_B "pq-host-b"
|
||||
#define HOST_C "pq-host-c"
|
||||
|
||||
typedef struct {
|
||||
xce_scheduler_t sched;
|
||||
xce_resource_t* slots;
|
||||
xce_resource_t* mem;
|
||||
xce_resource_t* rack;
|
||||
xce_resource_t* lic; /* shared (XCE_RESF_GLOBAL) */
|
||||
xce_sched_plugin_t* plugin;
|
||||
xce_sched_obj_t obj;
|
||||
xce_primary_schedobj_t primary_obj;
|
||||
xce_sched_ctx_t ctx;
|
||||
int ctx_ready;
|
||||
} eq_fixture_t;
|
||||
|
||||
/* One primary request: `mem` per task, `lic` per job (0 = not requested),
|
||||
* optional `rack == N` select. */
|
||||
typedef struct {
|
||||
xce_primary_resreq_t req;
|
||||
rusage_value_t mem_rusage;
|
||||
rusage_value_t lic_rusage;
|
||||
xce_select_t select;
|
||||
xce_select_node_t sel_res, sel_num, sel_eq;
|
||||
} eq_primary_t;
|
||||
|
||||
/* Shared pool "licenses" has `lic_total` capacity, none in use. */
|
||||
static void eq_fixture_init(eq_fixture_t* f, float lic_total)
|
||||
{
|
||||
int idx[1];
|
||||
float val[1];
|
||||
|
||||
memset(f, 0, sizeof(*f));
|
||||
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(f->sched.core)));
|
||||
/* xce_sched_core_fini() finalizes these; both drivers calloc them */
|
||||
for (idx[0] = 0; idx[0] < 2; idx[0]++) {
|
||||
f->sched.core.pend_updates[idx[0]]
|
||||
= (dataq_t*)calloc(1, sizeof(dataq_t));
|
||||
TEST_ASSERT_NOT_NULL(f->sched.core.pend_updates[idx[0]]);
|
||||
dataq_init(f->sched.core.pend_updates[idx[0]], NULL);
|
||||
}
|
||||
|
||||
#define EQ_RES(n, fl) \
|
||||
makeResource(f->sched.core.resmgr, &(xce_resource_t){ .name = (char*)n, \
|
||||
.flags = fl })
|
||||
f->slots = EQ_RES("slots", 0);
|
||||
f->mem = EQ_RES("mem", 0);
|
||||
f->rack = EQ_RES("rack", 0);
|
||||
f->lic = EQ_RES("licenses", XCE_RESF_GLOBAL);
|
||||
#undef EQ_RES
|
||||
TEST_ASSERT_NOT_NULL(f->slots);
|
||||
TEST_ASSERT_NOT_NULL(f->mem);
|
||||
TEST_ASSERT_NOT_NULL(f->rack);
|
||||
TEST_ASSERT_NOT_NULL(f->lic);
|
||||
|
||||
idx[0] = f->lic->idx;
|
||||
val[0] = lic_total;
|
||||
put_resvals(&(f->sched.core.sharedInst), 1, idx, val);
|
||||
|
||||
f->plugin = parjob_allocator_new_plugin();
|
||||
TEST_ASSERT_NOT_NULL(f->plugin);
|
||||
TEST_ASSERT_NOT_NULL(f->plugin->job_alloc_fn);
|
||||
f->plugin->sched = &(f->sched);
|
||||
f->plugin->core = &(f->sched.core);
|
||||
}
|
||||
|
||||
static xce_sched_worker_t* eq_add_host(eq_fixture_t* f, const char* name,
|
||||
float slots, float mem, float rack)
|
||||
{
|
||||
xce_sched_worker_t* w;
|
||||
int idx[3] = { f->slots->idx, f->mem->idx, f->rack->idx };
|
||||
float total[3] = { slots, mem, rack };
|
||||
float inuse[2] = { 0.0f, 0.0f };
|
||||
|
||||
w = (xce_sched_worker_t*)calloc(1, sizeof(*w));
|
||||
TEST_ASSERT_NOT_NULL(w);
|
||||
w->name = makeString((char*)name);
|
||||
TEST_ASSERT_NOT_NULL(w->name);
|
||||
zarray_init(&(w->resvals), 8, (zarray_item_killer_t)kill_sched_resval);
|
||||
zarray_init(&(w->resInUse), 8, (zarray_item_killer_t)kill_sched_resval);
|
||||
put_resvals(&(w->resvals), 3, idx, total);
|
||||
put_resvals(&(w->resInUse), 2, idx, inuse);
|
||||
|
||||
TEST_ASSERT_TRUE(sched_add_worker(&(f->sched.core), w) >= 0);
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
static void eq_primary_init(eq_primary_t* p, int max_tasks, float mem,
|
||||
float lic, const eq_fixture_t* f, int rack)
|
||||
{
|
||||
memset(p, 0, sizeof(*p));
|
||||
p->req.minTasks = (max_tasks > 0) ? 1 : 0;
|
||||
p->req.maxTasks = max_tasks;
|
||||
xlist_init(&(p->req.rusages), 4);
|
||||
|
||||
if (mem > 0.0f) {
|
||||
p->mem_rusage.res = (char*)"mem";
|
||||
p->mem_rusage.value = mem;
|
||||
p->mem_rusage.per_flag = PER_TASK;
|
||||
xlist_store(&(p->req.rusages), &(p->mem_rusage));
|
||||
}
|
||||
if (lic > 0.0f) {
|
||||
p->lic_rusage.res = (char*)"licenses";
|
||||
p->lic_rusage.value = lic;
|
||||
p->lic_rusage.per_flag = PER_JOB;
|
||||
xlist_store(&(p->req.rusages), &(p->lic_rusage));
|
||||
}
|
||||
if (rack > 0) {
|
||||
p->sel_res.type = SEL_RESNAME;
|
||||
p->sel_res.u.resname.res_idx = f->rack->idx;
|
||||
p->sel_res.u.resname.res_type = RES_TYPE_NUMERIC;
|
||||
p->sel_num.type = SEL_NUM;
|
||||
p->sel_num.u.numval = (double)rack;
|
||||
p->sel_eq.type = SEL_EQ;
|
||||
p->sel_eq.u.binop.left = &(p->sel_res);
|
||||
p->sel_eq.u.binop.right = &(p->sel_num);
|
||||
p->select.root = &(p->sel_eq);
|
||||
p->req.select = &(p->select);
|
||||
}
|
||||
}
|
||||
|
||||
/* ctx over a PRIMARY obj for `p`, job-level prepare pass included
|
||||
* (sched_ctx_set_primary() -> plugin_prepare_allocation(), ctx->worker
|
||||
* NULL), exactly as sched_make_primary_allocation() drives it. */
|
||||
static void eq_ctx_begin_primary(eq_fixture_t* f, eq_primary_t* p)
|
||||
{
|
||||
f->primary_obj.resreq = &(p->req);
|
||||
f->obj.tag = XCE_RESREQ_PRIMARY;
|
||||
f->obj.u.primary = &(f->primary_obj);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, sched_ctx_init(&(f->ctx), &(f->sched.core),
|
||||
&(f->obj)));
|
||||
f->ctx_ready = 1;
|
||||
sched_ctx_set_primary(&(f->ctx), &(p->req));
|
||||
}
|
||||
|
||||
static int eq_alloc(eq_fixture_t* f)
|
||||
{
|
||||
return f->plugin->job_alloc_fn(f->plugin, &(f->ctx));
|
||||
}
|
||||
|
||||
static void eq_fixture_fini(eq_fixture_t* f)
|
||||
{
|
||||
if (f->ctx_ready) {
|
||||
free_JobAlloc_ptr(&(f->ctx.alloc));
|
||||
free_PendReason_ptr(&(f->ctx.reason));
|
||||
sched_ctx_fini(&(f->ctx));
|
||||
}
|
||||
f->plugin->fini_fn(f->plugin);
|
||||
xce_sched_core_fini(&(f->sched.core));
|
||||
/* xce_sched_core_init() creates the process-wide string table on every
|
||||
* call and xce_sched_core_fini() leaves it; release it per fixture */
|
||||
xstring_table_fini();
|
||||
}
|
||||
|
||||
static void eq_primary_fini(eq_primary_t* p) { xlist_fini(&(p->req.rusages)); }
|
||||
|
||||
static void eq_finish(eq_fixture_t* f, eq_primary_t* p)
|
||||
{
|
||||
eq_fixture_fini(f);
|
||||
eq_primary_fini(p);
|
||||
}
|
||||
|
||||
static float eq_host_uncomit(eq_fixture_t* f, xce_sched_worker_t* w,
|
||||
xce_resource_t* r)
|
||||
{
|
||||
xvec_t* v = (xvec_t*)zarray_get(&(f->ctx.uncomitted.hosts), w->idx);
|
||||
|
||||
return (v == NULL) ? 0.0f : ((float*)v->data)[r->idx];
|
||||
}
|
||||
|
||||
/* Per-host debit left in the attempt context: mem then slots. */
|
||||
static void eq_assert_debit(eq_fixture_t* f, xce_sched_worker_t* w, float mem,
|
||||
float slots)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_FLOAT(mem, eq_host_uncomit(f, w, f->mem));
|
||||
TEST_ASSERT_EQUAL_FLOAT(slots, eq_host_uncomit(f, w, f->slots));
|
||||
}
|
||||
|
||||
static float eq_shared_uncomit(eq_fixture_t* f)
|
||||
{
|
||||
return ((float*)f->ctx.uncomitted.shared->data)[f->lic->idx];
|
||||
}
|
||||
|
||||
static void eq_assert_record(xce_alloc_t* a, const char* resname, float value)
|
||||
{
|
||||
TEST_ASSERT_NOT_NULL(a);
|
||||
TEST_ASSERT_EQUAL_STRING(resname, a->resname);
|
||||
TEST_ASSERT_EQUAL_STRING("", a->reserve);
|
||||
TEST_ASSERT_EQUAL_FLOAT(value, a->value);
|
||||
TEST_ASSERT_EQUAL_INT(-1, a->duration);
|
||||
}
|
||||
|
||||
/* One host allocation carrying mem then slots (parjob's per-host prepare
|
||||
* computes the rusage wants first, then stores the bucket's slot count). */
|
||||
static void eq_assert_host(xce_host_alloc_t* ha, const char* host, float mem,
|
||||
float slots)
|
||||
{
|
||||
TEST_ASSERT_NOT_NULL(ha);
|
||||
TEST_ASSERT_EQUAL_STRING(host, ha->host);
|
||||
TEST_ASSERT_EQUAL_INT(2, ha->nallocs);
|
||||
TEST_ASSERT_NOT_NULL(ha->allocs);
|
||||
eq_assert_record(ha->allocs[0], "mem", mem);
|
||||
eq_assert_record(ha->allocs[1], "slots", slots);
|
||||
}
|
||||
|
||||
static void eq_assert_reason_counts(eq_fixture_t* f, int main_reason,
|
||||
int nhost, int nres)
|
||||
{
|
||||
TEST_ASSERT_NOT_NULL(f->ctx.reason);
|
||||
TEST_ASSERT_NOT_NULL(f->ctx.reason->reasons);
|
||||
TEST_ASSERT_EQUAL_INT(main_reason, f->ctx.reason->reasons->main_reason);
|
||||
TEST_ASSERT_EQUAL_INT(nhost, f->ctx.reason->reasons->nhost_reasons);
|
||||
TEST_ASSERT_EQUAL_INT(nres, f->ctx.reason->reasons->nres_reasons);
|
||||
}
|
||||
|
||||
static void eq_assert_res_reason(eq_fixture_t* f, int i, const char* host,
|
||||
const char* res)
|
||||
{
|
||||
xce_res_reason_t* rr = f->ctx.reason->reasons->res_based_reasons[i];
|
||||
|
||||
TEST_ASSERT_NOT_NULL(rr);
|
||||
TEST_ASSERT_EQUAL_STRING(host, rr->host);
|
||||
TEST_ASSERT_EQUAL_STRING(res, rr->resource);
|
||||
TEST_ASSERT_EQUAL_INT(PREASON_NO_RESOURCE, rr->reason);
|
||||
}
|
||||
|
||||
static void eq_assert_calls(int make_allocs, int host_alloc, int job_alloc)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(make_allocs, g.make_allocs_calls);
|
||||
TEST_ASSERT_EQUAL_INT(host_alloc, g.host_alloc_calls);
|
||||
TEST_ASSERT_EQUAL_INT(job_alloc, g.job_alloc_calls);
|
||||
}
|
||||
|
||||
/* A fixture: `pool` shared licenses; hosts A, B, C (rack 1) with the given
|
||||
* slots and mem; a job of `tasks` tasks, `task_mem` mem per task and `lic`
|
||||
* licenses per job. */
|
||||
typedef struct {
|
||||
float pool;
|
||||
int nhosts;
|
||||
float slots[3], mem[3];
|
||||
int tasks;
|
||||
float task_mem, lic;
|
||||
} eq_spec_t;
|
||||
|
||||
/* 1: plan [4] fails (slots), plan [2,2] claims A then B */
|
||||
static const eq_spec_t EQ_TWO_BUCKETS = { 10, 2, { 2, 2 }, { 8, 8 }, 4, 1, 3 };
|
||||
/* 2: 2 licenses, 3 wanted */
|
||||
static const eq_spec_t EQ_SHORT_SHARED = { 2, 2, { 4, 4 }, { 8, 8 }, 2, 1, 3 };
|
||||
/* 3: plans [2] and [1,1] both fail (mem, and slots for [2]) */
|
||||
static const eq_spec_t EQ_SHORT_HOSTS = { 10, 2, { 1, 1 }, { 1, 1 }, 2, 4, 3 };
|
||||
/* 4: [4] fails; [2,2] claims A, fails bucket 2, releases A; [2,1,1] wins */
|
||||
static const eq_spec_t EQ_ROLLBACK
|
||||
= { 10, 3, { 2, 1, 1 }, { 8, 8, 8 }, 4, 1, 3 };
|
||||
|
||||
static void eq_setup(eq_fixture_t* f, eq_primary_t* p, const eq_spec_t* s,
|
||||
xce_sched_worker_t** w)
|
||||
{
|
||||
static const char* names[3] = { HOST_A, HOST_B, HOST_C };
|
||||
int i;
|
||||
|
||||
eq_fixture_init(f, s->pool);
|
||||
for (i = 0; i < s->nhosts; i++) {
|
||||
w[i] = eq_add_host(f, names[i], s->slots[i], s->mem[i], 1.0f);
|
||||
}
|
||||
eq_primary_init(p, s->tasks, s->task_mem, s->lic, f, 0);
|
||||
eq_ctx_begin_primary(f, p);
|
||||
}
|
||||
|
||||
static int eq_run(eq_fixture_t* f, eq_primary_t* p, const eq_spec_t* s,
|
||||
xce_sched_worker_t** w)
|
||||
{
|
||||
eq_setup(f, p, s, w);
|
||||
return eq_alloc(f);
|
||||
}
|
||||
|
||||
/* Main and host reasons none; at least `n` resource reasons, the first `n`
|
||||
* of which both rules give (count asserted only in `_today` tests). */
|
||||
static void eq_assert_reason_prefix(eq_fixture_t* f, int n)
|
||||
{
|
||||
TEST_ASSERT_NOT_NULL(f->ctx.reason);
|
||||
TEST_ASSERT_EQUAL_INT(0, f->ctx.reason->reasons->main_reason);
|
||||
TEST_ASSERT_EQUAL_INT(0, f->ctx.reason->reasons->nhost_reasons);
|
||||
TEST_ASSERT_TRUE(f->ctx.reason->reasons->nres_reasons >= n);
|
||||
}
|
||||
|
||||
/* EQ_TWO_BUCKETS: plan [4] checks A first (slots short) under both rules. */
|
||||
static void eq_assert_plan1_reasons(eq_fixture_t* f)
|
||||
{
|
||||
eq_assert_reason_prefix(f, 1);
|
||||
eq_assert_res_reason(f, 0, HOST_A, "slots");
|
||||
}
|
||||
|
||||
/* 1. Multi-bucket success: two host allocations in claim order, each sized
|
||||
* from its own bucket (2 tasks), one shared record. */
|
||||
static void test_parjob_success_two_buckets_with_shared(void)
|
||||
{
|
||||
eq_fixture_t f;
|
||||
eq_primary_t p;
|
||||
xce_sched_worker_t* w[3];
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, eq_run(&f, &p, &EQ_TWO_BUCKETS, w));
|
||||
|
||||
TEST_ASSERT_NOT_NULL(f.ctx.alloc);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, f.ctx.alloc->jobid);
|
||||
TEST_ASSERT_EQUAL_INT(2, f.ctx.alloc->nhosts);
|
||||
eq_assert_host(f.ctx.alloc->allocs[0], HOST_A, 2.0f, 2.0f);
|
||||
eq_assert_host(f.ctx.alloc->allocs[1], HOST_B, 2.0f, 2.0f);
|
||||
TEST_ASSERT_EQUAL_INT(1, f.ctx.alloc->nres_shared);
|
||||
eq_assert_record(f.ctx.alloc->allocs_shared[0], "licenses", 3.0f);
|
||||
|
||||
eq_assert_debit(&f, w[0], 2.0f, 2.0f);
|
||||
eq_assert_debit(&f, w[1], 2.0f, 2.0f);
|
||||
TEST_ASSERT_EQUAL_FLOAT(3.0f, eq_shared_uncomit(&f));
|
||||
|
||||
eq_assert_plan1_reasons(&f);
|
||||
eq_assert_calls(3, 2, 1);
|
||||
|
||||
eq_finish(&f, &p);
|
||||
}
|
||||
|
||||
/* 2. Pending on the one shared resource the job requests, which is short:
|
||||
* no host is tried and nothing is counted. */
|
||||
static void test_parjob_pending_on_short_shared_resource(void)
|
||||
{
|
||||
eq_fixture_t f;
|
||||
eq_primary_t p;
|
||||
xce_sched_worker_t* w[3];
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, eq_run(&f, &p, &EQ_SHORT_SHARED, w));
|
||||
TEST_ASSERT_NULL(f.ctx.alloc);
|
||||
TEST_ASSERT_NOT_NULL(f.ctx.reason);
|
||||
TEST_ASSERT_EQUAL_INT(0, f.ctx.reason->reasons->main_reason);
|
||||
TEST_ASSERT_EQUAL_INT(0, f.ctx.reason->reasons->nhost_reasons);
|
||||
|
||||
TEST_ASSERT_NULL(zarray_get(&(f.ctx.uncomitted.hosts), w[0]->idx));
|
||||
TEST_ASSERT_NULL(zarray_get(&(f.ctx.uncomitted.hosts), w[1]->idx));
|
||||
TEST_ASSERT_EQUAL_FLOAT(0.0f, eq_shared_uncomit(&f));
|
||||
eq_assert_calls(0, 0, 0);
|
||||
|
||||
eq_finish(&f, &p);
|
||||
}
|
||||
|
||||
/* 2, resource reasons, TODAY's behaviour: none. The reason loop skips
|
||||
* every unmet entry because vget() returns 0 on success and the loop
|
||||
* tests `rc <= 0` (bug-1893). Requirement 2.2 changes this in task 4.1
|
||||
* (one host-less NO_RESOURCE reason naming "licenses"). */
|
||||
static void test_parjob_pending_on_short_shared_no_reason_today(void)
|
||||
{
|
||||
eq_fixture_t f;
|
||||
eq_primary_t p;
|
||||
xce_sched_worker_t* w[3];
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, eq_run(&f, &p, &EQ_SHORT_SHARED, w));
|
||||
eq_assert_reason_counts(&f, 0, 0, 0);
|
||||
|
||||
eq_finish(&f, &p);
|
||||
}
|
||||
|
||||
/* 3. Pending on hosts: no debit remains; the met shared request stays
|
||||
* counted as uncommitted. */
|
||||
static void test_parjob_pending_on_hosts(void)
|
||||
{
|
||||
eq_fixture_t f;
|
||||
eq_primary_t p;
|
||||
xce_sched_worker_t* w[3];
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, eq_run(&f, &p, &EQ_SHORT_HOSTS, w));
|
||||
TEST_ASSERT_NULL(f.ctx.alloc);
|
||||
|
||||
/* plan [2]'s first candidate A, under both rules */
|
||||
eq_assert_reason_prefix(&f, 2);
|
||||
eq_assert_res_reason(&f, 0, HOST_A, "mem");
|
||||
eq_assert_res_reason(&f, 1, HOST_A, "slots");
|
||||
|
||||
eq_assert_debit(&f, w[0], 0.0f, 0.0f);
|
||||
eq_assert_debit(&f, w[1], 0.0f, 0.0f);
|
||||
TEST_ASSERT_EQUAL_FLOAT(3.0f, eq_shared_uncomit(&f));
|
||||
eq_assert_calls(0, 0, 0);
|
||||
|
||||
eq_finish(&f, &p);
|
||||
}
|
||||
|
||||
/* 4. Mid-plan rollback, then a later plan wins: only the winning plan's
|
||||
* debits remain (A holds 2 slots, not 4). */
|
||||
static void test_parjob_mid_plan_rollback_then_later_plan_wins(void)
|
||||
{
|
||||
eq_fixture_t f;
|
||||
eq_primary_t p;
|
||||
xce_sched_worker_t* w[3];
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, eq_run(&f, &p, &EQ_ROLLBACK, w));
|
||||
|
||||
TEST_ASSERT_NOT_NULL(f.ctx.alloc);
|
||||
TEST_ASSERT_EQUAL_INT(3, f.ctx.alloc->nhosts);
|
||||
eq_assert_host(f.ctx.alloc->allocs[0], HOST_A, 2.0f, 2.0f);
|
||||
eq_assert_host(f.ctx.alloc->allocs[1], HOST_B, 1.0f, 1.0f);
|
||||
eq_assert_host(f.ctx.alloc->allocs[2], HOST_C, 1.0f, 1.0f);
|
||||
TEST_ASSERT_EQUAL_INT(1, f.ctx.alloc->nres_shared);
|
||||
eq_assert_record(f.ctx.alloc->allocs_shared[0], "licenses", 3.0f);
|
||||
|
||||
eq_assert_debit(&f, w[0], 2.0f, 2.0f);
|
||||
eq_assert_debit(&f, w[1], 1.0f, 1.0f);
|
||||
eq_assert_debit(&f, w[2], 1.0f, 1.0f);
|
||||
TEST_ASSERT_EQUAL_FLOAT(3.0f, eq_shared_uncomit(&f));
|
||||
|
||||
/* A then B, slots: plan [4]'s first two checks (new rule), or plan
|
||||
* [4]'s A and plan [2,2] bucket 2's B (today) */
|
||||
eq_assert_reason_prefix(&f, 2);
|
||||
eq_assert_res_reason(&f, 0, HOST_A, "slots");
|
||||
eq_assert_res_reason(&f, 1, HOST_B, "slots");
|
||||
eq_assert_calls(4, 3, 1);
|
||||
|
||||
eq_finish(&f, &p);
|
||||
}
|
||||
|
||||
/* 1, 3, 4: host reasons, TODAY's behaviour (stale pre-filter): only each
|
||||
* bucket search's first short candidate gets reasons. Req 3.6/3.7 change
|
||||
* this in task 4.1 to every selection-matching unclaimed short host:
|
||||
* 1 -> (A,slots) (B,slots); 3 -> A mem, A slots, B mem, B slots, A mem,
|
||||
* B mem; 4 -> A, B, C (plan [4]) then B, C (plan [2,2] bucket 2), slots. */
|
||||
static void test_parjob_host_reasons_first_candidate_only_today(void)
|
||||
{
|
||||
eq_fixture_t f;
|
||||
eq_primary_t p;
|
||||
xce_sched_worker_t* w[3];
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, eq_run(&f, &p, &EQ_TWO_BUCKETS, w));
|
||||
eq_assert_reason_counts(&f, 0, 0, 1);
|
||||
eq_finish(&f, &p);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, eq_run(&f, &p, &EQ_SHORT_HOSTS, w));
|
||||
eq_assert_reason_counts(&f, 0, 0, 3);
|
||||
eq_assert_res_reason(&f, 2, HOST_A, "mem");
|
||||
eq_finish(&f, &p);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, eq_run(&f, &p, &EQ_ROLLBACK, w));
|
||||
eq_assert_reason_counts(&f, 0, 0, 2);
|
||||
eq_finish(&f, &p);
|
||||
}
|
||||
|
||||
/* One host (4 slots, 8 mem), 10 licenses; 2 tasks of 1 mem, 3 licenses. */
|
||||
static const eq_spec_t EQ_ONE_HOST = { 10, 1, { 4 }, { 8 }, 2, 1, 3 };
|
||||
|
||||
/* Nothing checked, counted or built, and no pend-reason record. */
|
||||
static void eq_assert_untouched(eq_fixture_t* f, xce_sched_worker_t* a)
|
||||
{
|
||||
TEST_ASSERT_NULL(f->ctx.alloc);
|
||||
TEST_ASSERT_NULL(f->ctx.reason);
|
||||
TEST_ASSERT_NULL(zarray_get(&(f->ctx.uncomitted.hosts), a->idx));
|
||||
TEST_ASSERT_EQUAL_FLOAT(0.0f, eq_shared_uncomit(f));
|
||||
eq_assert_calls(0, 0, 0);
|
||||
}
|
||||
|
||||
/* 5. A task count of 0 or 1 reaching parjob directly (bug-1767, Req 5.2):
|
||||
* pending, no pend-reason record, no resource check. */
|
||||
static void test_parjob_task_count_zero_and_one_pend_without_reason(void)
|
||||
{
|
||||
int tasks;
|
||||
|
||||
for (tasks = 0; tasks <= 1; tasks++) {
|
||||
eq_fixture_t f;
|
||||
eq_primary_t p;
|
||||
xce_sched_worker_t* w[3];
|
||||
eq_spec_t s = EQ_ONE_HOST;
|
||||
|
||||
s.tasks = tasks;
|
||||
TEST_ASSERT_EQUAL_INT(0, eq_run(&f, &p, &s, w));
|
||||
eq_assert_untouched(&f, w[0]);
|
||||
eq_finish(&f, &p);
|
||||
}
|
||||
}
|
||||
|
||||
/* 6. Missing core, scheduler back-reference or primary (Req 5.1):
|
||||
* -EINVAL, no pend-reason record, nothing checked or counted. */
|
||||
static void test_parjob_missing_input_einval_no_reason(void)
|
||||
{
|
||||
int which;
|
||||
|
||||
for (which = 0; which < 3; which++) {
|
||||
eq_fixture_t f;
|
||||
eq_primary_t p;
|
||||
xce_sched_worker_t* w[3];
|
||||
|
||||
eq_setup(&f, &p, &EQ_ONE_HOST, w);
|
||||
if (which == 0) {
|
||||
f.plugin->core = NULL;
|
||||
} else if (which == 1) {
|
||||
f.plugin->sched = NULL;
|
||||
} else {
|
||||
f.ctx.primary = NULL;
|
||||
}
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, eq_alloc(&f));
|
||||
eq_assert_untouched(&f, w[0]);
|
||||
eq_finish(&f, &p);
|
||||
}
|
||||
}
|
||||
|
||||
/* 7. Out of memory while building the result of EQ_TWO_BUCKETS (claims A
|
||||
* then B). Host stage: the claims not yet turned into a host allocation
|
||||
* are released, converted ones keep their debit; job allocation and shared
|
||||
* records: every debit stays (design Error table; today's parjob too).
|
||||
* `a_kept` / `b_kept`: whether A's / B's debit (2 mem, 2 slots) remains.
|
||||
* Cases: records of host 0 / host 1 (sched_make_allocs() call 1 / 2),
|
||||
* make_host_alloc() for host 0 / host 1, make_job_alloc(), shared records
|
||||
* (sched_make_allocs() call 3, after both hosts). */
|
||||
static void eq_check_oom(int make_allocs_fail_on, int host_alloc_fail_on,
|
||||
int job_alloc_fail_on, int a_kept, int b_kept,
|
||||
int make_allocs, int host_alloc, int job_alloc)
|
||||
{
|
||||
eq_fixture_t f;
|
||||
eq_primary_t p;
|
||||
xce_sched_worker_t* w[3];
|
||||
|
||||
g.make_allocs_fail_on = make_allocs_fail_on;
|
||||
g.host_alloc_fail_on = host_alloc_fail_on;
|
||||
g.job_alloc_fail_on = job_alloc_fail_on;
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-ENOMEM, eq_run(&f, &p, &EQ_TWO_BUCKETS, w));
|
||||
TEST_ASSERT_NULL(f.ctx.alloc);
|
||||
|
||||
eq_assert_debit(&f, w[0], a_kept ? 2.0f : 0.0f, a_kept ? 2.0f : 0.0f);
|
||||
eq_assert_debit(&f, w[1], b_kept ? 2.0f : 0.0f, b_kept ? 2.0f : 0.0f);
|
||||
TEST_ASSERT_EQUAL_FLOAT(3.0f, eq_shared_uncomit(&f));
|
||||
eq_assert_plan1_reasons(&f);
|
||||
eq_assert_calls(make_allocs, host_alloc, job_alloc);
|
||||
|
||||
eq_finish(&f, &p);
|
||||
}
|
||||
|
||||
/* One named test per injected step: fail-on counts, kept debits, calls. */
|
||||
#define EQ_OOM_TEST(name, ...) \
|
||||
static void name(void) { eq_check_oom(__VA_ARGS__); }
|
||||
EQ_OOM_TEST(test_parjob_oom_records_host0_releases_all,
|
||||
1, 0, 0, 0, 0, 1, 0, 0)
|
||||
EQ_OOM_TEST(test_parjob_oom_records_later_host_releases_unconverted,
|
||||
2, 0, 0, 1, 0, 2, 1, 0)
|
||||
EQ_OOM_TEST(test_parjob_oom_host_alloc0_releases_all,
|
||||
0, 1, 0, 0, 0, 1, 1, 0)
|
||||
EQ_OOM_TEST(test_parjob_oom_host_alloc_later_releases_unconverted,
|
||||
0, 2, 0, 1, 0, 2, 2, 0)
|
||||
EQ_OOM_TEST(test_parjob_oom_job_alloc_debits_stay,
|
||||
0, 0, 1, 1, 1, 2, 2, 1)
|
||||
EQ_OOM_TEST(test_parjob_oom_shared_records_debits_stay,
|
||||
3, 0, 0, 1, 1, 3, 2, 1)
|
||||
|
||||
/* 8. COMPOUND: two primaries allocated in turn on one ctx, as
|
||||
* sched_make_compound_alloc_() does; rack 1 / rack 2 selects keep them on
|
||||
* distinct hosts (bug-1892 not exercised). Each is sized from its own
|
||||
* request (Req 5.8): 2 x 1 mem + 3 licenses on A, 3 x 2 mem on B. */
|
||||
static void test_parjob_compound_second_primary_sized_from_own_request(void)
|
||||
{
|
||||
eq_fixture_t f;
|
||||
eq_primary_t p1;
|
||||
eq_primary_t p2;
|
||||
xce_compound_resreq_t cres;
|
||||
xce_compound_schedobj_t cobj;
|
||||
xce_job_alloc_t* first = NULL;
|
||||
xce_sched_worker_t * a, *b;
|
||||
|
||||
eq_fixture_init(&f, 10.0f);
|
||||
a = eq_add_host(&f, HOST_A, 2.0f, 8.0f, 1.0f);
|
||||
b = eq_add_host(&f, HOST_B, 4.0f, 8.0f, 2.0f);
|
||||
eq_primary_init(&p1, 2, 1.0f, 3.0f, &f, 1);
|
||||
eq_primary_init(&p2, 3, 2.0f, 0.0f, &f, 2);
|
||||
|
||||
xlist_init(&(cres.primaries), 2);
|
||||
xlist_store(&(cres.primaries), &(p1.req));
|
||||
xlist_store(&(cres.primaries), &(p2.req));
|
||||
memset(&cobj, 0, sizeof(cobj));
|
||||
cobj.owner = &(f.obj);
|
||||
cobj.resreq = &cres;
|
||||
f.obj.tag = XCE_RESREQ_COMPOUND;
|
||||
f.obj.u.compound = &cobj;
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, sched_ctx_init(&(f.ctx), &(f.sched.core),
|
||||
&(f.obj)));
|
||||
f.ctx_ready = 1;
|
||||
|
||||
sched_ctx_set_primary(&(f.ctx), &(p1.req));
|
||||
TEST_ASSERT_EQUAL_INT(1, eq_alloc(&f));
|
||||
MOVE_PTR(first, f.ctx.alloc);
|
||||
|
||||
sched_ctx_set_primary(&(f.ctx), &(p2.req));
|
||||
TEST_ASSERT_EQUAL_INT(1, eq_alloc(&f));
|
||||
|
||||
TEST_ASSERT_NOT_NULL(first);
|
||||
TEST_ASSERT_EQUAL_INT(1, first->nhosts);
|
||||
eq_assert_host(first->allocs[0], HOST_A, 2.0f, 2.0f);
|
||||
TEST_ASSERT_EQUAL_INT(1, first->nres_shared);
|
||||
eq_assert_record(first->allocs_shared[0], "licenses", 3.0f);
|
||||
|
||||
TEST_ASSERT_NOT_NULL(f.ctx.alloc);
|
||||
TEST_ASSERT_EQUAL_INT(1, f.ctx.alloc->nhosts);
|
||||
eq_assert_host(f.ctx.alloc->allocs[0], HOST_B, 6.0f, 3.0f);
|
||||
TEST_ASSERT_EQUAL_INT(0, f.ctx.alloc->nres_shared);
|
||||
TEST_ASSERT_NULL(f.ctx.alloc->allocs_shared);
|
||||
|
||||
eq_assert_debit(&f, a, 2.0f, 2.0f);
|
||||
eq_assert_debit(&f, b, 6.0f, 3.0f);
|
||||
TEST_ASSERT_EQUAL_FLOAT(3.0f, eq_shared_uncomit(&f));
|
||||
eq_assert_reason_counts(&f, 0, 0, 0);
|
||||
eq_assert_calls(3, 2, 2);
|
||||
|
||||
free_JobAlloc_ptr(&first);
|
||||
xlist_fini(&(cres.primaries));
|
||||
eq_fixture_fini(&f);
|
||||
eq_primary_fini(&p1);
|
||||
eq_primary_fini(&p2);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_parjob_success_two_buckets_with_shared);
|
||||
RUN_TEST(test_parjob_pending_on_short_shared_resource);
|
||||
RUN_TEST(test_parjob_pending_on_short_shared_no_reason_today);
|
||||
RUN_TEST(test_parjob_pending_on_hosts);
|
||||
RUN_TEST(test_parjob_mid_plan_rollback_then_later_plan_wins);
|
||||
RUN_TEST(test_parjob_host_reasons_first_candidate_only_today);
|
||||
RUN_TEST(test_parjob_task_count_zero_and_one_pend_without_reason);
|
||||
RUN_TEST(test_parjob_missing_input_einval_no_reason);
|
||||
RUN_TEST(test_parjob_oom_records_host0_releases_all);
|
||||
RUN_TEST(test_parjob_oom_records_later_host_releases_unconverted);
|
||||
RUN_TEST(test_parjob_oom_host_alloc0_releases_all);
|
||||
RUN_TEST(test_parjob_oom_host_alloc_later_releases_unconverted);
|
||||
RUN_TEST(test_parjob_oom_job_alloc_debits_stay);
|
||||
RUN_TEST(test_parjob_oom_shared_records_debits_stay);
|
||||
RUN_TEST(test_parjob_compound_second_primary_sized_from_own_request);
|
||||
return UNITY_END();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue