E2 Phase 3: per-project/per-queue license quotas via limit vres + controller shared-alloc migration fix
Strategy layer (spec .kiro/specs/external-license-resources design.md 3.5/3.6): - limits.yaml samples: extlic_share (per-project, projA/projB, cap 3.0) and extlic_qdist (per-queue, normal/night, cap 4.0) on the externally reported extlic pool -- instances "limit=extlic_share/p=<proj>" and "limit=extlic_qdist/q=<queue>" under the existing limit vres mechanism. Multi-daemon redundancy is free (resval last-writer-wins); manual override REST deliberately out of scope. - White-box scheduler test (test_limit_project_scope.c): project-scope match + vres instantiation, per-project instance fan-out with caps, unknown underlying resource failing clean. 3/3 under ASAN. Controller fix (the Phase 3 E2E blocker, TDD red->green): - dispatch_a_job_() rebuilt j->job.allocs from the scheduler's dispatch payload but migrated only the per-host records -- allocs_shared (the limit vres charge among them) was dropped, and its payload pointer leaked. That rebuilt record is what sched_job_update_fun() re-sends to the scheduler as ground truth (the JOB_UPDATE adoption there rebuilds job->accounts from it), so sharedInUse never accumulated the vres charge: unsatisfy_ admitted past every global limit forever. Fix: MOVE the shared records alongside the host records (test test_dc_success_migrates_shared_allocs). t1 E2E (4 licenses, extlic_share projA/projB cap 3.0): 3 projA RUN, 4th projA PEND, projB unaffected (own per-project instance); killing one RUN job re-admits the pending projA; external free pool and per-project shares enforced independently; all daemons healthy throughout. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
857f987f3c
commit
76ddd75fd1
5 changed files with 648 additions and 0 deletions
|
|
@ -93,3 +93,35 @@ limits:
|
|||
resname: mem
|
||||
value: 0.2
|
||||
percent: true
|
||||
|
||||
# external-license-resources (E2 Phase 3): strategy-layer quotas on the
|
||||
# externally reported extlic pool. These ride the EXISTING limit vres
|
||||
# mechanism -- no new daemon, no new admission rule -- on top of Phase
|
||||
# 1's underlying-resource check (total >= uncomit + wanted). Two
|
||||
# distribution shapes:
|
||||
#
|
||||
# 1. per-project shares: projA and projB each hold their own
|
||||
# "limit=extlic_share/p=<proj>" usage instance under a 3.0 cap
|
||||
# (independent accounting per project; the scope lists who may
|
||||
# match at all). literal members here; named project groups are
|
||||
# project-config territory (out of this spec's scope).
|
||||
- name: extlic_share
|
||||
projects:
|
||||
members:
|
||||
- projA
|
||||
- projB
|
||||
resname: extlic
|
||||
value: 3.0
|
||||
per_project: true
|
||||
|
||||
# 2. per-queue distribution: queues each get their own
|
||||
# "limit=extlic_qdist/q=<queue>" instance under a 4.0 cap -- how a
|
||||
# site partitions one license pool across queues.
|
||||
- name: extlic_qdist
|
||||
queues:
|
||||
members:
|
||||
- normal
|
||||
- night
|
||||
resname: extlic
|
||||
value: 4.0
|
||||
per_queue: true
|
||||
|
|
|
|||
|
|
@ -1936,6 +1936,19 @@ static int dispatch_a_job_(cluster_t* c, endpoint_t* ep, xce_job_alloc_t* alloc)
|
|||
MOVE_PTR(j->job.allocs->allocs, allocs);
|
||||
alloc->allocs = NULL;
|
||||
|
||||
/* external-license-resources Phase 3: the scheduler's dispatch
|
||||
* payload also carries the shared-pool records (allocs_shared -- the
|
||||
* per-project/per-queue limit vres charge among them). This rebuild
|
||||
* is the record sched_job_update_fun() re-sends to the scheduler as
|
||||
* ground truth (the JOB_UPDATE adoption there rebuilds job->accounts
|
||||
* from it), so dropping the shared records here starves the
|
||||
* scheduler's sharedInUse accounting and every global limit admits
|
||||
* forever. Migrate them (MOVE, not copy -- the caller frees the
|
||||
* payload) exactly like the per-host records above. */
|
||||
MOVE_PTR(j->job.allocs->allocs_shared, alloc->allocs_shared);
|
||||
j->job.allocs->nres_shared = alloc->nres_shared;
|
||||
alloc->nres_shared = 0;
|
||||
|
||||
j->job.state = JOB_STAT_RUN;
|
||||
|
||||
/* bug-1697: start_time is now stamped HERE, synchronously alongside
|
||||
|
|
|
|||
|
|
@ -393,6 +393,78 @@ static void test_dc_success_notifies_job_update_hook_exactly_once(void)
|
|||
printf("test_dc_success_notifies_job_update_hook_exactly_once: PASS\n");
|
||||
}
|
||||
|
||||
#define DC_JOBID_SHARED 91006ULL
|
||||
|
||||
/* external-license-resources Phase 3 (vres charge) RED/GREEN proof: the
|
||||
* scheduler's dispatch payload (sched_finalize_job_alloc_, alloc_runner.c)
|
||||
* carries BOTH the per-host records AND the shared-pool records
|
||||
* (allocs_shared -- the vres instance "limit=extlic_share/p=projA" charge
|
||||
* among them). The controller's dispatch-time commit (job.c) rebuilds
|
||||
* j->job.allocs from that payload, and that rebuilt record is what
|
||||
* sched_job_update_fun() re-sends to the scheduler as ground truth
|
||||
* (bug-1860 adoption in the scheduler's JOB_UPDATE handler rebuilds
|
||||
* job->accounts from it) -- so if the rebuild drops allocs_shared, the
|
||||
* scheduler's accounts lose every shared record, sharedInUse never
|
||||
* accumulates the vres charge, and unsatisfy_ admits past the limit
|
||||
* forever.
|
||||
*
|
||||
* RED against the pre-fix code: j->job.allocs->nres_shared stays 0 and
|
||||
* allocs_shared stays NULL (the rebuild MOVE_PTRs only allocs->allocs).
|
||||
* GREEN after the fix: the shared records are migrated intact (count,
|
||||
* names, values), and the payload's own shared pointer is moved out (no
|
||||
* double free). */
|
||||
static void test_dc_success_migrates_shared_allocs(void)
|
||||
{
|
||||
job_t* j;
|
||||
xce_job_alloc_t* alloc;
|
||||
xce_alloc_t** shared;
|
||||
int rc;
|
||||
|
||||
dc_setup();
|
||||
|
||||
j = dc_seed_ready(DC_JOBID_SHARED, 0, "dq", "wkr-dc-shared");
|
||||
|
||||
alloc = make_job_alloc(DC_JOBID_SHARED, 1,
|
||||
make_host_allocs(1,
|
||||
make_host_alloc((char*)"wkr-dc-shared", 1,
|
||||
make_allocs(1,
|
||||
make_alloc((char*)"slots", NULL, 0, 1.0f, 0)))));
|
||||
TEST_ASSERT_NOT_NULL(alloc);
|
||||
|
||||
shared = make_allocs(2,
|
||||
make_alloc((char*)"limit=extlic_share/p=projA", NULL, 0, 1.0f, 0),
|
||||
make_alloc((char*)"extlic", NULL, 0, 1.0f, 0));
|
||||
TEST_ASSERT_NOT_NULL(shared);
|
||||
alloc->allocs_shared = shared;
|
||||
alloc->nres_shared = 2;
|
||||
|
||||
rc = dispatch_a_job_test(&g_dc, NULL, alloc);
|
||||
TEST_ASSERT_TRUE_MESSAGE(rc >= 0,
|
||||
"a dispatch to a live, no-iomgr worker endpoint must succeed");
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(JOB_STAT_RUN, j->job.state);
|
||||
TEST_ASSERT_NOT_NULL(j->job.allocs);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(2, j->job.allocs->nres_shared,
|
||||
"the dispatch-time rebuild must migrate the payload's shared-pool "
|
||||
"records -- dropping them starves the scheduler's vres usage "
|
||||
"accounting (sharedInUse) and defeats every global limit");
|
||||
TEST_ASSERT_NOT_NULL(j->job.allocs->allocs_shared);
|
||||
TEST_ASSERT_EQUAL_STRING("limit=extlic_share/p=projA",
|
||||
j->job.allocs->allocs_shared[0]->resname);
|
||||
TEST_ASSERT_EQUAL_FLOAT(1.0f, j->job.allocs->allocs_shared[0]->value);
|
||||
TEST_ASSERT_EQUAL_STRING("extlic",
|
||||
j->job.allocs->allocs_shared[1]->resname);
|
||||
TEST_ASSERT_EQUAL_FLOAT(1.0f, j->job.allocs->allocs_shared[1]->value);
|
||||
TEST_ASSERT_NULL_MESSAGE(alloc->allocs_shared,
|
||||
"the shared records must be MOVE_PTR'd out of the payload, not "
|
||||
"copied -- a copy would double-free when the caller frees it");
|
||||
|
||||
free_JobAlloc_ptr(&alloc);
|
||||
dc_drain_ep(&g_dc_worker_ep);
|
||||
dc_teardown();
|
||||
printf("test_dc_success_migrates_shared_allocs: PASS\n");
|
||||
}
|
||||
|
||||
#define DC_JOBID_NOTFOUND 91002ULL
|
||||
|
||||
/* Regression safety net (untouched failure branch): a dispatch whose
|
||||
|
|
@ -801,6 +873,7 @@ void test_dispatch_commit(void)
|
|||
RUN_TEST(test_dc_success_commits_alloc_state_and_resets_counter);
|
||||
RUN_TEST(test_dc_success_stamps_start_time_synchronously);
|
||||
RUN_TEST(test_dc_success_notifies_job_update_hook_exactly_once);
|
||||
RUN_TEST(test_dc_success_migrates_shared_allocs);
|
||||
RUN_TEST(test_dc_worker_not_found_leaves_job_pend_and_alloc_uncommitted);
|
||||
RUN_TEST(test_dc_success_appends_wal_job_dispatch_durably);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1634,6 +1634,23 @@ sched_limit_plugin_test = executable('sched_limit_plugin_test',
|
|||
dependencies: [scheduler_test_deps],
|
||||
install: false)
|
||||
|
||||
# external-license-resources Phase 3 (LSF 策略层): white-box regression
|
||||
# coverage for the "GLOBAL limit on a GLOBAL|SHARED|EXTERNAL resource"
|
||||
# shape (the per-project/per-queue/per-user license-quota mapping onto the
|
||||
# existing limit vres mechanism). It #includes ../src/plugins/limit.c +
|
||||
# limit_config.c directly (same convention as sched_limit_plugin_test
|
||||
# above) so it drives the real match_job_global_limit_() -- `static`,
|
||||
# otherwise unreachable. Unlike test_limit_plugin.c, its get_resvals()/
|
||||
# put_resvals() are FAITHFUL COPIES of resource.c's (the value path is the
|
||||
# subject), and the fixture carries a real core->resmgr +
|
||||
# sharedInst/sharedInUse -- see the file's own header comment.
|
||||
sched_limit_project_scope_test = executable(
|
||||
'sched_limit_project_scope_test',
|
||||
['test_limit_project_scope.c'],
|
||||
include_directories:[incdir],
|
||||
dependencies: [scheduler_test_deps],
|
||||
install: false)
|
||||
|
||||
# White-box regression test for bug-1686 (live SIGSEGV in
|
||||
# fcfs_cursor_enter_queue() <- builtin_fcfs_next_job(), a heap-use-after-free
|
||||
# on priv->cursor/job_cursor -- raw ordq_item_t* values that persist across
|
||||
|
|
@ -2070,6 +2087,8 @@ test('scheduler snapshot load allocator regression test',
|
|||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
test('scheduler limit plugin test', sched_limit_plugin_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
test('scheduler limit project scope test', sched_limit_project_scope_test,
|
||||
suite: 'scheduler', env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
test('scheduler builtin fcfs cursor uaf test',
|
||||
sched_builtin_fcfs_cursor_uaf_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
|
|
|
|||
511
src/scheduler/testsuite/test_limit_project_scope.c
Normal file
511
src/scheduler/testsuite/test_limit_project_scope.c
Normal file
|
|
@ -0,0 +1,511 @@
|
|||
/*
|
||||
* test_limit_project_scope.c - external-license-resources Phase 3 (LSF
|
||||
* 策略层, design.md 3.5): per-project/per-queue/per-user token distribution
|
||||
* maps onto the EXISTING limit vres mechanism. A limit on a GLOBAL
|
||||
* underlying resource (e.g. the extlic license pool, SHARED|GLOBAL|
|
||||
* EXTERNAL per mapping.py's location: [external] branch) with a projects:
|
||||
* scope must:
|
||||
*
|
||||
* 1. match jobs by their obj->project string (literal project members --
|
||||
* make_g_member()'s literal fallback; named project groups are
|
||||
* project-config's territory, deliberately not exercised here);
|
||||
* 2. intern a usage instance named "limit=<name>" (no /p= -- per_project
|
||||
* false means one shared pool for all members of the project scope),
|
||||
* or "limit=<name>/p=<proj>" when per_project is set (independent
|
||||
* usage per project under one configured cap);
|
||||
* 3. write its cap into core->sharedInst at the vres's own idx
|
||||
* (update_limit_res_value());
|
||||
* 4. take the matched vres into obj->glimits (match_job_global_limit_
|
||||
* -> xlist_store), so sched_fill_limit_requests() (resource.c) can
|
||||
* stage the job's want into alloc_req.shared and the generic satisfy
|
||||
* check can compare want+uncomit against the cap like any resource.
|
||||
*
|
||||
* The mechanical shape match_job_global_limit_ needs from the core:
|
||||
* - core->resmgr: must already contain the UNDERLYING resource named by
|
||||
* limit->val.resname (instantiate_limit() returns -ENOENT otherwise)
|
||||
* AND grows the limit's vres via add_vres() -- exactly what
|
||||
* sched_init_resources() does live;
|
||||
* - core->sharedInst: the cap's zarray (update_limit_res_value()'s
|
||||
* put_resvals target);
|
||||
* - core->sharedInUse: where the generic check reads RUN jobs' usage
|
||||
* (init here for parity; not touched by the plugin).
|
||||
*
|
||||
* White-box: #include's the real ../src/plugins/limit.c +
|
||||
* limit_config.c (same convention as test_limit_plugin.c, whose fixture
|
||||
* this file mirrors -- real group managers, real plugin private state).
|
||||
* match_job_global_limit_() is `static`, otherwise unreachable outside
|
||||
* its TU. get_resvals()/put_resvals() are NOT no-op stubs here (unlike
|
||||
* test_limit_plugin.c, whose tests never exercise the value path): the
|
||||
* value path IS this file's subject, so both are faithful copies of
|
||||
* resource.c's (that file is not part of this TU; the copies preserve the
|
||||
* production zarray protocol -- xce_val_t entries in Value_u_fvalue form,
|
||||
* created zero-filled on first put, read as 0.0 when absent) so the
|
||||
* fixture's sharedInst ends in the same state production would leave,
|
||||
* and the cap assertions below prove the plugin put the RIGHT value at
|
||||
* the RIGHT idx, not merely that it called something.
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "group.h"
|
||||
#include "kvmgr.h"
|
||||
#include "scheduler.h"
|
||||
#include "xce/types.h"
|
||||
#include "xcelimit.h"
|
||||
#include "xstring.h"
|
||||
|
||||
/* Faithful copies of resource.c's get_resvals()/put_resvals() (see file
|
||||
* header). The plugin always calls them with nres == 1, so the copies
|
||||
* need no dedup-by-duplicate-idx handling (resource.c's bug-1569 block is
|
||||
* dead weight at nres 1). */
|
||||
int get_resvals(zarray_t* array, int nres, int* idxs, float* vals)
|
||||
{
|
||||
xce_val_t* xvals[1];
|
||||
int rc;
|
||||
|
||||
if ((array == NULL) || (nres < 0) || (idxs == NULL) || (vals == NULL)) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if (nres == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
memset(vals, 0x0, sizeof(*vals) * nres);
|
||||
|
||||
rc = zarray_get_ex(array, nres, idxs, (void**)xvals);
|
||||
if (rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
if ((xvals[0] != NULL) && (xvals[0]->which == Value_u_fvalue)) {
|
||||
vals[0] = xvals[0]->u.fvalue;
|
||||
}
|
||||
|
||||
return nres;
|
||||
}
|
||||
|
||||
int put_resvals(zarray_t* array, int nres, int* idxs, float* vals)
|
||||
{
|
||||
xce_val_t* xvals[1];
|
||||
int rc;
|
||||
|
||||
if ((array == NULL) || (nres < 0) || (idxs == NULL) || (vals == NULL)) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if (nres == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
rc = zarray_get_ex(array, nres, idxs, (void**)xvals);
|
||||
if (rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (idxs[0] < 0) {
|
||||
return nres;
|
||||
}
|
||||
|
||||
if (xvals[0] == NULL) {
|
||||
xce_val_t* val = (xce_val_t*)calloc(1, sizeof(*val));
|
||||
if (val == NULL) {
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
val->which = Value_u_fvalue;
|
||||
val->u.fvalue = 0.0;
|
||||
zarray_put(array, idxs[0], val);
|
||||
xvals[0] = val;
|
||||
}
|
||||
|
||||
xvals[0]->u.fvalue = vals[0];
|
||||
|
||||
return nres;
|
||||
}
|
||||
|
||||
/* limit_job_election() calls reset_sched_obj() on the REAL schedobj;
|
||||
* this TU drives match_job_global_limit_() directly, so a stub purely
|
||||
* for the linker -- same as test_limit_plugin.c. */
|
||||
void reset_sched_obj(xce_sched_obj_t* obj)
|
||||
{
|
||||
(void)obj;
|
||||
}
|
||||
|
||||
int plugin_register(void* sched, void* plugin)
|
||||
{
|
||||
(void)sched;
|
||||
(void)plugin;
|
||||
return 0;
|
||||
}
|
||||
|
||||
#include "../src/plugins/limit.c"
|
||||
#include "../src/plugins/limit_config.c"
|
||||
|
||||
void setUp(void) { return; }
|
||||
void tearDown(void) { return; }
|
||||
|
||||
/* Same fixture as test_limit_plugin.c's fixture_init(): real group
|
||||
* managers for all four consumer kinds (build_limit()'s build_consumer()
|
||||
* resolves each consumer against its own manager -- the "all" group for
|
||||
* any scope the config leaves NULL), plus a real limit plugin private
|
||||
* state. plugin->core is the bug-1964 honest reference. Extended here
|
||||
* with the two pieces production's xce_sched_core_init() builds and the
|
||||
* plugin's value path touches: core->resmgr (a kvmgr with no datakiller
|
||||
* -- suite convention, detect_leaks=0, no teardown) and the
|
||||
* sharedInst/sharedInUse zarays. */
|
||||
static void fixture_init(xce_scheduler_t* sched, xce_sched_plugin_t* plugin)
|
||||
{
|
||||
kvmgr_params_t resmgr_params = { .key_type = KVMGR_KEY_STRING,
|
||||
.nslots = 8,
|
||||
.delta = 4,
|
||||
.datakiller = NULL };
|
||||
int rc;
|
||||
|
||||
memset(sched, 0, sizeof(*sched));
|
||||
|
||||
sched->core.resmgr = makeInstance(&xkvmgr_type, &resmgr_params);
|
||||
TEST_ASSERT_NOT_NULL(sched->core.resmgr);
|
||||
|
||||
groupmgr_init(&(sched->core.ugmgr), 4, 4);
|
||||
groupmgr_init(&(sched->core.qgmgr), 4, 4);
|
||||
groupmgr_init(&(sched->core.pgmgr), 4, 4);
|
||||
groupmgr_init(&(sched->core.hgmgr), 4, 4);
|
||||
TEST_ASSERT_NOT_NULL(sched->core.ugmgr);
|
||||
TEST_ASSERT_NOT_NULL(sched->core.qgmgr);
|
||||
TEST_ASSERT_NOT_NULL(sched->core.pgmgr);
|
||||
TEST_ASSERT_NOT_NULL(sched->core.hgmgr);
|
||||
|
||||
zarray_init(&(sched->core.sharedInst), 8, NULL);
|
||||
zarray_init(&(sched->core.sharedInUse), 8, NULL);
|
||||
|
||||
memset(plugin, 0, sizeof(*plugin));
|
||||
plugin->sched = sched;
|
||||
plugin->core = &(sched->core);
|
||||
|
||||
rc = limit_init(plugin);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_NOT_NULL(plugin->data);
|
||||
}
|
||||
|
||||
/* sched_init_resources()'s role, reduced to what the limit plugin needs:
|
||||
* the UNDERLYING resource must exist in core->resmgr before the first
|
||||
* match instantiates the vres (instantiate_limit()'s key_find of
|
||||
* limit->val.resname returns -ENOENT otherwise -- the "unknown resource"
|
||||
* failure mode test 3 pins). `flags` picks the underlying resource's own
|
||||
* nature: SHARED|GLOBAL|EXTERNAL is the exact shape mapping.py's
|
||||
* location: [external] branch produces for extlic. NOTE: makeResource_
|
||||
* assigns the resource's idx itself (the kvmgr slot), so the caller's
|
||||
* wire.idx is ignored -- tests must read idx back, never assume it. */
|
||||
static xce_resource_t* register_resource(xce_sched_core_t* core,
|
||||
const char* name, int flags)
|
||||
{
|
||||
xce_resource_t wire;
|
||||
xce_resource_t* made = NULL;
|
||||
|
||||
memset(&wire, 0, sizeof(wire));
|
||||
wire.name = (char*)name;
|
||||
wire.flags = flags;
|
||||
|
||||
made = (xce_resource_t*)makeResource(core->resmgr, &wire);
|
||||
TEST_ASSERT_NOT_NULL(made);
|
||||
|
||||
return made;
|
||||
}
|
||||
|
||||
/* push limit configs through the plugin's config hook (both drivers do
|
||||
* this -- plugin_config_update() live, plugin_config_update_core() sim) */
|
||||
static int push_limits(xce_sched_plugin_t* plugin, xce_limit_config_t** limits,
|
||||
int n)
|
||||
{
|
||||
xce_conf4sched_t config;
|
||||
|
||||
memset(&config, 0, sizeof(config));
|
||||
config.nlimits = n;
|
||||
config.limits = limits;
|
||||
|
||||
return limit_update_config(plugin, &config);
|
||||
}
|
||||
|
||||
/* a heap limit config: scope = literal project members (heap group config
|
||||
* -- the plugin's built limit references the group config's members for
|
||||
* the consumer group's lifetime), absolute value cap */
|
||||
static xce_limit_config_t* make_project_limit(const char* name,
|
||||
const char* resname, float cap,
|
||||
const char* const* projects,
|
||||
int nprojects)
|
||||
{
|
||||
xce_limit_config_t* cfg = (xce_limit_config_t*)calloc(1, sizeof(*cfg));
|
||||
xce_limit_val_t* val = (xce_limit_val_t*)calloc(1, sizeof(*val));
|
||||
xce_group_config_t* g = (xce_group_config_t*)calloc(1, sizeof(*g));
|
||||
char** members = (char**)calloc((size_t)nprojects,
|
||||
sizeof(char*));
|
||||
int i;
|
||||
|
||||
TEST_ASSERT_NOT_NULL(cfg);
|
||||
TEST_ASSERT_NOT_NULL(val);
|
||||
TEST_ASSERT_NOT_NULL(g);
|
||||
TEST_ASSERT_NOT_NULL(members);
|
||||
|
||||
val->resname = (char*)resname;
|
||||
val->percent = 0;
|
||||
val->val = cap;
|
||||
|
||||
for (i = 0; i < nprojects; i++) {
|
||||
members[i] = (char*)projects[i];
|
||||
}
|
||||
g->members.count = nprojects;
|
||||
g->members.data = members;
|
||||
|
||||
cfg->name = (char*)name;
|
||||
cfg->val = val;
|
||||
cfg->projects = g;
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
static void free_limit_config(xce_limit_config_t* cfg)
|
||||
{
|
||||
FREEUP(cfg->val);
|
||||
FREEUP(cfg->projects->members.data);
|
||||
FREEUP(cfg->projects);
|
||||
FREEUP(cfg);
|
||||
}
|
||||
|
||||
static xce_limit_t* find_limit(xce_sched_plugin_t* plugin, const char* name)
|
||||
{
|
||||
limit_plugin_data_t* privdata = (limit_plugin_data_t*)plugin->data;
|
||||
xce_limit_t* limit = NULL;
|
||||
|
||||
key_find(privdata->limits, (void*)name, (void**)&limit);
|
||||
|
||||
return limit;
|
||||
}
|
||||
|
||||
static xce_vres_t* find_instance(xce_sched_plugin_t* plugin, const char* name)
|
||||
{
|
||||
limit_plugin_data_t* privdata = (limit_plugin_data_t*)plugin->data;
|
||||
xce_vres_t* vres = NULL;
|
||||
|
||||
key_find(privdata->instances, (void*)name, (void**)&vres);
|
||||
|
||||
return vres;
|
||||
}
|
||||
|
||||
/* a minimal xce_sched_obj_t: project identity + the glimits xlist the
|
||||
* plugin stores matched vres into. make_sched_obj() (schedobj.c) builds
|
||||
* the real one; its project normalization ("default" for empty) is
|
||||
* core_events.c's job, and the plugin consumes the result as-is -- so the
|
||||
* fixture hands obj->project the string the scheduler would have. */
|
||||
static void make_obj(xce_sched_obj_t* obj, const char* project)
|
||||
{
|
||||
memset(obj, 0, sizeof(*obj));
|
||||
obj->user = makeString("user1");
|
||||
obj->queue = makeString("normal");
|
||||
obj->project = makeString((char*)project);
|
||||
xlist_init(&(obj->glimits), 8);
|
||||
}
|
||||
|
||||
/* drive match_job_global_limit_() the way limit_job_election() does:
|
||||
* throwaway ctx, only ctx.schedobj is read. */
|
||||
static int match_global(xce_sched_plugin_t* plugin, xce_sched_obj_t* obj,
|
||||
xce_limit_t* limit)
|
||||
{
|
||||
xce_sched_ctx_t ctx = { 0 };
|
||||
|
||||
ctx.schedobj = obj;
|
||||
|
||||
return match_job_global_limit_(plugin, &ctx, limit);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* 1. A project-scoped limit on a GLOBAL|SHARED|EXTERNAL resource matches
|
||||
* by obj->project (literal member), is global (lands in glimits --
|
||||
* is_host_based_limit() keys on the HOST consumer, never the underlying
|
||||
* resource's own GLOBAL flag), and instantiates the usage instance named
|
||||
* "limit=<name>" (per_project false -> one shared pool for the project
|
||||
* scope; no /p= segment). Members outside the scope match nothing and
|
||||
* store nothing. */
|
||||
static void test_project_scope_match_and_instantiate(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_sched_plugin_t plugin;
|
||||
xce_sched_obj_t obj;
|
||||
xce_limit_config_t* limits[1];
|
||||
xce_limit_t* limit;
|
||||
xce_vres_t* vres;
|
||||
const char* const scope_a[] = { "projA" };
|
||||
char vresname[64];
|
||||
int rc;
|
||||
|
||||
fixture_init(&sched, &plugin);
|
||||
|
||||
/* the extlic shape: SHARED (cluster pool) + GLOBAL (admitted in the
|
||||
* shared leg) + EXTERNAL (free-value reporting; Phase 1's unsatisfy_
|
||||
* rule -- asserted independently in testsuite/test.c). */
|
||||
register_resource(&(sched.core), "extlic",
|
||||
XCE_RESF_SHARED | XCE_RESF_GLOBAL | XCE_RESF_EXTERNAL);
|
||||
|
||||
limits[0] = make_project_limit("extlic_projA", "extlic", 2.0f, scope_a, 1);
|
||||
TEST_ASSERT_EQUAL_INT(0, push_limits(&plugin, limits, 1));
|
||||
|
||||
limit = find_limit(&plugin, "extlic_projA");
|
||||
TEST_ASSERT_NOT_NULL(limit);
|
||||
|
||||
/* in-scope project matches. match_job_global_limit_()'s own rc
|
||||
* contract is "0 on success" (its tail rc = xlist_store() returns the
|
||||
* stored idx -- 0 for the first entry; only < 0 is failure, the same
|
||||
* rc<0 break limit_job_election() applies), so the success signal is
|
||||
* rc >= 0 AND the vres landing in obj.glimits -- asserted below. */
|
||||
make_obj(&obj, "projA");
|
||||
rc = match_global(&plugin, &obj, limit);
|
||||
TEST_ASSERT_GREATER_OR_EQUAL_INT(0, rc);
|
||||
|
||||
/* the instance is keyed by the bare vres name (per_project false) */
|
||||
snprintf(vresname, sizeof(vresname), "limit=extlic_projA");
|
||||
vres = find_instance(&plugin, vresname);
|
||||
TEST_ASSERT_NOT_NULL(vres);
|
||||
TEST_ASSERT_EQUAL_PTR(limit, vres->limit);
|
||||
TEST_ASSERT_NOT_NULL(vres->vres);
|
||||
TEST_ASSERT_EQUAL_STRING(vresname, vres->vres->name);
|
||||
/* the vres is a REAL resource now: SHARED|GLOBAL (its own nature, from
|
||||
* add_vres()'s `shared` flag derived from is_host_based_limit()) +
|
||||
* LIMIT (the satisfy check's PREASON_LIMIT_GLOBAL discriminator). */
|
||||
TEST_ASSERT_TRUE((vres->vres->flags & XCE_RESF_LIMIT) != 0);
|
||||
TEST_ASSERT_TRUE((vres->vres->flags & XCE_RESF_SHARED) != 0);
|
||||
TEST_ASSERT_TRUE((vres->vres->flags & XCE_RESF_GLOBAL) != 0);
|
||||
/* the underlying resource is linked */
|
||||
TEST_ASSERT_NOT_NULL(vres->res);
|
||||
TEST_ASSERT_EQUAL_STRING("extlic", vres->res->name);
|
||||
|
||||
/* the matched vres was stored into obj->glimits for
|
||||
* sched_fill_limit_requests() to stage */
|
||||
TEST_ASSERT_EQUAL_INT(1, obj.glimits.count);
|
||||
TEST_ASSERT_EQUAL_PTR(vres, xlist_get(&(obj.glimits), 0));
|
||||
|
||||
/* out-of-scope project: no match, nothing stored */
|
||||
make_obj(&obj, "projB");
|
||||
rc = match_global(&plugin, &obj, limit);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(0, obj.glimits.count);
|
||||
|
||||
free_limit_config(limits[0]);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* 2. The cap lands in core->sharedInst at the vres's own idx, and the
|
||||
* per_project variant slices by project ("limit=<name>/p=<proj>"): two
|
||||
* projects get independent usage entries under one configured cap. */
|
||||
static void test_cap_value_and_per_project_instance_names(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_sched_plugin_t plugin;
|
||||
xce_sched_obj_t obj;
|
||||
xce_limit_config_t* limits[2];
|
||||
xce_limit_t* shared_pool;
|
||||
xce_limit_t* per_project;
|
||||
xce_vres_t* vres;
|
||||
xce_val_t* v;
|
||||
const char* const scope_a[] = { "projA" };
|
||||
const char* const scope_ab[] = { "projA", "projB" };
|
||||
char name[64];
|
||||
int rc;
|
||||
|
||||
fixture_init(&sched, &plugin);
|
||||
|
||||
register_resource(&(sched.core), "extlic",
|
||||
XCE_RESF_SHARED | XCE_RESF_GLOBAL | XCE_RESF_EXTERNAL);
|
||||
|
||||
limits[0] = make_project_limit("extlic_pool", "extlic", 2.0f, scope_a, 1);
|
||||
/* per-project slicing needs BOTH projects in the limit's scope: the
|
||||
* scope decides who matches at all; per_project then splits each
|
||||
* match into its own "limit=<name>/p=<proj>" usage instance. */
|
||||
limits[1] = make_project_limit("extlic_share", "extlic", 3.0f, scope_ab,
|
||||
2);
|
||||
limits[1]->per_project = 1;
|
||||
TEST_ASSERT_EQUAL_INT(0, push_limits(&plugin, limits, 2));
|
||||
|
||||
/* shared-pool limit: cap written at the instance's own vres idx */
|
||||
shared_pool = find_limit(&plugin, "extlic_pool");
|
||||
TEST_ASSERT_NOT_NULL(shared_pool);
|
||||
make_obj(&obj, "projA");
|
||||
TEST_ASSERT_GREATER_OR_EQUAL_INT(0, match_global(&plugin, &obj,
|
||||
shared_pool));
|
||||
|
||||
vres = (xce_vres_t*)xlist_get(&(obj.glimits), 0);
|
||||
TEST_ASSERT_NOT_NULL(vres);
|
||||
v = (xce_val_t*)zarray_get(&(sched.core.sharedInst), vres->vres->idx);
|
||||
TEST_ASSERT_NOT_NULL(v);
|
||||
TEST_ASSERT_EQUAL_FLOAT(2.0f, v->u.fvalue);
|
||||
|
||||
/* per-project limit: /p= slices the name, so projA and projB get
|
||||
* independent instances (independent usage) under one cap */
|
||||
per_project = find_limit(&plugin, "extlic_share");
|
||||
TEST_ASSERT_NOT_NULL(per_project);
|
||||
|
||||
make_obj(&obj, "projA");
|
||||
rc = match_global(&plugin, &obj, per_project);
|
||||
TEST_ASSERT_GREATER_OR_EQUAL_INT(0, rc);
|
||||
make_obj(&obj, "projB");
|
||||
rc = match_global(&plugin, &obj, per_project);
|
||||
TEST_ASSERT_GREATER_OR_EQUAL_INT(0, rc);
|
||||
|
||||
snprintf(name, sizeof(name), "limit=extlic_share/p=projA");
|
||||
TEST_ASSERT_NOT_NULL(find_instance(&plugin, name));
|
||||
snprintf(name, sizeof(name), "limit=extlic_share/p=projB");
|
||||
TEST_ASSERT_NOT_NULL(find_instance(&plugin, name));
|
||||
|
||||
free_limit_config(limits[0]);
|
||||
free_limit_config(limits[1]);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* 3. The unknown-underlying-resource failure is a clean -ENOENT: a limit
|
||||
* naming a resource the core never registered must not crash and must
|
||||
* leave nothing in obj->glimits (the failure mode instantiate_limit()'s
|
||||
* own comment warns of -- the resname must exist before the first match). */
|
||||
static void test_unknown_underlying_resource_fails_clean(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_sched_plugin_t plugin;
|
||||
xce_sched_obj_t obj;
|
||||
xce_limit_config_t* limits[1];
|
||||
const char* const scope_a[] = { "projA" };
|
||||
int rc;
|
||||
|
||||
fixture_init(&sched, &plugin);
|
||||
/* deliberately NO resource registered */
|
||||
|
||||
limits[0] = make_project_limit("ghost_limit", "ghost_lic", 2.0f, scope_a,
|
||||
1);
|
||||
TEST_ASSERT_EQUAL_INT(0, push_limits(&plugin, limits, 1));
|
||||
TEST_ASSERT_NOT_NULL(find_limit(&plugin, "ghost_limit"));
|
||||
|
||||
make_obj(&obj, "projA");
|
||||
rc = match_global(&plugin, &obj, find_limit(&plugin, "ghost_limit"));
|
||||
TEST_ASSERT_EQUAL_INT(-ENOENT, rc);
|
||||
TEST_ASSERT_EQUAL_INT(0, obj.glimits.count);
|
||||
|
||||
free_limit_config(limits[0]);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int rc;
|
||||
|
||||
/* group names are interned strings (makeString(), group.c) */
|
||||
xstring_table_init(139);
|
||||
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_project_scope_match_and_instantiate);
|
||||
RUN_TEST(test_cap_value_and_per_project_instance_names);
|
||||
RUN_TEST(test_unknown_underlying_resource_fails_clean);
|
||||
rc = UNITY_END();
|
||||
|
||||
xstring_table_fini();
|
||||
|
||||
return rc;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue