myxce/.kiro/specs/scheduler-sim-diagnostics/design.md
Rongsong Shen 80e096b85e
docs(scheduler-sim-diagnostics): finalize requirements, design, and tasks for limit inspection
Requirement set 1 (info limit <name|all>) of the open-ended scheduler-sim-diagnostics
spec: requirements refined for effective vs. configured scope and exclusion-group
membership, design grounded in the actual limit-plugin/REPL source (accessor boundary,
GLOBAL/SHARED-vs-per-host read split, existence-vs-zero-usage distinction), and an
8-subtask implementation plan with full requirements traceability.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
2026-09-17 08:22:11 +00:00

32 KiB
Raw Blame History

Design Document: scheduler-sim-diagnostics (Requirement Set 1 — Limit Inspection)

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).

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.

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).

Out of Boundary

  • fmt_limit(), fmt_consumer(), fmt_group() (src/lib/xcelimit.c, src/lib/group.c) — reused as-is, not modified.
  • 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.

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.

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".

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 --> FmtLimit[fmt_limit existing]
    InfoLimit --> HostCheck[is_host_based_limit existing]
    FmtLimit --> FmtConsumer[fmt_consumer existing]
    FmtConsumer --> FmtGroup[fmt_group existing]
    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

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.
  • 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.

No new files. No existing formatter, matcher, or allocation-path file changes.

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/fmt_group
    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->>Fmt: fmt_limit matched limit
    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-->>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 —

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 Limit Plugin Accessor (P0), fmt_limit/fmt_group (P0), is_host_based_limit (P0), core.sharedInst/core.workers (P1)

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 consumers[CONSUMER_HOST] == NULL, print the Requirement 1.6 effective-host-scope note.
  • 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.

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).

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.