feat(scheduler-sim-diagnostics): add read-only limit plugin accessors

limit_get_configured_limits()/limit_get_usage_instances() cross the limit
plugin's private-state boundary (plugin->data, previously unreachable outside
limit.c) via key_find on the name-keyed plugin registry, returning NULL when
the plugin is absent/uninitialized and a raw iterable kvmgr handle otherwise.
Pure reads only -- no allocation, mutation, or hook triggering. Foundation for
the upcoming `info limit` REPL command (task 1 of 5).

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-17 09:03:09 +00:00
parent 80e096b85e
commit 915c0e4435
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
4 changed files with 255 additions and 1 deletions

View file

@ -1,6 +1,6 @@
# Implementation Plan: scheduler-sim-diagnostics (Requirement Set 1 — Limit Inspection)
- [ ] 1. Add read-only accessors exposing the limit plugin's configured-limits and live-usage-instances tables
- [x] 1. Add read-only accessors exposing the limit plugin's configured-limits and live-usage-instances tables
- Two new functions (`limit_get_configured_limits()`, `limit_get_usage_instances()`) returning raw kvmgr handles (or `NULL` when the plugin is absent), declared in `plugin.h` alongside `register_limit()`, defined in `plugins/limit.c`
- Never allocate, mutate, or trigger plugin hooks — a pure read of already-existing pointers
- New/extended unit tests in `test_limit_plugin.c` cover: plugin registered with configured limits (handle returned, iterable), plugin registered with zero limits, plugin not registered (`NULL`)

View file

@ -114,6 +114,20 @@ extern int plugin_job_rusage_update(void*, xce_sched_job_t*,
extern int register_fcfs(void*);
extern int register_builtin_fcfs(void*);
extern int register_limit(void*);
/* limit_get_configured_limits/limit_get_usage_instances - scheduler-sim-
* diagnostics spec, Requirement Set 1 (Limit Plugin Accessor), task 1.
* Read-only crossing of the plugin-private-state boundary: each looks up
* the limit plugin ("xce_sched_limit") in core->plugins and returns the
* requested field of its private limit_plugin_data_t (plugins/limit.c) --
* a raw kvmgr handle, iterable via iter_make()/iter_next() -- or NULL if
* the plugin is not registered or not yet initialized (plugin->data ==
* NULL). Never allocates, mutates, or triggers plugin hooks; performs no
* locking beyond what key_find() already does internally. NULL is a
* semantic signal only ("nothing is configured yet"), not an error -- the
* caller decides what to display for it. */
extern var limit_get_configured_limits(xce_sched_core_t* core);
extern var limit_get_usage_instances(xce_sched_core_t* core);
extern int register_seqjob_allocator(void*);
extern int register_parjob_allocator(void*);

View file

@ -551,6 +551,61 @@ static void limit_fini(xce_sched_plugin_t* plugin)
FREEUP(plugin);
}
/* limit_get_configured_limits/limit_get_usage_instances -- scheduler-sim-
* diagnostics spec, Requirement Set 1 (Limit Plugin Accessor), task 1. Pure
* reads of already-existing pointers: look up the limit plugin by its
* fixed registration name (owned here, hardcoded nowhere else) and return
* the requested privdata field. NULL whenever the plugin is not registered
* or not yet initialized (plugin->data == NULL) -- never a partially-valid
* handle. No locking beyond what key_find() already does internally
* (matches plugin_job_update()'s (plugin.c) equally lock-free
* core->plugins reads); no allocation, mutation, or hook invocation. */
var limit_get_configured_limits(xce_sched_core_t* core)
{
xce_sched_plugin_t* plugin = NULL;
limit_plugin_data_t* privdata;
int rc;
if (core == NULL) {
return NULL;
}
rc = key_find(core->plugins, "xce_sched_limit", (void**)&plugin);
if ((rc <= 0) || (plugin == NULL)) {
return NULL;
}
privdata = (limit_plugin_data_t*)plugin->data;
if (privdata == NULL) {
return NULL;
}
return privdata->limits;
}
var limit_get_usage_instances(xce_sched_core_t* core)
{
xce_sched_plugin_t* plugin = NULL;
limit_plugin_data_t* privdata;
int rc;
if (core == NULL) {
return NULL;
}
rc = key_find(core->plugins, "xce_sched_limit", (void**)&plugin);
if ((rc <= 0) || (plugin == NULL)) {
return NULL;
}
privdata = (limit_plugin_data_t*)plugin->data;
if (privdata == NULL) {
return NULL;
}
return privdata->instances;
}
int register_limit(void* sched)
{
static xce_sched_plugin_t limit_plugin = {

View file

@ -238,10 +238,195 @@ static void test_distinct_limit_names_both_succeed(void)
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);
}
int main(void)
{
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);
return UNITY_END();
}