feat(scheduler-sim-live-allocation): make limit matching core-safe (task 2.3)

Migrates match_job_global_limit_()/limit_job_election()/
match_job_host_limit_() (plugins/limit.c) to read plugin->core (added
task 1.2) instead of deriving xce_scheduler_t* from plugin->sched,
which stays NULL for a plugin registered via plugin_register_core()
(simulation mode). Removes the dead ctx.sched write flagged during
task 2.1's review -- verified this ctx never has .sched read anywhere
downstream.

Fixes bug-1828 (found while writing this task's own TDD test for a
previously untested code path): match_job_host_limit_() had the same
key_find()-negative-rc-treated-as-fatal defect bug-1786 already fixed
in its global-limit sibling, but never applied here -- meaning
host-scoped resource limits were never actually enforced, in any
mode, until this fix (not merely a display gap; the limit's vres was
never added to obj->hlimits, so sched_fill_limit_requests() always
saw an empty list for it).

TDD: new tests drive the real hook-registered entry points against a
plugin_register_core()-registered plugin, asserting real usage
instances via limit_get_usage_instances() for both global and
host-scoped limits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
This commit is contained in:
Rongsong Shen 2026-09-18 16:21:25 +00:00
parent ecdea8bf55
commit fc23ea8d31
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
4 changed files with 347 additions and 27 deletions

View file

@ -2570,6 +2570,244 @@ test_sim_bootstrap_limit_plugin_registers_without_crash_when_config_null(
"config_null: PASS\n");
}
/* ---------------------------------------------------------------------- */
/* scheduler-sim-live-allocation spec, task 2.3 (Limit Plugin Core-Safe */
/* Matching, Req 2.1/2.2): the sim_bootstrap_limit_plugin() tests above */
/* prove REGISTRATION and CONFIG-SEEDING work against a plugin_register_ */
/* core()-registered plugin (plugin->sched left NULL by design). Neither */
/* proves MATCHING works -- limit_job_election()/match_job_global_limit_()*/
/* and limit_prepare_allocation()/match_job_host_limit_() have always */
/* derived their own `xce_scheduler_t* sched` from `plugin->sched`, so a */
/* sim-registered plugin (sched == NULL) either NULL-derefs (global path, */
/* limit_job_election()'s own `sched->core.configver` read, no prior NULL */
/* guard) or returns -EINVAL without matching anything (host-scoped path, */
/* match_job_host_limit_()'s own `sched == NULL` guard) -- both are the */
/* "genuinely never invoked/no-op in sim mode" gap bug-1826/this task's own*/
/* brief describe. These two tests drive the real hook-registered entry */
/* points directly (the same (void* arg, ...) shape plugin_job_election()/ */
/* plugin_prepare_allocation() -- both already core-safe, task 1.2/2.2 -- */
/* dispatch through in production) against a plugin registered ONLY via */
/* plugin_register_core(), proving matching now reads plugin->core (task */
/* 1.2's honest core-state reference) instead of plugin->sched. */
/* ---------------------------------------------------------------------- */
/*
* (1) Global limit match (Req 2.1): a limit configured on a plugin
* registered via plugin_register_core() must still match a real job and
* register its usage instance, verifiable through limit_get_usage_
* instances() -- the exact "Observable" this task's brief specifies.
* `obj.user`/`.queue`/`.project` are non-NULL empty strings (never NULL --
* match_limit()/is_in_group() both reject NULL outright), matching against
* the limit's default "all"-group fallback (add_configured_limit()'s own
* config shape, seeded here via limit_seed_config() through sim_bootstrap_
* limit_plugin() instead, since plugin->sched is NULL for this plugin).
*/
static void
test_limit_job_election_matches_global_limit_via_sim_registered_plugin(void)
{
xce_sched_sim_driver_t sim;
xce_sched_plugin_t* plugin = NULL;
xce_limit_config_t* limits[1];
xce_conf4sched_t config;
xce_sched_obj_t obj;
xce_sched_job_t job;
var instances_handle;
iterator_t it;
void* item = NULL;
int rc;
memset(&sim, 0, sizeof(sim));
rc = xce_sched_core_init(&(sim.core));
TEST_ASSERT_EQUAL_INT(0, rc);
/* instantiate_limit() (xcelimit.c) requires the limit's underlying
* resource (val.resname, "mem" -- make_limit_config()'s own hardcoded
* shape) to genuinely exist in core->resmgr (matching production, where
* sched_init_resources() registers it at config load) -- every other
* test in this file that drives real matching (not just registration/
* config-seeding) does the same via register_per_host_resource(). Its
* non-GLOBAL/non-SHARED flags are harmless here: this limit's own
* percent=0/per_host=0 shape (make_limit_config()) still resolves as a
* GLOBAL limit via is_host_based_limit()'s own fallback rule, matching
* instantiate_and_register_global_vres()'s identical comment above. */
register_per_host_resource(&sim, "mem");
limits[0] = make_limit_config("sim_global_election_limit");
memset(&config, 0, sizeof(config));
config.nlimits = 1;
config.limits = limits;
sim.core.config = &config;
/* Real production bootstrap: plugin_register_core() + limit_seed_
* config(), exactly as sched_sim_driver_run() calls it (sim_limit.c). */
sim_bootstrap_limit_plugin(&sim);
rc = key_find(sim.core.plugins, "xce_sched_limit", (void**)&plugin);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(plugin);
TEST_ASSERT_NULL_MESSAGE(
plugin->sched,
"sanity: plugin_register_core() must leave plugin->sched NULL -- "
"this test proves matching works WITHOUT it, via plugin->core");
TEST_ASSERT_EQUAL_PTR_MESSAGE(
&(sim.core), plugin->core,
"sanity: plugin->core (task 1.2) must reference this real sim "
"core");
/* limit_job_election() skips any schedobj whose own configver is
* already >= core->configver (the "already evaluated against this
* config" guard, core_events.c's own config-push bump being the
* production trigger) -- both default to 0 (xce_sched_core_init() /
* this memset()), so bump core->configver past the fresh obj's 0 here,
* exactly as a real CONFIG_PUSH event would before this job is ever
* offered to the picker. */
sim.core.configver = 1;
memset(&obj, 0, sizeof(obj));
obj.user = (char*)"";
obj.queue = (char*)"";
obj.project = (char*)"";
xlist_init(&(obj.glimits), 8);
zarray_init(&(obj.hlimits), 8, NULL);
memset(&job, 0, sizeof(job));
job.jobid = 1;
job.schedobj = &obj;
/* The real job_election_hook-registered entry point, called exactly
* the way the now-core-safe plugin_job_election() dispatcher (plugin.c,
* task 1.2) calls it in production -- `arg` is the plugin instance. */
rc = limit_job_election((void*)plugin, &job);
TEST_ASSERT_TRUE_MESSAGE(
rc >= 0,
"limit_job_election() must succeed for a plugin registered via "
"plugin_register_core(), reading plugin->core instead of the "
"always-NULL-here plugin->sched");
instances_handle = limit_get_usage_instances(&(sim.core));
TEST_ASSERT_NOT_NULL(instances_handle);
rc = iter_make(&it, instances_handle);
TEST_ASSERT_TRUE(rc >= 0);
rc = iter_next(&it, &item);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_EQUAL(RTTI_ITER_NO_DATA, rc);
TEST_ASSERT_NOT_NULL_MESSAGE(
item,
"a global limit match via the simulation-mode-registered plugin "
"must create a real usage instance, where today it silently never "
"matches at all (plugin->sched is NULL for a plugin_register_"
"core()-registered plugin)");
iter_destroy(&it);
free_limit_config(limits[0]);
printf(
"test_limit_job_election_matches_global_limit_via_sim_registered_"
"plugin: PASS\n");
}
/*
* (2) Host-scoped limit match (Req 2.2): same proof as (1), but for the
* prepare_allocation_hook path -- a percent-based limit on a real,
* non-GLOBAL/non-SHARED per-host resource (register_per_host_resource(),
* the exact shape add_configured_percent_limit() uses for the normal-mode
* equivalent of this scenario), matched via limit_prepare_allocation()
* against a real xce_sched_ctx_t/xce_sched_worker_t, on a plugin registered
* ONLY via plugin_register_core() (plugin->sched NULL).
*/
static void
test_limit_prepare_allocation_matches_host_limit_via_sim_registered_plugin(
void)
{
xce_sched_sim_driver_t sim;
xce_sched_plugin_t* plugin = NULL;
xce_limit_val_t val = { .resname = (char*)"sim_host_res",
.percent = 1,
.val = 50.0 };
xce_limit_config_t limit_cfg = { .name = (char*)"sim_host_limit",
.val = &val };
xce_limit_config_t* limits[1] = { &limit_cfg };
xce_conf4sched_t config;
xce_sched_obj_t obj;
xce_sched_worker_t worker;
xce_sched_ctx_t ctx;
var instances_handle;
iterator_t it;
void* item = NULL;
int rc;
memset(&sim, 0, sizeof(sim));
rc = xce_sched_core_init(&(sim.core));
TEST_ASSERT_EQUAL_INT(0, rc);
register_per_host_resource(&sim, "sim_host_res");
memset(&config, 0, sizeof(config));
config.nlimits = 1;
config.limits = limits;
sim.core.config = &config;
sim_bootstrap_limit_plugin(&sim);
rc = key_find(sim.core.plugins, "xce_sched_limit", (void**)&plugin);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(plugin);
TEST_ASSERT_NULL_MESSAGE(
plugin->sched,
"sanity: plugin_register_core() must leave plugin->sched NULL");
memset(&obj, 0, sizeof(obj));
obj.user = (char*)"";
obj.queue = (char*)"";
obj.project = (char*)"";
xlist_init(&(obj.glimits), 8);
zarray_init(&(obj.hlimits), 8, NULL);
memset(&worker, 0, sizeof(worker));
worker.name = (char*)"sim_host1";
worker.idx = 0;
memset(&ctx, 0, sizeof(ctx));
ctx.worker = &worker;
ctx.schedobj = &obj;
/* The real prepare_allocation_hook-registered entry point, called
* exactly the way the now-core-safe plugin_prepare_allocation()
* dispatcher (plugin.c, task 1.2) and sched_ctx_set_primary()
* (schedobj.c, task 2.1) call it in production. */
rc = limit_prepare_allocation((void*)plugin, &ctx);
TEST_ASSERT_TRUE_MESSAGE(
rc >= 0,
"limit_prepare_allocation() must succeed for a plugin registered "
"via plugin_register_core(), reading plugin->core instead of the "
"always-NULL-here plugin->sched");
instances_handle = limit_get_usage_instances(&(sim.core));
TEST_ASSERT_NOT_NULL(instances_handle);
rc = iter_make(&it, instances_handle);
TEST_ASSERT_TRUE(rc >= 0);
rc = iter_next(&it, &item);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_EQUAL(RTTI_ITER_NO_DATA, rc);
TEST_ASSERT_NOT_NULL_MESSAGE(
item,
"a host-scoped limit match via the simulation-mode-registered "
"plugin must create a real usage instance, where today it either "
"no-ops (-EINVAL from match_job_host_limit_()'s own sched==NULL "
"guard) or never matches at all");
iter_destroy(&it);
printf(
"test_limit_prepare_allocation_matches_host_limit_via_sim_"
"registered_plugin: PASS\n");
}
int main(void)
{
UNITY_BEGIN();
@ -2624,5 +2862,9 @@ int main(void)
test_sim_bootstrap_limit_plugin_seeds_real_config_limit_when_config_present);
RUN_TEST(
test_sim_bootstrap_limit_plugin_registers_without_crash_when_config_null);
RUN_TEST(
test_limit_job_election_matches_global_limit_via_sim_registered_plugin);
RUN_TEST(
test_limit_prepare_allocation_matches_host_limit_via_sim_registered_plugin);
return UNITY_END();
}