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
403 lines
51 KiB
Markdown
403 lines
51 KiB
Markdown
# 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
|
||
|
||
```mermaid
|
||
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
|
||
|
||
```mermaid
|
||
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 | `<name>|all` invocation shape | REPL dispatch | `repl_cmd_info()` new branch | Sequence diagram |
|
||
| 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) | Service |
|
||
| 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
|
||
```c
|
||
/* 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|all>`: format configured setting, effective scope, and live per-instance usage |
|
||
| 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
|
||
```c
|
||
/* 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 `#include`s `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
|
||
```c
|
||
/* 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`:
|
||
|
||
```mermaid
|
||
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).
|