From 0e68ed991ac110e24f5fd79e8266b51b43b4fd7c Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Wed, 30 Sep 2026 09:27:32 +0000 Subject: [PATCH] feat(scheduler-allocator-runner): runner result assembly (task 3.3) sched_finalize_job_alloc_() turns a satisfied plan's claims into the job allocation exactly as parjob's commit tail does (host records from the claim-time snapshots in claim order, shared records, jobid 0, no duration) and publishes ctx->alloc only on success. Every out-of-memory step frees what it built and releases the debits of claims not yet turned into host allocations; each claim snapshot is freed exactly once. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../specs/scheduler-allocator-runner/tasks.md | 2 +- src/scheduler/src/alloc_runner.c | 93 ++- src/scheduler/testsuite/meson.build | 17 + .../testsuite/test_alloc_runner_result.c | 544 ++++++++++++++++++ .../testsuite/test_alloc_runner_search.c | 59 +- 5 files changed, 666 insertions(+), 49 deletions(-) create mode 100644 src/scheduler/testsuite/test_alloc_runner_result.c diff --git a/.kiro/specs/scheduler-allocator-runner/tasks.md b/.kiro/specs/scheduler-allocator-runner/tasks.md index 336e9935..0ea0d0eb 100644 --- a/.kiro/specs/scheduler-allocator-runner/tasks.md +++ b/.kiro/specs/scheduler-allocator-runner/tasks.md @@ -48,7 +48,7 @@ - Done when runner tests covering order, single shared count, distinct hosts, rollback debits, empty-request-before-every-candidate with complete reasons, slot/host counts, growth, malformed plans and snapshot failure pass under ASAN - _Requirements: 2.3, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 5.5, 5.7, 7.2_ -- [ ] 3.3 Build result assembly and its out-of-memory paths, test first +- [x] 3.3 Build result assembly and its out-of-memory paths, test first - One host allocation per claim in claim order from the claimed host's name and its request snapshot; one shared record per shared-request entry, none when empty; job identifier 0, empty reservation name, no duration - The result is built privately and published to the attempt only on success, which returns a positive result - Out of memory at the host array, any host allocation, the job allocation or the shared records returns out-of-memory, leaves no allocation on the attempt, frees every partly built piece and releases the debits of claims not yet turned into a host allocation; claim snapshots are freed exactly once on every path diff --git a/src/scheduler/src/alloc_runner.c b/src/scheduler/src/alloc_runner.c index acac3100..518490b6 100644 --- a/src/scheduler/src/alloc_runner.c +++ b/src/scheduler/src/alloc_runner.c @@ -14,6 +14,8 @@ * 4. the shared-resource precheck (Req 2.2-2.4) * 5. the plan generator (Req 3.4) and the plan and claim search with * rollback (Req 3.1-3.8, 5.5, 5.7) + * 6. result assembly from the winning plan's claims (Req 4.1-4.4, + * 5.4-5.6) * * Returns > 0 when satisfied (ctx->alloc set), 0 when pending, < 0 a * negative errno. No plugin file or plugin-specific symbol is referenced @@ -30,6 +32,7 @@ #include "sched/types.h" #include "scheduler.h" #include "utils.h" +#include "xcealloc.h" #include "xvec.h" /* Creates the attempt's pend-reason record unless an earlier primary of @@ -329,14 +332,88 @@ static int sched_alloc_claims_reserve_(sched_alloc_claim_t** claims, int* cap, return 0; } +/* Builds the attempt's result from a satisfied plan's claims and + * publishes it on ctx->alloc (Req 4.1-4.4, 5.6): one host allocation per + * claim, in claim order, from the claimed host's name and its request + * snapshot; one shared record per shared-request entry, none when the + * shared request is empty; job id 0, empty reservation, no duration. + * + * Consumes the claims: each snapshot is freed once its host allocation is + * built; on failure the claims not yet converted are rolled back (debit + * released, snapshot freed). Converted claims' debits stay in ctx. + * + * 1, or -ENOMEM with ctx->alloc untouched and every piece built freed + * (Req 5.4, 5.5). */ +static int sched_finalize_job_alloc_(xce_scheduler_t* sched, + xce_sched_ctx_t* ctx, + sched_alloc_claim_t* claims, int nclaimed) +{ + xce_host_alloc_t** hallocs; + xce_job_alloc_t* local; + int nshared = ctx->alloc_req.shared.count; + int i; + + hallocs = (xce_host_alloc_t**)calloc((size_t)nclaimed, sizeof(*hallocs)); + if (hallocs == NULL) { + sched_rollback_claims_(ctx, claims, nclaimed); + return -ENOMEM; + } + + for (i = 0; i < nclaimed; i++) { + int nres = claims[i].host_req.count; + xce_alloc_t** allocs = sched_make_allocs( + sched, "", nres, (int*)claims[i].host_req.idxs.data, + (float*)claims[i].host_req.vals.data, + /* FIXME: no duration now */ NULL); + + if ((allocs != NULL) || (nres <= 0)) { + hallocs[i] = make_host_alloc(claims[i].w->name, nres, allocs); + if (hallocs[i] == NULL) { + free_ResAlloc_list(nres, allocs); + } + } + if (hallocs[i] == NULL) { + sched_rollback_claims_(ctx, claims + i, nclaimed - i); + free_HostAlloc_list(nclaimed, hallocs); + return -ENOMEM; + } + + xvec_idval_fini(&(claims[i].host_req)); + } + + local = make_job_alloc(0, nclaimed, hallocs); + if (local == NULL) { + free_HostAlloc_list(nclaimed, hallocs); + return -ENOMEM; + } + + local->allocs_shared = NULL; + local->nres_shared = 0; + if (nshared > 0) { + local->allocs_shared = sched_make_allocs( + sched, "", nshared, (int*)ctx->alloc_req.shared.idxs.data, + (float*)ctx->alloc_req.shared.vals.data, NULL); + if (local->allocs_shared == NULL) { + free_JobAlloc_ptr(&local); + return -ENOMEM; + } + local->nres_shared = nshared; + } + + ctx->alloc = local; + + return 1; +} + /* Tries the generator's plans in order until one is satisfied (Req 3.1). * The generator is started and is finished here on every path. * - * This runner builds no allocation result: a satisfied plan's claims - * are released and the attempt reports pending (0). No plan satisfied: 0. - * Claims-buffer growth or snapshot failure: that error. */ + * Satisfied: the result built from the plan's claims, which the + * assembly consumes (1, or -ENOMEM). No plan satisfied: 0. Claims-buffer + * growth or snapshot failure: that error. No snapshot is freed here. */ static int sched_alloc_search_(const xce_sched_allocator_iface_t* iface, - xce_sched_core_t* core, xce_sched_ctx_t* ctx) + xce_scheduler_t* sched, xce_sched_core_t* core, + xce_sched_ctx_t* ctx) { xce_alloc_plan_iter_t iter; xce_alloc_plan_t* plan; @@ -368,10 +445,7 @@ static int sched_alloc_search_(const xce_sched_allocator_iface_t* iface, } if (rc > 0) { - /* no allocation result is built from the claims: they are - * released and the attempt reports pending */ - sched_rollback_claims_(ctx, claims, n); - rc = 0; + rc = sched_finalize_job_alloc_(sched, ctx, claims, n); } iface->plan_fini(&iter); @@ -411,5 +485,6 @@ int sched_allocator_run(void* plugin_arg, xce_sched_ctx_t* ctx) return rc; } - return sched_alloc_search_(iface, plugin->core, ctx); + return sched_alloc_search_(iface, (xce_scheduler_t*)plugin->sched, + plugin->core, ctx); } diff --git a/src/scheduler/testsuite/meson.build b/src/scheduler/testsuite/meson.build index 8c076de9..0924cd06 100644 --- a/src/scheduler/testsuite/meson.build +++ b/src/scheduler/testsuite/meson.build @@ -1768,6 +1768,20 @@ sched_alloc_runner_search_test = executable( link_args: ['-Wl,--wrap=xvec_dup'], install: false) +# scheduler-allocator-runner task 3.3 (Req 4.1-4.4, 5.4-5.7, 7.2): the +# runner's result assembly and its out-of-memory paths, same white-box +# stack as above. make_host_alloc/make_job_alloc are wrapped to fail on +# call N; the host-array calloc is replaced inside the test file +# (alloc_runner.c only). +sched_alloc_runner_result_test = executable( + 'sched_alloc_runner_result_test', + ['test_alloc_runner_result.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 @@ -2046,6 +2060,9 @@ test('scheduler alloc runner test', # Leak detection stays on: every fixture tears down its ctx and core. test('scheduler alloc runner search test', sched_alloc_runner_search_test, suite: 'scheduler') +# Leak detection stays on: every fixture tears down its ctx and core. +test('scheduler alloc runner result test', + sched_alloc_runner_result_test, suite: 'scheduler') test('scheduler seqjob rewrap equivalence test', sched_seqjob_rewrap_equivalence_test, suite: 'scheduler', env: ['ASAN_OPTIONS=detect_leaks=0']) diff --git a/src/scheduler/testsuite/test_alloc_runner_result.c b/src/scheduler/testsuite/test_alloc_runner_result.c new file mode 100644 index 00000000..ba903809 --- /dev/null +++ b/src/scheduler/testsuite/test_alloc_runner_result.c @@ -0,0 +1,544 @@ +/* + * test_alloc_runner_result.c - scheduler-allocator-runner task 3.3: the + * runner's result assembly (Req 4.1-4.4) and its out-of-memory paths + * (Req 5.4-5.7, 7.2), through sched_allocator_run() with a scripted fake + * plan generator over real workers. + * + * Injectable steps, in the order the runner reaches them: + * HOST_ARRAY calloc() of the host-allocation array: a call-counting + * test_calloc() replacing calloc in alloc_runner.c only + * RECORDS sched_make_allocs() (stubbed here): host 0's records, + * host 1's records, then the shared records + * HOST_ALLOC make_host_alloc() (--wrap): host 0, then host 1 + * JOB_ALLOC make_job_alloc() (--wrap) + * Each is armed with the 1-based call number that fails; 0 = never. + */ +#include +#include +#include + +#include + +#include "preason.h" +#include "resobj.h" +#include "resreq.h" +#include "sched/alloc_iter.h" +#include "sched/types.h" +#include "scheduler.h" +#include "utils.h" +#include "xcealloc.h" +#include "xstring.h" +#include "xvec.h" + +/* White-box sources under test; calloc() is replaced in the runner only */ +static void* test_calloc(size_t n, size_t sz); +#define calloc test_calloc +#include "../src/alloc_runner.c" +#undef calloc +#include "../src/core.c" +#include "../src/host_iter.c" +#include "../src/resource.c" +#include "../src/schedobj.c" + +/* ---------------------------------------------------------------------- */ +/* Link-satisfying stubs (same convention as test_alloc_runner.c) */ +/* ---------------------------------------------------------------------- */ + +int plugin_register(void* s, void* p) { return (void)s, (void)p, 0; } +void plugin_unregister(void* arg) { (void)arg; } + +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; +} + +/* ---------------------------------------------------------------------- */ +/* Fault injection */ +/* ---------------------------------------------------------------------- */ + +typedef struct { + int calls; + int fail_on; /* 1-based call number; 0 = never */ +} inj_t; + +enum { + HOST_ARRAY, + RECORDS, + HOST_ALLOC, + JOB_ALLOC, + NSTEPS +}; +static inj_t g_inj[NSTEPS]; + +static int inj_fails(int step) +{ + g_inj[step].calls++; + return (g_inj[step].fail_on > 0) + && (g_inj[step].calls == g_inj[step].fail_on); +} + +static void* test_calloc(size_t n, size_t sz) +{ + if (inj_fails(HOST_ARRAY)) { + errno = ENOMEM; + return NULL; + } + return calloc(n, sz); +} + +/* Stub for scheduler.c's sched_make_allocs(): real make_alloc() records + * (resmgr name, requested value, the given reservation, duration -1 for a + * NULL duration array); 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; + + if (inj_fails(RECORDS)) { + 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)); + TEST_ASSERT_NOT_NULL(allocs); + for (i = 0; i < nres; i++) { + xce_resource_t* r = NULL; + + vget(sched->core.resmgr, idxs[i], (void**)&r); + TEST_ASSERT_NOT_NULL(r); + allocs[i] = make_alloc(r->name, reserve, 0, vals[i], + (duration != NULL) ? duration[i] : -1); + TEST_ASSERT_NOT_NULL(allocs[i]); + } + + 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) +{ + if (inj_fails(HOST_ALLOC)) { + 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) +{ + if (inj_fails(JOB_ALLOC)) { + errno = ENOMEM; + return NULL; + } + return __real_make_job_alloc(jobid, nhost, hallocs); +} + +/* ---------------------------------------------------------------------- */ +/* plugin_prepare_allocation() stub */ +/* ---------------------------------------------------------------------- */ + +static int g_slots_idx = -1; +static int g_mem_idx = -1; +static int g_lic_idx = -1; +static int g_lic2_idx = -1; +static int g_shared_on = 0; /* job wants lic 2 and lic2 5 */ +static int g_host_none = 0; /* per-host request left empty */ + +/* Job-level pass: the shared wants. Per-host pass: mem 10 per slot, then + * the bucket's slot count. */ +int plugin_prepare_allocation(xce_sched_core_t* sched, xce_sched_ctx_t* ctx) +{ + (void)sched; + xvec_idval_fini(&(ctx->alloc_req.host)); + xvec_idval_init(&(ctx->alloc_req.host), 8); + if (ctx->worker == NULL) { + xvec_idval_fini(&(ctx->alloc_req.shared)); + xvec_idval_init(&(ctx->alloc_req.shared), 8); + if (g_shared_on) { + TEST_ASSERT_TRUE( + xvec_idval_store(&(ctx->alloc_req.shared), g_lic_idx, 2.0f) + > 0); + TEST_ASSERT_TRUE( + xvec_idval_store(&(ctx->alloc_req.shared), g_lic2_idx, 5.0f) + > 0); + } + return 0; + } + if (g_host_none) { + return 0; + } + + TEST_ASSERT_TRUE(xvec_idval_store(&(ctx->alloc_req.host), g_mem_idx, + 10.0f * (float)ctx->nslots) + > 0); + TEST_ASSERT_TRUE(xvec_idval_store(&(ctx->alloc_req.host), g_slots_idx, + (float)ctx->nslots) + > 0); + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* Scripted fake plan generator: plan [3,1], or none */ +/* ---------------------------------------------------------------------- */ + +static int g_no_plan = 0; +static int g_next_calls = 0; +static int g_fini_calls = 0; +static int g_slots[2] = { 3, 1 }; +static xce_alloc_plan_t g_plan; + +static int fake_plan_init(xce_alloc_plan_iter_t* iter, + xce_primary_resreq_t* resreq, xce_sched_core_t* core) +{ + (void)resreq, (void)core; + iter->private = NULL; + return 0; +} + +static xce_alloc_plan_t* fake_plan_next(xce_alloc_plan_iter_t* iter) +{ + (void)iter; + if (g_no_plan || (g_next_calls++ > 0)) { + return NULL; + } + memset(&g_plan, 0, sizeof(g_plan)); + g_plan.nbuckets = 2; + g_plan.bucket_slots = g_slots; + return &g_plan; +} + +static void fake_plan_fini(xce_alloc_plan_iter_t* iter) +{ + (void)iter; + g_fini_calls++; +} + +static xce_sched_allocator_iface_t g_iface = { + .accepts = NULL, + .plan_init = fake_plan_init, + .plan_next = fake_plan_next, + .plan_fini = fake_plan_fini, +}; + +void setUp(void) +{ + memset(g_inj, 0, sizeof(g_inj)); + g_shared_on = 0; + g_host_none = 0; + g_no_plan = 0; + g_next_calls = 0; + g_fini_calls = 0; +} + +void tearDown(void) { } + +/* ---------------------------------------------------------------------- */ +/* Fixture: hosts ra-a and ra-b (4 slots, 100 mem), lic/lic2 10 each */ +/* ---------------------------------------------------------------------- */ + +typedef struct { + xce_scheduler_t sched; + xce_resource_t * slots, *mem, *lic, *lic2; + xce_sched_worker_t * a, *b; + xce_sched_plugin_t plugin; + xce_primary_resreq_t req; + xce_sched_obj_t obj; + xce_primary_schedobj_t primary_obj; + xce_sched_ctx_t ctx; +} rr_fixture_t; + +static xce_resource_t* rr_res(rr_fixture_t* f, const char* name, int flags) +{ + xce_resource_t* r = makeResource( + f->sched.core.resmgr, + &(xce_resource_t) { .name = (char*)name, .flags = flags }); + + TEST_ASSERT_NOT_NULL(r); + return r; +} + +static xce_sched_worker_t* rr_add_host(rr_fixture_t* f, const char* name) +{ + xce_sched_worker_t* w; + int idx[2] = { f->slots->idx, f->mem->idx }; + float total[2] = { 4.0f, 100.0f }; + 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), 2, idx, total); + put_resvals(&(w->resInUse), 2, idx, inuse); + TEST_ASSERT_TRUE(sched_add_worker(&(f->sched.core), w) >= 0); + + return w; +} + +static void rr_fixture_init(rr_fixture_t* f) +{ + int i; + int idx[2]; + float total[2] = { 10.0f, 10.0f }; + + 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 (i = 0; i < 2; i++) { + f->sched.core.pend_updates[i] = (dataq_t*)calloc(1, sizeof(dataq_t)); + TEST_ASSERT_NOT_NULL(f->sched.core.pend_updates[i]); + dataq_init(f->sched.core.pend_updates[i], NULL); + } + + f->slots = rr_res(f, "slots", 0); + f->mem = rr_res(f, "mem", 0); + f->lic = rr_res(f, "lic", XCE_RESF_GLOBAL); + f->lic2 = rr_res(f, "lic2", XCE_RESF_GLOBAL); + g_slots_idx = f->slots->idx; + g_mem_idx = f->mem->idx; + g_lic_idx = f->lic->idx; + g_lic2_idx = f->lic2->idx; + idx[0] = f->lic->idx; + idx[1] = f->lic2->idx; + put_resvals(&(f->sched.core.sharedInst), 2, idx, total); + + f->a = rr_add_host(f, "ra-a"); + f->b = rr_add_host(f, "ra-b"); + + f->plugin.name = (char*)"fake_allocator"; + f->plugin.sched = &(f->sched); + f->plugin.core = &(f->sched.core); + f->plugin.allocator_iface = &g_iface; + + f->req.minTasks = 1; + f->req.maxTasks = 4; + f->primary_obj.resreq = &(f->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))); + sched_ctx_set_primary(&(f->ctx), &(f->req)); +} + +static void rr_fixture_fini(rr_fixture_t* f) +{ + free_JobAlloc_ptr(&(f->ctx.alloc)); + free_PendReason_ptr(&(f->ctx.reason)); + sched_ctx_fini(&(f->ctx)); + xce_sched_core_fini(&(f->sched.core)); + xstring_table_fini(); +} + +static int rr_run(rr_fixture_t* f) +{ + return sched_allocator_run(&(f->plugin), &(f->ctx)); +} + +static float rr_debit(rr_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]; +} + +/* Host debit kept (the claim's request) or released (0) */ +static void rr_assert_debit(rr_fixture_t* f, xce_sched_worker_t* w, float slots) +{ + TEST_ASSERT_EQUAL_FLOAT(10.0f * slots, rr_debit(f, w, f->mem)); + TEST_ASSERT_EQUAL_FLOAT(slots, rr_debit(f, w, f->slots)); +} + +static void rr_assert_shared_debit(rr_fixture_t* f) +{ + float* v = (float*)f->ctx.uncomitted.shared->data; + + TEST_ASSERT_EQUAL_FLOAT(2.0f, v[f->lic->idx]); + TEST_ASSERT_EQUAL_FLOAT(5.0f, v[f->lic2->idx]); +} + +static void rr_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); +} + +static void rr_assert_host(xce_host_alloc_t* ha, const char* host, 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); + rr_assert_record(ha->allocs[0], "mem", 10.0f * slots); + rr_assert_record(ha->allocs[1], "slots", slots); +} + +/* ---------------------------------------------------------------------- */ +/* Success (Req 4.1-4.4, 5.6) */ +/* ---------------------------------------------------------------------- */ + +/* Plan [3,1]: bucket 1 on ra-a, bucket 2 on ra-b; two shared records. */ +static void test_success_builds_hosts_and_shared_records(void) +{ + rr_fixture_t f; + xce_job_alloc_t* ja; + + g_shared_on = 1; + rr_fixture_init(&f); + + TEST_ASSERT_EQUAL_INT(1, rr_run(&f)); + ja = f.ctx.alloc; + TEST_ASSERT_NOT_NULL(ja); + TEST_ASSERT_TRUE(ja->jobid == 0); + TEST_ASSERT_EQUAL_INT(2, ja->nhosts); + rr_assert_host(ja->allocs[0], "ra-a", 3.0f); + rr_assert_host(ja->allocs[1], "ra-b", 1.0f); + TEST_ASSERT_EQUAL_INT(2, ja->nres_shared); + TEST_ASSERT_NOT_NULL(ja->allocs_shared); + rr_assert_record(ja->allocs_shared[0], "lic", 2.0f); + rr_assert_record(ja->allocs_shared[1], "lic2", 5.0f); + TEST_ASSERT_EQUAL_INT(3, g_inj[RECORDS].calls); + TEST_ASSERT_EQUAL_INT(1, g_inj[HOST_ARRAY].calls); + + /* debits stay with the attempt context */ + rr_assert_debit(&f, f.a, 3.0f); + rr_assert_debit(&f, f.b, 1.0f); + rr_assert_shared_debit(&f); + TEST_ASSERT_EQUAL_INT(1, g_fini_calls); + rr_fixture_fini(&f); +} + +/* No shared request: no shared record, no shared records call. */ +static void test_success_without_shared_request(void) +{ + rr_fixture_t f; + + rr_fixture_init(&f); + + TEST_ASSERT_EQUAL_INT(1, rr_run(&f)); + TEST_ASSERT_NOT_NULL(f.ctx.alloc); + TEST_ASSERT_EQUAL_INT(2, f.ctx.alloc->nhosts); + TEST_ASSERT_NULL(f.ctx.alloc->allocs_shared); + TEST_ASSERT_EQUAL_INT(0, f.ctx.alloc->nres_shared); + TEST_ASSERT_EQUAL_INT(2, g_inj[RECORDS].calls); + rr_fixture_fini(&f); +} + +/* An empty per-host request builds a host allocation with no record. */ +static void test_success_with_empty_host_request(void) +{ + rr_fixture_t f; + + g_host_none = 1; + rr_fixture_init(&f); + + TEST_ASSERT_EQUAL_INT(1, rr_run(&f)); + TEST_ASSERT_NOT_NULL(f.ctx.alloc); + TEST_ASSERT_EQUAL_INT(2, f.ctx.alloc->nhosts); + TEST_ASSERT_EQUAL_STRING("ra-a", f.ctx.alloc->allocs[0]->host); + TEST_ASSERT_EQUAL_INT(0, f.ctx.alloc->allocs[0]->nallocs); + TEST_ASSERT_NULL(f.ctx.alloc->allocs[0]->allocs); + TEST_ASSERT_EQUAL_STRING("ra-b", f.ctx.alloc->allocs[1]->host); + rr_fixture_fini(&f); +} + +/* No plan satisfied: pending, nothing on the attempt. */ +static void test_pending_leaves_no_alloc(void) +{ + rr_fixture_t f; + + g_no_plan = 1; + rr_fixture_init(&f); + + TEST_ASSERT_EQUAL_INT(0, rr_run(&f)); + TEST_ASSERT_NULL(f.ctx.alloc); + TEST_ASSERT_EQUAL_INT(0, g_inj[HOST_ARRAY].calls); + TEST_ASSERT_EQUAL_INT(1, g_fini_calls); + rr_fixture_fini(&f); +} + +/* ---------------------------------------------------------------------- */ +/* Out of memory (Req 5.4-5.7, 7.2) */ +/* ---------------------------------------------------------------------- */ + +/* Fails `step` at call `nth` (shared request on): -ENOMEM, no allocation + * on the attempt, generator finished once; ra-a / ra-b keep their debit + * only if their claim became a host allocation (or the failure came after + * all of them); the shared amounts stay counted. Leaks and double frees + * are ASAN's to report. */ +static void rr_check_oom(int step, int nth, int keep_a, int keep_b) +{ + rr_fixture_t f; + + setUp(); + g_shared_on = 1; + rr_fixture_init(&f); + g_inj[step].fail_on = nth; + + TEST_ASSERT_EQUAL_INT(-ENOMEM, rr_run(&f)); + TEST_ASSERT_EQUAL_INT(nth, g_inj[step].calls); + TEST_ASSERT_NULL(f.ctx.alloc); + TEST_ASSERT_EQUAL_INT(1, g_fini_calls); + rr_assert_debit(&f, f.a, keep_a ? 3.0f : 0.0f); + rr_assert_debit(&f, f.b, keep_b ? 1.0f : 0.0f); + rr_assert_shared_debit(&f); + rr_fixture_fini(&f); +} + +static void test_oom_host_array(void) { rr_check_oom(HOST_ARRAY, 1, 0, 0); } + +static void test_oom_host0_records(void) { rr_check_oom(RECORDS, 1, 0, 0); } + +static void test_oom_host1_records(void) { rr_check_oom(RECORDS, 2, 1, 0); } + +static void test_oom_host0_host_alloc(void) +{ + rr_check_oom(HOST_ALLOC, 1, 0, 0); +} + +static void test_oom_host1_host_alloc(void) +{ + rr_check_oom(HOST_ALLOC, 2, 1, 0); +} + +static void test_oom_job_alloc(void) { rr_check_oom(JOB_ALLOC, 1, 1, 1); } + +static void test_oom_shared_records(void) { rr_check_oom(RECORDS, 3, 1, 1); } + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_success_builds_hosts_and_shared_records); + RUN_TEST(test_success_without_shared_request); + RUN_TEST(test_success_with_empty_host_request); + RUN_TEST(test_pending_leaves_no_alloc); + RUN_TEST(test_oom_host_array); + RUN_TEST(test_oom_host0_records); + RUN_TEST(test_oom_host1_records); + RUN_TEST(test_oom_host0_host_alloc); + RUN_TEST(test_oom_host1_host_alloc); + RUN_TEST(test_oom_job_alloc); + RUN_TEST(test_oom_shared_records); + return UNITY_END(); +} diff --git a/src/scheduler/testsuite/test_alloc_runner_search.c b/src/scheduler/testsuite/test_alloc_runner_search.c index 6acd7cdd..55d21f4e 100644 --- a/src/scheduler/testsuite/test_alloc_runner_search.c +++ b/src/scheduler/testsuite/test_alloc_runner_search.c @@ -56,14 +56,25 @@ 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; } -/* Never reached: this runner builds no allocation result. */ +/* Records for a satisfied plan's result (asserted in + * test_alloc_runner_result.c): resmgr name and requested value. */ xce_alloc_t** sched_make_allocs(xce_scheduler_t* sched, char* reserve, int nres, int* idxs, float* vals, int* duration) { - (void)sched, (void)reserve, (void)nres, (void)idxs, (void)vals; + xce_alloc_t** allocs = (xce_alloc_t**)calloc(nres, sizeof(*allocs)); + int i; + (void)duration; - errno = ENOSYS; - return NULL; + TEST_ASSERT_NOT_NULL(allocs); + for (i = 0; i < nres; i++) { + xce_resource_t* r = NULL; + + vget(sched->core.resmgr, idxs[i], (void**)&r); + TEST_ASSERT_NOT_NULL(r); + allocs[i] = make_alloc(r->name, reserve, 0, vals[i], -1); + TEST_ASSERT_NOT_NULL(allocs[i]); + } + return allocs; } /* ---------------------------------------------------------------------- */ @@ -402,7 +413,7 @@ static void test_plans_in_order_first_satisfied_wins(void) script_plan(2, 2, 2, 0); script_plan(4, 1, 1, 1); - TEST_ASSERT_EQUAL_INT(0, sr_run(&f)); + TEST_ASSERT_EQUAL_INT(1, sr_run(&f)); TEST_ASSERT_EQUAL_INT(2, g_next_calls); TEST_ASSERT_EQUAL_INT(1, g_init_calls); TEST_ASSERT_EQUAL_INT(1, g_fini_calls); @@ -539,14 +550,14 @@ static void test_rollback_then_later_plan_with_bucket_counts(void) script_plan(3, 1, 1, 1); script_plan(1, 2, 0, 0); - TEST_ASSERT_EQUAL_INT(0, sr_run(&f)); + TEST_ASSERT_EQUAL_INT(1, sr_run(&f)); TEST_ASSERT_EQUAL_INT(2, g_next_calls); /* [1,1,1]: A, B claimed; bucket 3 has no unclaimed host */ TEST_ASSERT_EQUAL_INT(3, g_nlog); sr_assert_log(0, a, 1); sr_assert_log(1, b, 1); sr_assert_log(2, a, 2); - TEST_ASSERT_EQUAL_FLOAT(0.0f, sr_slots_debit(&f, a)); + TEST_ASSERT_EQUAL_FLOAT(2.0f, sr_slots_debit(&f, a)); /* [2] won */ TEST_ASSERT_EQUAL_FLOAT(0.0f, sr_slots_debit(&f, b)); TEST_ASSERT_EQUAL_INT(1, g_fini_calls); sr_fixture_fini(&f); @@ -605,7 +616,7 @@ static void test_malformed_plans_skipped(void) script_plan(2, -1, 2, 0); script_plan(1, 2, 0, 0); - TEST_ASSERT_EQUAL_INT(0, sr_run(&f)); + TEST_ASSERT_EQUAL_INT(1, sr_run(&f)); TEST_ASSERT_EQUAL_INT(4, g_next_calls); TEST_ASSERT_EQUAL_INT(1, g_nlog); sr_assert_log(0, a, 2); @@ -628,7 +639,7 @@ static void test_claims_buffer_grows_to_larger_plan(void) script_plan(1, 3, 0, 0); script_plan(3, 1, 1, 1); - TEST_ASSERT_EQUAL_INT(0, sr_run(&f)); + TEST_ASSERT_EQUAL_INT(1, sr_run(&f)); TEST_ASSERT_EQUAL_INT(2, g_realloc_calls); TEST_ASSERT_EQUAL_INT(2, g_next_calls); TEST_ASSERT_EQUAL_INT(6, g_nlog); @@ -735,35 +746,6 @@ static void test_try_plan_snapshot_failure_reports_no_claim(void) sr_fixture_fini(&f); } -/* ---------------------------------------------------------------------- */ -/* Satisfied plan, current runner (Req 3.1; task 3.3 replaces this test) */ -/* ---------------------------------------------------------------------- */ - -/* This runner builds no allocation result: a satisfied plan's claims are - * released and the attempt pends, with nothing on ctx->alloc. */ -static void test_satisfied_plan_releases_claims_and_pends_interim(void) -{ - sr_fixture_t f; - xce_sched_worker_t *a, *b; - - sr_fixture_init(&f); - a = sr_add_host(&f, "sr-a", 2.0f); - b = sr_add_host(&f, "sr-b", 2.0f); - g_shared_want = 3.0f; - sr_ctx_begin(&f, 4); - script_plan(2, 2, 2, 0); - - TEST_ASSERT_EQUAL_INT(0, sr_run(&f)); - TEST_ASSERT_NULL(f.ctx.alloc); - TEST_ASSERT_EQUAL_INT(2, g_nlog); - TEST_ASSERT_EQUAL_FLOAT(0.0f, sr_slots_debit(&f, a)); - TEST_ASSERT_EQUAL_FLOAT(0.0f, sr_slots_debit(&f, b)); - TEST_ASSERT_EQUAL_FLOAT(3.0f, sr_shared_debit(&f)); - TEST_ASSERT_EQUAL_INT(1, g_next_calls); - TEST_ASSERT_EQUAL_INT(1, g_fini_calls); - sr_fixture_fini(&f); -} - int main(void) { UNITY_BEGIN(); @@ -779,6 +761,5 @@ int main(void) RUN_TEST(test_claims_buffer_growth_failure); RUN_TEST(test_snapshot_failure_releases_candidate_and_claims); RUN_TEST(test_try_plan_snapshot_failure_reports_no_claim); - RUN_TEST(test_satisfied_plan_releases_claims_and_pends_interim); return UNITY_END(); }