limit_seed_config_impl() key_add()ed each pushed limit into the existing table and returned at the first failure, so on any second push (live config upgrade, or a simulator CONFIG_PUSH since bug-1964) the first already-present name failed with -EEXIST: changed limits kept the old definition and later new limits were never added. A push now builds the whole new limit set into a fresh table and swaps it in (all-or-nothing: on any build/register failure the previous set and its instances stay). Usage instances are carried across: an instance of a kept limit whose scoping and resource are unchanged is rebound to the new definition; an instance of a removed limit, or of one whose per-consumer scoping, resource or percent changed, is dropped and re-created on the next match. Usage itself lives in the resource manager under the instance's virtual resource, so counts survive either way. Not part of the snapshot (a snapshot carries the config and reseeds from it). The seed moves to plugins/limit_config.c (limit.c is near the 800-line cap) with the private data type in plugins/limit_internal.h. Two REPL limit-table fixtures relied on the additive behaviour (two single-limit pushes) and now configure both limits in one push. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
768 lines
28 KiB
C
768 lines
28 KiB
C
/*
|
|
* test_limit_plugin.c - white-box regression test for bug-1685: a live
|
|
* SIGABRT (glibc malloc_printerr, "double free or corruption"-class heap
|
|
* corruption abort) in limit_update_config() (../src/plugins/limit.c)
|
|
* whenever a scheduler config update contains two xce_limit_config_t
|
|
* entries with the SAME limit name.
|
|
*
|
|
* Root cause: privdata->limits is a kvmgr created with
|
|
* .datakiller = kill_limit (limit_priv_data_init(), limit.c). key_add() on
|
|
* that kvmgr dispatches to xkvmgr_add() (../../lib/kvmgr.c), which -- when
|
|
* the SECOND, kvtab-level key_add() fails (e.g. -EEXIST for a duplicate
|
|
* name, after the value has already been vstore()'d) -- itself rolls back
|
|
* via vclear(kvm->data, idx). vclear() dispatches to xzarray_clear()
|
|
* (../../lib/zarray.c), which fires the configured killer (kill_limit) on
|
|
* the just-stored value exactly once as part of that rollback. Before this
|
|
* fix, limit_update_config()'s own error path called kill_limit(limit) a
|
|
* SECOND time on the same already-freed pointer -- a double free that
|
|
* glibc's malloc_printerr aborts on. The fix removes that second,
|
|
* redundant kill_limit() call from limit_update_config()'s error path (see
|
|
* that function's own comment for the full mechanism and the accepted,
|
|
* documented residual gap for the separate vstore()-failure sub-case).
|
|
*
|
|
* RED (pre-fix): running test_duplicate_limit_name_no_double_free()
|
|
* against a limit_update_config() that still calls kill_limit(limit) in
|
|
* its error path crashes with a real SIGABRT ("double free or corruption")
|
|
* on the second (duplicate-name) config entry -- not a clean
|
|
* TEST_ASSERT failure, an actual process abort, matching bug-1685's
|
|
* captured live crash signature (malloc_printerr <- kill_limit <-
|
|
* limit_update_config, rc=-17/-EEXIST at the crash frame).
|
|
* GREEN (post-fix): limit_update_config() returns a negative rc (-EEXIST)
|
|
* on the duplicate-name collision, with no crash and no double free.
|
|
*
|
|
* #include's the real ../src/plugins/limit.c (white-box, same convention
|
|
* as test_plugin_picker_registration.c/test_handlers.c/
|
|
* test_job_array_expand.c) so this test exercises the real
|
|
* limit_update_config()/limit_priv_data_init()/limit_init() -- not a
|
|
* re-implementation of any of them. All three are `static`; limit_init()/
|
|
* limit_priv_data_init() are otherwise unreachable outside this TU, and
|
|
* limit_update_config() is reachable in production only via
|
|
* plugin_config_update()'s hooks.config_hook fan-out (plugin.c) -- calling
|
|
* it directly here is the same "drive the static entry point directly"
|
|
* convention test_job_update.c/test_handlers.c use for their own static
|
|
* targets.
|
|
*
|
|
* Stubs: limit.c's limit_job_election()/limit_prepare_allocation()
|
|
* (neither exercised by this test -- only limit_update_config() is)
|
|
* reference reset_sched_obj() (../src/schedobj.c) and get_resvals()/
|
|
* put_resvals() (../src/resource.c), none of which is part of this
|
|
* white-box TU. Stubbed as no-ops purely to satisfy the linker -- same
|
|
* pattern test_plugin_picker_registration.c uses for its
|
|
* sched_fill_limit_requests() stub.
|
|
*/
|
|
#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"
|
|
|
|
void reset_sched_obj(xce_sched_obj_t* obj)
|
|
{
|
|
(void)obj;
|
|
}
|
|
|
|
int get_resvals(zarray_t* array, int nres, int* idxs, float* vals)
|
|
{
|
|
(void)array;
|
|
(void)nres;
|
|
(void)idxs;
|
|
(void)vals;
|
|
return 0;
|
|
}
|
|
|
|
int put_resvals(zarray_t* array, int nres, int* idxs, float* vals)
|
|
{
|
|
(void)array;
|
|
(void)nres;
|
|
(void)idxs;
|
|
(void)vals;
|
|
return 0;
|
|
}
|
|
|
|
/* register_limit() (limit.c, the plugin's public entry point) calls
|
|
* plugin_register() (../src/plugin.c, not part of this TU); this test
|
|
* never calls register_limit() itself (it drives limit_init()/
|
|
* limit_update_config() directly), so a no-op stub is sufficient purely
|
|
* to satisfy the linker -- same rationale as the other stubs above. */
|
|
int plugin_register(void* sched, void* plugin)
|
|
{
|
|
(void)sched;
|
|
(void)plugin;
|
|
return 0;
|
|
}
|
|
|
|
#include "../src/plugins/limit.c"
|
|
#include "../src/plugins/limit_config.c" /* bug-1967: limit.c's seed */
|
|
|
|
void setUp(void) { return; }
|
|
void tearDown(void) { return; }
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Fixture: a real xce_scheduler_t with real group managers for
|
|
* core.ugmgr/qgmgr/pgmgr/hgmgr (build_limit()'s build_consumer() calls
|
|
* get_group(mgr, "all") for every consumer type our test configs leave
|
|
* NULL -- same minimal setup ../../lib/testsuite/test_limit.c's own
|
|
* test_build_limit_ok1() uses for build_limit() directly), plus a real
|
|
* limit plugin (limit_init()) so plugin.data is a real
|
|
* limit_plugin_data_t with privdata->limits configured with
|
|
* .datakiller = kill_limit -- the exact kvmgr shape bug-1685 crashed on.
|
|
* ---------------------------------------------------------------------- */
|
|
static void fixture_init(xce_scheduler_t* sched, xce_sched_plugin_t* plugin)
|
|
{
|
|
int rc;
|
|
|
|
memset(sched, 0, sizeof(*sched));
|
|
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);
|
|
|
|
memset(plugin, 0, sizeof(*plugin));
|
|
plugin->sched = sched;
|
|
/* as plugin_register() sets it; limit_update_config() reads it
|
|
* (bug-1964) */
|
|
plugin->core = &(sched->core);
|
|
|
|
rc = limit_init(plugin);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
TEST_ASSERT_NOT_NULL(plugin->data);
|
|
}
|
|
|
|
/* A minimal, heap-allocated xce_limit_config_t: no per-consumer groups
|
|
* (per_user/per_queue/per_project/per_host all 0, users/queues/projects/
|
|
* hosts all NULL), so build_limit()'s build_consumer() falls back to
|
|
* get_group(mgr, "all") for every consumer -- the same "all"-group path
|
|
* ../../lib/testsuite/test_limit.c's own fixtures rely on. `name` is
|
|
* expected to be a string literal (build_limit() strdup()s it into the
|
|
* built xce_limit_t, so no ownership is transferred here). */
|
|
static xce_limit_config_t* make_limit_config(const char* name)
|
|
{
|
|
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));
|
|
|
|
TEST_ASSERT_NOT_NULL(cfg);
|
|
TEST_ASSERT_NOT_NULL(val);
|
|
|
|
val->resname = "mem";
|
|
val->percent = 0;
|
|
val->val = 10.0;
|
|
|
|
cfg->name = (char*)name;
|
|
cfg->val = val;
|
|
|
|
return cfg;
|
|
}
|
|
|
|
static void free_limit_config(xce_limit_config_t* cfg)
|
|
{
|
|
FREEUP(cfg->val);
|
|
FREEUP(cfg);
|
|
}
|
|
|
|
/*
|
|
* THE regression: a config update with TWO xce_limit_config_t entries
|
|
* sharing the SAME name ("dup_limit") -- the exact duplicate-name
|
|
* collision bug-1685's live crash captured (rc=-17/-EEXIST at the crash
|
|
* frame, on/near the first limit of a 7-limit config). Pre-fix, this
|
|
* aborts the process (SIGABRT, "double free or corruption"); post-fix,
|
|
* limit_update_config() must return a negative rc cleanly, with no crash.
|
|
* Under ASAN, a double free is caught unconditionally regardless of
|
|
* whether this test also makes an explicit assertion about it -- the
|
|
* absence of an abort IS the proof (same rationale the task description
|
|
* for this test uses: "that's literally what caught the original bug").
|
|
*/
|
|
static void test_duplicate_limit_name_no_double_free(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
xce_sched_plugin_t plugin;
|
|
xce_limit_config_t* limits[2];
|
|
xce_conf4sched_t config;
|
|
int rc;
|
|
|
|
fixture_init(&sched, &plugin);
|
|
|
|
limits[0] = make_limit_config("dup_limit");
|
|
limits[1] = make_limit_config("dup_limit");
|
|
|
|
memset(&config, 0, sizeof(config));
|
|
config.nlimits = 2;
|
|
config.limits = limits;
|
|
|
|
rc = limit_update_config(&plugin, &config);
|
|
|
|
TEST_ASSERT_LESS_THAN_INT(0, rc);
|
|
TEST_ASSERT_EQUAL_INT(-EEXIST, rc);
|
|
|
|
free_limit_config(limits[0]);
|
|
free_limit_config(limits[1]);
|
|
}
|
|
|
|
/*
|
|
* Control case: a config update with DISTINCT limit names succeeds for
|
|
* both entries, proving the fix didn't just suppress freeing on failure
|
|
* in general -- it only stopped the redundant free on THIS specific
|
|
* (kvtab-key_add-failure) rollback path. Both built xce_limit_t values
|
|
* are now owned by privdata->limits (torn down, if at all, only by the
|
|
* kvmgr's own datakiller at process exit -- suite convention:
|
|
* ASAN_OPTIONS=detect_leaks=0, fixtures are never explicitly torn down,
|
|
* see meson.build).
|
|
*/
|
|
static void test_distinct_limit_names_both_succeed(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
xce_sched_plugin_t plugin;
|
|
xce_limit_config_t* limits[2];
|
|
xce_conf4sched_t config;
|
|
int rc;
|
|
|
|
fixture_init(&sched, &plugin);
|
|
|
|
limits[0] = make_limit_config("limit_a");
|
|
limits[1] = make_limit_config("limit_b");
|
|
|
|
memset(&config, 0, sizeof(config));
|
|
config.nlimits = 2;
|
|
config.limits = limits;
|
|
|
|
rc = limit_update_config(&plugin, &config);
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
free_limit_config(limits[0]);
|
|
free_limit_config(limits[1]);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* scheduler-sim-diagnostics spec, Requirement Set 1 (Limit Plugin
|
|
* Accessor), task 1: limit_get_configured_limits()/limit_get_usage_
|
|
* instances() (plugin.h, this file). Pure read accessors crossing the
|
|
* plugin-private-state boundary. These tests prove (a) a registered
|
|
* plugin's real limits/instances kvmgr handles come back non-NULL and
|
|
* genuinely iterable via iter_make()/iter_next() -- including yielding
|
|
* back the exact same limit that was configured, (b) a registered-but-
|
|
* empty plugin (limit_init() ran, but no limit/instance was ever added)
|
|
* still returns a non-NULL, empty-iterating handle rather than NULL, and
|
|
* (c) an unregistered plugin returns NULL from both accessors.
|
|
*
|
|
* fixture_init() (above) builds a real limit plugin via limit_init()
|
|
* directly (plugin_register() is stubbed to a no-op in this TU), so
|
|
* plugin->plugin_id/core->plugins are never populated by it -- these
|
|
* accessors' key_find(core->plugins, "xce_sched_limit", &plugin) lookup
|
|
* needs a real core->plugins kvmgr with the plugin actually key_add()'d
|
|
* into it under that exact name. The real plugin_register() (plugin.c)
|
|
* is not linked into this TU (scheduler_test_deps carries no scheduler-
|
|
* library dependency -- see this file's own header comment), so
|
|
* xce_sched_core_init() (core.c) can't be called either. The two helpers
|
|
* below replicate just the one piece of it these tests need: the same
|
|
* core->plugins kvmgr core.c's xce_sched_core_init() creates (same key
|
|
* type/sizing as its plugins_params), minus its .datakiller
|
|
* (plugin_unregister -- also unlinked here; NULL is safe since this
|
|
* suite never tears fixtures down, matching every other fixture's
|
|
* "ASAN_OPTIONS=detect_leaks=0" convention, meson.build).
|
|
*/
|
|
|
|
/* Registered case: core->plugins is a real kvmgr with the plugin key_add()'d
|
|
* into it under its real registration name, exactly as plugin_register()
|
|
* (plugin.c) itself would do -- so the accessors' key_find() success path
|
|
* is genuinely exercised, not just limit_init()'s privdata setup. */
|
|
static void fixture_register_plugin_in_core(xce_sched_core_t* core,
|
|
xce_sched_plugin_t* plugin)
|
|
{
|
|
kvmgr_params_t plugins_params = { .key_type = KVMGR_KEY_STRING,
|
|
.nslots = 16,
|
|
.delta = 8,
|
|
.datakiller = NULL };
|
|
int rc;
|
|
|
|
core->plugins = makeInstance(&xkvmgr_type, &plugins_params);
|
|
TEST_ASSERT_NOT_NULL(core->plugins);
|
|
|
|
plugin->name = "xce_sched_limit";
|
|
|
|
rc = key_add(core->plugins, (void*)plugin->name, (void*)plugin);
|
|
TEST_ASSERT_TRUE(rc >= 0);
|
|
}
|
|
|
|
/* Not-registered case: core->plugins is still a real, initialized
|
|
* (non-NULL) kvmgr -- matching the accessors' documented precondition
|
|
* that core is xce_sched_core_init()-initialized -- but the
|
|
* "xce_sched_limit" key is never added to it. This is the realistic
|
|
* not-registered scenario: core->plugins itself always exists once a
|
|
* core is initialized; only the specific plugin entry is ever missing. */
|
|
static void fixture_init_empty_plugin_registry(xce_sched_core_t* core)
|
|
{
|
|
kvmgr_params_t plugins_params = { .key_type = KVMGR_KEY_STRING,
|
|
.nslots = 16,
|
|
.delta = 8,
|
|
.datakiller = NULL };
|
|
|
|
core->plugins = makeInstance(&xkvmgr_type, &plugins_params);
|
|
TEST_ASSERT_NOT_NULL(core->plugins);
|
|
}
|
|
|
|
/* (c) plugin not registered -> NULL from both accessors. */
|
|
static void test_accessors_null_when_plugin_not_registered(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
xce_sched_plugin_t plugin;
|
|
var limits_handle;
|
|
var instances_handle;
|
|
|
|
fixture_init(&sched, &plugin);
|
|
fixture_init_empty_plugin_registry(&(sched.core));
|
|
|
|
limits_handle = limit_get_configured_limits(&(sched.core));
|
|
instances_handle = limit_get_usage_instances(&(sched.core));
|
|
|
|
TEST_ASSERT_NULL(limits_handle);
|
|
TEST_ASSERT_NULL(instances_handle);
|
|
}
|
|
|
|
/* (a) The observable case: registered plugin with a known configured
|
|
* limit -- limit_get_configured_limits()'s returned handle, iterated via
|
|
* iter_make()/iter_next(), yields that same limit back. Also confirms
|
|
* limit_get_usage_instances() reads a genuinely different field (not an
|
|
* alias of the limits handle) and, since no job has matched against this
|
|
* limit yet, is itself non-NULL but empty (Requirement 2.2's "no active
|
|
* usage instance yet"). */
|
|
static void test_get_configured_limits_handle_iterates_registered_limit(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
xce_sched_plugin_t plugin;
|
|
xce_limit_config_t* limits[1];
|
|
xce_conf4sched_t config;
|
|
var limits_handle;
|
|
var instances_handle;
|
|
iterator_t it;
|
|
xce_limit_t* found = NULL;
|
|
int rc;
|
|
|
|
fixture_init(&sched, &plugin);
|
|
fixture_register_plugin_in_core(&(sched.core), &plugin);
|
|
|
|
limits[0] = make_limit_config("observable_limit");
|
|
|
|
memset(&config, 0, sizeof(config));
|
|
config.nlimits = 1;
|
|
config.limits = limits;
|
|
|
|
rc = limit_update_config(&plugin, &config);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
limits_handle = limit_get_configured_limits(&(sched.core));
|
|
TEST_ASSERT_NOT_NULL(limits_handle);
|
|
|
|
rc = iter_make(&it, limits_handle);
|
|
TEST_ASSERT_TRUE(rc >= 0);
|
|
|
|
rc = iter_next(&it, (void**)&found);
|
|
TEST_ASSERT_TRUE(rc >= 0);
|
|
TEST_ASSERT_NOT_EQUAL(RTTI_ITER_NO_DATA, rc);
|
|
TEST_ASSERT_NOT_NULL(found);
|
|
TEST_ASSERT_EQUAL_STRING("observable_limit", found->name);
|
|
|
|
rc = iter_next(&it, (void**)&found);
|
|
TEST_ASSERT_EQUAL_INT(RTTI_ITER_NO_DATA, rc);
|
|
|
|
iter_destroy(&it);
|
|
|
|
instances_handle = limit_get_usage_instances(&(sched.core));
|
|
TEST_ASSERT_NOT_NULL(instances_handle);
|
|
TEST_ASSERT_TRUE(instances_handle != limits_handle);
|
|
|
|
{
|
|
void* item = NULL;
|
|
|
|
rc = iter_make(&it, instances_handle);
|
|
TEST_ASSERT_TRUE(rc >= 0);
|
|
|
|
rc = iter_next(&it, &item);
|
|
TEST_ASSERT_EQUAL_INT(RTTI_ITER_NO_DATA, rc);
|
|
|
|
iter_destroy(&it);
|
|
}
|
|
|
|
free_limit_config(limits[0]);
|
|
}
|
|
|
|
/* (b) plugin registered with zero limits: privdata->limits is a real,
|
|
* freshly created (limit_init()) kvmgr with nothing added to it yet --
|
|
* the accessor must still return a non-NULL handle (distinguishing
|
|
* "plugin present, zero limits" from "plugin absent" per Requirement
|
|
* 1.7), and iterating it must yield no elements. */
|
|
static void test_get_configured_limits_registered_with_zero_limits(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
xce_sched_plugin_t plugin;
|
|
var limits_handle;
|
|
iterator_t it;
|
|
void* item = NULL;
|
|
int rc;
|
|
|
|
fixture_init(&sched, &plugin);
|
|
fixture_register_plugin_in_core(&(sched.core), &plugin);
|
|
|
|
limits_handle = limit_get_configured_limits(&(sched.core));
|
|
TEST_ASSERT_NOT_NULL(limits_handle);
|
|
|
|
rc = iter_make(&it, limits_handle);
|
|
TEST_ASSERT_TRUE(rc >= 0);
|
|
|
|
rc = iter_next(&it, &item);
|
|
TEST_ASSERT_EQUAL_INT(RTTI_ITER_NO_DATA, rc);
|
|
|
|
iter_destroy(&it);
|
|
}
|
|
|
|
/* bug-1933: build_limit() takes (ugmgr, qgmgr, hgmgr, pgmgr) but the
|
|
* plugin passed (ugmgr, qgmgr, pgmgr, hgmgr), so a limit's hosts: scope
|
|
* resolved its members against the project groups (a host-group name
|
|
* became a literal host name) and its projects: scope against the host
|
|
* groups (a project named like a host group matched that group's hosts).
|
|
* With host group "hg1" = {hostA}: a hosts: [hg1] limit must match hostA,
|
|
* and a projects: [hg1] limit must not match project "hostA". */
|
|
static void test_limit_scopes_resolve_against_own_group_kind(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
xce_sched_plugin_t plugin;
|
|
xce_limit_config_t* limits[2];
|
|
xce_conf4sched_t config;
|
|
xce_group_config_t hg1 = {
|
|
.name = "hg1",
|
|
.members = { .count = 1, .data = (char*[]) { "hostA" } }
|
|
};
|
|
xce_group_config_t* hgroups[1] = { &hg1 };
|
|
xce_group_config_t scope = {
|
|
.members = { .count = 1, .data = (char*[]) { "hg1" } }
|
|
};
|
|
xce_limit_t* by_host = NULL;
|
|
xce_limit_t* by_project = NULL;
|
|
int rc;
|
|
|
|
fixture_init(&sched, &plugin);
|
|
fixture_register_plugin_in_core(&(sched.core), &plugin);
|
|
|
|
rc = groupmgr_load_configs(sched.core.hgmgr, 1, hgroups);
|
|
TEST_ASSERT_TRUE(rc >= 0);
|
|
|
|
limits[0] = make_limit_config("host_scoped");
|
|
limits[0]->hosts = &scope;
|
|
limits[1] = make_limit_config("project_scoped");
|
|
limits[1]->projects = &scope;
|
|
|
|
memset(&config, 0, sizeof(config));
|
|
config.nlimits = 2;
|
|
config.limits = limits;
|
|
|
|
rc = limit_update_config(&plugin, &config);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
{
|
|
var limits_handle = limit_get_configured_limits(&(sched.core));
|
|
iterator_t it;
|
|
xce_limit_t* l = NULL;
|
|
|
|
TEST_ASSERT_NOT_NULL(limits_handle);
|
|
rc = iter_make(&it, limits_handle);
|
|
TEST_ASSERT_TRUE(rc >= 0);
|
|
while ((rc = iter_next(&it, (void**)&l)) >= 0) {
|
|
if (rc == RTTI_ITER_NO_DATA) {
|
|
break;
|
|
}
|
|
if (strcmp(l->name, "host_scoped") == 0) {
|
|
by_host = l;
|
|
} else if (strcmp(l->name, "project_scoped") == 0) {
|
|
by_project = l;
|
|
}
|
|
}
|
|
iter_destroy(&it);
|
|
}
|
|
TEST_ASSERT_NOT_NULL(by_host);
|
|
TEST_ASSERT_NOT_NULL(by_project);
|
|
|
|
/* host scope: hg1 is a host group, so its host matches */
|
|
TEST_ASSERT_EQUAL_INT(1, match_limit(by_host, 0, "u1", "normal",
|
|
"default", "hostA"));
|
|
TEST_ASSERT_EQUAL_INT(0, match_limit(by_host, 0, "u1", "normal",
|
|
"default", "hostB"));
|
|
|
|
/* project scope: hg1 is a literal project name, never the host group */
|
|
TEST_ASSERT_EQUAL_INT(0, match_limit(by_project, 1, "u1", "normal",
|
|
"hostA"));
|
|
TEST_ASSERT_EQUAL_INT(1, match_limit(by_project, 1, "u1", "normal",
|
|
"hg1"));
|
|
|
|
limits[0]->hosts = NULL;
|
|
limits[1]->projects = NULL;
|
|
free_limit_config(limits[0]);
|
|
free_limit_config(limits[1]);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* bug-1967: a re-pushed config (live config upgrade, or a mid-replay
|
|
* CONFIG_PUSH in the simulator) REPLACES the configured limit set. Before
|
|
* the fix every name of the second push already existed, so the first
|
|
* key_add() failed with -EEXIST and the push changed nothing.
|
|
* ---------------------------------------------------------------------- */
|
|
|
|
/* pushes `n` limit configs through the config hook, as both drivers do */
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
static int count_entries(var table)
|
|
{
|
|
iterator_t it;
|
|
void* item = NULL;
|
|
int n = 0;
|
|
|
|
TEST_ASSERT_TRUE(iter_make(&it, table) >= 0);
|
|
while (iter_next(&it, &item) == RTTI_ITER_OK) {
|
|
n++;
|
|
}
|
|
iter_destroy(&it);
|
|
|
|
return n;
|
|
}
|
|
|
|
/* a usage instance as match_job_global_limit_() interns it: keyed by its
|
|
* virtual resource's name, pointing at the configured limit */
|
|
static void add_instance(xce_sched_plugin_t* plugin, xce_resource_t* res,
|
|
const char* limit_name)
|
|
{
|
|
limit_plugin_data_t* privdata = (limit_plugin_data_t*)plugin->data;
|
|
xce_vres_t* vres = (xce_vres_t*)calloc(1, sizeof(*vres));
|
|
|
|
TEST_ASSERT_NOT_NULL(vres);
|
|
vres->vres = res;
|
|
vres->limit = find_limit(plugin, limit_name);
|
|
TEST_ASSERT_NOT_NULL(vres->limit);
|
|
|
|
TEST_ASSERT_TRUE(key_add(privdata->instances, (void*)res->name, vres)
|
|
>= 0);
|
|
}
|
|
|
|
static void test_repush_changed_value_takes_effect(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
xce_sched_plugin_t plugin;
|
|
xce_limit_config_t* limits[1];
|
|
|
|
fixture_init(&sched, &plugin);
|
|
|
|
limits[0] = make_limit_config("lim_a");
|
|
TEST_ASSERT_EQUAL_INT(0, push_limits(&plugin, limits, 1));
|
|
|
|
limits[0]->val->val = 20.0;
|
|
TEST_ASSERT_EQUAL_INT(0, push_limits(&plugin, limits, 1));
|
|
|
|
TEST_ASSERT_NOT_NULL(find_limit(&plugin, "lim_a"));
|
|
TEST_ASSERT_EQUAL_FLOAT(20.0, find_limit(&plugin, "lim_a")->val.val);
|
|
|
|
free_limit_config(limits[0]);
|
|
}
|
|
|
|
static void test_repush_adds_new_limit_after_existing(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
xce_sched_plugin_t plugin;
|
|
xce_limit_config_t* limits[2];
|
|
|
|
fixture_init(&sched, &plugin);
|
|
|
|
limits[0] = make_limit_config("lim_a");
|
|
limits[1] = make_limit_config("lim_b");
|
|
TEST_ASSERT_EQUAL_INT(0, push_limits(&plugin, limits, 1));
|
|
TEST_ASSERT_EQUAL_INT(0, push_limits(&plugin, limits, 2));
|
|
|
|
TEST_ASSERT_NOT_NULL(find_limit(&plugin, "lim_a"));
|
|
TEST_ASSERT_NOT_NULL(find_limit(&plugin, "lim_b"));
|
|
|
|
free_limit_config(limits[0]);
|
|
free_limit_config(limits[1]);
|
|
}
|
|
|
|
static void test_repush_removes_dropped_limit(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
xce_sched_plugin_t plugin;
|
|
xce_limit_config_t* limits[2];
|
|
limit_plugin_data_t* privdata;
|
|
|
|
fixture_init(&sched, &plugin);
|
|
privdata = (limit_plugin_data_t*)plugin.data;
|
|
|
|
limits[0] = make_limit_config("lim_a");
|
|
limits[1] = make_limit_config("lim_b");
|
|
TEST_ASSERT_EQUAL_INT(0, push_limits(&plugin, limits, 2));
|
|
TEST_ASSERT_EQUAL_INT(0, push_limits(&plugin, &limits[1], 1));
|
|
|
|
TEST_ASSERT_NULL(find_limit(&plugin, "lim_a"));
|
|
TEST_ASSERT_NOT_NULL(find_limit(&plugin, "lim_b"));
|
|
TEST_ASSERT_EQUAL_INT(1, count_entries(privdata->limits));
|
|
|
|
free_limit_config(limits[0]);
|
|
free_limit_config(limits[1]);
|
|
}
|
|
|
|
/* A kept limit's instance is rebound to the new definition (its usage lives
|
|
* on in the resource manager under the same virtual resource); a removed
|
|
* limit's instance is dropped, so none points into a freed definition. */
|
|
static void test_repush_rebinds_kept_and_drops_removed_instances(void)
|
|
{
|
|
static xce_resource_t res_a = { .name = "limit=lim_a", .idx = 7 };
|
|
static xce_resource_t res_b = { .name = "limit=lim_b", .idx = 8 };
|
|
xce_scheduler_t sched;
|
|
xce_sched_plugin_t plugin;
|
|
xce_limit_config_t* limits[2];
|
|
limit_plugin_data_t* privdata;
|
|
xce_vres_t* vres;
|
|
|
|
fixture_init(&sched, &plugin);
|
|
privdata = (limit_plugin_data_t*)plugin.data;
|
|
|
|
limits[0] = make_limit_config("lim_a");
|
|
limits[1] = make_limit_config("lim_b");
|
|
TEST_ASSERT_EQUAL_INT(0, push_limits(&plugin, limits, 2));
|
|
add_instance(&plugin, &res_a, "lim_a");
|
|
add_instance(&plugin, &res_b, "lim_b");
|
|
|
|
limits[0]->val->val = 20.0;
|
|
TEST_ASSERT_EQUAL_INT(0, push_limits(&plugin, limits, 1));
|
|
|
|
vres = find_instance(&plugin, "limit=lim_a");
|
|
TEST_ASSERT_NOT_NULL(vres);
|
|
TEST_ASSERT_EQUAL_PTR(find_limit(&plugin, "lim_a"), vres->limit);
|
|
TEST_ASSERT_EQUAL_FLOAT(20.0, vres->limit->val.val);
|
|
TEST_ASSERT_EQUAL_PTR(&res_a, vres->vres);
|
|
|
|
TEST_ASSERT_NULL(find_instance(&plugin, "limit=lim_b"));
|
|
TEST_ASSERT_EQUAL_INT(1, count_entries(privdata->instances));
|
|
|
|
free_limit_config(limits[0]);
|
|
free_limit_config(limits[1]);
|
|
}
|
|
|
|
/* A change that alters the instance's identity (here per_user: the vres
|
|
* name now carries the user, so the old shared instance no longer names
|
|
* the usage the limit counts) drops the instance; the next job match
|
|
* instantiates the right one. */
|
|
static void test_repush_scope_change_drops_instance(void)
|
|
{
|
|
static xce_resource_t res_a = { .name = "limit=lim_a", .idx = 7 };
|
|
xce_scheduler_t sched;
|
|
xce_sched_plugin_t plugin;
|
|
xce_limit_config_t* limits[1];
|
|
|
|
fixture_init(&sched, &plugin);
|
|
|
|
limits[0] = make_limit_config("lim_a");
|
|
TEST_ASSERT_EQUAL_INT(0, push_limits(&plugin, limits, 1));
|
|
add_instance(&plugin, &res_a, "lim_a");
|
|
|
|
limits[0]->per_user = 1;
|
|
TEST_ASSERT_EQUAL_INT(0, push_limits(&plugin, limits, 1));
|
|
|
|
TEST_ASSERT_NOT_NULL(find_limit(&plugin, "lim_a"));
|
|
TEST_ASSERT_NULL(find_instance(&plugin, "limit=lim_a"));
|
|
|
|
free_limit_config(limits[0]);
|
|
}
|
|
|
|
/* A push that fails part-way (a duplicate name) leaves the previous limit
|
|
* set and its instances untouched instead of half-replacing them. */
|
|
static void test_repush_failure_keeps_previous_set(void)
|
|
{
|
|
static xce_resource_t res_a = { .name = "limit=lim_a", .idx = 7 };
|
|
xce_scheduler_t sched;
|
|
xce_sched_plugin_t plugin;
|
|
xce_limit_config_t* limits[3];
|
|
xce_vres_t* vres;
|
|
|
|
fixture_init(&sched, &plugin);
|
|
|
|
limits[0] = make_limit_config("lim_a");
|
|
TEST_ASSERT_EQUAL_INT(0, push_limits(&plugin, limits, 1));
|
|
add_instance(&plugin, &res_a, "lim_a");
|
|
|
|
limits[1] = make_limit_config("lim_c");
|
|
limits[2] = make_limit_config("lim_c");
|
|
limits[0]->val->val = 20.0;
|
|
TEST_ASSERT_EQUAL_INT(-EEXIST, push_limits(&plugin, limits, 3));
|
|
|
|
TEST_ASSERT_EQUAL_FLOAT(10.0, find_limit(&plugin, "lim_a")->val.val);
|
|
TEST_ASSERT_NULL(find_limit(&plugin, "lim_c"));
|
|
vres = find_instance(&plugin, "limit=lim_a");
|
|
TEST_ASSERT_NOT_NULL(vres);
|
|
TEST_ASSERT_EQUAL_PTR(find_limit(&plugin, "lim_a"), vres->limit);
|
|
|
|
free_limit_config(limits[0]);
|
|
free_limit_config(limits[1]);
|
|
free_limit_config(limits[2]);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
int rc;
|
|
|
|
/* group names are interned strings (makeString(), group.c) */
|
|
xstring_table_init(139);
|
|
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_duplicate_limit_name_no_double_free);
|
|
RUN_TEST(test_distinct_limit_names_both_succeed);
|
|
RUN_TEST(test_accessors_null_when_plugin_not_registered);
|
|
RUN_TEST(test_get_configured_limits_handle_iterates_registered_limit);
|
|
RUN_TEST(test_get_configured_limits_registered_with_zero_limits);
|
|
RUN_TEST(test_limit_scopes_resolve_against_own_group_kind);
|
|
RUN_TEST(test_repush_changed_value_takes_effect);
|
|
RUN_TEST(test_repush_adds_new_limit_after_existing);
|
|
RUN_TEST(test_repush_removes_dropped_limit);
|
|
RUN_TEST(test_repush_rebinds_kept_and_drops_removed_instances);
|
|
RUN_TEST(test_repush_scope_change_drops_instance);
|
|
RUN_TEST(test_repush_failure_keeps_previous_set);
|
|
rc = UNITY_END();
|
|
|
|
xstring_table_fini();
|
|
|
|
return rc;
|
|
}
|