myxce/src/scheduler/testsuite/test_resource_accounting.c
Rongsong Shen e8866641a6
feat(scheduler-sim-live-allocation): make plugin hook dispatch core-safe (task 1.2)
Re-types plugin_job_election()/plugin_prepare_allocation()/
plugin_commit_decision() from (void* arg, ...) to (xce_sched_core_t*
core, ...), iterating core->plugins directly instead of deriving it
from a normal-mode-only driver struct -- mirroring
plugin_begin_session()/plugin_end_session()'s already-completed
migration.

Adds a new, honestly-typed xce_sched_plugin_t.core field, set by both
plugin_register() (normal mode) and plugin_register_core() (simulation
mode, closing a gap left since bug-1825 where this path set nothing).
The existing plugin->sched field and its plugins/fcfs.c legacy
consumer are left untouched.

Necessarily updates the 4 existing normal-mode call sites (scheduler.c,
resource.c, schedobj.c) to pass &(...->core) -- a C signature migration
cannot keep the build green without touching every caller in the same
commit; each fix is a minimal argument wrap with no algorithm change,
matching design.md's own Implementation Notes for this component.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
2026-09-18 11:48:08 +00:00

1314 lines
56 KiB
C

/*
* test_resource_accounting.c - Requirement 1.4 proof for the Core-Level
* Resource Accounting component (scheduler-sim-decision-parity spec, task
* 1.1; design.md's "Core-Level Resource Accounting" Unit Tests bullet):
* "produces identical resInUse/sharedInUse output whether called with a
* core obtained from xce_scheduler_t or from xce_sched_sim_driver_t."
*
* Task 1.1 itself is a pure signature relocation (xce_scheduler_t* ->
* xce_sched_core_t*) of sched_account_jobs()/sched_charge_account()/
* sched_uncharge_account()/sched_update_account()/
* sched_update_res_accounts()/sched_commit_job_alloc() -- body unchanged,
* every reference already `sched->core.*` before the move (confirmed via
* `grep "sched->" resource.c | grep -v "sched->core"` returning zero
* matches, design.md's Existing Architecture Analysis). Requirement 1.4 is
* nonetheless a genuinely NEW observable property worth its own test (not
* just "nothing broke"): now that these functions take xce_sched_core_t*
* directly, they are usable identically regardless of which driver-level
* container struct (xce_scheduler_t, the normal-mode driver's; or
* xce_sched_sim_driver_t, the simulation driver's, sim.h) embeds the core.
* This file drives the REAL sched_charge_account()/sched_uncharge_account()/
* sched_commit_job_alloc() (resource.c) against two independently
* initialized xce_sched_core_t instances -- one embedded in an
* xce_scheduler_t, one embedded in an xce_sched_sim_driver_t -- with
* identical worker/account state, and asserts byte-identical
* resInUse/sharedInUse results.
*
* sched_account_jobs() itself (scheduler.c) is NOT white-box #included here
* -- unlike this file's own sched_charge_account()/sched_commit_job_alloc()
* targets (resource.c, a lightweight, already-proven-includable file per
* test.c's own #include "../src/resource.c"), scheduler.c is a ~1400-line
* file with a much larger link-time dependency surface (controller-join,
* capnp decision publishing, HA candidate handling, picker/allocator thread
* lifecycle) that no existing test in this directory white-box #includes for
* exactly that reason (confirmed via grep across testsuite/ before writing
* this file). sched_account_jobs() itself is a thin, ~20-line wrapper
* (iterate core->jobs, call sched_charge_account() for each RUN-state job
* with a known allocation) with no accounting logic of its own -- its
* driver-ordering discipline (called at the right point in the round
* boundary, on both drivers) is already proven by
* test_driver_session_ordering.c (a spy, by design -- see that file's own
* header comment) and this feature's later tasks (2.x/3.x, sim_loop.c's own
* round-boundary integration). This file instead proves the actual
* accounting *body* -- the part design.md's Testing Strategy bullet is
* about -- via its real callees, exercised directly.
*
* White-box #include of ../src/core.c (for the real
* xce_sched_core_init()/sched_add_worker()/sched_get_worker() the fixture
* needs), ../src/resource.c (the functions under test), and
* ../src/schedobj.c (for the real sched_ctx_get_uncomit_host() that the
* new sched_ctx_uncommit_host() under test itself calls -- white-box
* #included here rather than reimplemented, the same convention
* test_picker_config_fields.c's own #include "../src/core.c" +
* #include "../src/schedobj.c" pairing already established in this
* directory "so it exercises the real functions, not a re-implementation
* of any of them"). None of the three needs core_clock.c/core_events.c/
* core_session.c/decision.c/plugin.c -- this test never calls
* xce_core_push_events()/xce_core_begin_session()/anything decision- or
* plugin-registration-related, only the plain add-worker/charge-account/
* uncommit-host entry points core.c/resource.c/schedobj.c themselves
* define. The kill_sched_* family (the kvmgr datakillers
* xce_sched_core_init() references) is now the REAL implementation
* pulled in from schedobj.c, not a stub -- only plugin_unregister remains
* a link-satisfying no-op (plugin.c is still not white-box #included).
* sched_worker_release_host_ctx() is a real (non-stub) symbol this pulls
* from resource.c itself, not from core_session.c -- unused here (this
* test never calls it), listed only because resource.c defines it.
* plugin_job_update()/plugin_queue_update()/plugin_job_rusage_update()
* below are new link-satisfying stubs for schedobj.c's OWN unrelated
* functions (sched_update_existing_queue()/sched_update_existing_job()/
* sched_apply_job_rusage()) that reference them (plugin.c, still not
* included) -- never actually invoked, since this file never calls any
* of those three schedobj.c functions.
*/
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unity/unity.h>
#include "resobj.h"
#include "scheduler.h"
#include "sim.h"
/* Pull in the real xce_sched_core_init()/sched_add_worker()/
* sched_get_worker() (core.c), sched_charge_account()/
* sched_uncharge_account()/sched_commit_job_alloc()/get_resvals()/
* sched_ctx_uncommit_host() (resource.c, the functions under test), and
* sched_ctx_get_uncomit_host() (schedobj.c) into this TU (white-box
* #include, same convention as every other file in this directory --
* see this file's own header comment for why schedobj.c specifically). */
#include "../src/core.c"
#include "../src/resource.c"
#include "../src/schedobj.c"
void plugin_unregister(void* arg) { (void)arg; }
/* Link-satisfying stub for resource.c's sched_try_alloc_on_host_() (its
* OWN callee plugin_prepare_allocation() lives in plugin.c, not white-box
* #included here; its other callee, sched_ctx_get_uncomit_host(), is now
* the real function pulled in from schedobj.c above). Never actually
* invoked: this file never calls sched_try_alloc_on_hosts()/
* sched_try_alloc_on_host_(), only sched_charge_account()/
* sched_uncharge_account()/sched_commit_job_alloc(). Same stub test.c's
* own #include "../src/resource.c" already establishes needing. */
int plugin_prepare_allocation(xce_sched_core_t* s, xce_sched_ctx_t* c)
{
(void)s;
(void)c;
return 0;
}
/* Link-satisfying stubs for schedobj.c's OWN unrelated functions
* (sched_update_existing_queue()/sched_update_existing_job()/
* sched_apply_job_rusage()) that reference plugin_job_update()/
* plugin_queue_update()/plugin_job_rusage_update() (plugin.c, not
* white-box #included here). Never actually invoked: this file never
* calls any of those three schedobj.c functions. */
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;
}
void setUp(void) { return; }
void tearDown(void) { return; }
/* ---------------------------------------------------------------------- */
/* Fixture helpers */
/* ---------------------------------------------------------------------- */
/*
* setup_core_with_worker - real xce_sched_core_init() (core.c) plus one
* real worker (sched_add_worker(), core.c), its resvals/resInUse zarrays
* freshly zarray_init()'d exactly the way a real worker join would leave
* them (sched_add_worker() itself does not do this -- callers own it, same
* as every other white-box fixture in this directory that adds a worker
* directly rather than through the JOBS_PUSH/WORKERS_PUSH event path).
*/
static void setup_core_with_worker(xce_sched_core_t* core,
const char* worker_name)
{
xce_sched_worker_t* w;
int rc;
rc = xce_sched_core_init(core);
TEST_ASSERT_EQUAL_INT(0, rc);
w = (xce_sched_worker_t*)calloc(1, sizeof(*w));
TEST_ASSERT_NOT_NULL(w);
w->name = strdup(worker_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);
rc = sched_add_worker(core, w);
TEST_ASSERT_TRUE(rc >= 0);
}
/*
* make_test_account - one shared-resource charge (residx `shared_residx`)
* plus one per-host charge (residx `host_residx`, against `worker_name`) --
* enough to exercise both of sched_update_account()'s two branches
* (core->sharedInUse and a specific worker's resInUse). residx is set
* non-negative directly (bypassing sched_update_res_accounts()'s
* resmgr-lookup branch entirely) -- this test targets the charge/uncharge
* arithmetic itself, not resource-name resolution, which is unrelated,
* already-existing logic this task's relocation does not touch.
*/
static xce_job_account_t*
make_test_account(const char* worker_name, int shared_residx,
double shared_used, double shared_reserved, int host_residx,
double host_used, double host_reserved)
{
xce_job_account_t* acct
= (xce_job_account_t*)calloc(1, sizeof(*acct));
TEST_ASSERT_NOT_NULL(acct);
acct->nres_shared = 1;
acct->accounts_shared = (xce_res_account_t**)calloc(
1, sizeof(*acct->accounts_shared));
TEST_ASSERT_NOT_NULL(acct->accounts_shared);
acct->accounts_shared[0]
= (xce_res_account_t*)calloc(1, sizeof(xce_res_account_t));
TEST_ASSERT_NOT_NULL(acct->accounts_shared[0]);
acct->accounts_shared[0]->residx = shared_residx;
acct->accounts_shared[0]->used = shared_used;
acct->accounts_shared[0]->reserved = shared_reserved;
acct->nhosts = 1;
acct->accounts = (xce_host_account_t**)calloc(1, sizeof(*acct->accounts));
TEST_ASSERT_NOT_NULL(acct->accounts);
acct->accounts[0]
= (xce_host_account_t*)calloc(1, sizeof(xce_host_account_t));
TEST_ASSERT_NOT_NULL(acct->accounts[0]);
acct->accounts[0]->host = strdup(worker_name);
acct->accounts[0]->naccount = 1;
acct->accounts[0]->accounts
= (xce_res_account_t**)calloc(1, sizeof(*acct->accounts[0]->accounts));
TEST_ASSERT_NOT_NULL(acct->accounts[0]->accounts);
acct->accounts[0]->accounts[0]
= (xce_res_account_t*)calloc(1, sizeof(xce_res_account_t));
TEST_ASSERT_NOT_NULL(acct->accounts[0]->accounts[0]);
acct->accounts[0]->accounts[0]->residx = host_residx;
acct->accounts[0]->accounts[0]->used = host_used;
acct->accounts[0]->accounts[0]->reserved = host_reserved;
return acct;
}
/* ---------------------------------------------------------------------- */
/* Tests */
/* ---------------------------------------------------------------------- */
#define WORKER_NAME "worker-a"
#define SHARED_RESIDX 0
#define HOST_RESIDX 1
#define SHARED_USED 3.0
#define SHARED_RESERVED 1.5
#define HOST_USED 2.0
#define HOST_RESERVED 0.5
/* SHARED_USED + SHARED_RESERVED, HOST_USED + HOST_RESERVED */
#define EXPECTED_SHARED_CHARGE 4.5f
#define EXPECTED_HOST_CHARGE 2.5f
/*
* test_charge_account_identical_across_core_containers - Requirement 1.4:
* sched_charge_account()/sched_uncharge_account() (resource.c) produce
* byte-identical resInUse/sharedInUse results whether `core` came from an
* xce_scheduler_t (sched.core, the normal-mode driver's container) or an
* xce_sched_sim_driver_t (sim.core, the simulation driver's container) --
* the exact property that makes the simulation driver's build now able to
* call the same accounting service the normal-mode driver already used.
*/
static void test_charge_account_identical_across_core_containers(void)
{
xce_scheduler_t sched;
xce_sched_sim_driver_t sim;
xce_job_account_t* acct;
xce_sched_worker_t* w_sched;
xce_sched_worker_t* w_sim;
float shared_sched[1];
float shared_sim[1];
float host_sched[1];
float host_sim[1];
int shared_idx[1] = { SHARED_RESIDX };
int host_idx[1] = { HOST_RESIDX };
int rc;
memset(&sched, 0, sizeof(sched));
memset(&sim, 0, sizeof(sim));
setup_core_with_worker(&(sched.core), WORKER_NAME);
setup_core_with_worker(&(sim.core), WORKER_NAME);
/* Same account struct fed to both cores -- sched_charge_account() never
* mutates it when residx is already non-negative (see
* make_test_account()'s own comment), so reuse is safe and keeps the
* two calls genuinely identical, not merely equal-by-construction. */
acct = make_test_account(WORKER_NAME, SHARED_RESIDX, SHARED_USED,
SHARED_RESERVED, HOST_RESIDX, HOST_USED,
HOST_RESERVED);
rc = sched_charge_account(&(sched.core), acct);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = sched_charge_account(&(sim.core), acct);
TEST_ASSERT_EQUAL_INT(0, rc);
/* --- sharedInUse: identical across both containers, and correct --- */
get_resvals(&(sched.core.sharedInUse), 1, shared_idx, shared_sched);
get_resvals(&(sim.core.sharedInUse), 1, shared_idx, shared_sim);
TEST_ASSERT_EQUAL_FLOAT(EXPECTED_SHARED_CHARGE, shared_sched[0]);
TEST_ASSERT_EQUAL_FLOAT(shared_sched[0], shared_sim[0]);
/* --- per-worker resInUse: identical across both containers --- */
w_sched = sched_get_worker(&(sched.core), WORKER_NAME);
w_sim = sched_get_worker(&(sim.core), WORKER_NAME);
TEST_ASSERT_NOT_NULL(w_sched);
TEST_ASSERT_NOT_NULL(w_sim);
get_resvals(&(w_sched->resInUse), 1, host_idx, host_sched);
get_resvals(&(w_sim->resInUse), 1, host_idx, host_sim);
TEST_ASSERT_EQUAL_FLOAT(EXPECTED_HOST_CHARGE, host_sched[0]);
TEST_ASSERT_EQUAL_FLOAT(host_sched[0], host_sim[0]);
/* --- sched_uncharge_account() mirrors this identically too --- */
rc = sched_uncharge_account(&(sched.core), acct);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = sched_uncharge_account(&(sim.core), acct);
TEST_ASSERT_EQUAL_INT(0, rc);
get_resvals(&(sched.core.sharedInUse), 1, shared_idx, shared_sched);
get_resvals(&(sim.core.sharedInUse), 1, shared_idx, shared_sim);
TEST_ASSERT_EQUAL_FLOAT(0.0f, shared_sched[0]);
TEST_ASSERT_EQUAL_FLOAT(shared_sched[0], shared_sim[0]);
get_resvals(&(w_sched->resInUse), 1, host_idx, host_sched);
get_resvals(&(w_sim->resInUse), 1, host_idx, host_sim);
TEST_ASSERT_EQUAL_FLOAT(0.0f, host_sched[0]);
TEST_ASSERT_EQUAL_FLOAT(host_sched[0], host_sim[0]);
}
/*
* test_commit_job_alloc_identical_across_core_containers - Requirement 1.4
* for sched_commit_job_alloc() specifically (the third relocated group
* member scheduler.c's sched_commit_decision() calls, design.md's own File
* Structure Plan entry for scheduler.c): both its no-op (job->accounts ==
* NULL) and its charging path behave identically regardless of which
* driver-level struct the core came from.
*/
static void test_commit_job_alloc_identical_across_core_containers(void)
{
xce_scheduler_t sched;
xce_sched_sim_driver_t sim;
xce_sched_job_t job_sched;
xce_sched_job_t job_sim;
xce_job_account_t* acct;
float shared_sched[1];
float shared_sim[1];
int shared_idx[1] = { SHARED_RESIDX };
int rc;
memset(&sched, 0, sizeof(sched));
memset(&sim, 0, sizeof(sim));
memset(&job_sched, 0, sizeof(job_sched));
memset(&job_sim, 0, sizeof(job_sim));
setup_core_with_worker(&(sched.core), WORKER_NAME);
setup_core_with_worker(&(sim.core), WORKER_NAME);
/* --- job->accounts == NULL: documented no-op, identical on both --- */
job_sched.accounts = NULL;
job_sim.accounts = NULL;
rc = sched_commit_job_alloc(&(sched.core), &job_sched);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = sched_commit_job_alloc(&(sim.core), &job_sim);
TEST_ASSERT_EQUAL_INT(0, rc);
get_resvals(&(sched.core.sharedInUse), 1, shared_idx, shared_sched);
get_resvals(&(sim.core.sharedInUse), 1, shared_idx, shared_sim);
TEST_ASSERT_EQUAL_FLOAT(0.0f, shared_sched[0]);
TEST_ASSERT_EQUAL_FLOAT(shared_sched[0], shared_sim[0]);
/* --- job->accounts set: charges through sched_charge_account(),
* identical on both --- */
acct = make_test_account(WORKER_NAME, SHARED_RESIDX,
SHARED_USED, SHARED_RESERVED,
HOST_RESIDX, HOST_USED,
HOST_RESERVED);
job_sched.accounts = acct;
job_sim.accounts = acct;
rc = sched_commit_job_alloc(&(sched.core), &job_sched);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = sched_commit_job_alloc(&(sim.core), &job_sim);
TEST_ASSERT_EQUAL_INT(0, rc);
get_resvals(&(sched.core.sharedInUse), 1, shared_idx, shared_sched);
get_resvals(&(sim.core.sharedInUse), 1, shared_idx, shared_sim);
TEST_ASSERT_EQUAL_FLOAT(EXPECTED_SHARED_CHARGE, shared_sched[0]);
TEST_ASSERT_EQUAL_FLOAT(shared_sched[0], shared_sim[0]);
}
/*
* test_charge_account_skips_reconciling_or_unavail_worker_host_charge -
* bug-1799 fix: sched_charge_account()'s per-host resInUse charge must be
* skipped for a worker currently IS_WORKER_RECONCILING or IS_WORKER_UNAVAIL.
* core_reset_workers() (core_session.c, bug-1751) already skips RESETTING
* such a worker's resInUse every round; sched_account_jobs()'s own
* per-round re-derive pass reaches resInUse only through this same
* sched_charge_account()->sched_update_account() path, so without a
* matching skip here it re-added a full charge ON TOP of the not-reset
* total every round the worker stayed RECONCILING/UNAVAIL -- resInUse grew
* unboundedly and could exceed the resource's real capacity (live-observed:
* a 22-slot worker showing used=26.00). core->sharedInUse has no
* per-worker gating at all (its own reset, core_reset_shared_resval(), is
* unconditional) and must keep charging normally regardless of any one
* host's status.
*/
static void
test_charge_account_skips_reconciling_or_unavail_worker_host_charge(void)
{
xce_scheduler_t sched;
xce_job_account_t* acct;
xce_sched_worker_t* w;
float shared_val[1];
float host_val[1];
int shared_idx[1] = { SHARED_RESIDX };
int host_idx[1] = { HOST_RESIDX };
int rc;
memset(&sched, 0, sizeof(sched));
setup_core_with_worker(&(sched.core), WORKER_NAME);
w = sched_get_worker(&(sched.core), WORKER_NAME);
TEST_ASSERT_NOT_NULL(w);
acct = make_test_account(WORKER_NAME, SHARED_RESIDX, SHARED_USED,
SHARED_RESERVED, HOST_RESIDX, HOST_USED,
HOST_RESERVED);
/* --- RECONCILING: shared charges normally, per-host charge skipped --- */
w->status = XCE_WORKER_RECONCILING;
rc = sched_charge_account(&(sched.core), acct);
TEST_ASSERT_EQUAL_INT(0, rc);
get_resvals(&(sched.core.sharedInUse), 1, shared_idx, shared_val);
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
EXPECTED_SHARED_CHARGE, shared_val[0],
"shared resources have no per-worker gating and must still charge "
"while the host is RECONCILING");
get_resvals(&(w->resInUse), 1, host_idx, host_val);
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
0.0f, host_val[0],
"bug-1799: a RECONCILING worker's per-host resInUse must NOT be "
"charged -- core_reset_workers() already skips resetting it, so "
"charging on top would grow it every round it stays RECONCILING");
/* --- UNAVAIL: same per-host skip --- */
w->status = XCE_WORKER_UNAVAIL;
rc = sched_charge_account(&(sched.core), acct);
TEST_ASSERT_EQUAL_INT(0, rc);
get_resvals(&(w->resInUse), 1, host_idx, host_val);
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
0.0f, host_val[0],
"bug-1799: an UNAVAIL worker's per-host resInUse must NOT be "
"charged either -- the same reset-skip condition covers both bits");
/* --- status clears: normal charging resumes on the very next call --- */
w->status = 0;
rc = sched_charge_account(&(sched.core), acct);
TEST_ASSERT_EQUAL_INT(0, rc);
get_resvals(&(w->resInUse), 1, host_idx, host_val);
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
EXPECTED_HOST_CHARGE, host_val[0],
"once the worker's status bit clears, charging must resume "
"normally on the very next call -- this is a per-call skip, not a "
"permanent block");
}
/*
* resource-accounting.md's reservation-vs-actual-usage reconciliation for
* a REAL (gauge-style) resource, e.g. "mem" -- sched_reconcile_host_
* resource_usage() (the single-account-entry primitive) and
* sched_reconcile_job_resource_usage() (the rusages-report wrapper it's
* built for). Both are defined in resource.c, already white-box #included
* above.
*/
#define MEM_RESIDX 2
#define MEM_RESERVED_MB 4.0
/* make_mem_account - a single-host xce_job_account_t with one "mem"
* xce_res_account_t entry (residx MEM_RESIDX, reserved MEM_RESERVED_MB,
* used 0.0 -- a job that has just been dispatched and never had a rusage
* tick applied yet). resname is real (strdup'd), unlike make_test_account's
* residx-only entries above, since sched_reconcile_job_resource_usage()
* looks accounts up BY NAME. */
static xce_job_account_t* make_mem_account(const char* worker_name)
{
xce_job_account_t* acct = (xce_job_account_t*)calloc(1, sizeof(*acct));
TEST_ASSERT_NOT_NULL(acct);
acct->nhosts = 1;
acct->accounts = (xce_host_account_t**)calloc(1, sizeof(*acct->accounts));
TEST_ASSERT_NOT_NULL(acct->accounts);
acct->accounts[0]
= (xce_host_account_t*)calloc(1, sizeof(xce_host_account_t));
TEST_ASSERT_NOT_NULL(acct->accounts[0]);
acct->accounts[0]->host = strdup(worker_name);
acct->accounts[0]->naccount = 1;
acct->accounts[0]->accounts
= (xce_res_account_t**)calloc(1, sizeof(*acct->accounts[0]->accounts));
TEST_ASSERT_NOT_NULL(acct->accounts[0]->accounts);
acct->accounts[0]->accounts[0]
= (xce_res_account_t*)calloc(1, sizeof(xce_res_account_t));
TEST_ASSERT_NOT_NULL(acct->accounts[0]->accounts[0]);
acct->accounts[0]->accounts[0]->resname = strdup("mem");
acct->accounts[0]->accounts[0]->residx = MEM_RESIDX;
acct->accounts[0]->accounts[0]->used = 0.0;
acct->accounts[0]->accounts[0]->reserved = MEM_RESERVED_MB;
acct->accounts[0]->accounts[0]->allocated = MEM_RESERVED_MB;
return acct;
}
static xce_job_rusages_t* make_mem_rusages(float memory_rss_mb)
{
xce_job_rusages_t* r = (xce_job_rusages_t*)calloc(1, sizeof(*r));
TEST_ASSERT_NOT_NULL(r);
r->count = 1;
r->usages = (xce_rusage_t**)calloc(1, sizeof(*r->usages));
TEST_ASSERT_NOT_NULL(r->usages);
r->usages[0] = (xce_rusage_t*)calloc(1, sizeof(xce_rusage_t));
TEST_ASSERT_NOT_NULL(r->usages[0]);
r->usages[0]->resname = strdup("memory_rss");
r->usages[0]->value = memory_rss_mb;
return r;
}
static float get_worker_resinuse(xce_sched_worker_t* w, int residx)
{
int idx[1] = { residx };
float val[1];
get_resvals(&(w->resInUse), 1, idx, val);
return val[0];
}
/*
* test_reconcile_host_resource_usage_below_reservation_keeps_reservation -
* resource-accounting.md case (a): actual usage below the reservation must
* still charge the FULL reservation (used stays 0, since overage is 0).
*/
static void
test_reconcile_host_resource_usage_below_reservation_keeps_reservation(void)
{
xce_scheduler_t sched;
xce_sched_worker_t* w;
xce_res_account_t acct_entry = { 0 };
int rc;
memset(&sched, 0, sizeof(sched));
setup_core_with_worker(&(sched.core), WORKER_NAME);
w = sched_get_worker(&(sched.core), WORKER_NAME);
TEST_ASSERT_NOT_NULL(w);
acct_entry.residx = MEM_RESIDX;
acct_entry.reserved = MEM_RESERVED_MB;
acct_entry.used = 0.0;
/* First charge: reservation only (mirrors sched_charge_account() at
* dispatch time -- used=0, reserved=4 -> pool charge 4). */
rc = sched_reconcile_host_resource_usage(&(sched.core), w, &acct_entry,
0.0);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_FLOAT(0.0, (float)acct_entry.used);
/* actual usage (1MB) is below the 4MB reservation -- used must stay 0,
* the pool must still show the full 4MB reservation charged, not the
* 1MB actually in use. */
rc = sched_reconcile_host_resource_usage(&(sched.core), w, &acct_entry,
1.0);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_FLOAT(0.0, (float)acct_entry.used);
}
/*
* test_reconcile_host_resource_usage_above_reservation_charges_actual -
* resource-accounting.md case (b): actual usage at/above the reservation
* must charge the REAL usage instead of the stale reservation.
*/
static void
test_reconcile_host_resource_usage_above_reservation_charges_actual(void)
{
xce_scheduler_t sched;
xce_sched_worker_t* w;
xce_res_account_t acct_entry = { 0 };
int rc;
int idx[1] = { MEM_RESIDX };
float val[1];
memset(&sched, 0, sizeof(sched));
setup_core_with_worker(&(sched.core), WORKER_NAME);
w = sched_get_worker(&(sched.core), WORKER_NAME);
TEST_ASSERT_NOT_NULL(w);
acct_entry.residx = MEM_RESIDX;
acct_entry.reserved = MEM_RESERVED_MB;
acct_entry.used = 0.0;
/* Seed the pool with the initial reservation charge, exactly as
* sched_charge_account() would at dispatch time. */
val[0] = (float)(acct_entry.used + acct_entry.reserved);
put_resvals(&(w->resInUse), 1, idx, val);
/* actual usage (6MB) EXCEEDS the 4MB reservation -- overage = 2MB, so
* used must become 2 and the pool must show 6 (reserved 4 + overage 2 =
* actual 6), not the stale 4MB reservation. */
rc = sched_reconcile_host_resource_usage(&(sched.core), w, &acct_entry,
6.0);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_FLOAT(2.0, (float)acct_entry.used);
TEST_ASSERT_EQUAL_FLOAT(6.0, get_worker_resinuse(w, MEM_RESIDX));
/* usage drops back to 3MB (still >= reserved? no: 3 < 4) -- back to
* charging the flat reservation, pool returns to 4. */
rc = sched_reconcile_host_resource_usage(&(sched.core), w, &acct_entry,
3.0);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_FLOAT(0.0, (float)acct_entry.used);
TEST_ASSERT_EQUAL_FLOAT(4.0, get_worker_resinuse(w, MEM_RESIDX));
}
/*
* test_reconcile_host_resource_usage_repeated_growth_applies_only_delta -
* confirms the pool is updated by DELTA each call, not a full recompute
* that would double-count a sibling resource's own charge in the same
* pool array (resource.c's own doc comment on this function).
*/
static void
test_reconcile_host_resource_usage_repeated_growth_applies_only_delta(void)
{
xce_scheduler_t sched;
xce_sched_worker_t* w;
xce_res_account_t acct_entry = { 0 };
int slots_idx[1] = { 0 }; /* a sibling resource */
float slots_val[1] = { 7.0f };
int rc;
memset(&sched, 0, sizeof(sched));
setup_core_with_worker(&(sched.core), WORKER_NAME);
w = sched_get_worker(&(sched.core), WORKER_NAME);
TEST_ASSERT_NOT_NULL(w);
/* A sibling resource ("slots", a different residx) already charged in
* the same pool array -- must be untouched by every call below. */
put_resvals(&(w->resInUse), 1, slots_idx, slots_val);
acct_entry.residx = MEM_RESIDX;
acct_entry.reserved = MEM_RESERVED_MB;
acct_entry.used = 0.0;
rc = sched_reconcile_host_resource_usage(&(sched.core), w, &acct_entry,
5.0); /* overage 1 */
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_FLOAT(1.0, get_worker_resinuse(w, MEM_RESIDX));
rc = sched_reconcile_host_resource_usage(&(sched.core), w, &acct_entry,
9.0); /* overage 5 -- delta +4 */
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_FLOAT(5.0, get_worker_resinuse(w, MEM_RESIDX));
/* Sibling resource never touched by any of the above. */
TEST_ASSERT_EQUAL_FLOAT(7.0, get_worker_resinuse(w, 0));
}
static void test_reconcile_host_resource_usage_null_args_return_einval(void)
{
xce_scheduler_t sched;
xce_res_account_t acct_entry = { 0 };
memset(&sched, 0, sizeof(sched));
setup_core_with_worker(&(sched.core), WORKER_NAME);
TEST_ASSERT_EQUAL_INT(
-EINVAL, sched_reconcile_host_resource_usage(NULL, NULL, NULL, 0.0));
TEST_ASSERT_EQUAL_INT(-EINVAL,
sched_reconcile_host_resource_usage(
&(sched.core), NULL, &acct_entry, 0.0));
}
/*
* test_reconcile_host_resource_usage_skips_reconciling_or_unavail_worker -
* bug-1814 follow-up: sched_reconcile_host_resource_usage() is a THIRD
* w->resInUse-mutating path (alongside sched_charge_account()/sched_
* uncharge_account(), both already gated by bug-1801) that must respect the
* same "frozen while RECONCILING/UNAVAIL" invariant core_reset_workers()
* (bug-1751) and sched_update_account() (bug-1801) already share -- a live
* replay showed a job's "mem used" growing and never self-correcting until
* the job finished, root-caused to this exact gap: a rusage tick landing
* mid-resync kept mutating resInUse (and advancing acct_entry->used) on top
* of a pool a concurrent per-round rebuild was deliberately leaving alone.
*/
static void
test_reconcile_host_resource_usage_skips_reconciling_or_unavail_worker(void)
{
xce_scheduler_t sched;
xce_sched_worker_t* w;
xce_res_account_t acct_entry = { 0 };
int rc;
memset(&sched, 0, sizeof(sched));
setup_core_with_worker(&(sched.core), WORKER_NAME);
w = sched_get_worker(&(sched.core), WORKER_NAME);
TEST_ASSERT_NOT_NULL(w);
acct_entry.residx = MEM_RESIDX;
acct_entry.reserved = MEM_RESERVED_MB;
acct_entry.used = 0.0;
/* --- RECONCILING: no mutation at all, not to the pool, not to
* acct_entry->used --- */
w->status = XCE_WORKER_RECONCILING;
rc = sched_reconcile_host_resource_usage(&(sched.core), w,
&acct_entry, 9.0);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
0.0f, get_worker_resinuse(w, MEM_RESIDX),
"bug-1814: a RECONCILING worker's resInUse must not be touched by a "
"rusage tick -- it is deliberately frozen until a clean per-round "
"rebuild can reconcile it, same invariant bug-1801 already "
"restores for the regular charge path");
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
0.0, (float)acct_entry.used,
"the account's own used baseline must also stay untouched while "
"skipped, so the eventual real reconcile call sees a clean delta");
/* --- UNAVAIL: same skip --- */
w->status = XCE_WORKER_UNAVAIL;
rc = sched_reconcile_host_resource_usage(&(sched.core), w,
&acct_entry, 9.0);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
0.0f, get_worker_resinuse(w, MEM_RESIDX),
"bug-1814: an UNAVAIL worker's resInUse must not be touched either");
/* --- status clears: reconciliation resumes normally on the very next
* call, not a permanent block --- */
w->status = 0;
rc = sched_reconcile_host_resource_usage(&(sched.core), w,
&acct_entry, 9.0);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
5.0, (float)acct_entry.used,
"overage = 9 - 4 reserved = 5, applied on the first call once the "
"worker is no longer RECONCILING/UNAVAIL");
TEST_ASSERT_EQUAL_FLOAT(5.0, get_worker_resinuse(w, MEM_RESIDX));
}
/*
* test_reconcile_job_resource_usage_* - the rusages-report wrapper: resname
* mapping ("memory_rss" on the wire -> "mem" in the account), the single-
* host scope, and the no-request no-op.
*/
static void
test_reconcile_job_resource_usage_maps_memory_rss_to_mem_account(void)
{
xce_scheduler_t sched;
xce_sched_worker_t* w;
xce_job_account_t* jacct;
xce_job_rusages_t* rusages;
int rc;
memset(&sched, 0, sizeof(sched));
setup_core_with_worker(&(sched.core), WORKER_NAME);
w = sched_get_worker(&(sched.core), WORKER_NAME);
TEST_ASSERT_NOT_NULL(w);
jacct = make_mem_account(WORKER_NAME);
rusages = make_mem_rusages(7.0f); /* > 4MB reserved -> overage 3 */
/* Seed the pool with the initial dispatch-time reservation charge
* (mirrors what sched_charge_account()/sched_commit_job_alloc() would
* already have done before any rusage tick ever arrives -- the
* function under test only ever applies a DELTA on top of this, per
* its own doc comment, never a from-scratch total). */
{
int idx[1] = { MEM_RESIDX };
float val[1] = { (float)MEM_RESERVED_MB };
put_resvals(&(w->resInUse), 1, idx, val);
}
rc = sched_reconcile_job_resource_usage(&(sched.core), jacct, rusages);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_FLOAT(3.0,
(float)jacct->accounts[0]->accounts[0]->used);
TEST_ASSERT_EQUAL_FLOAT(7.0, get_worker_resinuse(w, MEM_RESIDX));
}
static void test_reconcile_job_resource_usage_skips_multihost_jobs(void)
{
xce_scheduler_t sched;
xce_job_account_t jacct = { 0 };
xce_job_rusages_t* rusages = make_mem_rusages(99.0f);
int rc;
memset(&sched, 0, sizeof(sched));
setup_core_with_worker(&(sched.core), WORKER_NAME);
jacct.nhosts = 2; /* multi-host: no per-report host attribution possible
* (see sched_reconcile_job_resource_usage()'s own
* doc comment) -- must be a documented no-op. */
rc = sched_reconcile_job_resource_usage(&(sched.core), &jacct, rusages);
TEST_ASSERT_EQUAL_INT(0, rc);
}
/*
* test_reconcile_job_resource_usage_charges_unreserved_job_full_usage -
* resource-accounting.md's correction: a job with NO rusage[mem=...] at
* all is reserved=0, but that must NOT mean "nothing to charge". This
* codebase's admission check has no live OS read at decision time (see
* the steering doc) -- w->resInUse is the ONLY place a job's real memory
* footprint is ever reflected, so an unreserved job's actual usage must
* still be charged in full (max(0, actual) == actual), or the scheduler's
* own capacity tracking silently under-counts real consumption and can
* over-admit new jobs onto memory a live, unreserved job is genuinely
* sitting on.
*/
static void
test_reconcile_job_resource_usage_charges_unreserved_job_full_usage(void)
{
xce_scheduler_t sched;
xce_sched_worker_t* w;
xce_job_account_t* jacct;
xce_job_rusages_t* rusages = make_mem_rusages(5.0f);
xce_resource_t mem_res = { 0 };
xce_resource_t* registered;
int rc;
memset(&sched, 0, sizeof(sched));
setup_core_with_worker(&(sched.core), WORKER_NAME);
w = sched_get_worker(&(sched.core), WORKER_NAME);
TEST_ASSERT_NOT_NULL(w);
/* "mem" must be a real, registered resource for ensure_host_account_
* by_resname()'s key_find() lookup to resolve a residx -- mirrors how
* a real cluster config would already have it registered long before
* any job ever runs. */
mem_res.name = strdup(SCHED_MEM_RESOURCE_NAME);
registered = makeResource(sched.core.resmgr, &mem_res);
TEST_ASSERT_NOT_NULL(registered);
/* A single-host account with NO "mem" entry at all -- the job never
* requested rusage[mem=...]. */
jacct = (xce_job_account_t*)calloc(1, sizeof(*jacct));
jacct->nhosts = 1;
jacct->accounts = (xce_host_account_t**)calloc(1, sizeof(*jacct->accounts));
jacct->accounts[0] = (xce_host_account_t*)calloc(1, sizeof(xce_host_account_t));
jacct->accounts[0]->host = strdup(WORKER_NAME);
jacct->accounts[0]->naccount = 0;
rc = sched_reconcile_job_resource_usage(&(sched.core), jacct, rusages);
TEST_ASSERT_EQUAL_INT(0, rc);
/* An account entry now exists (created on first use), reserved=0,
* used == the full actual usage, and the pool reflects it. */
TEST_ASSERT_EQUAL_INT(1, jacct->accounts[0]->naccount);
TEST_ASSERT_EQUAL_STRING(SCHED_MEM_RESOURCE_NAME,
jacct->accounts[0]->accounts[0]->resname);
TEST_ASSERT_EQUAL_FLOAT(0.0,
(float)jacct->accounts[0]->accounts[0]->reserved);
TEST_ASSERT_EQUAL_FLOAT(5.0, (float)jacct->accounts[0]->accounts[0]->used);
TEST_ASSERT_EQUAL_FLOAT(
5.0, get_worker_resinuse(w, jacct->accounts[0]->accounts[0]->residx));
}
static void
test_reconcile_job_resource_usage_noop_when_mem_not_a_registered_resource(void)
{
xce_scheduler_t sched;
xce_job_account_t* jacct;
xce_job_rusages_t* rusages = make_mem_rusages(99.0f);
int rc;
memset(&sched, 0, sizeof(sched));
setup_core_with_worker(&(sched.core), WORKER_NAME);
/* Deliberately do NOT register "mem" into sched.core.resmgr -- a
* misconfigured cluster with no such resource defined at all. */
jacct = (xce_job_account_t*)calloc(1, sizeof(*jacct));
jacct->nhosts = 1;
jacct->accounts = (xce_host_account_t**)calloc(1, sizeof(*jacct->accounts));
jacct->accounts[0] = (xce_host_account_t*)calloc(1, sizeof(xce_host_account_t));
jacct->accounts[0]->host = strdup(WORKER_NAME);
jacct->accounts[0]->naccount = 0;
rc = sched_reconcile_job_resource_usage(&(sched.core), jacct, rusages);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(0, jacct->accounts[0]->naccount);
}
static void test_reconcile_job_resource_usage_null_args_return_einval(void)
{
xce_scheduler_t sched;
memset(&sched, 0, sizeof(sched));
setup_core_with_worker(&(sched.core), WORKER_NAME);
TEST_ASSERT_EQUAL_INT(-EINVAL,
sched_reconcile_job_resource_usage(NULL, NULL, NULL));
}
/*
* test_ctx_uncommit_host_reverses_provisional_debit - Requirement 4.2
* (scheduler-allocation spec, task 1.4): sched_ctx_uncommit_host()
* (resource.c, new) reverses exactly the provisional per-host resource
* debit an earlier successful host-claim made.
*
* Debits ctx->uncomitted.hosts[idx] directly via xvec_idval(...,
* XVEC_ADD_FLOAT) -- the exact call sched_try_alloc_on_host_() makes on a
* successful host claim (resource.c line ~544) -- rather than driving the
* full plugin_prepare_allocation()/sched_check_host_satisfy() chain, which
* needs a real worker/select-expression/resmgr fixture unrelated to what
* this rollback primitive itself does (this file's own convention: see
* make_test_account()'s residx-only shortcut above for the same rationale).
* Then calls the new sched_ctx_uncommit_host() and asserts the per-host
* uncommitted vector is back to its pre-debit (zero) baseline -- proving
* debit-then-reverse leaves provisional usage unchanged, this task's
* Observable.
*
* sched_ctx_get_uncomit_host() (the baseline/re-read get-or-create
* accessor called below) and host_uncommit_killer() (the zarray item
* killer passed to zarray_init() below) are both the REAL functions
* white-box #included from schedobj.c above, not reimplementations --
* host_uncommit_killer() is `static` in schedobj.c, but the #include
* pastes it into this same translation unit, so it resolves normally.
* This is the same "exercise the real functions, not a re-implementation
* of any of them" precedent test_picker_config_fields.c already
* established for combining core.c + schedobj.c in one white-box test.
*/
#define UNCOMMIT_HOST_IDX 3
#define UNCOMMIT_RESIDX 0
#define UNCOMMIT_DEBIT 2.5f
static void test_ctx_uncommit_host_reverses_provisional_debit(void)
{
xce_sched_ctx_t ctx;
xvec_t* uncomit = NULL;
xvec_idval_t committed;
float* data;
int rc;
memset(&ctx, 0, sizeof(ctx));
rc = zarray_init(&(ctx.uncomitted.hosts), 8,
(zarray_item_killer_t)host_uncommit_killer);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = xvec_idval_init(&committed, 4);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = xvec_idval_store(&committed, UNCOMMIT_RESIDX, UNCOMMIT_DEBIT);
TEST_ASSERT_TRUE(rc >= 0);
/* Baseline: a freshly created per-host uncommitted vector reads 0. */
rc = sched_ctx_get_uncomit_host(&uncomit, &ctx, UNCOMMIT_HOST_IDX);
TEST_ASSERT_EQUAL_INT(1, rc);
TEST_ASSERT_NOT_NULL(uncomit);
data = (float*)uncomit->data;
TEST_ASSERT_EQUAL_FLOAT(0.0f, data[UNCOMMIT_RESIDX]);
/* Debit: the exact commit call sched_try_alloc_on_host_() makes on a
* successful host claim. */
rc = xvec_idval(uncomit, &committed, XVEC_ADD_FLOAT);
TEST_ASSERT_EQUAL_INT(committed.count, rc);
TEST_ASSERT_EQUAL_FLOAT(UNCOMMIT_DEBIT, data[UNCOMMIT_RESIDX]);
/* Reverse: the new primitive under test. */
rc = sched_ctx_uncommit_host(&ctx, UNCOMMIT_HOST_IDX, &committed);
TEST_ASSERT_EQUAL_INT(committed.count, rc);
/* Back to the pre-debit baseline -- same vector instance, re-read. */
TEST_ASSERT_EQUAL_FLOAT(0.0f, data[UNCOMMIT_RESIDX]);
/* Same per-host vector instance is returned again on a second lookup
* (get-or-create semantics unchanged by the reversal). */
uncomit = NULL;
rc = sched_ctx_get_uncomit_host(&uncomit, &ctx, UNCOMMIT_HOST_IDX);
TEST_ASSERT_EQUAL_INT(1, rc);
TEST_ASSERT_EQUAL_FLOAT(0.0f, ((float*)uncomit->data)[UNCOMMIT_RESIDX]);
}
/*
* bug-1789/1790, limit-plugin-ownership refactor: sched_fill_limit_requests()
* (resource.c) computes, for each matched limit, how many units of the
* limit's own virtual resource (vres->vres) a job wants -- by reading how
* many units of the limit's UNDERLYING resource (vres->res) the job already
* requested. The write destination is one of alloc_req->shared/.host,
* chosen by vres->vres's own XCE_RESF_GLOBAL flag (set by instantiate_
* limit()->add_vres() from the same is_host_based_limit() call that put
* this limit in obj->glimits vs obj->hlimits in the first place).
*
* The read source depends on BOTH flags together: three of the four
* (scope, underlying-resource) combinations read straight out of alloc_req
* (already populated by whichever allocator plugin ran). This test proves
* the fourth, different-in-kind combination (bug-1789's actual reported
* case, generalized in bug-1790): a GLOBAL limit (write target
* alloc_req->shared) on a per-host underlying resource that IS a normal
* declared rusage -- e.g. "mem" (a future global mem cap, bug-1790's own
* regression coverage) -- alloc_req->host never holds a job-WIDE total for
* this (only per-host/per-bucket-specific data for the real per-host
* capacity check), so the want is computed directly from the job's own
* rusages instead, via calc_wanted_job_total_resval() (resreq.c) --
* PER_TASK: value * nslots, using nslots=3 here specifically to prove the
* job's WHOLE total is used, not any one host's/bucket's share (this is
* the limit-evaluation layer, allocator-agnostic -- no seqjob/parjob-
* specific test needed for this any more). "slots" is deliberately NOT
* used as the underlying resource here -- see the sibling test below for
* why it needs its own, separate coverage.
*/
static void
test_fill_limit_requests_global_limit_on_host_resource_reads_host(void)
{
xce_scheduler_t sched;
xce_resource_t mem_res_in = { 0 };
xce_resource_t limit_res_in = { 0 };
xce_resource_t* mem_res;
xce_resource_t* limit_res;
xce_sched_alloc_req_t alloc_req;
xlist_t limits;
xce_vres_t vres;
xlist_t rusages;
rusage_value_t rv;
int rc;
memset(&sched, 0, sizeof(sched));
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core)));
/* "mem" -- deliberately NOT XCE_RESF_GLOBAL, matching the real
* makeResource() call every host-capacity resource in this codebase
* goes through, AND (unlike "slots") a resource that genuinely
* appears as a normal declared rusage entry when requested via
* -R "rusage[mem=...]" -- the real production shape this test's
* on-demand rusages lookup needs to match. */
mem_res_in.name = strdup(SCHED_MEM_RESOURCE_NAME);
mem_res = makeResource(sched.core.resmgr, &mem_res_in);
TEST_ASSERT_NOT_NULL(mem_res);
/* the LIMIT's own virtual resource -- GLOBAL, exactly as
* instantiate_limit()->add_vres() would flag it for a limit like
* ug1_global_mem (is_host_based_limit() == 0). */
limit_res_in.name = strdup("limit=ug1_global_mem");
limit_res_in.flags = XCE_RESF_GLOBAL;
limit_res = makeResource(sched.core.resmgr, &limit_res_in);
TEST_ASSERT_NOT_NULL(limit_res);
memset(&vres, 0, sizeof(vres));
vres.res = mem_res;
vres.vres = limit_res;
rc = xlist_init(&limits, 4);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = xlist_store(&limits, &vres);
TEST_ASSERT_TRUE(rc >= 0);
/* the job's own declared "mem" rusage -- 2.0/task, 3 tasks total
* (nslots below), for an expected job-wide total of 6.0. */
rc = xlist_init(&rusages, 4);
TEST_ASSERT_EQUAL_INT(0, rc);
memset(&rv, 0, sizeof(rv));
rv.res = strdup(SCHED_MEM_RESOURCE_NAME);
rv.value = 2.0f;
rv.per_flag = PER_TASK;
rc = xlist_store(&rusages, &rv);
TEST_ASSERT_TRUE(rc >= 0);
rc = xvec_idval_init(&(alloc_req.shared), 8);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = xvec_idval_init(&(alloc_req.host), 8);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = sched_fill_limit_requests(&alloc_req, &limits, &rusages, 1, 3);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(1, alloc_req.shared.count);
TEST_ASSERT_EQUAL_INT(limit_res->idx, ((int*)alloc_req.shared.idxs.data)[0]);
TEST_ASSERT_EQUAL_FLOAT(6.0f, ((float*)alloc_req.shared.vals.data)[0]);
/* .host is untouched -- the limit's own vres entry went to .shared,
* and this combination never reads/writes .host at all. */
TEST_ASSERT_EQUAL_INT(0, alloc_req.host.count);
xlist_fini(&rusages);
FREEUP(rv.res);
xlist_fini(&limits);
}
/*
* bug-1790 regression (found live, gitea#46): "slots" is NEVER a declared
* rusage entry -- seqjob_prepare_allocation()/parjob_prepare_allocation()
* derive its want directly from the job's own task count (1.0 for seqjob,
* always maxTasks==1; (float)ctx->nslots for parjob), via their own
* unconditional key_find("slots", ...) + store into alloc_req->host,
* regardless of phase, entirely outside the normal rusages loop. The
* general on-demand path (calc_wanted_job_total_resval(), keyed by
* scanning rusages for a matching entry) always misses for "slots" as a
* result -- confirmed live against a fresh deploy of limits.yaml's actual
* ug1_slots/ug1_normal_slots/limit1 (all global, slots-resname'd):
* matching worked, but every job's "wanted" silently stayed 0, so the caps
* never bound at all, reintroducing bug-1789's exact original symptom.
*
* sched_fill_limit_requests() (resource.c) now reads alloc_req->host FIRST
* for this combination -- since the allocator's own unconditional "add
* slots" step already puts exactly the job-wide total there, no name-based
* special case is needed, just reading the data that's already staged --
* and only falls back to the rusages-based computation when nothing is
* staged (any OTHER per-host resource). This test proves the "slots" path
* by pre-staging alloc_req->host exactly as the allocator plugins already
* do (value == the job's whole nslots, matching parjob's own (float)ctx->
* nslots store) and leaving rusages EMPTY -- the real production shape,
* unlike this test's own first version, which artificially added a
* "slots" rusages entry and masked this exact gap.
*/
static void test_fill_limit_requests_global_limit_on_slots_reads_host(void)
{
xce_scheduler_t sched;
xce_resource_t slots_res_in = { 0 };
xce_resource_t limit_res_in = { 0 };
xce_resource_t* slots_res;
xce_resource_t* limit_res;
xce_sched_alloc_req_t alloc_req;
xlist_t limits;
xce_vres_t vres;
xlist_t rusages;
int rc;
memset(&sched, 0, sizeof(sched));
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core)));
slots_res_in.name = strdup("slots");
slots_res = makeResource(sched.core.resmgr, &slots_res_in);
TEST_ASSERT_NOT_NULL(slots_res);
limit_res_in.name = strdup("limit=ug1_normal_slots");
limit_res_in.flags = XCE_RESF_GLOBAL;
limit_res = makeResource(sched.core.resmgr, &limit_res_in);
TEST_ASSERT_NOT_NULL(limit_res);
memset(&vres, 0, sizeof(vres));
vres.res = slots_res;
vres.vres = limit_res;
rc = xlist_init(&limits, 4);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = xlist_store(&limits, &vres);
TEST_ASSERT_TRUE(rc >= 0);
/* deliberately EMPTY -- "slots" never appears here in real production
* either. */
rc = xlist_init(&rusages, 4);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = xvec_idval_init(&(alloc_req.shared), 8);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = xvec_idval_init(&(alloc_req.host), 8);
TEST_ASSERT_EQUAL_INT(0, rc);
/* pre-staged into .host, exactly as seqjob/parjob_prepare_allocation()
* always do for "slots" regardless of phase. */
rc = xvec_idval_store(&(alloc_req.host), slots_res->idx, 4.0f);
TEST_ASSERT_TRUE(rc >= 0);
rc = sched_fill_limit_requests(&alloc_req, &limits, &rusages, 1, 4);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(1, alloc_req.shared.count);
TEST_ASSERT_EQUAL_INT(limit_res->idx, ((int*)alloc_req.shared.idxs.data)[0]);
TEST_ASSERT_EQUAL_FLOAT(4.0f, ((float*)alloc_req.shared.vals.data)[0]);
/* .host stays exactly as pre-staged -- the limit's own vres entry went
* to .shared, nothing new was written to .host. */
TEST_ASSERT_EQUAL_INT(1, alloc_req.host.count);
xlist_fini(&rusages);
xlist_fini(&limits);
}
/*
* bug-1789 (already-working sibling case, unaffected by the fix): a
* HOST-scoped limit (its own vres NOT GLOBAL-flagged, so its write
* target is alloc_req->host) on a per-host resource (e.g. "mem", also
* not XCE_RESF_GLOBAL) reads from alloc_req->host too -- same vector as
* its own write target, exactly as before this fix.
*/
static void
test_fill_limit_requests_host_limit_on_host_resource_reads_host(void)
{
xce_scheduler_t sched;
xce_resource_t mem_res_in = { 0 };
xce_resource_t limit_res_in = { 0 };
xce_resource_t* mem_res;
xce_resource_t* limit_res;
xce_sched_alloc_req_t alloc_req;
xlist_t limits;
xce_vres_t vres;
int rc;
memset(&sched, 0, sizeof(sched));
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core)));
mem_res_in.name = strdup(SCHED_MEM_RESOURCE_NAME);
mem_res = makeResource(sched.core.resmgr, &mem_res_in);
TEST_ASSERT_NOT_NULL(mem_res);
/* the LIMIT's own virtual resource -- NOT GLOBAL (host-scoped limit,
* is_host_based_limit() == 1); flags left at the struct's zero
* default is sufficient, mirroring add_vres()'s XCE_RESF_DEFAULT_HOST
* (not GLOBAL) branch. */
limit_res_in.name = strdup("limit=mem_cap");
limit_res = makeResource(sched.core.resmgr, &limit_res_in);
TEST_ASSERT_NOT_NULL(limit_res);
memset(&vres, 0, sizeof(vres));
vres.res = mem_res;
vres.vres = limit_res;
rc = xlist_init(&limits, 4);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = xlist_store(&limits, &vres);
TEST_ASSERT_TRUE(rc >= 0);
rc = xvec_idval_init(&(alloc_req.shared), 8);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = xvec_idval_init(&(alloc_req.host), 8);
TEST_ASSERT_EQUAL_INT(0, rc);
/* pre-staged, as calc_wanted_host_resvals() would have already done
* for a real "mem" rusage want. */
rc = xvec_idval_store(&(alloc_req.host), mem_res->idx, 3.0f);
TEST_ASSERT_TRUE(rc >= 0);
rc = sched_fill_limit_requests(&alloc_req, &limits, NULL, 0, 0);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(2, alloc_req.host.count);
TEST_ASSERT_EQUAL_INT(limit_res->idx, ((int*)alloc_req.host.idxs.data)[1]);
TEST_ASSERT_EQUAL_FLOAT(3.0f, ((float*)alloc_req.host.vals.data)[1]);
xlist_fini(&limits);
}
/*
* bug-1789 (symmetric case, proves the dispatch is a genuine flag-based
* rule and not a "slots" special case): a HOST-scoped limit (its own
* vres NOT GLOBAL-flagged, so its write target is alloc_req->host) on a
* resource that IS XCE_RESF_GLOBAL (e.g. a pooled license count) must
* read from alloc_req->shared instead -- the opposite vector from its
* write target, mirroring exactly how calc_wanted_global_resvals() is
* the only one of the pair that ever populates a GLOBAL-flagged
* resource's want.
*/
static void
test_fill_limit_requests_host_limit_on_global_resource_reads_shared(void)
{
xce_scheduler_t sched;
xce_resource_t license_res_in = { 0 };
xce_resource_t limit_res_in = { 0 };
xce_resource_t* license_res;
xce_resource_t* limit_res;
xce_sched_alloc_req_t alloc_req;
xlist_t limits;
xce_vres_t vres;
int rc;
memset(&sched, 0, sizeof(sched));
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core)));
license_res_in.name = strdup("licenses");
license_res_in.flags = XCE_RESF_GLOBAL;
license_res = makeResource(sched.core.resmgr, &license_res_in);
TEST_ASSERT_NOT_NULL(license_res);
/* the LIMIT's own virtual resource -- NOT GLOBAL (host-scoped limit),
* same rationale as the sibling test above. */
limit_res_in.name = strdup("limit=license_cap");
limit_res = makeResource(sched.core.resmgr, &limit_res_in);
TEST_ASSERT_NOT_NULL(limit_res);
memset(&vres, 0, sizeof(vres));
vres.res = license_res;
vres.vres = limit_res;
rc = xlist_init(&limits, 4);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = xlist_store(&limits, &vres);
TEST_ASSERT_TRUE(rc >= 0);
rc = xvec_idval_init(&(alloc_req.shared), 8);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = xvec_idval_init(&(alloc_req.host), 8);
TEST_ASSERT_EQUAL_INT(0, rc);
/* pre-staged into .shared (this job's earlier global-phase pass), NOT
* .host -- .host (this limit's own write target, derived from
* limit_res's non-GLOBAL flag) starts empty. */
rc = xvec_idval_store(&(alloc_req.shared), license_res->idx, 5.0f);
TEST_ASSERT_TRUE(rc >= 0);
rc = sched_fill_limit_requests(&alloc_req, &limits, NULL, 0, 0);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(1, alloc_req.host.count);
TEST_ASSERT_EQUAL_INT(limit_res->idx, ((int*)alloc_req.host.idxs.data)[0]);
TEST_ASSERT_EQUAL_FLOAT(5.0f, ((float*)alloc_req.host.vals.data)[0]);
xlist_fini(&limits);
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_charge_account_identical_across_core_containers);
RUN_TEST(test_commit_job_alloc_identical_across_core_containers);
RUN_TEST(test_charge_account_skips_reconciling_or_unavail_worker_host_charge);
RUN_TEST(
test_reconcile_host_resource_usage_below_reservation_keeps_reservation);
RUN_TEST(
test_reconcile_host_resource_usage_above_reservation_charges_actual);
RUN_TEST(
test_reconcile_host_resource_usage_repeated_growth_applies_only_delta);
RUN_TEST(test_reconcile_host_resource_usage_null_args_return_einval);
RUN_TEST(
test_reconcile_host_resource_usage_skips_reconciling_or_unavail_worker);
RUN_TEST(test_reconcile_job_resource_usage_maps_memory_rss_to_mem_account);
RUN_TEST(test_reconcile_job_resource_usage_skips_multihost_jobs);
RUN_TEST(test_reconcile_job_resource_usage_charges_unreserved_job_full_usage);
RUN_TEST(test_reconcile_job_resource_usage_noop_when_mem_not_a_registered_resource);
RUN_TEST(test_reconcile_job_resource_usage_null_args_return_einval);
RUN_TEST(test_ctx_uncommit_host_reverses_provisional_debit);
RUN_TEST(test_fill_limit_requests_global_limit_on_host_resource_reads_host);
RUN_TEST(test_fill_limit_requests_global_limit_on_slots_reads_host);
RUN_TEST(test_fill_limit_requests_host_limit_on_host_resource_reads_host);
RUN_TEST(test_fill_limit_requests_host_limit_on_global_resource_reads_shared);
return UNITY_END();
}