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
51 KiB
Design Document: scheduler-sim-diagnostics (Requirement Sets 1 & 6 — Limit Inspection, Human-Readable Units)
Overview
This design covers the first capability of the open-ended scheduler-sim-diagnostics spec: a new info limit <name|all> command in the scheduler simulation REPL that displays a resource limit's configured setting and its live, per-instance accounted usage.
Purpose: Lets an operator answer "what limit is in effect, and how close is it to being hit" from inside a simulation replay, without reading cluster configuration files or adding ad hoc instrumentation.
Users: Operators and developers debugging scheduler behavior via xce_scheduler -i/-s <replay>.
Impact: Extends the existing REPL info command family (scheduler-core, implementation-complete) with one new subcommand. No existing subcommand's behavior changes. No scheduling, allocation, or limit-matching logic changes.
Goals
- Display a limit's configured setting: constrained resource, value/percent, and effective scope dimensions (Requirements 1.1–1.7).
- Display live accounted usage per distinct scope instance (Requirements 2.1–2.3, 3.1–3.4).
- Fit the existing
info <name|all>command shape exactly (Requirements 4.1–4.2). - Guarantee zero effect on scheduling decisions, accounting state, or other
infosubcommands (Requirements 5.1–5.3). - Display a byte-valued limit's configured amount and live usage amounts in human-readable units, consistent with the REPL's existing units-mode convention for every other byte-valued resource display (Requirements 6.1–6.5, added 2026-09-19).
Non-Goals
- Decision-reasoning trace (seed capability 2 of this same open-ended spec) — separate, future requirement set.
- Any live, controller-connected (non-simulation) inspection.
- Any REPL command that mutates limit configuration or usage accounting.
- Any change to
match_limit()/is_host_based_limit()/allocation logic — this design reads existing state only. - Introducing a new units-mode toggle or a second, limit-specific display-mode concept — Requirement 6 reuses the REPL's one existing units-mode selection unchanged (Requirement 6 AC2).
- Any change to a limit's stored/wire value, its matching arithmetic, or its accounting charge — this remains display-only, mirroring
repl_format_bytes_human()'s own existing "display-only, never feeds back" discipline.
Boundary Commitments
This Spec Owns
- The
info limit <name|all>REPL command and its output format (repl_info_limit(),repl_info.c). - Two new read-only accessor functions exposing the limit plugin's private configured-limits and live-usage-instances tables to REPL code (
limit_get_configured_limits(),limit_get_usage_instances(),plugins/limit.c). - (Requirement 6, added 2026-09-19) A new shared-library units-formatting primitive (
xce_units_mode_t,xce_format_bytes_human(),xce_is_bytes_valued_resource(),xce_format_units_value(),src/lib/utils.c/src/include/utils.h), promoted from the scheduler-only equivalentsrepl_fmt.calready had — this spec owns the promotion and the new shared symbols, notutils.c's pre-existing content. - (Requirement 6) A new sibling entry point,
fmt_limit_units()(src/lib/xcelimit.c/src/include/xcelimit.h), and the correction ofrepl_print_limit_usage()/repl_print_limit_usage_per_host()'s resource-name argument bug (repl_info.c).
Out of Boundary
fmt_consumer(),fmt_group()(src/lib/xcelimit.c,src/lib/group.c) — reused as-is, not modified.fmt_limit()'s existing 3-argument signature and output contract — explicitly preserved, not broken, by Requirement 6 (see the Reversed Decision note below); still out of boundary in the sense that no existing caller offmt_limit()needs to change.is_host_based_limit(),match_limit(), and the rest of the limit-matching/allocation path — read from, never modified.repl_info_resource()/repl_info_resource_one()/repl_info_worker_one()— unmodified; this design follows their established GLOBAL/SHARED-vs-per-host read pattern but does not touch their code.- The pre-existing fact that a GLOBAL-scoped limit's synthetic capacity resource can already appear in
info resource all/info resource <name>(flaggedLIMIT) once instantiated — this is existingscheduler-corebehavior, not introduced or altered here (Requirement 5.3 is satisfied by not touching that code path at all). - Decision-reasoning trace (seed capability 2) — future requirement set of this same spec.
- Any change to
xce_repl_units_mode_t(repl_internal.h) or its three existing call sites outsiderepl_info.c's own two new/modified functions — Requirement 6 is satisfied with a small local translation at the seam, not by touching the REPL's existing units-mode plumbing. src/controller/testsuite/test.c's ownformat_bytes_human()— a separate, test-only, pre-existing duplicate of the same algorithm; not consolidated by this spec (no production consumer of it exists to justify touching controller test code here).
Reversed decision note: research.md's original "Decision: Reuse fmt_limit()/fmt_group()" and "Decision: Effective-scope annotation is REPL-layer, not library-layer" (Requirement Set 1) both concluded fmt_limit() should stay untouched, reasoning that it is "a general-purpose library formatter used elsewhere" whose contract a single REPL caller should not reshape. Requirement 6 revisits this: confirmed by re-checking every caller (repl_info.c's two call sites, src/lib/testsuite/test_limit.c's five call sites — no other production or test consumer exists anywhere in the tree), fmt_limit() has exactly one production caller family (this spec's own REPL command) and one direct-unit-test file. Given that, and given the operator-facing need to select a display mode Requirement 6 AC2 requires, this design adds a new sibling entry point (fmt_limit_units()) that takes the additional units-mode parameter fmt_limit()'s own callers never needed, and makes fmt_limit() itself a one-line wrapper around it (fmt_limit_units(buf, len, limit, XCE_UNITS_RAW)) — provably byte-identical output to today's fmt_limit() for every input shape (see Requirement Traceability / Testing Strategy below), so none of test_limit.c's five existing assertions change. This satisfies the reformat-at-the-source approach the user selected during discovery without breaking fmt_limit()'s existing contract for its one other consumer (the library's own unit tests).
Allowed Dependencies
core->resmgr,core->sharedInst,core->sharedInUse— already read byrepl_info_resource()/repl_info_resource_one()today.core->workers[].resvals/core->workers[].resInUse— the capacity side (resvals) is already read byrepl_info_worker_one(); the in-use side (resInUse) is new to the REPL layer but is the exact patternsched_check_host_satisfy_()(resource.c) already uses in production.core->plugins(name-keyed plugin registry) — read elsewhere in the scheduler (plugin.c's hook fan-outs) but genuinely new to the REPLinfolayer; this design is the firstinfosubcommand to cross into plugin-private state, which is why the Limit Plugin Accessor exists as its own boundary rather than being folded intorepl_info.cdirectly.- The limit plugin's fixed registration name
"xce_sched_limit"— owned entirely insidelimit.c; no other file may hardcode it. - (Requirement 6)
xce_vres_t.limit(existing back-pointer to the owningxce_limit_t, already read byrepl_print_limit_usage()'s ownvres->limit != limitfilter) — the fix for the resource-name argument bug readsvres->limit->val.resnamefrom this existing pointer; no new field is added. - (Requirement 6)
src/include/utils.h/src/lib/utils.c— already linked into every component vialibxce(fmt_jobid()lives here, the direct precedent for a small, generic, cross-component display-formatting helper with no domain coupling).
Revalidation Triggers
- A change to
xce_consumer_type_t(adding/removing a scope dimension) — Requirement 1.1/3.1 enumerate the four dimensions explicitly. - A change to
is_host_based_limit()'s percent/global-resource rule — Requirement 1.6/3.2 depend on its exact current semantics. - A change to how
add_vres()flags synthetic capacity resources, or torepl_info_resource_one()'s GLOBAL/SHARED-vs-host read split — this design's live-usage read path mirrors that split and must be re-checked if it changes. - Renaming the limit plugin's registration string away from
"xce_sched_limit". - (Requirement 6) A change to
xce_repl_units_mode_t's three values (repl_internal.h) must keeprepl_info.c's newtranslate_units_mode()helper's mapping toxce_units_mode_t(utils.h) in sync — the two enums are intentionally independent types, not a shared definition, so nothing enforces this mapping mechanically. - A future second production caller of
fmt_limit()/fmt_limit_units()(today there are none besides this spec's own REPL command and the library's own unit tests) should re-confirm the RAW-mode-is-byte-identical claim this design relies on still holds.
Architecture
Existing Architecture Analysis
scheduler-core already establishes the pattern this design extends: repl_dispatch() (repl.c) routes to repl_cmd_info() (repl_info.c), which routes on a second token to one repl_info_<subcommand>() per family member. Of the raw handles this pattern already reads, only core->jobs/core->queues/core->workers/core->resmgr have existing REPL-layer precedent (repl_info_job(s)/repl_info_queue/repl_info_worker/repl_info_resource); core->plugins is already a raw, name-keyed handle used elsewhere in the scheduler (plugin.c's hook fan-outs) but has never been read by repl_info.c, and plugin-private state (behind plugin->data) has no REPL precedent at all — crossing that specific boundary is the one genuinely new seam this design adds.
Architecture Pattern & Boundary Map
graph TB
Dispatch[repl_dispatch] --> CmdInfo[repl_cmd_info]
CmdInfo --> InfoLimit[repl_info_limit new]
InfoLimit --> Accessor1[limit_get_configured_limits new]
InfoLimit --> Accessor2[limit_get_usage_instances new]
InfoLimit --> FmtLimitUnits[fmt_limit_units new]
FmtLimit[fmt_limit existing] --> FmtLimitUnits
InfoLimit --> HostCheck[is_host_based_limit existing]
FmtLimitUnits --> FmtConsumer[fmt_consumer existing]
FmtConsumer --> FmtGroup[fmt_group existing]
FmtLimitUnits --> SharedFmt[xce_format_units_value new]
InfoLimit --> SharedFmt
SharedFmt --> BytesHuman[xce_format_bytes_human new]
InfoLimit --> IsBytes[xce_is_bytes_valued_resource new]
Accessor1 --> PluginRegistry[core.plugins existing]
Accessor2 --> PluginRegistry
InfoLimit --> SharedState[core.sharedInst core.sharedInUse existing]
InfoLimit --> WorkerState[core.workers resvals and resInUse existing]
Architecture Integration:
- Selected pattern: two-level command handler (
repl_info_limit()forall, an inner per-limit printer for<name>), identical shape torepl_info_resource()/repl_info_resource_one(). - Domain/feature boundaries: REPL formatting stays in
repl_info.c; plugin-private data access stays behind the two newlimit.caccessors; nothing in the REPL layer toucheslimit_plugin_data_tdirectly. - Existing patterns preserved: name-keyed plugin lookup (
core->plugins), raw-handle-plus-shared-iterator access (iter_make/iter_next/iter_destroy), GLOBAL/SHARED-vs-per-host value read split (bug-1593 precedent). - New components rationale: the two accessors are the only way to cross the plugin-private-state boundary that has blocked this capability since it was first scoped (brief.md).
- Steering compliance: read-only (
ha-async-io.md's "never block"/never-mutate concerns are inapplicable — this is in-process, synchronous, simulation-only REPL code, not on any async I/O or Raft path).
Technology Stack
| Layer | Choice / Version | Role in Feature | Notes |
|---|---|---|---|
| Scheduler REPL (C) | Existing src/scheduler toolchain (Meson/GCC, ASAN debug profile per build-deployment.md) |
New info limit command |
No new dependency |
| Scheduler plugin (C) | Existing src/scheduler/src/plugins/limit.c |
New read-only accessors | No new dependency |
| Shared lib (C) | Existing src/lib/xcelimit.c, src/lib/group.c |
Reused formatters (fmt_limit, fmt_consumer, fmt_group) |
No new dependency; already built into libxce |
| Shared lib (C) | Existing src/lib/utils.c/src/include/utils.h |
(Requirement 6) New units-formatting primitives, promoted from scheduler-only equivalents | No new dependency; already built into libxce, already linked by controller/worker/scheduler/apiserver(CGO) |
No new external dependency is introduced.
File Structure Plan
Modified Files
src/scheduler/include/plugin.h— add twoexterndeclarations:limit_get_configured_limits(),limit_get_usage_instances(), placed next to the existingregister_limit()declaration.src/scheduler/src/plugins/limit.c— define the two accessor functions. Each doeskey_find(core->plugins, "xce_sched_limit", &plugin), returnsNULLif not found orplugin->data == NULL, otherwise returns the relevantlimit_plugin_data_tfield (limitsorinstances).src/scheduler/src/repl_info.c— addrepl_info_limit()(theallcase: iteratelimit_get_configured_limits()viaiter_make()/iter_next(), print each) and an inner per-limit printer (the<name>case:key_find()directly onlimit_get_configured_limits()'s handle — the plugin's kvmgr is keyed by limit name, confirmed via bug-1685's duplicate-name-EEXISThandling, so this is a direct lookup, not a scan; report "no such limit" per Requirement 1.3 if not found, report "no limits configured" per Requirement 1.7 if the accessor returnsNULLor an empty handle on theallpath); add one newelse if (strcmp(tokens[1], "limit") == 0)branch torepl_cmd_info(), and extend its usage-string literal to includelimit. (Requirement 6) Also: add a smallstatic xce_units_mode_t translate_units_mode(xce_repl_units_mode_t)helper; switch the two existingfmt_limit()call sites (inside the per-limit printer, one for theallpath and one for the named-lookup path) tofmt_limit_units(buf, len, limit, translate_units_mode(repl_current_units_mode(sim))); fixrepl_print_limit_usage()/repl_print_limit_usage_per_host()to passlimit->val.resname/vres->limit->val.resname(notvres->vres->name) torepl_format_consumable_value_and_used(); retyperepl_format_numeric_resource_value()'s byte-valued branch to delegate to the newxce_format_units_value()viatranslate_units_mode().src/scheduler/src/repl_fmt.c— (Requirement 6)repl_format_bytes_human()/repl_is_bytes_valued_resource()become one-line wrappers delegating to the newxce_format_bytes_human()/xce_is_bytes_valued_resource()(utils.h), removing the duplicated algorithm while leaving every existing scheduler call site and its function name completely unchanged.src/include/utils.h— (Requirement 6, new declarations in an existing file) addxce_units_mode_t(XCE_UNITS_RAW/XCE_UNITS_HUMAN/XCE_UNITS_BOTH),extern void xce_format_bytes_human(double, char*, size_t),extern bool xce_is_bytes_valued_resource(const char*),extern void xce_format_units_value(xce_units_mode_t, double, char*, size_t)— placed near the existingfmt_jobid()/fmt_jobid_ex()declarations, the closest existing precedent for a small generic display-formatting helper.src/lib/utils.c— (Requirement 6) implement the three functions above, bodies promoted verbatim fromrepl_fmt.c's currentrepl_format_bytes_human()/repl_is_bytes_valued_resource(), plus a newxce_format_units_value()promoted fromrepl_info.c's currentrepl_format_numeric_resource_value()'s byte-valued branch (the RAW/HUMAN/BOTH%f/%s/%f (%s)composition only — the non-byte-valued fallback formatting stays inrepl_info.c, sincefmt_limit_units()never needs it).src/include/xcelimit.h— (Requirement 6) add#include "utils.h"(forxce_units_mode_t) andextern int fmt_limit_units(char*, int, xce_limit_t*, xce_units_mode_t);, placed next to the existingfmt_limit()declaration.src/lib/xcelimit.c— (Requirement 6) implementfmt_limit_units()(identical consumer-dump loop to today'sfmt_limit(); its final value line branches onlimit->val.percentfirst — percent case keeps today's exact"%f%%"output regardless of mode — then, for the non-percent case, onxce_is_bytes_valued_resource(limit->val.resname)plus the same"swp"MB-special-case check the scheduler's own formatter already applies, callingxce_format_units_value()only in that branch); retypefmt_limit()itself toreturn fmt_limit_units(buf, len, limit, XCE_UNITS_RAW);— provably byte-identical to today's output for every input (RAW mode's%fcomposition matches today's bare%fexactly in both the percent and non-percent cases), so this is a behavior-preserving refactor offmt_limit(), not a breaking change.src/scheduler/testsuite/test_limit_plugin.c— add unit tests for the two new accessors (registered-plugin case, not-yet-initialized case).src/scheduler/testsuite/test_repl_limit_table.c(new sibling oftest_repl_resource_table.c, matching its dispatch-capture harness — kept separate rather than added totest_repl_resource_table.citself, since it needs a different white-box#includeset, see Implementation Notes below) — REPL-level tests forinfo limit <name>/info limit all. (Requirement 6) Its existing "mem"-resourced fixtures'accounted=/capacity=assertions (added under Requirement Set 1, e.g."accounted=4.00") were asserting on the resource-name-argument bug's buggy fallback output (plain%.2f, sincerepl_is_bytes_valued_resource()was never actually being asked about"mem"due to the bug) — these must be updated to expect the corrected human-formatted output once the bug is fixed, not left as a false "still passing" signal; see Testing Strategy below for the required RED→GREEN treatment.src/lib/testsuite/test_limit.c— (Requirement 6) no changes expected —fmt_limit()'s output is unchanged by construction (see the Reversed Decision note above); its five existingfmt_limit()assertions are the regression proof for that claim, not a file this design needs to edit.src/lib/testsuite/test_utils.c— (Requirement 6) add unit tests forxce_format_bytes_human(),xce_is_bytes_valued_resource(),xce_format_units_value()(all three modes), mirroringtest_limit.c's existing assertion style.src/scheduler/testsuite/test_repl_resource_formatting.c— (Requirement 6) no changes expected —repl_format_bytes_human()/repl_is_bytes_valued_resource()'s observable behavior is unchanged (delegation only); this file's existing assertions are the regression proof for that claim.
One new pair of shared-library symbols is added to an existing file (utils.c/utils.h); one new sibling function is added to an existing file (xcelimit.c/xcelimit.h). No new files besides the already-planned test_repl_limit_table.c. No change to match_limit(), is_host_based_limit(), instantiate_limit(), or any allocation-path file.
System Flows
sequenceDiagram
participant Op as Operator
participant Dispatch as repl_dispatch
participant Info as repl_cmd_info
participant Limit as repl_info_limit
participant Plugin as limit.c accessors
participant Fmt as fmt_limit_units/fmt_group
participant Units as xce_format_units_value
participant Core as core state
Op->>Dispatch: info limit ug1_slots
Dispatch->>Info: repl_cmd_info
Info->>Limit: repl_info_limit sim ug1_slots
Limit->>Plugin: limit_get_configured_limits core
Plugin->>Core: key_find core.plugins xce_sched_limit
Plugin-->>Limit: limits handle or NULL
Limit->>Limit: key_find name lookup
Limit->>Limit: translate_units_mode repl_current_units_mode
Limit->>Fmt: fmt_limit_units matched limit units_mode
Fmt->>Units: xce_format_units_value when byte valued and not percent
Units-->>Fmt: raw human or both text
Fmt-->>Limit: configured setting text
Limit->>Limit: is_host_based_limit effective scope check
Limit->>Plugin: limit_get_usage_instances core
Plugin-->>Limit: instances handle
Limit->>Limit: filter instances by pointer equality to matched limit
Limit->>Core: read sharedInst/sharedInUse or per-worker resvals/resInUse
Limit->>Units: xce_format_units_value via limit val resname when byte valued
Units-->>Limit: accounted and capacity text
Limit-->>Op: configured setting plus per-instance usage
Key decisions not obvious from the diagram: the "no such limit"/"no limits configured" cases (Requirements 1.3, 1.7) short-circuit before any formatter or core-state read is attempted, so a missing/empty case never risks printing partial data. The all path repeats this exact per-limit sequence once per configured limit rather than a bulk query, matching repl_info_resource()'s existing per-resource iteration shape.
Requirements Traceability
| Requirement | Summary | Components | Interfaces | Flows |
|---|---|---|---|---|
| 1.1 | Display name, resource, value, percent flag, effective scope | REPL Limit Info Command | repl_info_limit(), fmt_limit(), is_host_based_limit() |
Sequence diagram |
| 1.2 | all shows every configured limit |
REPL Limit Info Command | limit_get_configured_limits() |
— |
| 1.3 | Unknown name reports absence, no fabrication | REPL Limit Info Command | repl_info_limit() key_find() lookup |
Sequence diagram (short-circuit) |
| 1.4 | Percent resolves to current absolute amount, possibly per-host-instance | Limit Plugin Accessor + REPL Limit Info Command | limit_get_usage_instances(), shared/per-host read split |
— |
| 1.5 | Inclusion/exclusion membership shown | REPL Limit Info Command (reused formatter) | fmt_group() |
— |
| 1.6 | Effective host-scope shown even when not explicitly configured | REPL Limit Info Command | is_host_based_limit() |
Sequence diagram |
| 1.7 | Empty "all limits" reports explicitly | REPL Limit Info Command | limit_get_configured_limits() NULL/empty check |
Sequence diagram (short-circuit) |
| 2.1 | Per-instance accounted amount + capacity | Limit Plugin Accessor + REPL Limit Info Command | limit_get_usage_instances(), shared/per-host read split |
Sequence diagram |
| 2.2 | Not-yet-evaluated limit reports no instance, not fabricated zero | REPL Limit Info Command | instance-filter (no matching xce_vres_t) |
— |
| 2.3 | Usage reflects state at command issue time | REPL Limit Info Command | direct core.sharedInst/core.workers read, no caching |
— |
| 3.1 | Multi-dimension combinations each own instance | Limit Plugin Accessor + REPL Limit Info Command | xce_vres_t per unique build_vres_name() user/queue/project combination (existing), fanned out per-worker by the REPL when host-scoped |
— |
| 3.2 | Host dimension broken out per host, including implicit case | REPL Limit Info Command | per-worker resvals/resInUse fan-out over one xce_vres_t (host is not part of build_vres_name()) |
Sequence diagram |
| 3.3 | No scope dimension → single simulation-wide instance | REPL Limit Info Command | core.sharedInst read |
— |
| 3.4 | Each instance labeled by scope values | REPL Limit Info Command | build_vres_name()-derived label (existing naming, parsed/echoed) |
— |
| 4.1 | ` | all` invocation shape | REPL dispatch | repl_cmd_info() new branch |
| 4.2 | Accepted anywhere other info subcommands are |
REPL dispatch | repl_cmd_info() new branch |
— |
| 5.1 | No mutation of config/accounting/decisions | REPL Limit Info Command | read-only accessors, no write calls | — |
| 5.2 | Identical replay outcome whether or not queried | REPL Limit Info Command | no state written, no hook installed | — |
| 5.3 | Existing info subcommands unchanged |
REPL dispatch | additive-only repl_cmd_info() branch |
— |
| 6.1 | Byte-valued amounts shown in human-readable scale | Shared Units Formatter, REPL Limit Info Command | xce_format_units_value(), fmt_limit_units() |
Sequence diagram (updated) |
| 6.2 | Respects the operator's existing units-mode selection | REPL Limit Info Command | translate_units_mode(), repl_current_units_mode() (existing) |
— |
| 6.3 | Non-byte-valued and percentage amounts excluded from scaling | Shared Units Formatter, REPL Limit Info Command | xce_is_bytes_valued_resource(), fmt_limit_units()'s percent-first branch |
— |
| 6.4 | Percentage-of-byte-resource resolved amount scaled the same way | REPL Limit Info Command | corrected repl_format_consumable_value_and_used() call (resname bug fix) |
Sequence diagram (updated) |
| 6.5 | Consistent units convention across config display and usage display | Shared Units Formatter | one shared xce_format_units_value()/xce_is_bytes_valued_resource() pair used by both surfaces |
— |
Components and Interfaces
| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies (P0/P1) | Contracts |
|---|---|---|---|---|---|
| Limit Plugin Accessor | Scheduler Plugin (limit.c) |
Expose the limit plugin's private configured-limits/usage-instances tables as raw iterable handles | 1.1, 1.2, 1.4, 2.1, 3.1 | core->plugins (P0) |
Service |
| REPL Limit Info Command | Scheduler REPL (repl_info.c) |
Format and print `info limit <name | all>` output | 1.1–1.7, 2.1–2.3, 3.1–3.4, 4.1–4.2, 5.1–5.3, 6.2, 6.3, 6.4 | Limit Plugin Accessor (P0), fmt_limit_units/fmt_group (P0), is_host_based_limit (P0), core.sharedInst/core.workers (P1), Shared Units Formatter (P0) |
| Shared Units Formatter | Shared Library (utils.c, xcelimit.c) |
Human-readable byte-unit scaling and mode-aware value composition, usable by any libxce consumer |
6.1, 6.3, 6.5 | none (leaf utility) | Service |
Scheduler Plugin
Limit Plugin Accessor
| Field | Detail |
|---|---|
| Intent | Cross the plugin-private-state boundary read-only, without exposing limit_plugin_data_t or the plugin's registration name outside limit.c |
| Requirements | 1.1, 1.2, 1.4, 2.1, 3.1 |
Responsibilities & Constraints
- Owns the
"xce_sched_limit"plugin-name lookup; no other file may hardcode it. - Returns
NULL(never a partially-valid handle) when the plugin is not registered or not yet initialized — callers must treatNULLas the state "nothing is configured," never as an error to surface differently. This is a semantic signal only: which user-facing message a caller prints for aNULLresult is the caller's own decision (the REPL Limit Info Command prints a different message depending on whether the operator asked forallor a specific name — see its Responsibilities below); this accessor does not prescribe wording. - Performs no locking beyond what
key_find(core->plugins, ...)already does internally (matches every othercore->pluginsconsumer, e.g.plugin_job_update()). - Never allocates, mutates, or triggers plugin hooks — a pure read of already-existing pointers.
Dependencies
- Inbound: REPL Limit Info Command — the only caller (P0)
- Outbound:
core->plugins(existing name-keyed registry) — P0
Contracts: Service [x] / API [ ] / Event [ ] / Batch [ ] / State [ ]
Service Interface
/* src/scheduler/include/plugin.h */
/* Returns the plugin's configured-limits handle (iterable via
* iter_make()/iter_next(), elements are xce_limit_t*), or NULL if the
* limit plugin is not registered or not yet initialized. */
extern var limit_get_configured_limits(xce_sched_core_t* core);
/* Returns the plugin's live usage-instances handle (iterable via
* iter_make()/iter_next(), elements are xce_vres_t*), or NULL under the
* same conditions as above. */
extern var limit_get_usage_instances(xce_sched_core_t* core);
- Preconditions:
corenon-NULL andxce_sched_core_init()-initialized (same precondition every othercore->-prefixed accessor in this codebase already assumes). - Postconditions: return value is either
NULLor a handle safe to pass toiter_make(); no side effects. - Invariants: never mutates
core,core->plugins, or the limit plugin's private state.
Implementation Notes
- Integration: declared in
plugin.hnext toregister_limit(), matching where every other plugin-registration-adjacent declaration already lives. - Validation: unit tests cover (a) plugin registered with configured limits, (b) plugin registered with zero limits (empty handle, not NULL, per Requirement 1.7's "report explicitly" needing to distinguish "no plugin" from "plugin present, zero limits" only if that distinction is observable — both collapse to the same REPL message either way), (c) plugin not registered (
NULL). - Risks: none — pure read accessor, no new failure mode beyond "plugin absent," which is already a normal simulation configuration (a replay with no limits configured never registers/initializes any limit).
Scheduler REPL
REPL Limit Info Command
| Field | Detail |
|---|---|
| Intent | Implement `info limit <name |
| Requirements | 1.1–1.7, 2.1–2.3, 3.1–3.4, 4.1–4.2, 5.1–5.3 |
Responsibilities & Constraints
- Single entry point
repl_info_limit(xce_sched_sim_driver_t* sim, const char* arg), called fromrepl_cmd_info()exactly likerepl_info_resource(sim, tokens[2]). arg == "all": iteratelimit_get_configured_limits(); ifNULL/empty, print the Requirement 1.7 "no limits configured" message and return; otherwise print each limit via the per-limit path below.arg != "all": iflimit_get_configured_limits()returnsNULL(plugin not registered — aNULLhandle is never passed tokey_find()), orkey_find()on the returned handle finds no match, print the Requirement 1.3 "no such limit<name>" message and return (no partial output). Requirement 1.7's "no limits configured" message is reserved for theallpath specifically — its own acceptance criterion scopes it to "when an operator requests information for all limits" — so aNULL-handle result on the named-lookup path reports absence of that name, not absence of configuration in general.- Per-limit output:
fmt_limit()into a stack buffer, printed line-by-line with the existing[sim]prefix convention; then, only ifis_host_based_limit(core->resmgr, limit) > 0AND the limit's own per-host consumer was not explicitly configured, print the Requirement 1.6 effective-host-scope note. Implementation note (task 2.3, corrected from an earlier inaccurate gloss here):limit->consumers[CONSUMER_HOST]is never literallyNULLfor a production-built limit —build_consumer()(xcelimit.c) unconditionally allocates a consumer for all four dimensions, defaulting to the built-in "all" group withper_consumer = 0when unconfigured. The real discriminator isconsumers[CONSUMER_HOST]->per_consumer(the same fieldis_host_based_limit()itself falls back to, and the same onefmt_consumer()uses to choose"HOST ="vs"PER_HOST =") — check(host_consumer == NULL) || (host_consumer->per_consumer == 0), not pointer-nullity alone. - Live usage: iterate
limit_get_usage_instances(), filter byvres->limit == matched_limit(pointer equality).build_vres_name()(existing,xcelimit.c) never encodes host in the vresname — so there is exactly onexce_vres_tper matched (user, queue, project) combination regardless of host-scoping; host breakout is NOT "onexce_vres_tper host," it is a per-worker fan-out over that singlexce_vres_t's shared resource index. Concretely, for each matchingxce_vres_t:- If
vres->vresisXCE_RESF_GLOBAL/XCE_RESF_SHARED: exactly one displayed instance, reading capacity fromcore.sharedInst[vres->vres->idx]and accounted usage fromcore.sharedInUse[vres->vres->idx]— the exact readrepl_info_resource_one()already implements for a GLOBAL/SHARED resource (bug-1593). - Otherwise (host-scoped, explicit or implicit per Requirement 1.6): one displayed instance per worker that has a populated entry at
vres->vres->idx— iteratecore.workers, and for each workerw, include it only ifzarray_get(&(w->resvals), vres->vres->idx)is non-NULL (a workerput_resvals()has never written to, e.g. an unevaluated host, must not produce a fabricated zero-usage line, matching Requirement 2.2's no-fabrication rule at per-host granularity). For an included worker, capacity comes fromw->resvals[vres->vres->idx]and accounted usage fromw->resInUse[vres->vres->idx]— the sibling capacity/in-use zarray pairsched_check_host_satisfy_()(resource.c) already reads the identical way for ordinary consumable resources. - Existence-of-instance vs. zero-usage are distinct signals, and must not be conflated. A matched
xce_vres_t's capacity entry (sharedInst/resvals) is populated synchronously with thexce_vres_tbeing created (instantiate_limit()andupdate_limit_res_value()run back-to-back in the same call, perresearch.md), so an existingxce_vres_t/populated-worker-entry always has a real capacity to show. Its in-use entry (sharedInUse/resInUse) is populated only once something has actually been charged against it, independently of capacity — so aNULLread there means genuine zero current usage (displayaccounted=0), never "no instance" (Requirement 2.2's message is reserved for the case where thexce_vres_t/worker-entry itself does not exist at all, i.e. the limit or that host has never been evaluated against any job). - This is the mechanism behind Requirement 3.1's "per-host and per-user together" example: the
xce_vres_tcount only reflects the user dimension; the host dimension is produced entirely by this per-worker fan-out, one displayed instance per (vres, worker) pair. - Zero included instances for a matched
xce_vres_t(the GLOBAL/SHARED capacity entry is absent, or no worker has a populated capacity entry) → print the Requirement 2.2 "no active usage instance yet" message for that limit, not a fabricated zero.
- If
- Every instance line is labeled with its scope values (Requirement 3.4), derived from the same
limit=<name>[/u=<user>][/q=<queue>][/p=<project>]namingbuild_vres_name()already produces, plus the host name when the instance came from a per-worker read. - Never calls
instantiate_limit(),match_limit(),update_limit_res_value(), or any other mutating/allocating limit-plugin function — read-only per Requirement 5.1.
Dependencies
- Inbound:
repl_cmd_info()— the only caller (P0) - Outbound: Limit Plugin Accessor (P0),
fmt_limit()/fmt_consumer()/fmt_group()(P0),is_host_based_limit()(P0),core.sharedInst/core.sharedInUse/core.workers[].resvals/core.workers[].resInUse(P1)
Contracts: Service [x] / API [ ] / Event [ ] / Batch [ ] / State [ ]
Service Interface
/* src/scheduler/src/repl_info.c (static, mirrors repl_info_resource shape) */
static void repl_info_limit(xce_sched_sim_driver_t* sim, const char* arg);
- Preconditions:
simnon-NULL,sim->coreinitialized (same as every otherrepl_info_*function). - Postconditions: writes to
stdoutonly; no field ofsim,sim->core, or any plugin's private state is modified. - Invariants: output for a given
argis a pure function of current simulation state — calling it twice in a row with no interveningstep/continueproduces identical output (Requirement 5.2).
Implementation Notes
- Integration: one new
else if (strcmp(tokens[1], "limit") == 0) { ... repl_info_limit(sim, tokens[2]); ... }branch inrepl_cmd_info()(repl_info.c), plus updating that function's own usage-string literal ("info <break|session|config|job|jobs|queue|worker|resource|summary|limit> [arg]") to keep it accurate — the same maintenance step every priorinfosubcommand addition has made. All output must usefprintf(stdout, ...), never bareprintf()—src/scheduler/testsuite/test_no_bare_printf.cregression-tests every file undersrc/scheduler/src/for this (bug-1599 is a known false-positive in that scanner's comments, not a license to use bareprintf()in real code). - Validation: dispatch-capture tests per Requirement Traceability above, following
test_repl_resource_table.c's existingrun_dispatch_for_capture()harness; at minimum one fixture with a percent-based limit on a per-host resource configured withoutper_host(exercises Requirement 1.6/3.2's implicit-scope case) and one fixture with an exclusion-based group (exercises Requirement 1.5). - Risks: Build wiring —
test_repl_resource_table.cwhite-box#includesrepl_info.cbut deliberately does NOT includeplugins/limit.corresource.ctoday (confirmed by reading its include list; its own comment notes "resource.c, not linked here"). Adding calls fromrepl_info_limit()to the newlimit_get_configured_limits()/limit_get_usage_instances()accessors without includingplugins/limit.c(and, transitively, eitherresource.cor link-satisfying stubs forget_resvals()/put_resvals(), the same patterntest_limit_plugin.calready uses) produces undefined-reference link failures — the exact failure class bug-1762 hit and remediated for a different scheduler-sim-repl-verbosity command. This is the reasontest_repl_limit_table.cis a new file rather than an addition totest_repl_resource_table.c: it needs its own white-box include set. Because this test needs to assert real, non-trivial accounted-usage values (not a stubbed-zero), it should white-boxresource.cfor real (not stubget_resvals/put_resvalsas no-ops the waytest_limit_plugin.cdoes for its narrower config-update-only scope) — left as a task-level decision, flagged here so it is made deliberately rather than discovered via a failed build.
Shared Library
Shared Units Formatter
| Field | Detail |
|---|---|
| Intent | Provide human-readable byte-unit scaling and mode-aware value composition as a leaf utility any libxce consumer can call, replacing the scheduler-only duplicate this design promotes from |
| Requirements | 6.1, 6.3, 6.5 |
Responsibilities & Constraints
- Pure, stateless, display-only functions — never touch a stored/wire value, never allocate beyond the caller-supplied buffer.
xce_is_bytes_valued_resource()is a fixed 5-name list (mem,tmp,maxmem,maxtmp,maxswap) — identical to today'srepl_is_bytes_valued_resource(); adding a sixth byte-valued resource name in the future means updating this one list, not two.xce_format_units_value()assumes its caller has already decided the value is byte-valued (viaxce_is_bytes_valued_resource()or the caller's own equivalent check, e.g.fmt_limit_units()'s"swp"special case) — it does not re-derive that decision itself, keeping it a pure three-mode composer.- Must not depend on any scheduler-only type (
xce_sched_core_t,xce_repl_units_mode_t, etc.) —xce_units_mode_tis its own, independently-defined enum; crossing from the scheduler'sxce_repl_units_mode_thappens at the scheduler-side call site (repl_info.c'stranslate_units_mode()), never inside this component.
Dependencies
- Inbound: REPL Limit Info Command (
fmt_limit_units(),repl_format_numeric_resource_value()'s delegation) (P0) - Outbound: none — a leaf utility
Contracts: Service [x]
Service Interface
/* src/include/utils.h */
typedef enum {
XCE_UNITS_RAW = 1,
XCE_UNITS_HUMAN,
XCE_UNITS_BOTH
} xce_units_mode_t;
extern void xce_format_bytes_human(double bytes, char* buf, size_t bufsz);
extern bool xce_is_bytes_valued_resource(const char* name);
extern void xce_format_units_value(xce_units_mode_t mode, double bytes_value,
char* buf, size_t bufsz);
/* src/include/xcelimit.h */
extern int fmt_limit_units(char* buf, int len, xce_limit_t* limit,
xce_units_mode_t units_mode);
- Preconditions:
buf/bufszdescribe a valid writable buffer;namemay beNULL(returnsfalse). - Postconditions:
xce_format_units_value(XCE_UNITS_RAW, v, ...)always produces the bare"%f"rendering ofv— the exact contractfmt_limit_units(..., XCE_UNITS_RAW)relies on to keepfmt_limit()byte-identical to today. - Invariants: none of these functions mutate any input;
fmt_limit_units()'s consumer-dump loop is byte-for-byte identical to today'sfmt_limit()— only the final value line's formatting branches onunits_mode, and only whenlimit->val.percent == 0and the resource is byte-valued.
Implementation Notes
- Integration:
repl_fmt.c'srepl_format_bytes_human()/repl_is_bytes_valued_resource()andrepl_info.c'srepl_format_numeric_resource_value()become thin callers of these new functions (see File Structure Plan) — zero change to any of their own existing call sites' function names or signatures. - Validation:
test_utils.cgets direct unit tests for all three functions and all three modes;test_limit.c's five existingfmt_limit()assertions serve as the regression proof thatfmt_limit()'s output is unchanged. - Risks: none identified beyond the Revalidation Trigger already noted (a future second production caller of
fmt_limit()/fmt_limit_units()should re-confirm the byte-identical claim).
Data Models
Domain Model
No new persisted or transient domain entities are introduced. This design reads three existing entities already defined in src/include/xce/types.h:
graph TB
Limit[xce_limit_t] --> Consumer[xce_consumer_t x4]
Consumer --> Group[xce_group_t]
Limit -.instantiates one per user queue project combo.-> Vres[xce_vres_t]
Vres --> Capacity[xce_resource_t capacity vres field]
Vres --> Underlying[xce_resource_t underlying res field]
Capacity -.read from.-> Shared[core sharedInst sharedInUse when global]
Capacity -.fanned out over.-> Worker[core workers resvals resInUse when host scoped]
xce_limit_t— one per configured limit; owns up to fourxce_consumer_t(one per scope dimension), each referencing anxce_group_t(inclusionmembers+ exclusionex_members).xce_vres_t— one per distinct live (user, queue, project) combination of a limit; created lazily on first job match (instantiate_limit(), unmodified by this design). Links back to its owningxce_limit_t(.limit), its synthetic capacity resource (.vres), and the underlying constrained resource (.res). Host is not part of this cardinality —build_vres_name()never encodes it, so a host-scoped limit still has exactly onexce_vres_tper (user, queue, project); the host dimension is produced entirely at display time by reading.vres's shared resource index across every worker that has a populated entry (see the REPL Limit Info Command's Responsibilities above). A displayed "instance" (Requirement 3's unit of enumeration) is therefore a(xce_vres_t, worker)pair when host-scoped, and the barexce_vres_twhen GLOBAL/SHARED.- No new fields, tables, or relationships. This design is a read-only projection over the above.
Logical Data Model
Not applicable — no schema change, no new storage.
Error Handling
Error Strategy
This is a read-only diagnostic command; there is no user-input validation beyond "does a limit by this name exist" (Requirement 1.3) and "are there any limits at all" (Requirement 1.7) — both are reported as plain REPL messages, matching every other info subcommand's existing style (e.g. repl_info_job()'s "no such job" message), not as process-level errors.
Error Categories and Responses
- Unknown limit name (Requirement 1.3): report "no such limit
<name>", no partial output — mirrorsrepl_info_job()'s exact pattern for an unknown jobid. - No limits configured at all (Requirement 1.7): report "no limits configured", not an empty listing.
- Limit configured but never evaluated (Requirement 2.2): report "no active usage instance yet" for that limit, not a fabricated zero-usage line.
- Limit plugin not registered/initialized: a simulation with no limits configured never registers the plugin's private tables, so this is not a distinct error case from the operator's point of view — it collapses into Requirement 1.7's "no limits configured" message on the
allpath, and into Requirement 1.3's "no such limit<name>" message on the named-lookup path (matching that requirement's own scoping — see the REPL Limit Info Command's Responsibilities above).
Monitoring
Not applicable — this is an interactive/replay-local REPL command with no persistent logging requirement beyond what the REPL already does.
Testing Strategy
Unit Tests
limit_get_configured_limits()/limit_get_usage_instances()return the correct handle when the plugin is registered and initialized with configured limits (test_limit_plugin.c).- Both accessors return
NULLwhen the limit plugin has not been registered (test_limit_plugin.c). repl_info_limit()'skey_find()lookup reports "no such limit" for an unconfigured name without touchingstdoutbeyond that one line (test_repl_limit_table.cor equivalent).
Integration Tests
info limit allagainst a fixture with zero configured limits reports "no limits configured" (Requirement 1.7).info limit <name>against a percent-based limit configured on a per-host resource withoutper_hostset reports the effective host-scope note and breaks out usage per host (Requirements 1.6, 3.2) — the central scope-ambiguity finding fromresearch.md.info limit <name>against a limit scoped with an exclusion group ("all except X") shows both inclusion and exclusion membership viafmt_group()(Requirement 1.5).info limit <name>issued twice in a row with no interveningstepproduces byte-identical output, and a subsequentstep/continuesequence produces identical scheduling decisions whether or notinfo limitwas issued in between (Requirement 5.1, 5.2) — extends the existing normal-vs-sim equivalence test discipline (scheduler-driver-equivalence.md) to this new read path specifically.
E2E/REPL Tests
- Full dispatch-capture round trip for
info limit ug1_slotsandinfo limit allagainst a realistic multi-limit fixture (mirrorslimits.yaml's shape referenced inresearch.md), asserting every existinginfosubcommand's output is byte-identical before and after this change (Requirement 5.3).
Requirement 6 Tests (added 2026-09-19)
xce_format_bytes_human(): adaptive B/KB/MB/GB/TB/PB scaling for representative magnitudes, including the existing algorithm's own boundary cases (exactly 1024, just under it) (test_utils.c).xce_is_bytes_valued_resource(): true for all five listed names, false forNULL, an empty string, and a non-listed name like"slots"(test_utils.c).xce_format_units_value(): all three modes produce the documented%f/%s/%f (%s)shapes for a representative byte value (test_utils.c).fmt_limit()(unchanged contract): re-runtest_limit.c's five existing assertions unmodified — they must still pass byte-for-byte, proving thefmt_limit_units(..., XCE_UNITS_RAW)refactor is behavior-preserving.fmt_limit_units(): a byte-valued, non-percent limit (e.g."mem") renders per mode (raw/human/both); a percent-based limit's%suffix and value are unaffected by mode; a non-byte-valued limit (e.g."slots") is unaffected by mode (test_limit.cor a new sibling — task-level decision).info limit <name>end-to-end: a"mem"-resourced limit's configured-value line and itsaccounted=/capacity=usage line(s) both show human-formatted amounts under the default units mode, and both switch consistently when the operator changes units mode (Requirement 6.1, 6.2, 6.5) (test_repl_limit_table.c).- A percent-based limit on a byte-valued resource (e.g.
night_mem_pctfrom the transcript that motivated this requirement) shows its resolved absolute amount human-formatted (Requirement 6.4) (test_repl_limit_table.c). - A slot-count limit's configured value and usage amounts are unaffected by units mode (Requirement 6.3) (
test_repl_limit_table.c). - Required RED→GREEN treatment for the resource-name bug fix: before changing
repl_print_limit_usage()'s argument, capture thattest_repl_limit_table.c's existing"mem"-resourced fixtures'accounted=X.00-style assertions currently pass only because the classifier never actually received"mem"(the bug); after the fix, update those specific assertions to expect the corrected human-formatted output, and confirm via a temporary revert that the OLD assertion now fails for the right reason (proving the fix, not a coincidental pass) before finalizing the new assertion.
Constraints
- Project Invariant Cross-Check (
scheduler-driver-equivalence.md): this design touches none ofresource.c,core_events.c,driver.c,sim_loop.c, orcore_session.c— the files that invariant's testing discipline gates — and is read-only by Requirement 5.1/5.2, so it cannot introduce normal-mode-vs-sim-mode accounting divergence. Checked, not applicable beyond the read-only guarantee already covered by the Integration Tests above. ha-async-io.md: not applicable — this code runs entirely inside the standalone simulation REPL process, never on the controller's iomgr/delaymgr/vtask threads.resource-accounting.md(reservation-vs-actual-usage reconciliation for real resources likemem): not applicable, checked by flag rather than by analogy — that document scopes itself explicitly to resources flagged bothXCE_RESF_DYNAMICandXCE_RESF_CONSUMABLE. A limit's synthetic capacity resource (vres->vres,add_vres()inxcelimit.c) is flaggedXCE_RESF_LIMIT | XCE_RESF_CONSUMABLE | XCE_RESF_RELEASEplusXCE_RESF_GLOBAL|XCE_RESF_SHAREDorXCE_RESF_DEFAULT_HOST— neverXCE_RESF_DYNAMIC. Its capacity/in-use pair is an exact scheduler-computed accounting number (charged/released by the same dispatch/completion path as any consumable resource), not an estimate reconciled against independently-drifting OS-measured usage the waymemis — so this design's plain read of those two values has no reconciliation-window or staleness concern to account for.- Requirement 6's shared-library change (
fmt_limit()/utils.c): checked for blast radius beyond this spec, sincelibxceis linked by controller, worker, scheduler, and (via CGO) the apiserver (src/lib/CLAUDE.md). Confirmed via full-tree grep thatfmt_limit()has exactly one production caller (repl_info.c, this spec's own command) and one direct-unit-test file (test_limit.c);utils.c/utils.hgain new symbols only (no existing symbol's signature or behavior changes), so no otherlibxceconsumer is affected. This is display-only, per this design's own Non-Goals — no wire format, matching arithmetic, or persisted value is touched, sopersistence.md's andscheduler-driver-equivalence.md's invariants are not implicated (the same reasoning the original Requirement Set 1 design already applied above, extended to the new files).