fix(scheduler): percent-limit capacity reads maxmem/maxswap, not mem/swap (bug-1834)
update_limit_res_value() computed a percent-based limit's capacity from the resource's own live, fluctuating value (e.g. "mem"), letting the enforced cap drift with unrelated memory pressure instead of staying pinned to a stable fraction of total capacity. instantiate_limit() now resolves a static capacity-source resource once at instantiation time (mem->maxmem, swap->maxswap, table-driven), stored on a new xce_vres_t.percent_res field; a missing counterpart falls back to today's behavior rather than failing instantiation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
This commit is contained in:
parent
7982f28557
commit
ea76f4771f
8 changed files with 368 additions and 15 deletions
|
|
@ -24711,6 +24711,37 @@
|
|||
],
|
||||
"occurrences": 1,
|
||||
"last_seen": "2026-09-20T11:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "bug-1834",
|
||||
"timestamp": "2026-09-20T09:35:00Z",
|
||||
"error_message": "User-identified design gap (not yet reproduced as a live symptom, code-traced and confirmed): for a percent-based limit whose `resname` is a dynamic/consumable resource (e.g. `mem`, `swap`), the limit's CAPACITY is computed as percent * the resource's current LIVE value, not percent * the resource's static total. This means the same limit's enforced cap drifts up and down as unrelated memory pressure on the host changes, rather than staying pinned to a stable fraction of total capacity (e.g. 'this queue may use up to 20% of the host's total memory' should not shrink because some other job is currently using more memory).",
|
||||
"file": "src/scheduler/src/plugins/limit.c",
|
||||
"root_cause": "update_limit_res_value() (limit.c:142-165) computes `val = get_resvals(array, 1, &(vres->res->idx), &val); val = val * vres->limit->val.val;` for any percent!=0 limit -- `vres->res` is resolved in instantiate_limit() (xcelimit.c:574) via a single key_find(resmgr, limit->val.resname, ...), i.e. exactly the resource named by the limit's OWN `resname` field. `resname` today serves two roles that this code conflates: (1) selecting the accounting/consumable resource the limit's virtual resource (`vres->vres`) is keyed against for usage tracking (correctly `mem`, since jobs consume `mem`), and (2) supplying the live base value multiplied by percent to derive capacity (currently ALSO `mem`, the dynamic worker-reported load index per .kiro/steering/resource-accounting.md -- 'mem is not maxmem... mem is a dynamic load index... refreshed on the worker's regular load-report cadence'). Role (2) should read a static/non-consumable resource instead (`maxmem` for `mem`, `maxswap` for `swap`) -- both already exist as registered per-host resources (src/configs/resources.yaml:108 `maxmem`, :112 `maxswap`, 'self-reported once by the worker at join time and never refreshed' per resource-accounting.md) so no new resource type is needed, only a change to which resource the percent multiplication reads. xce_limit_val_t/xce_limit_config_t (types.h:800-818) have no field today to name this second resource explicitly -- resname is single-purpose in the struct.",
|
||||
"fix": "FIXED (2026-09-20). Added xce_vres_t.percent_res (src/include/xce/types.h) -- a resolved xce_resource_t* pointer for the percent-branch capacity read, kept separate from `res` (which stays the limit's own accounting resource, unchanged) so the two roles are no longer conflated. instantiate_limit() (src/lib/xcelimit.c) resolves it ONCE at instantiation time via a small table-driven static lookup (percent_capacity_resource_map[]: mem->maxmem, swap->maxswap) -- table-driven per user request, so a future dynamic/static pair (e.g. tmp/maxtmp) is a one-line addition. A missing/unregistered counterpart is NOT a hard failure: percent_res stays NULL and update_limit_res_value() (src/scheduler/src/plugins/limit.c) falls back to `res`, preserving today's behavior for any resource with no known static pair -- this is what keeps every pre-existing percent-limit test passing unchanged (they never register 'maxmem' in their resmgr fixtures, and none of them exercise update_limit_res_value()'s real computation anyway -- its existing white-box harness, test_repl_limit_table.c, stubs get_resvals()/put_resvals() as no-ops). New coverage: 4 unit tests in src/lib/testsuite/test_limit.c proving percent_res resolves to maxmem/maxswap for mem/swap, stays NULL for percent=0, and stays NULL (graceful fallback, not -ENOENT) for a percent-based limit on a resource with no known static counterpart (e.g. ncpus). Extended the shared src/lib/testsuite/test_resmgr_fixture.c/.h with maxmem/swap/maxswap test resources for this. Full lib (314 tests) and scheduler (85 test binaries) suites pass with zero regressions -- confirmed via raw testlog grep for ':FAIL:', not just the meson aggregate summary.",
|
||||
"tags": [
|
||||
"scheduler",
|
||||
"limit-plugin",
|
||||
"resource-accounting",
|
||||
"mem-vs-maxmem",
|
||||
"swap-vs-maxswap",
|
||||
"percent-limit-capacity",
|
||||
"update_limit_res_value",
|
||||
"instantiate_limit",
|
||||
"design-gap",
|
||||
"user-identified",
|
||||
"needs-owner",
|
||||
"related-to-bug-1831",
|
||||
"related-to-bug-1832",
|
||||
"night_mem_pct",
|
||||
"fixed"
|
||||
],
|
||||
"related_bugs": [
|
||||
"bug-1831",
|
||||
"bug-1832"
|
||||
],
|
||||
"occurrences": 1,
|
||||
"last_seen": "2026-09-20T10:15:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -241,3 +241,4 @@
|
|||
- **(2026-09-15, controller-status Req 12 discovery/requirements) Any new `ctrlstatus`-response field that describes a property of the current PRIMARY (not just the responding node itself) forces an explicit standby-fidelity design fork, and it is worth asking the user about it up front rather than assuming.** Options are: (a) propagate the value to standbys (learned via heartbeat, matching the precedent Req 10 already set for committedLsn/committedConfigVersion) so any reachable node answers accurately, or (b) report "unknown" on a standby even when it already knows the primary's name/term. Asked explicitly for the new "primary elected in term X at time Y" fields; user chose (a), full learned fidelity — consistent with how `primaryName` (Req 2.3) and committed position (Req 10) already work off-primary. Default to proposing (a) as the recommended option when this fork recurs, since it's the established pattern for this spec, but still confirm rather than assume, since it materially changes design/task scope (heartbeat payload changes) vs. a pure read of local state.
|
||||
- **(2026-09-15) `controller-status`'s spec.json tracks its delta/reopen history in a single top-level `delta` object, not an array** — when starting a new delta cycle on top of one that already reached `validate_impl.decision: "GO"`, preserve the just-closed cycle by nesting it under `delta.prior_delta` (reopened_at/reason/new_requirements/impl_status) rather than overwriting it outright, so the requirement-number provenance ("which delta added Req N") stays recoverable from spec.json alone without needing git blame.
|
||||
- **(2026-09-20, bug-1833) `xceadmin`'s bundled JSON schemas exist in TWO copies on disk, and editing the wrong one is silently discarded on the next build.** `src/configs/schema/*.schema.json` is the REAL canonical source; `src/xceadmin/src/xcecommon/schema_files/json/*.schema.json` is a checked-in mirror that `meson-build.sh:104` unconditionally overwrites from the canonical copy on every `xceadmin` build (`cp "$SRC_DIR/configs/schema/"*.schema.json "$SCHEMA_DST/json/"`). Editing only the xceadmin-tree copy produces a schema change that appears correct in `git diff` but has ZERO effect on any real build or test run — confirmed by editing only that copy, rebuilding, and watching a new schema constraint simply not fire (no error, just silent no-op), then finding the sync line and editing both copies to fix it. **Before editing any file under `src/xceadmin/src/xcecommon/schema_files/`, grep `meson-build.sh` for a sync/cp step first** — the same "editing xceadmin source doesn't retroactively update an already-built snapshot" caution documented elsewhere in this file extends to specific FILES within the tree being silently resynced from elsewhere, not just whole-package staleness.
|
||||
- **(2026-09-20, bug-1834) `var` (rtti's `typeinfo.h`) is `#define var void*` — a raw macro, not a typedef — so `var a, b, c;` does NOT declare three `void*`s.** It expands to `void* a, b, c;`, which in C only pointer-types the FIRST declarator; `b` and `c` silently fall back to an implicit `int` (a legacy K&R extension GCC still tolerates by default), producing `-Wincompatible-pointer-types`/`-Wint-conversion` errors at every later use, often far from the declaration and easy to misread as unrelated call-site bugs. Hit writing 4 new tests in `test_limit.c` that copied `var umgr, qmgr, hmgr, pmgr;` from a working single-variable pattern elsewhere in the file. **Always declare each `var` on its own line** (`var umgr;` / `var qmgr;` / ...) — this codebase's own existing tests (`test_build_limit_ok1` et al.) already do this consistently; the comma-list form is the anomaly, not the norm, despite looking harmless for an ordinary typedef'd type.
|
||||
|
|
|
|||
|
|
@ -826,6 +826,15 @@ typedef struct {
|
|||
typedef struct {
|
||||
xce_resource_t* vres;
|
||||
xce_resource_t* res;
|
||||
/* bug-1834: resolved capacity-source resource for a percent-based
|
||||
* limit on a dynamic/consumable resource (e.g. "mem" -> "maxmem",
|
||||
* "swap" -> "maxswap") -- NULL when `res` itself already IS the right
|
||||
* source (percent==0, or no static counterpart is registered/known).
|
||||
* update_limit_res_value() (plugins/limit.c) reads this in preference
|
||||
* to `res` so a percent limit's capacity is pinned to the resource's
|
||||
* static total instead of drifting with its live, fluctuating value
|
||||
* (see .kiro/steering/resource-accounting.md's "mem is not maxmem"). */
|
||||
xce_resource_t* percent_res;
|
||||
xce_limit_t* limit;
|
||||
} xce_vres_t;
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
#include "xcelimit.h"
|
||||
#include "xstring.h"
|
||||
|
||||
#include "test_resmgr_fixture.h"
|
||||
|
||||
static void test_build_consumer1(void)
|
||||
{
|
||||
xce_consumer_t* c = NULL;
|
||||
|
|
@ -647,6 +649,239 @@ static void test_fmt_limit_wrapper_matches_fmt_limit_units_raw_mode(void)
|
|||
destroy(umgr);
|
||||
}
|
||||
|
||||
/*
|
||||
* bug-1834: a percent-based limit's capacity must be pinned to the
|
||||
* resource's static total, not its live/fluctuating value -- instantiate_
|
||||
* limit() (xcelimit.c) resolves this ONCE at instantiation time into
|
||||
* vres->percent_res, which update_limit_res_value() (scheduler/plugins/
|
||||
* limit.c) then prefers over vres->res for the percent multiplication.
|
||||
* These four tests exercise the resolution itself (the only part of the
|
||||
* fix testable at this level -- update_limit_res_value() is scheduler-
|
||||
* private and its existing white-box test harness stubs get_resvals()/
|
||||
* put_resvals() as no-ops, see test_repl_limit_table.c).
|
||||
*
|
||||
* add_vres() (xcelimit.c) key_add()s the limit's own new "limit=<name>"
|
||||
* vres into resmgr with no datakiller, so each test frees that one entry
|
||||
* by hand before tearing down the shared make_test_resmgr() fixture --
|
||||
* mirroring free_test_resmgr()'s own manual-free pattern for its 8
|
||||
* pre-registered resources.
|
||||
*/
|
||||
static void free_instantiated_vres(var resmgr, xce_vres_t* vres)
|
||||
{
|
||||
if (vres == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (vres->vres != NULL) {
|
||||
key_del(resmgr, (void*)vres->vres->name);
|
||||
FREEUP(vres->vres->name);
|
||||
FREEUP(vres->vres);
|
||||
}
|
||||
|
||||
FREEUP(vres);
|
||||
}
|
||||
|
||||
static void
|
||||
test_instantiate_limit_percent_mem_limit_resolves_percent_res_to_maxmem(
|
||||
void)
|
||||
{
|
||||
var umgr;
|
||||
var qmgr;
|
||||
var hmgr;
|
||||
var pmgr;
|
||||
var resmgr;
|
||||
xce_limit_t* limit;
|
||||
xce_vres_t* vres = NULL;
|
||||
int rc;
|
||||
xce_limit_config_t limit_config
|
||||
= { .name = "pct_mem_limit",
|
||||
.val
|
||||
= &(xce_limit_val_t) { .resname = "mem", .percent = 1,
|
||||
.val = 0.2 } };
|
||||
|
||||
groupmgr_init(&umgr, 4, 4);
|
||||
groupmgr_init(&qmgr, 4, 4);
|
||||
groupmgr_init(&hmgr, 4, 4);
|
||||
groupmgr_init(&pmgr, 4, 4);
|
||||
resmgr = make_test_resmgr();
|
||||
TEST_ASSERT_NOT_NULL(resmgr);
|
||||
|
||||
rc = build_limit(&limit, umgr, qmgr, hmgr, pmgr, &limit_config);
|
||||
TEST_ASSERT_TRUE(rc == 1);
|
||||
|
||||
rc = instantiate_limit(&vres, resmgr, limit, "", "", "");
|
||||
TEST_ASSERT_TRUE(rc >= 0);
|
||||
TEST_ASSERT_NOT_NULL(vres);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
vres->percent_res,
|
||||
"bug-1834: a percent-based 'mem' limit must resolve a static "
|
||||
"capacity-source resource ('maxmem'), not leave capacity pinned "
|
||||
"to mem's own live, fluctuating value");
|
||||
TEST_ASSERT_EQUAL_STRING("maxmem", vres->percent_res->name);
|
||||
TEST_ASSERT_EQUAL_INT(TEST_RES_MAXMEM_IDX, vres->percent_res->idx);
|
||||
|
||||
free_instantiated_vres(resmgr, vres);
|
||||
kill_limit(limit);
|
||||
free_test_resmgr(resmgr);
|
||||
destroy(pmgr);
|
||||
destroy(hmgr);
|
||||
destroy(qmgr);
|
||||
destroy(umgr);
|
||||
|
||||
printf("test_instantiate_limit_percent_mem_limit_resolves_percent_res_"
|
||||
"to_maxmem: PASS\n");
|
||||
}
|
||||
|
||||
static void
|
||||
test_instantiate_limit_percent_swap_limit_resolves_percent_res_to_maxswap(
|
||||
void)
|
||||
{
|
||||
var umgr;
|
||||
var qmgr;
|
||||
var hmgr;
|
||||
var pmgr;
|
||||
var resmgr;
|
||||
xce_limit_t* limit;
|
||||
xce_vres_t* vres = NULL;
|
||||
int rc;
|
||||
xce_limit_config_t limit_config
|
||||
= { .name = "pct_swap_limit",
|
||||
.val
|
||||
= &(xce_limit_val_t) { .resname = "swap", .percent = 1,
|
||||
.val = 0.5 } };
|
||||
|
||||
groupmgr_init(&umgr, 4, 4);
|
||||
groupmgr_init(&qmgr, 4, 4);
|
||||
groupmgr_init(&hmgr, 4, 4);
|
||||
groupmgr_init(&pmgr, 4, 4);
|
||||
resmgr = make_test_resmgr();
|
||||
TEST_ASSERT_NOT_NULL(resmgr);
|
||||
|
||||
rc = build_limit(&limit, umgr, qmgr, hmgr, pmgr, &limit_config);
|
||||
TEST_ASSERT_TRUE(rc == 1);
|
||||
|
||||
rc = instantiate_limit(&vres, resmgr, limit, "", "", "");
|
||||
TEST_ASSERT_TRUE(rc >= 0);
|
||||
TEST_ASSERT_NOT_NULL(vres);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
vres->percent_res,
|
||||
"bug-1834: a percent-based 'swap' limit must resolve a static "
|
||||
"capacity-source resource ('maxswap'), same pairing as mem/maxmem");
|
||||
TEST_ASSERT_EQUAL_STRING("maxswap", vres->percent_res->name);
|
||||
TEST_ASSERT_EQUAL_INT(TEST_RES_MAXSWAP_IDX, vres->percent_res->idx);
|
||||
|
||||
free_instantiated_vres(resmgr, vres);
|
||||
kill_limit(limit);
|
||||
free_test_resmgr(resmgr);
|
||||
destroy(pmgr);
|
||||
destroy(hmgr);
|
||||
destroy(qmgr);
|
||||
destroy(umgr);
|
||||
|
||||
printf("test_instantiate_limit_percent_swap_limit_resolves_percent_res_"
|
||||
"to_maxswap: PASS\n");
|
||||
}
|
||||
|
||||
static void
|
||||
test_instantiate_limit_absolute_value_mem_limit_leaves_percent_res_null(
|
||||
void)
|
||||
{
|
||||
var umgr;
|
||||
var qmgr;
|
||||
var hmgr;
|
||||
var pmgr;
|
||||
var resmgr;
|
||||
xce_limit_t* limit;
|
||||
xce_vres_t* vres = NULL;
|
||||
int rc;
|
||||
xce_limit_config_t limit_config
|
||||
= { .name = "abs_mem_limit",
|
||||
.val
|
||||
= &(xce_limit_val_t) { .resname = "mem", .percent = 0,
|
||||
.val = 10.0 } };
|
||||
|
||||
groupmgr_init(&umgr, 4, 4);
|
||||
groupmgr_init(&qmgr, 4, 4);
|
||||
groupmgr_init(&hmgr, 4, 4);
|
||||
groupmgr_init(&pmgr, 4, 4);
|
||||
resmgr = make_test_resmgr();
|
||||
TEST_ASSERT_NOT_NULL(resmgr);
|
||||
|
||||
rc = build_limit(&limit, umgr, qmgr, hmgr, pmgr, &limit_config);
|
||||
TEST_ASSERT_TRUE(rc == 1);
|
||||
|
||||
rc = instantiate_limit(&vres, resmgr, limit, "", "", "");
|
||||
TEST_ASSERT_TRUE(rc >= 0);
|
||||
TEST_ASSERT_NOT_NULL(vres);
|
||||
TEST_ASSERT_NULL_MESSAGE(
|
||||
vres->percent_res,
|
||||
"an absolute-value (percent=0) limit never reads a live resource "
|
||||
"value for its capacity at all (update_limit_res_value() uses "
|
||||
"limit->val.val directly) -- no substitution is needed or made");
|
||||
|
||||
free_instantiated_vres(resmgr, vres);
|
||||
kill_limit(limit);
|
||||
free_test_resmgr(resmgr);
|
||||
destroy(pmgr);
|
||||
destroy(hmgr);
|
||||
destroy(qmgr);
|
||||
destroy(umgr);
|
||||
|
||||
printf("test_instantiate_limit_absolute_value_mem_limit_leaves_percent_"
|
||||
"res_null: PASS\n");
|
||||
}
|
||||
|
||||
static void
|
||||
test_instantiate_limit_percent_limit_without_known_static_counterpart_leaves_percent_res_null(
|
||||
void)
|
||||
{
|
||||
var umgr;
|
||||
var qmgr;
|
||||
var hmgr;
|
||||
var pmgr;
|
||||
var resmgr;
|
||||
xce_limit_t* limit;
|
||||
xce_vres_t* vres = NULL;
|
||||
int rc;
|
||||
xce_limit_config_t limit_config
|
||||
= { .name = "pct_ncpus_limit",
|
||||
.val
|
||||
= &(xce_limit_val_t) { .resname = "ncpus", .percent = 1,
|
||||
.val = 0.5 } };
|
||||
|
||||
groupmgr_init(&umgr, 4, 4);
|
||||
groupmgr_init(&qmgr, 4, 4);
|
||||
groupmgr_init(&hmgr, 4, 4);
|
||||
groupmgr_init(&pmgr, 4, 4);
|
||||
resmgr = make_test_resmgr();
|
||||
TEST_ASSERT_NOT_NULL(resmgr);
|
||||
|
||||
rc = build_limit(&limit, umgr, qmgr, hmgr, pmgr, &limit_config);
|
||||
TEST_ASSERT_TRUE(rc == 1);
|
||||
|
||||
rc = instantiate_limit(&vres, resmgr, limit, "", "", "");
|
||||
TEST_ASSERT_TRUE(rc >= 0);
|
||||
TEST_ASSERT_NOT_NULL(vres);
|
||||
TEST_ASSERT_NULL_MESSAGE(
|
||||
vres->percent_res,
|
||||
"bug-1834's lookup table only knows mem->maxmem and swap->maxswap "
|
||||
"-- a percent-based limit on any other resource must gracefully "
|
||||
"fall back to `res` (today's behavior), not fail instantiation");
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
vres->res, "the limit's own resname must still resolve normally");
|
||||
|
||||
free_instantiated_vres(resmgr, vres);
|
||||
kill_limit(limit);
|
||||
free_test_resmgr(resmgr);
|
||||
destroy(pmgr);
|
||||
destroy(hmgr);
|
||||
destroy(qmgr);
|
||||
destroy(umgr);
|
||||
|
||||
printf("test_instantiate_limit_percent_limit_without_known_static_"
|
||||
"counterpart_leaves_percent_res_null: PASS\n");
|
||||
}
|
||||
|
||||
void test_limit(void)
|
||||
{
|
||||
printf("\n>> limit test\n\n");
|
||||
|
|
@ -667,6 +902,14 @@ void test_limit(void)
|
|||
test_fmt_limit_units_non_byte_valued_resource_ignores_units_mode);
|
||||
RUN_TEST(test_fmt_limit_units_swp_resource_applies_mb_to_bytes_conversion);
|
||||
RUN_TEST(test_fmt_limit_wrapper_matches_fmt_limit_units_raw_mode);
|
||||
RUN_TEST(
|
||||
test_instantiate_limit_percent_mem_limit_resolves_percent_res_to_maxmem);
|
||||
RUN_TEST(
|
||||
test_instantiate_limit_percent_swap_limit_resolves_percent_res_to_maxswap);
|
||||
RUN_TEST(
|
||||
test_instantiate_limit_absolute_value_mem_limit_leaves_percent_res_null);
|
||||
RUN_TEST(
|
||||
test_instantiate_limit_percent_limit_without_known_static_counterpart_leaves_percent_res_null);
|
||||
|
||||
xstring_table_fini();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,11 +29,14 @@ var make_test_resmgr(void)
|
|||
var resmgr = makeInstance(&kvtab_type, ¶ms);
|
||||
if (resmgr == NULL) return NULL;
|
||||
|
||||
add_resource(resmgr, "type", TEST_RES_TYPE_IDX, XCE_VAL_STRING);
|
||||
add_resource(resmgr, "mem", TEST_RES_MEM_IDX, XCE_VAL_NUMERIC);
|
||||
add_resource(resmgr, "gpu", TEST_RES_GPU_IDX, XCE_VAL_BOOLEAN);
|
||||
add_resource(resmgr, "ncpus", TEST_RES_NCPUS_IDX, XCE_VAL_NUMERIC);
|
||||
add_resource(resmgr, "arch", TEST_RES_ARCH_IDX, XCE_VAL_STRING);
|
||||
add_resource(resmgr, "type", TEST_RES_TYPE_IDX, XCE_VAL_STRING);
|
||||
add_resource(resmgr, "mem", TEST_RES_MEM_IDX, XCE_VAL_NUMERIC);
|
||||
add_resource(resmgr, "gpu", TEST_RES_GPU_IDX, XCE_VAL_BOOLEAN);
|
||||
add_resource(resmgr, "ncpus", TEST_RES_NCPUS_IDX, XCE_VAL_NUMERIC);
|
||||
add_resource(resmgr, "arch", TEST_RES_ARCH_IDX, XCE_VAL_STRING);
|
||||
add_resource(resmgr, "maxmem", TEST_RES_MAXMEM_IDX, XCE_VAL_NUMERIC);
|
||||
add_resource(resmgr, "swap", TEST_RES_SWAP_IDX, XCE_VAL_NUMERIC);
|
||||
add_resource(resmgr, "maxswap", TEST_RES_MAXSWAP_IDX, XCE_VAL_NUMERIC);
|
||||
|
||||
return resmgr;
|
||||
}
|
||||
|
|
@ -44,7 +47,8 @@ void free_test_resmgr(var resmgr)
|
|||
|
||||
/* Free each xce_resource_t we allocated (datakiller=NULL means kvtab
|
||||
* won't free them, so we must do it here). */
|
||||
const char* names[] = { "type", "mem", "gpu", "ncpus", "arch", NULL };
|
||||
const char* names[] = { "type", "mem", "gpu", "ncpus",
|
||||
"arch", "maxmem", "swap", "maxswap", NULL };
|
||||
for (int i = 0; names[i] != NULL; i++) {
|
||||
xce_resource_t* r = NULL;
|
||||
int rc = key_find(resmgr, (void*)names[i], (void**)&r);
|
||||
|
|
|
|||
|
|
@ -4,13 +4,16 @@
|
|||
#include <rtti.h>
|
||||
|
||||
/* Test resource indices — must match make_test_resmgr() registration order */
|
||||
#define TEST_RES_TYPE_IDX 0 /* "type" — RES_TYPE_STRING */
|
||||
#define TEST_RES_MEM_IDX 1 /* "mem" — RES_TYPE_NUMERIC */
|
||||
#define TEST_RES_GPU_IDX 2 /* "gpu" — RES_TYPE_BOOL */
|
||||
#define TEST_RES_NCPUS_IDX 3 /* "ncpus" — RES_TYPE_NUMERIC */
|
||||
#define TEST_RES_ARCH_IDX 4 /* "arch" — RES_TYPE_STRING */
|
||||
#define TEST_RES_TYPE_IDX 0 /* "type" — RES_TYPE_STRING */
|
||||
#define TEST_RES_MEM_IDX 1 /* "mem" — RES_TYPE_NUMERIC */
|
||||
#define TEST_RES_GPU_IDX 2 /* "gpu" — RES_TYPE_BOOL */
|
||||
#define TEST_RES_NCPUS_IDX 3 /* "ncpus" — RES_TYPE_NUMERIC */
|
||||
#define TEST_RES_ARCH_IDX 4 /* "arch" — RES_TYPE_STRING */
|
||||
#define TEST_RES_MAXMEM_IDX 5 /* "maxmem" — RES_TYPE_NUMERIC (bug-1834) */
|
||||
#define TEST_RES_SWAP_IDX 6 /* "swap" — RES_TYPE_NUMERIC (bug-1834) */
|
||||
#define TEST_RES_MAXSWAP_IDX 7 /* "maxswap" — RES_TYPE_NUMERIC (bug-1834) */
|
||||
|
||||
/* Create a test resmgr with 5 pre-registered resources.
|
||||
/* Create a test resmgr with 8 pre-registered resources.
|
||||
* Returns a var (kvtab) that can be passed to parse_select / parse_resreq_primary.
|
||||
* Caller must call free_test_resmgr() when done. */
|
||||
extern var make_test_resmgr(void);
|
||||
|
|
|
|||
|
|
@ -472,6 +472,41 @@ int is_host_based_limit(var resmgr, xce_limit_t* limit)
|
|||
return c->per_consumer;
|
||||
}
|
||||
|
||||
/* bug-1834: a percent-based limit's capacity must come from a resource's
|
||||
* static total, not its live/fluctuating value -- "mem"/"swap" are both
|
||||
* XCE_RESF_DYNAMIC load indices (resource-accounting.md), so their own
|
||||
* live value is the wrong base for a stable percentage-of-capacity cap.
|
||||
* "maxmem"/"maxswap" are the static, self-reported-once counterparts
|
||||
* (resources.yaml) that should be read instead. Table-driven so a future
|
||||
* dynamic/static pair (e.g. tmp/maxtmp) is a one-line addition, not a new
|
||||
* branch. */
|
||||
static const struct {
|
||||
const char* resname;
|
||||
const char* percent_resname;
|
||||
} percent_capacity_resource_map[] = {
|
||||
{ "mem", "maxmem" },
|
||||
{ "swap", "maxswap" },
|
||||
};
|
||||
|
||||
static const char* percent_capacity_resname(const char* resname)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
if (resname == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (i = 0; i < sizeof(percent_capacity_resource_map)
|
||||
/ sizeof(percent_capacity_resource_map[0]);
|
||||
i++) {
|
||||
if (strcmp(resname, percent_capacity_resource_map[i].resname) == 0) {
|
||||
return percent_capacity_resource_map[i].percent_resname;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int add_vres(xce_resource_t** pvres, var resmgr, char* name, int global)
|
||||
{
|
||||
xce_resource_t* vres = NULL;
|
||||
|
|
@ -578,6 +613,23 @@ int instantiate_limit(xce_vres_t** pvres, var resmgr, xce_limit_t* limit,
|
|||
return -ENOENT;
|
||||
}
|
||||
|
||||
/* bug-1834: for a percent-based limit, resolve the static
|
||||
* capacity-source resource ONCE here rather than re-deriving it on
|
||||
* every update_limit_res_value() call. A missing/unregistered
|
||||
* counterpart is not a hard failure -- vres->percent_res simply stays
|
||||
* NULL and the caller falls back to `res` (this limit's own resname),
|
||||
* preserving today's behavior for any resource with no known static
|
||||
* pair. */
|
||||
if (limit->val.percent != 0) {
|
||||
const char* percent_resname = percent_capacity_resname(limit->val.resname);
|
||||
|
||||
if (percent_resname != NULL) {
|
||||
lock(resmgr, RTTI_LOCK_READ);
|
||||
key_find(resmgr, (void*)percent_resname, (void**)&(vres->percent_res));
|
||||
unlock(resmgr);
|
||||
}
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -141,8 +141,9 @@ typedef struct {
|
|||
|
||||
static int update_limit_res_value(zarray_t* array, xce_vres_t* vres)
|
||||
{
|
||||
float val = 0.0;
|
||||
int rc = 0;
|
||||
float val = 0.0;
|
||||
int rc = 0;
|
||||
xce_resource_t* capacity_res;
|
||||
|
||||
if ((array == NULL) || (vres == NULL)) {
|
||||
return -EINVAL;
|
||||
|
|
@ -151,7 +152,16 @@ static int update_limit_res_value(zarray_t* array, xce_vres_t* vres)
|
|||
if (vres->limit->val.percent == 0) {
|
||||
val = vres->limit->val.val;
|
||||
} else {
|
||||
rc = get_resvals(array, 1, &(vres->res->idx), &val);
|
||||
/* bug-1834: prefer the resolved static capacity-source resource
|
||||
* (e.g. "maxmem" for a "mem" limit) over `res` (the limit's own,
|
||||
* possibly-dynamic resname) so this limit's capacity is a stable
|
||||
* fraction of total capacity, not a fraction of whatever the
|
||||
* resource's live value happens to be right now. `percent_res` is
|
||||
* NULL (falls back to `res`, today's behavior) whenever
|
||||
* instantiate_limit() found no known static counterpart. */
|
||||
capacity_res = (vres->percent_res != NULL) ? vres->percent_res : vres->res;
|
||||
|
||||
rc = get_resvals(array, 1, &(capacity_res->idx), &val);
|
||||
if (rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue