myxce/.kiro/specs/scheduler-sim-diagnostics/design.md
Rongsong Shen eb23c158ae
feat(scheduler-sim-diagnostics): promote byte-unit formatting to shared lib (task 7)
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
2026-09-20 04:40:44 +00:00

51 KiB
Raw Blame History

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 info subcommands (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 equivalents repl_fmt.c already had — this spec owns the promotion and the new shared symbols, not utils.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 of repl_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 of fmt_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> (flagged LIMIT) once instantiated — this is existing scheduler-core behavior, 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 outside repl_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 own format_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 by repl_info_resource()/repl_info_resource_one() today. core->workers[].resvals/core->workers[].resInUse — the capacity side (resvals) is already read by repl_info_worker_one(); the in-use side (resInUse) is new to the REPL layer but is the exact pattern sched_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 REPL info layer; this design is the first info subcommand to cross into plugin-private state, which is why the Limit Plugin Accessor exists as its own boundary rather than being folded into repl_info.c directly.
  • The limit plugin's fixed registration name "xce_sched_limit" — owned entirely inside limit.c; no other file may hardcode it.
  • (Requirement 6) xce_vres_t.limit (existing back-pointer to the owning xce_limit_t, already read by repl_print_limit_usage()'s own vres->limit != limit filter) — the fix for the resource-name argument bug reads vres->limit->val.resname from this existing pointer; no new field is added.
  • (Requirement 6) src/include/utils.h/src/lib/utils.c — already linked into every component via libxce (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 to repl_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 keep repl_info.c's new translate_units_mode() helper's mapping to xce_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() for all, an inner per-limit printer for <name>), identical shape to repl_info_resource()/repl_info_resource_one().
  • Domain/feature boundaries: REPL formatting stays in repl_info.c; plugin-private data access stays behind the two new limit.c accessors; nothing in the REPL layer touches limit_plugin_data_t directly.
  • 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 two extern declarations: limit_get_configured_limits(), limit_get_usage_instances(), placed next to the existing register_limit() declaration.
  • src/scheduler/src/plugins/limit.c — define the two accessor functions. Each does key_find(core->plugins, "xce_sched_limit", &plugin), returns NULL if not found or plugin->data == NULL, otherwise returns the relevant limit_plugin_data_t field (limits or instances).
  • src/scheduler/src/repl_info.c — add repl_info_limit() (the all case: iterate limit_get_configured_limits() via iter_make()/iter_next(), print each) and an inner per-limit printer (the <name> case: key_find() directly on limit_get_configured_limits()'s handle — the plugin's kvmgr is keyed by limit name, confirmed via bug-1685's duplicate-name -EEXIST handling, 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 returns NULL or an empty handle on the all path); add one new else if (strcmp(tokens[1], "limit") == 0) branch to repl_cmd_info(), and extend its usage-string literal to include limit. (Requirement 6) Also: add a small static xce_units_mode_t translate_units_mode(xce_repl_units_mode_t) helper; switch the two existing fmt_limit() call sites (inside the per-limit printer, one for the all path and one for the named-lookup path) to fmt_limit_units(buf, len, limit, translate_units_mode(repl_current_units_mode(sim))); fix repl_print_limit_usage()/repl_print_limit_usage_per_host() to pass limit->val.resname/vres->limit->val.resname (not vres->vres->name) to repl_format_consumable_value_and_used(); retype repl_format_numeric_resource_value()'s byte-valued branch to delegate to the new xce_format_units_value() via translate_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 new xce_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) add xce_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 existing fmt_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 from repl_fmt.c's current repl_format_bytes_human()/repl_is_bytes_valued_resource(), plus a new xce_format_units_value() promoted from repl_info.c's current repl_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 in repl_info.c, since fmt_limit_units() never needs it).
  • src/include/xcelimit.h — (Requirement 6) add #include "utils.h" (for xce_units_mode_t) and extern int fmt_limit_units(char*, int, xce_limit_t*, xce_units_mode_t);, placed next to the existing fmt_limit() declaration.
  • src/lib/xcelimit.c — (Requirement 6) implement fmt_limit_units() (identical consumer-dump loop to today's fmt_limit(); its final value line branches on limit->val.percent first — percent case keeps today's exact "%f%%" output regardless of mode — then, for the non-percent case, on xce_is_bytes_valued_resource(limit->val.resname) plus the same "swp" MB-special-case check the scheduler's own formatter already applies, calling xce_format_units_value() only in that branch); retype fmt_limit() itself to return fmt_limit_units(buf, len, limit, XCE_UNITS_RAW); — provably byte-identical to today's output for every input (RAW mode's %f composition matches today's bare %f exactly in both the percent and non-percent cases), so this is a behavior-preserving refactor of fmt_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 of test_repl_resource_table.c, matching its dispatch-capture harness — kept separate rather than added to test_repl_resource_table.c itself, since it needs a different white-box #include set, see Implementation Notes below) — REPL-level tests for info 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, since repl_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 existing fmt_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 for xce_format_bytes_human(), xce_is_bytes_valued_resource(), xce_format_units_value() (all three modes), mirroring test_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 treat NULL as 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 a NULL result is the caller's own decision (the REPL Limit Info Command prints a different message depending on whether the operator asked for all or 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 other core->plugins consumer, 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: core non-NULL and xce_sched_core_init()-initialized (same precondition every other core->-prefixed accessor in this codebase already assumes).
  • Postconditions: return value is either NULL or a handle safe to pass to iter_make(); no side effects.
  • Invariants: never mutates core, core->plugins, or the limit plugin's private state.

Implementation Notes

  • Integration: declared in plugin.h next to register_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 from repl_cmd_info() exactly like repl_info_resource(sim, tokens[2]).
  • arg == "all": iterate limit_get_configured_limits(); if NULL/empty, print the Requirement 1.7 "no limits configured" message and return; otherwise print each limit via the per-limit path below.
  • arg != "all": if limit_get_configured_limits() returns NULL (plugin not registered — a NULL handle is never passed to key_find()), or key_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 the all path specifically — its own acceptance criterion scopes it to "when an operator requests information for all limits" — so a NULL-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 if is_host_based_limit(core->resmgr, limit) > 0 AND 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 literally NULL for a production-built limit — build_consumer() (xcelimit.c) unconditionally allocates a consumer for all four dimensions, defaulting to the built-in "all" group with per_consumer = 0 when unconfigured. The real discriminator is consumers[CONSUMER_HOST]->per_consumer (the same field is_host_based_limit() itself falls back to, and the same one fmt_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 by vres->limit == matched_limit (pointer equality). build_vres_name() (existing, xcelimit.c) never encodes host in the vresname — so there is exactly one xce_vres_t per matched (user, queue, project) combination regardless of host-scoping; host breakout is NOT "one xce_vres_t per host," it is a per-worker fan-out over that single xce_vres_t's shared resource index. Concretely, for each matching xce_vres_t:
    • If vres->vres is XCE_RESF_GLOBAL/XCE_RESF_SHARED: exactly one displayed instance, reading capacity from core.sharedInst[vres->vres->idx] and accounted usage from core.sharedInUse[vres->vres->idx] — the exact read repl_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 — iterate core.workers, and for each worker w, include it only if zarray_get(&(w->resvals), vres->vres->idx) is non-NULL (a worker put_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 from w->resvals[vres->vres->idx] and accounted usage from w->resInUse[vres->vres->idx] — the sibling capacity/in-use zarray pair sched_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 the xce_vres_t being created (instantiate_limit() and update_limit_res_value() run back-to-back in the same call, per research.md), so an existing xce_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 a NULL read there means genuine zero current usage (display accounted=0), never "no instance" (Requirement 2.2's message is reserved for the case where the xce_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_t count 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.
  • Every instance line is labeled with its scope values (Requirement 3.4), derived from the same limit=<name>[/u=<user>][/q=<queue>][/p=<project>] naming build_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: sim non-NULL, sim->core initialized (same as every other repl_info_* function).
  • Postconditions: writes to stdout only; no field of sim, sim->core, or any plugin's private state is modified.
  • Invariants: output for a given arg is a pure function of current simulation state — calling it twice in a row with no intervening step/continue produces identical output (Requirement 5.2).

Implementation Notes

  • Integration: one new else if (strcmp(tokens[1], "limit") == 0) { ... repl_info_limit(sim, tokens[2]); ... } branch in repl_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 prior info subcommand addition has made. All output must use fprintf(stdout, ...), never bare printf() — src/scheduler/testsuite/test_no_bare_printf.c regression-tests every file under src/scheduler/src/ for this (bug-1599 is a known false-positive in that scanner's comments, not a license to use bare printf() in real code).
  • Validation: dispatch-capture tests per Requirement Traceability above, following test_repl_resource_table.c's existing run_dispatch_for_capture() harness; at minimum one fixture with a percent-based limit on a per-host resource configured without per_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.c white-box #includes repl_info.c but deliberately does NOT include plugins/limit.c or resource.c today (confirmed by reading its include list; its own comment notes "resource.c, not linked here"). Adding calls from repl_info_limit() to the new limit_get_configured_limits()/limit_get_usage_instances() accessors without including plugins/limit.c (and, transitively, either resource.c or link-satisfying stubs for get_resvals()/put_resvals(), the same pattern test_limit_plugin.c already 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 reason test_repl_limit_table.c is a new file rather than an addition to test_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-box resource.c for real (not stub get_resvals/put_resvals as no-ops the way test_limit_plugin.c does 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's repl_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 (via xce_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_t is its own, independently-defined enum; crossing from the scheduler's xce_repl_units_mode_t happens at the scheduler-side call site (repl_info.c's translate_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/bufsz describe a valid writable buffer; name may be NULL (returns false).
  • Postconditions: xce_format_units_value(XCE_UNITS_RAW, v, ...) always produces the bare "%f" rendering of v — the exact contract fmt_limit_units(..., XCE_UNITS_RAW) relies on to keep fmt_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's fmt_limit() — only the final value line's formatting branches on units_mode, and only when limit->val.percent == 0 and the resource is byte-valued.

Implementation Notes

  • Integration: repl_fmt.c's repl_format_bytes_human()/repl_is_bytes_valued_resource() and repl_info.c's repl_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.c gets direct unit tests for all three functions and all three modes; test_limit.c's five existing fmt_limit() assertions serve as the regression proof that fmt_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 four xce_consumer_t (one per scope dimension), each referencing an xce_group_t (inclusion members + exclusion ex_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 owning xce_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 one xce_vres_t per (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 bare xce_vres_t when 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 — mirrors repl_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 all path, 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 NULL when the limit plugin has not been registered (test_limit_plugin.c).
  • repl_info_limit()'s key_find() lookup reports "no such limit" for an unconfigured name without touching stdout beyond that one line (test_repl_limit_table.c or equivalent).

Integration Tests

  • info limit all against 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 without per_host set reports the effective host-scope note and breaks out usage per host (Requirements 1.6, 3.2) — the central scope-ambiguity finding from research.md.
  • info limit <name> against a limit scoped with an exclusion group ("all except X") shows both inclusion and exclusion membership via fmt_group() (Requirement 1.5).
  • info limit <name> issued twice in a row with no intervening step produces byte-identical output, and a subsequent step/continue sequence produces identical scheduling decisions whether or not info limit was 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_slots and info limit all against a realistic multi-limit fixture (mirrors limits.yaml's shape referenced in research.md), asserting every existing info subcommand'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 for NULL, 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-run test_limit.c's five existing assertions unmodified — they must still pass byte-for-byte, proving the fmt_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.c or a new sibling — task-level decision).
  • info limit <name> end-to-end: a "mem"-resourced limit's configured-value line and its accounted=/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_pct from 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 that test_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 of resource.c, core_events.c, driver.c, sim_loop.c, or core_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 like mem): not applicable, checked by flag rather than by analogy — that document scopes itself explicitly to resources flagged both XCE_RESF_DYNAMIC and XCE_RESF_CONSUMABLE. A limit's synthetic capacity resource (vres->vres, add_vres() in xcelimit.c) is flagged XCE_RESF_LIMIT | XCE_RESF_CONSUMABLE | XCE_RESF_RELEASE plus XCE_RESF_GLOBAL|XCE_RESF_SHARED or XCE_RESF_DEFAULT_HOST — never XCE_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 way mem is — 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, since libxce is linked by controller, worker, scheduler, and (via CGO) the apiserver (src/lib/CLAUDE.md). Confirmed via full-tree grep that fmt_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.h gain new symbols only (no existing symbol's signature or behavior changes), so no other libxce consumer is affected. This is display-only, per this design's own Non-Goals — no wire format, matching arithmetic, or persisted value is touched, so persistence.md's and scheduler-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).