Adds Requirement 6 (Human-Readable Units for Byte-Valued Limit Amounts) to the spec: requirements, design, research, and tasks 7-10. Task 7 (Foundation): promotes the REPL's byte-unit scaling algorithm and byte-valued-resource classifier into src/lib/utils.c/utils.h (xce_format_bytes_human, xce_is_bytes_valued_resource), alongside a new display-mode-aware composer (xce_format_units_value, xce_units_mode_t) -- following the fmt_jobid()/fmt_jobid_ex() precedent. repl_fmt.c's scheduler-only functions become one-line delegating wrappers. repl_info.c's repl_format_numeric_resource_value() (shared by info worker/info resource, not just the upcoming info limit) is retyped to delegate to the new shared composer via a small translate_units_mode() bridge, so every consumer benefits from one implementation going forward. No existing symbol's signature changed; only new symbols were added. Confirmed behavior-preserving: full scheduler suite 85/85 unchanged, including info worker/info resource's own regression tests. One narrow, untested, out-of-requirement-scope corner case found and documented (swp's RAW/BOTH mode rendering under the new single-value composer interface) -- logged in tasks.md's Implementation Notes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
24 KiB
24 KiB
Research & Design Decisions
Summary
- Feature:
scheduler-sim-diagnostics— Requirement set 1 (info limitREPL command) - Discovery Scope: Extension (extends
scheduler-core's existing REPLinfocommand family and reuses the limit plugin's existing library-level formatters) - Key Findings:
- The REPL
infocommand family is a single dispatch function (repl_cmd_info(),repl_info.c) with oneelse ifbranch per subcommand — addinglimitis a mechanical extension of an existing, well-established pattern. - The limit plugin's private state (
limit_plugin_data_t.limits/.instances) is genuinely inaccessible outsidesrc/scheduler/src/plugins/limit.ctoday — no header declares the type — confirming the brief's stated gap. Two new one-line accessor functions close it. - Two library-level formatters already exist and already do most of Requirement 1's work:
fmt_limit()andfmt_consumer()/fmt_group()(src/lib/xcelimit.c,src/lib/group.c), including correct inclusion/exclusion membership rendering (fmt_group()already prefixes excluded members with~). These are adopted directly rather than rebuilt. - The live-usage read path (GLOBAL/SHARED single value vs. per-host breakout) is architecturally identical to the pattern
repl_info_resource_one()already implements for ordinary resources (bug-1593) — the limit's synthetic capacity resource (vres->vres) is a real, already-registeredxce_resource_tcarrying the sameXCE_RESF_GLOBAL/XCE_RESF_SHAREDflags, so the same shared-vs-per-host read logic applies unmodified.
- The REPL
Research Log
Limit plugin data model and matching semantics
- Context: Requirement 1 AC1/AC5/AC6 need to know exactly what "scope dimensions" and "restricted membership" mean at the code level, since an earlier requirements-review pass surfaced a scope-vs-runtime ambiguity.
- Sources Consulted:
src/include/xce/types.h(xce_limit_t,xce_limit_config_t,xce_consumer_t,xce_vres_t),src/lib/xcelimit.c(match_limit,build_vres_name,is_host_based_limit,instantiate_limit),src/scheduler/src/plugins/limit.c(match_job_host_limit_,match_job_global_limit_,update_limit_res_value,limit_prepare_allocation),.wolf/buglog.jsonbug-1784 through bug-1790 (the gitea#46 limit-matching bug chain, 2026-09-07). - Findings:
xce_consumer_type_thas exactly four values (CONSUMER_USER,CONSUMER_QUEUE,CONSUMER_PROJECT,CONSUMER_HOST) — the four scope dimensions in Requirement 1 AC1 are the complete set, no fifth dimension exists.is_host_based_limit()forces per-host live-instance tracking whenever the limit's value is a percentage of a resource that is notXCE_RESF_GLOBAL/XCE_RESF_SHARED— independent of whetherCONSUMER_HOSTwas configured. This is real, current behavior (not hypothetical), confirmed via source and via bug-1789/1790's live-verified fix history for the equivalent allocation-side computation.xce_group_tsupports bothmembersandex_members(exclusion). Confirmed as real production configuration via bug-1789's own note describing a live-configured limit ("limit1... all users except rshen").fmt_group()(src/lib/group.c:388) already renders both lists correctly (~nameprefix for exclusions,all/[]shortcuts for the builtin groups) and is alreadyextern-declared ingroup.h.fmt_consumer()/fmt_limit()(src/lib/xcelimit.c) already composefmt_group()into a full per-limit configuration dump.- The bug-1789/1790 chain (global limits under-counting usage against per-host resources) is fully closed as of 2026-09-07 — the underlying accounting this feature displays is trustworthy across all resource/scope combinations by the time this spec is implemented.
- Implications: Requirement 1's "configured setting" display can adopt
fmt_limit()directly instead of re-deriving formatting logic; only the effective-host-scope annotation (Req 1.6) and the live-instance/usage rendering (Req 2, Req 3) are genuinely new code.
REPL command dispatch and existing info resource precedent
- Context: Need the concrete integration point for a new
info limitsubcommand and a working precedent for the GLOBAL/SHARED-vs-per-host live-value read split Requirement 2/3 require. - Sources Consulted:
src/scheduler/src/repl.c(repl_dispatch),src/scheduler/src/repl_info.c(repl_cmd_info,repl_info_resource,repl_info_resource_one),src/scheduler/src/plugin.c(plugin_register,plugin_register_core),src/scheduler/include/plugin.h, bug-1593 (repl_info_resource_one's original GLOBAL/SHARED-vs-host-based display bug and fix). - Findings:
repl_cmd_info()is a flatstrcmpchain ontokens[1]; every existing subcommand (job,queue,worker,resource, ...) follows the identical<name>|allargument shape Requirement 4 asks for.repl_info_resource_one()(post bug-1593) reads a GLOBAL/SHARED resource's value fromsim->core.sharedInst/sharedInUseatr->idx, and explicitly declines to fabricate a value for a host-based resource, deferring toinfo worker <name>instead. This is the exact pattern Requirement 2/3's per-instance read path reuses.- Plugins are registered into
core->plugins, a name-keyed handle (key_add(core->plugins, plugin->name, plugin)); the limit plugin registers under the fixed name"xce_sched_limit"(limit.c:557). No plugin is ever renamed or re-registered under a different name at runtime. - Synthetic limit-capacity resources (
vres->vres, created byadd_vres()) are inserted into the samecore.resmgrtableinfo resource/info workeralready iterate, flaggedXCE_RESF_LIMIT(already a named flag inrepl_resource_flags_str's table). A GLOBAL-scoped limit's synthetic resource can therefore already appear ininfo resource all/info resource <name>today, labeled with theLIMITflag — this is pre-existing behavior, unrelated to and unmodified by this spec.
- Implications: New command wiring is a single new branch in
repl_cmd_info()plus one new static functionrepl_info_limit()inrepl_info.c, followingrepl_info_resource()'s two-level (_alliterates,_oneprints) shape.
Architecture Pattern Evaluation
| Option | Description | Strengths | Risks / Limitations | Notes |
|---|---|---|---|---|
| Direct struct exposure | Move limit_plugin_data_t into a shared header, let repl_info.c include it directly |
Fewer functions | Breaks the plugin's existing data encapsulation; every other plugin's private state stays file-local — this would be the only exception | Rejected |
| Opaque query API (iterator_t out-params, per-limit filtered iteration) | New multi-function query interface returning filtered iterators | Nice call-site ergonomics | New abstraction with only one consumer today; no existing accessor in this codebase does filtered iteration — the rest (sched_get_job, core->plugins, core->jobs) hand back a raw kvmgr/xlist handle and let the caller iter_make/iter_next |
Rejected — over-built for current scope |
| Selected: two raw-handle accessors | limit_get_configured_limits(core) / limit_get_usage_instances(core) return the plugin's existing var (kvmgr/xlist) handles, or NULL if the plugin isn't registered; caller iterates with the same iter_make/iter_next/iter_destroy idiom used everywhere else in this codebase |
Matches every existing accessor pattern (core->jobs, core->queues, core->plugins); minimal new surface (2 functions); no new types |
Caller must filter usage instances by vres->limit == target_limit itself (pointer comparison) since there is no per-limit index — acceptable, this table is small (configured-limit count × scope-instance count, not job count) |
Selected |
Design Decisions
Decision: Plugin accessor shape
- Context:
limit_plugin_data_t(limits,instances) is a file-local type inlimit.c; Requirement 1/2/3 need read access to both fromrepl_info.c. - Alternatives Considered:
- Expose the struct itself in a shared header.
- Build a filtered query API (per-name lookup, per-limit instance iteration) as new plugin-owned functions.
- Expose the two raw
varhandles as accessor functions, encapsulating the plugin-name lookup ("xce_sched_limit") inside the accessor itself sorepl_info.cnever needs to know it.
- Selected Approach: (3).
limit_get_configured_limits(xce_sched_core_t* core)andlimit_get_usage_instances(xce_sched_core_t* core), both declared insrc/scheduler/include/plugin.hnext toregister_limit, defined inlimit.c. Each does its ownkey_find(core->plugins, "xce_sched_limit", &plugin)and returnsplugin->data's relevant field (orNULLif the plugin is not registered or not yet initialized). - Rationale: Matches the codebase's existing accessor idiom exactly (raw handle + shared iterator trio), keeps the plugin-name string fully inside
limit.c(the one place that already owns it viaregister_limit()), and adds the minimum surface needed — no speculative per-limit-name lookup function, since both AC1's "specific name" and AC2's "all" cases are satisfiable by iterating the same handle and filtering in the caller. - Trade-offs:
repl_info_limit()finds a limit by name via a directkey_find()(privdata->limitsis a name-keyed kvmgr — confirmed by bug-1685's duplicate-name-EEXISThandling, so this is O(1)-ish, not a scan) and a linear scan + pointer-equality filter to find that limit's usage instances inprivdata->instances(keyed by vresname, not by owning limit — no index avoids this scan; accepted, same scale argument as below). - Follow-up: None — this is a stable, minimal boundary; a future capability under this same open-ended spec (e.g. decision-reasoning trace) does not need to touch this accessor.
Decision: Reuse fmt_limit()/fmt_group() for configured-setting display
- Context: Requirement 1 AC1/AC5 need to print a limit's name, per-dimension scope, and inclusion/exclusion membership.
- Alternatives Considered:
- Write new formatting code in
repl_info.cmirroring the otherrepl_info_*_one()functions' inlinefprintfstyle. - Call the existing
fmt_limit()(which internally callsfmt_consumer()/fmt_group()) into a stack buffer and print it, then append the REPL-specific effective-scope note and live-usage lines the library formatter cannot know about.
- Write new formatting code in
- Selected Approach: (2).
- Rationale:
fmt_limit()already produces correct, exclusion-aware output and is already used/tested at the library level (src/lib/testsuite/test_limit.c); duplicating that logic inrepl_info.cwould both waste effort and risk drifting out of sync with the library's own definition of "configured setting." Build-vs-adopt: adopt. - Trade-offs:
fmt_limit()'s output format (multi-line,PER_USER = [...]style) is fixed by the library; the REPL wraps it with a[sim]prefix per line rather than reformatting it, consistent with how other multi-lineinfooutput is already prefixed in this codebase (e.g.info jobs). - Follow-up: None.
Decision: Effective-scope annotation is REPL-layer, not library-layer
- Context: Requirement 1 AC6 (implicit host-scoping for a percent-based limit on a non-shared resource) is not something
fmt_limit()knows about — it only reflects the configuredconsumers[]array. - Alternatives Considered:
- Change
fmt_limit()itself to callis_host_based_limit()and inject a syntheticPER_HOSTline. - Keep
fmt_limit()unchanged (it is a general-purpose library formatter used elsewhere, e.g. by any future non-REPL consumer that wants the raw configured shape) and haverepl_info_limit()callis_host_based_limit()itself, appending an explanatory note only whenconsumers[CONSUMER_HOST] == NULLand the check returns true.
- Change
- Selected Approach: (2).
- Rationale:
is_host_based_limit()is alreadyextern-declared inxcelimit.hand used exactly this way (a yes/no runtime check) elsewhere; the "effective scope" concept is specific to what an operator inspecting the REPL needs to know, not a general property of the limit's configured shape — keeping it at the REPL layer avoids changing a shared library formatter's contract for one caller's needs. - Trade-offs: None material.
- Follow-up: None.
Risks & Mitigations
- Risk: Linear scan over
privdata->instancesto filter byvres->limit == target_limit(this table is keyed by vresname, not by owning limit, so no index avoids the scan) could be surprising if a deployment ever configures an unusually large number of distinct scope instances (e.g. thousands of users × per-user limits). — Mitigation: Accepted for this spec; the existingmatch_job_host_limit_/match_job_global_limit_matching path already does akey_findper job per limit against the same table, so this table's size is already load-bearing elsewhere in the scheduler's hot path — a read-only REPL scan over it is not a new scaling concern this spec introduces. - Risk: Because
is_host_based_limit()takes the plugin'sresmgrhandle (avar), calling it fromrepl_info.crequires reachingsim->core.resmgr— already public (used directly byrepl_info_resource()today), so no new exposure needed. — Mitigation: N/A, confirmed no new risk. - Risk:
build_vres_name()never encodes host in the vresname it builds — an implementer could reasonably assume "onexce_vres_tper (user, queue, project, host)" and design storage/iteration around a cardinality that does not exist. — Mitigation:design.md's Data Models section states the correct cardinality explicitly (onexce_vres_tper (user, queue, project); host is produced by a per-worker fan-out over that singlexce_vres_t's shared resource index, not by additionalxce_vres_tinstances) and cites this finding. - Risk: Build wiring for the new white-box test —
test_repl_resource_table.c's existing#includeset omitsplugins/limit.candresource.c; naively adding a limit-info test there, or to a careless new file, reproduces the exact undefined-reference class bug-1762 already hit once for a sibling REPL feature. — Mitigation:design.md's Testing Strategy calls this out explicitly and directs a new sibling test file (test_repl_limit_table.c) with its own white-box include set, real (not stubbed)resource.csince the test needs non-trivial accounted-usage values.
Additional Findings (design review pass)
xce_sched_worker_tcarries two separate zarrays per resource idx:.resvals(capacity) and.resInUse(accounted usage) — confirmed viasched_check_host_satisfy_()(resource.c), which reads both the identical way this design's per-host instance display needs to.instantiate_limit()andupdate_limit_res_value()run back-to-back inside the samematch_job_host_limit_/match_job_global_limit_call (plugins/limit.c) — a matchedxce_vres_t's capacity entry is therefore always populated by the time thexce_vres_titself is observable via the new accessors; only its in-use entry can legitimately be absent (genuine zero usage, not "no instance").resource-accounting.md's reservation-vs-actual-usage reconciliation is explicitly scoped to resources flagged bothXCE_RESF_DYNAMICandXCE_RESF_CONSUMABLE. A limit's synthetic capacity resource (add_vres()) is flaggedXCE_RESF_LIMIT | XCE_RESF_CONSUMABLE | XCE_RESF_RELEASEplusGLOBAL|SHAREDorDEFAULT_HOST— neverXCE_RESF_DYNAMIC— so that steering document does not apply here; the capacity/in-use pair this design reads is an exact scheduler-computed accounting number, not an OS-reconciled estimate.src/scheduler/testsuite/test_no_bare_printf.cregression-tests every file undersrc/scheduler/src/for bareprintf()calls (must usefprintf(stdout, ...)); bug-1599 documents a known scanner false-positive on comment prose, not an exemption for real code.
Requirement 6 Research (added 2026-09-19 — Human-Readable Units for Byte-Valued Limit Amounts)
Two independent root causes behind one operator-reported symptom
- Context: A live REPL transcript showed a limit's configured amount (
mem = 5000000000.000000) and its usage amount (accounted=9011200.00 capacity=4635344384.00) both as raw byte counts, despite the REPL already having a general byte-formatting convention used byinfo worker/info resource. - Sources Consulted:
src/scheduler/src/repl_info.c(repl_print_limit_usage(),repl_print_limit_usage_per_host(),repl_format_consumable_value_and_used(),repl_format_numeric_resource_value(),repl_current_units_mode()),src/scheduler/src/repl_fmt.c(repl_is_bytes_valued_resource(),repl_format_bytes_human()),src/lib/xcelimit.c(fmt_limit()),src/lib/testsuite/test_limit.c. - Findings:
- The usage lines (
accounted=/capacity=) already callrepl_format_consumable_value_and_used(), which already checksrepl_is_bytes_valued_resource()— but the argument passed isvres->vres->name, a synthesized compound name (build_vres_name(), e.g.limits2/u=rshen/q=xce/p=), never the literal resource name ("mem"). The classifier silently always returns false for a limit's usage instance. The correct string (limit->val.resname, orvres->limit->val.resnamein the per-host helper) is already reachable in scope — this is a genuine, isolated argument bug, not a missing feature. - The configured-value line is printed entirely by
fmt_limit()(src/lib/xcelimit.c), a shared-library function with a fixed"%s = %f%s\n"format and its own direct unit tests (test_limit.c, 5 call sites) — it has no REPL units-mode concept at all, andrepl_info.c's own existing comment explicitly documents a prior decision NOT to reformat its output. - Full-tree grep confirms
fmt_limit()has exactly one production caller (repl_info.c, this spec's own command) and one direct-test file — no other component (controller, worker, apiserver) calls it, despitelibxcebeing linked by all of them.
- The usage lines (
- Implications: the usage-line issue is a one-line-per-call-site bug fix. The config-line issue requires a real design decision (discovery phase surfaced 3 approaches; user selected "reformat at the source").
Reformat-at-the-source approach: how to add a units-mode parameter without breaking fmt_limit()'s contract
- Context: the user selected reformatting
fmt_limit()'s output at its source over a REPL-layer annotation, butfmt_limit()'s existing signature has no way to receive a display-mode preference, and its 5 existingtest_limit.cassertions pin its exact current output. - Alternatives Considered:
- Add a units-mode parameter directly to
fmt_limit()'s existing signature — breaks its contract; all 5test_limit.ccall sites and bothrepl_info.ccall sites need updating, and the meaning of "default" behavior forfmt_limit()'s own unit tests becomes ambiguous. - Add a new sibling function
fmt_limit_units()taking the extra parameter, and retypefmt_limit()itself into a one-line wrapper calling it with a fixed RAW mode. - Duplicate
fmt_limit()'s whole body into a new function rather than sharing it via delegation.
- Add a units-mode parameter directly to
- Selected Approach: (2).
- Rationale: verified algebraically that
xce_format_units_value(XCE_UNITS_RAW, v, ...)produces the bare"%f"rendering ofv, identical to whatfmt_limit()prints today in both its percent and non-percent branches — sofmt_limit(buf, len, limit) == fmt_limit_units(buf, len, limit, XCE_UNITS_RAW)for every input, provably byte-identical. This meanstest_limit.c's 5 existing assertions require zero changes, contrary to this spec's own discovery-phase estimate that they would need updating. Delegation (not duplication) avoids the two functions' consumer-dump loops drifting apart later. - Trade-offs: two function names to maintain instead of one, but this matches the codebase's own precedent (
fmt_jobid()/fmt_jobid_ex(),limit_seed_config()/limit_seed_config_impl()) for "add an extended sibling rather than break an existing signature." - Follow-up: none.
Where the byte-formatter should live once it needs a non-scheduler caller
- Context:
fmt_limit_units()lives insrc/lib/xcelimit.c(shared library) but needs the same byte-scaling algorithmrepl_format_bytes_human()(src/scheduler/src/repl_fmt.c, scheduler-only) already implements. - Alternatives Considered:
- Duplicate the algorithm into
xcelimit.cdirectly. - Promote the algorithm (and its companion classifier/composer) into an existing shared-lib file, with the scheduler's existing functions becoming thin wrappers.
- Create a brand-new shared-lib file solely for this.
- Duplicate the algorithm into
- Selected Approach: (2), landing in
src/lib/utils.c/src/include/utils.h— the existing home offmt_jobid()/fmt_jobid_ex(), the closest existing precedent for a small, generic, cross-component display-formatting helper with no domain coupling (confirmed no similar helper already exists elsewhere insrc/lib;src/controller/testsuite/test.c's ownformat_bytes_human()is test-only and not a production dependency, so not consolidated by this spec). - Rationale: avoids duplicating the same divide-by-1024-while->=1024 algorithm a third time in the codebase (it already exists twice:
repl_format_bytes_human()production,test.c'sformat_bytes_human()test-only);utils.c/utils.his already linked into every component that would ever need this, with zero new build-system surface. - Trade-offs:
xce_repl_units_mode_t(scheduler-private,repl_internal.h) and the newxce_units_mode_t(shared,utils.h) are two independently-defined enums with the same three semantic values, rather than one shared type — deliberate, since promoting the scheduler's own enum into shared-lib scope would be a layering violation in the other direction (asrc/libtype must not be defined in terms of a scheduler-internal concept, nor should the scheduler's already-tested, already-workingxce_repl_units_mode_tplumbing need to change for this). A smalltranslate_units_mode()switch at the one new call site (repl_info.c) bridges the two; a Revalidation Trigger indesign.mdflags this mapping for a future maintainer. - Follow-up: none.
Existing test fixtures were asserting on the bug, not a locked contract
- Context:
test_repl_limit_table.c(built under Requirement Set 1) has multiple fixtures using"mem"as the limit's constrained resource, with assertions likestrstr(captured, "accounted=4.00"). - Finding: these assertions currently pass BECAUSE of the resource-name argument bug (the classifier is never actually asked about
"mem"), not because"4.00"is a deliberately-chosen display contract. Fixing the bug will legitimately change this output to a human-formatted form. - Implication: this is expected, not a regression to avoid —
design.md's Testing Strategy requires the implementer to prove this via a temporary revert (old assertion fails for the right reason once the bug is fixed) before finalizing the updated assertion, the same RED→GREEN discipline as any other bug fix in this codebase.
References
.wolf/buglog.jsonbug-1784–bug-1790 (gitea#46 limit-matching chain, 2026-09-07) — internal, not a URL..wolf/buglog.jsonbug-1593 (info resourceGLOBAL/SHARED-vs-host-based display fix, 2026-08-19) — internal, not a URL..wolf/buglog.jsonbug-1685 (limit plugin double-free; also the evidence thatprivdata->limitsis name-keyed) — internal, not a URL..wolf/buglog.jsonbug-1762 (build-wiring precedent for white-box REPL test files) — internal, not a URL..wolf/buglog.jsonbug-1599 (bare-printf regression scanner false-positive) — internal, not a URL..kiro/steering/resource-accounting.md— reservation-vs-actual-usage policy, scoped toXCE_RESF_DYNAMIC; confirmed not applicable to this design.