From 6ecd418d217290884709f134660e55d40bd1e815 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Sat, 10 Oct 2026 03:42:59 +0000 Subject: [PATCH] fix(scheduler): resolve limit host and project scopes against their own groups (bug-1933) The limit plugin called build_limit() with the host-group and project-group managers swapped, so a limit's hosts: members were looked up among the project groups (a host-group name became a literal host name and the limit never applied) and its projects: members among the host groups. Pass them in the order build_limit() declares. New test test_limit_scopes_resolve_against_own_group_kind; the test's main() now initializes the interned-string table that group names need. Co-Authored-By: Claude Opus 5.5 (1M context) --- .wolf/buglog.json | 26 +++++ .wolf/bugs/bug-1933.md | 104 ++++++++++++++++++++ src/scheduler/src/plugins/limit.c | 4 +- src/scheduler/testsuite/test_limit_plugin.c | 97 +++++++++++++++++- 4 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 .wolf/bugs/bug-1933.md diff --git a/.wolf/buglog.json b/.wolf/buglog.json index 26aa8edd..094391a4 100644 --- a/.wolf/buglog.json +++ b/.wolf/buglog.json @@ -17722,6 +17722,32 @@ "record": ".wolf/bugs/bug-1932.md", "gitea_issue": 357, "gitea_synced_status": "fixed" + }, + { + "id": "bug-1933", + "status": "open", + "status_updated": "2026-10-10", + "status_note": "newly logged", + "component": "scheduler", + "severity": "wrong-result", + "timestamp": "2026-10-10T03:36:53Z", + "last_seen": "2026-10-10T03:36:53Z", + "occurrences": 1, + "error_message": "xce_scheduler limit plugin resolves hosts: scopes against project groups (host-group limits ignored)\n\n**Observed:**\nThe scheduler's limit plugin built every limit with the host-group and\nproject-group managers swapped. A limit scoped to a host group never\nmatched that group's hosts; a limit scoped to projects resolved its member\nnames against the host groups. The new regression test, before the fix:\n```\n../scheduler/testsuite/test_limit_plugin.c:491:test_limit_scopes_resolve_against_own_group_kind:FAIL: Expected 1 Was 0\n```\n(host group `hg1` = {`hostA`}; a limit with `hosts: [hg1]` did not match\nhost `hostA`.)\n\n**Expected:** a limit's `hosts:` members resolve against the host groups\n(`hosts: [hg1]` matches every host in `hg1`), and its `projects:` members\nresolve against the project groups (a project name never matches a host\ngroup).\n**Impact:** wrong result, silent. Every host limit that names a host group\napplies to no host (only to a host literally named like the group), so the\nlimit is never enforced. A project limit whose member name equals a host\ngroup's name matches the projects named like that group's hosts instead of\nthe project itself. Limits naming plain host or project names, and limits\nwithout host/project scopes, are unaffected. In addition, the groups a\nlimit builds for its own scopes were registered in the wrong manager\n(`-pg-group` in the host groups, `-hg-group` in the project\ngroups).\n**Found by:** review, during `/kiro-spec-requirements project-config`\n(2026-10-10, codebase survey of project-scoped limits).", + "file": "src/scheduler/src/plugins/limit.c, src/lib/xcelimit.c", + "root_cause": "`build_limit()` (`src/lib/xcelimit.c:292`) takes its group managers as\n`(ugmgr, qgmgr, hgmgr, pgmgr)`, but `limit_seed_config_impl()`\n(`src/scheduler/src/plugins/limit.c:77`) passed\n`(core->ugmgr, core->qgmgr, core->pgmgr, core->hgmgr)`. `build_consumer()`\ntherefore built the HOST consumer with the project-group manager and the\nPROJECT consumer with the host-group manager. `make_g_member()`\n(`src/lib/group.c`) treats a member name as a nested group only if the\nmanager it is given knows that group, so `hg1` became a literal host name\n(the project-group manager is empty) and a project member named like a host\ngroup became that host group. Since `ecf1916d` (2026-01-12, \"support\nlimit\"), the first version of the plugin call; carried through `7c229284`\nand `487e7b94`. Scope: `limit.c:77` is the only production caller of\n`build_limit()` (checked with `grep -rn \"build_limit(\"`); the lib tests\n(`src/lib/testsuite/test_limit.c`) pass the managers in the right order.", + "fix": "`src/scheduler/src/plugins/limit.c`: pass `(core->hgmgr), (core->pgmgr)` in\nthe order `build_limit()` declares, so each scope resolves against its own\nkind of group. No wire, WAL, snapshot or config change. The project-group\nmanager is still never loaded from config (projects have no config yet);\nthat is `project-config` scope (Requirement 6.7).\n`src/scheduler/testsuite/test_limit_plugin.c`: new regression test; `main()`\nnow initializes the interned-string table (`xstring_table_init()`), which\ngroup names need and which no earlier test in this binary exercised.\n\nVerification:\n- Test: `test_limit_scopes_resolve_against_own_group_kind`\n (`src/scheduler/testsuite/test_limit_plugin.c`): RED before the fix\n (`Expected 1 Was 0` at line 491, `hosts: [hg1]` not matching `hostA`),\n GREEN after (6/6 in `sched_limit_plugin_test`).\n- Suites: scheduler suite 119/119 OK, libxce tests 2/2 OK, asan\n (`xce.build`), 2026-10-10.\n- Live: n/a: the change is one argument order in config-time limit\n construction, fully covered by the unit test; no live limit run.", + "tags": [ + "scheduler", + "limit", + "project", + "hostgroup" + ], + "related_bugs": [ + "bug-1785" + ], + "fix_commits": [], + "record": ".wolf/bugs/bug-1933.md" } ] } diff --git a/.wolf/bugs/bug-1933.md b/.wolf/bugs/bug-1933.md new file mode 100644 index 00000000..17b37afd --- /dev/null +++ b/.wolf/bugs/bug-1933.md @@ -0,0 +1,104 @@ +--- +id: "bug-1933" +status: "open" +status_updated: "2026-10-10" +status_note: "newly logged" +component: "scheduler" +severity: "wrong-result" +timestamp: "2026-10-10T03:36:53Z" +last_seen: "2026-10-10T03:36:53Z" +occurrences: 1 +files: ["src/scheduler/src/plugins/limit.c", "src/lib/xcelimit.c"] +tags: ["scheduler", "limit", "project", "hostgroup"] +related: ["bug-1785"] +--- + +# bug-1933: xce_scheduler limit plugin resolves hosts: scopes against project groups (host-group limits ignored) + +## Description +**Observed:** +The scheduler's limit plugin built every limit with the host-group and +project-group managers swapped. A limit scoped to a host group never +matched that group's hosts; a limit scoped to projects resolved its member +names against the host groups. The new regression test, before the fix: +``` +../scheduler/testsuite/test_limit_plugin.c:491:test_limit_scopes_resolve_against_own_group_kind:FAIL: Expected 1 Was 0 +``` +(host group `hg1` = {`hostA`}; a limit with `hosts: [hg1]` did not match +host `hostA`.) + +**Expected:** a limit's `hosts:` members resolve against the host groups +(`hosts: [hg1]` matches every host in `hg1`), and its `projects:` members +resolve against the project groups (a project name never matches a host +group). +**Impact:** wrong result, silent. Every host limit that names a host group +applies to no host (only to a host literally named like the group), so the +limit is never enforced. A project limit whose member name equals a host +group's name matches the projects named like that group's hosts instead of +the project itself. Limits naming plain host or project names, and limits +without host/project scopes, are unaffected. In addition, the groups a +limit builds for its own scopes were registered in the wrong manager +(`-pg-group` in the host groups, `-hg-group` in the project +groups). +**Found by:** review, during `/kiro-spec-requirements project-config` +(2026-10-10, codebase survey of project-scoped limits). + +## Environment +- OS: no /etc/os-release; Docker container; NixOS kernel; kernel Linux 6.18.33 x86_64 +- libc: glibc 2.42-47 (interpreter /nix/store/wb6rhpznjfczwlwx23zmdrrw74bayxw4-glibc-2.42-47/lib/ld-linux-x86-64.so.2) +- Toolchain: gcc (GCC) 15.2.0, meson 1.9.1 +- Build: asan (xce.build), `ASAN_OPTIONS=detect_leaks=0` +- Code: scheduler-framework @ 25cdbc79-dirty +- Deployment: n/a (unit test of the scheduler limit plugin); the defect is in + code shared by the live scheduler and `xce_scheduler --sim`. + +## Reproduction +1. `cd src && nix develop . --command meson compile -C xce.build sched_limit_plugin_test` +2. `ASAN_OPTIONS=detect_leaks=0 ./xce.build/scheduler/testsuite/sched_limit_plugin_test` +3. Test `test_limit_scopes_resolve_against_own_group_kind` + (`src/scheduler/testsuite/test_limit_plugin.c`): loads host group + `hg1` = {`hostA`}, configures limit `host_scoped` with `hosts: [hg1]` and + limit `project_scoped` with `projects: [hg1]`, then checks + `match_limit()` for host `hostA`/`hostB` and project `hostA`/`hg1`. +- Frequency: deterministic +- Evidence: the test above; no logs kept. + +## Root cause +`build_limit()` (`src/lib/xcelimit.c:292`) takes its group managers as +`(ugmgr, qgmgr, hgmgr, pgmgr)`, but `limit_seed_config_impl()` +(`src/scheduler/src/plugins/limit.c:77`) passed +`(core->ugmgr, core->qgmgr, core->pgmgr, core->hgmgr)`. `build_consumer()` +therefore built the HOST consumer with the project-group manager and the +PROJECT consumer with the host-group manager. `make_g_member()` +(`src/lib/group.c`) treats a member name as a nested group only if the +manager it is given knows that group, so `hg1` became a literal host name +(the project-group manager is empty) and a project member named like a host +group became that host group. Since `ecf1916d` (2026-01-12, "support +limit"), the first version of the plugin call; carried through `7c229284` +and `487e7b94`. Scope: `limit.c:77` is the only production caller of +`build_limit()` (checked with `grep -rn "build_limit("`); the lib tests +(`src/lib/testsuite/test_limit.c`) pass the managers in the right order. + +## Fix +`src/scheduler/src/plugins/limit.c`: pass `(core->hgmgr), (core->pgmgr)` in +the order `build_limit()` declares, so each scope resolves against its own +kind of group. No wire, WAL, snapshot or config change. The project-group +manager is still never loaded from config (projects have no config yet); +that is `project-config` scope (Requirement 6.7). +`src/scheduler/testsuite/test_limit_plugin.c`: new regression test; `main()` +now initializes the interned-string table (`xstring_table_init()`), which +group names need and which no earlier test in this binary exercised. + +## Verification +- Test: `test_limit_scopes_resolve_against_own_group_kind` + (`src/scheduler/testsuite/test_limit_plugin.c`): RED before the fix + (`Expected 1 Was 0` at line 491, `hosts: [hg1]` not matching `hostA`), + GREEN after (6/6 in `sched_limit_plugin_test`). +- Suites: scheduler suite 119/119 OK, libxce tests 2/2 OK, asan + (`xce.build`), 2026-10-10. +- Live: n/a: the change is one argument order in config-time limit + construction, fully covered by the unit test; no live limit run. + +## References +- Found during: `.kiro/specs/project-config/requirements.md` (Requirement 6.7) +- Introduced: `ecf1916d` diff --git a/src/scheduler/src/plugins/limit.c b/src/scheduler/src/plugins/limit.c index 9de9f210..a24ea202 100644 --- a/src/scheduler/src/plugins/limit.c +++ b/src/scheduler/src/plugins/limit.c @@ -74,8 +74,10 @@ static int limit_seed_config_impl(xce_sched_plugin_t* plugin, int rc; xce_limit_t* limit; + /* bug-1933: build_limit() takes the host groups before the + * project groups */ rc = build_limit(&limit, (core->ugmgr), (core->qgmgr), - (core->pgmgr), (core->hgmgr), limits[i]); + (core->hgmgr), (core->pgmgr), limits[i]); if (rc < 0) { return rc; } diff --git a/src/scheduler/testsuite/test_limit_plugin.c b/src/scheduler/testsuite/test_limit_plugin.c index 52ecd8fc..681bff7e 100644 --- a/src/scheduler/testsuite/test_limit_plugin.c +++ b/src/scheduler/testsuite/test_limit_plugin.c @@ -61,6 +61,7 @@ #include "scheduler.h" #include "xce/types.h" #include "xcelimit.h" +#include "xstring.h" void reset_sched_obj(xce_sched_obj_t* obj) { @@ -420,13 +421,107 @@ static void test_get_configured_limits_registered_with_zero_limits(void) 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]); +} + 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); - return UNITY_END(); + RUN_TEST(test_limit_scopes_resolve_against_own_group_kind); + rc = UNITY_END(); + + xstring_table_fini(); + + return rc; }