fix(scheduler): fix limit-plugin usergroup/scope/error-code cascade (gitea#46)

While live-verifying gitea#46, found and fixed a cascade of 5 previously
undetected bugs that together made every usergroup-scoped limit in
limits.yaml silently non-functional:

- Named usergroups/hostgroups (users.yaml/hosts.yaml) were never
  registered into the scheduler's group managers (groupmgr_add() had zero
  production callers), so any limit referencing one by name could never
  resolve real membership.
- An unconfigured limit scope (e.g. no "projects:") arrived as an
  empty-but-present capnp group, not NULL, so build_consumer() built a
  real empty group (matching no one) instead of falling back to "all".
- Three separate call sites conflated key_find()'s negative "not found"
  return with a real error, short-circuiting before ever reaching the
  "instantiate on first use" logic right below them -- the same defect
  class already fixed once before in this codebase (bug-1495).
- Two call sites passed &(sched->core.resmgr) where a plain handle was
  expected, one level of indirection too many, causing RTTI's key_find()
  to reject it as an invalid handle.

All five are live-verified: fresh cluster deploys now correctly match and
instantiate usage tracking for all 7 limits in limits.yaml on every job
submission, reproduced across multiple independent from-scratch redeploys.

A 6th, architecturally distinct issue remains open (bug-1789, logged, not
fixed): sched_fill_limit_requests() computes a job's consumption of a
slots-scoped global limit by reading the wrong request vector, so the
numeric cap itself still never binds. This needs a design decision on
intended cross-scope semantics, not a mechanical fix, so it's left for a
follow-up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hb6Edtj4KbS8CrgEWYdA2E
This commit is contained in:
Rongsong Shen 2026-09-07 12:44:16 +00:00
parent 099dbdabdc
commit 22cfe61642
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
8 changed files with 432 additions and 19 deletions

View file

@ -200,7 +200,21 @@ static int match_job_global_limit_(xce_sched_plugin_t* plugin,
return -EINVAL;
}
if (is_host_based_limit(&(sched->core.resmgr), limit) > 0) {
/* bug-1787: is_host_based_limit()/instantiate_limit() both take a
* plain `var resmgr` (core.h: `var resmgr;` -- already a handle, not
* a var*), matching how every other resmgr/ugmgr/etc. consumer in
* this codebase (get_group(), is_in_group(), key_find() itself) takes
* its manager by value. Passing &(sched->core.resmgr) here instead
* (as this and the instantiate_limit() call below previously did)
* handed those functions the ADDRESS of the field instead of its
* value -- one level of indirection too many. RTTI's key_find()
* dispatch then rejected that as an invalid object handle: silently
* inside is_host_based_limit() (its own `rc<=0` guard turns ANY
* key_find() failure, including this one, into a uniform -ENOENT --
* see bug-1786's comment above for why that's not itself a bug), but
* fatally inside instantiate_limit()'s own key_find() call, which
* returned -EINVAL and aborted vres instantiation outright. */
if (is_host_based_limit(sched->core.resmgr, limit) > 0) {
return 0;
}
@ -216,18 +230,30 @@ static int match_job_global_limit_(xce_sched_plugin_t* plugin,
return rc;
}
/* bug-1786: key_find() returning <= 0 here means "not found" (this
* codebase's established kvmgr convention, matching get_group()'s and
* is_host_based_limit()'s own identical rc<=0 checks) -- the FIRST
* time ANY limit ever matches a given user/queue/project combination,
* this is always the case, since no vres has been instantiated for it
* yet. Returning early here (as this code previously did) never gave
* the instantiate_limit() block below a chance to run: vres stays
* uninitialized (calloc'd to NULL at declaration, never reassigned by
* a failed key_find), so `if (vres == NULL)` below is the correct,
* sufficient signal to instantiate -- this key_find's own rc need not
* gate anything further. Before this fix, bug-1784/1785's fixes made
* match_limit() correctly match for the first time in this codebase's
* history, which only then exposed this: a limit could match a job
* but its usage/threshold accounting could never actually initialize,
* so no limit's numeric cap (e.g. limits.yaml's ug1_normal_slots
* value: 8) was ever enforced, only the pass/fail match itself. */
lock(privdata->instances, RTTI_LOCK_READ);
rc = key_find(privdata->instances, (void*)vresname, (void*)&vres);
unlock(privdata->instances);
if (rc <= 0) {
return rc;
}
FREEUP(vresname);
if (vres == NULL) {
rc = instantiate_limit(&vres, &(sched->core.resmgr), limit, obj->user,
rc = instantiate_limit(&vres, sched->core.resmgr, limit, obj->user,
obj->queue, obj->project);
if (rc < 0) {
return rc;
@ -337,7 +363,7 @@ static int match_job_host_limit_(xce_sched_plugin_t* plugin,
return 0;
}
if (is_host_based_limit(&(sched->core.resmgr), limit) <= 0) {
if (is_host_based_limit(sched->core.resmgr, limit) <= 0) {
return 0;
}
@ -376,7 +402,7 @@ static int match_job_host_limit_(xce_sched_plugin_t* plugin,
FREEUP(vresname);
if (vres == NULL) {
rc = instantiate_limit(&vres, &(sched->core.resmgr), limit, obj->user,
rc = instantiate_limit(&vres, sched->core.resmgr, limit, obj->user,
obj->queue, obj->project);
if (rc < 0) {
return rc;

View file

@ -11,6 +11,7 @@
#include "client.h"
#include "controller.capnp.h"
#include "decision.h"
#include "group.h"
#include "kvmgr.h"
#include "misc.h"
#include "plugin.h"
@ -1094,6 +1095,16 @@ static int sched_init_queues(xce_scheduler_t* sched)
}
int sched_init_config(xce_scheduler_t* sched, xce_conf4sched_t* config)
{
/* bug-1784: register the cluster's NAMED usergroups/hostgroups (from
* users.yaml's "usergroups:"/hosts.yaml's "hostgroups:") into
* core.ugmgr/core.hgmgr BEFORE plugin_config_update() below -- a limit
* config that references one of these by name (e.g. a limit scoped to
* users.members=["ug1"]) resolves it via build_consumer()'s own
* makeGroup() call, which can only find an existing named group if it
* was already registered under that name. */
groupmgr_load_configs(sched->core.ugmgr, config->n_ugroups, config->ugroups);
groupmgr_load_configs(sched->core.hgmgr, config->n_hgroups, config->hgroups);
plugin_config_update(sched, config);
sched->core.config = config;