scheduler-sim-diagnostics, scheduler-sim-live-allocation, xce-test-cli-ux and eliminate-frontend-makefile had every sub-task checked but their parent boxes and spec.json phase still read tasks-generated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
27 KiB
Implementation Plan: scheduler-sim-diagnostics (Requirement Set 1 — Limit Inspection; Requirement 6 — Human-Readable Units)
-
1. Add read-only accessors exposing the limit plugin's configured-limits and live-usage-instances tables
- Two new functions (
limit_get_configured_limits(),limit_get_usage_instances()) returning raw kvmgr handles (orNULLwhen the plugin is absent), declared inplugin.halongsideregister_limit(), defined inplugins/limit.c - Never allocate, mutate, or trigger plugin hooks — a pure read of already-existing pointers
- New/extended unit tests in
test_limit_plugin.ccover: plugin registered with configured limits (handle returned, iterable), plugin registered with zero limits, plugin not registered (NULL) - Observable: a test that registers the limit plugin with a known limit and confirms the accessor's returned handle, when iterated via
iter_make()/iter_next(), yields that same limit; a sibling test confirmsNULLwhen the plugin was never registered - Requirements: 1.1, 1.2, 1.4, 2.1, 3.1
- Boundary: Limit Plugin Accessor
- Two new functions (
-
6. Bootstrap the limit plugin in sim mode so
info limitshows real data against a replay (fixesbug-1825)- Extract
limit_new_plugin()factory out ofregister_limit()(plugins/limit.c), mirroringbuiltin_fcfs_new_plugin()/register_builtin_fcfs()'s existing factory-extraction pattern exactly;register_limit()itself must be behaviorally unchanged (pure refactor) - New
sim_bootstrap_limit_plugin(xce_sched_sim_driver_t* sim)(new filesim_limit.c, mirroringsim_picker.c's structure/header-comment convention):limit_new_plugin()→plugin_register_core(&(sim->core), plugin)→ ifsim->core.config != NULL, callplugin->hooks.config_hook(plugin, sim->core.config)directly (NOT the type-unsafeplugin_config_update(), which casts itsvoid*arg toxce_scheduler_t*and would misreadxce_sched_sim_driver_t's embeddedcorefield) - Wire the call into
sim_loop.cright alongside the existingsim_bootstrap_picker(sim)call site — by that pointsim->core.configalready holds the snapshot's borrowed-reference config (set duringsched_sim_driver_start()'s snapshot load, which runs first) - Observable: a real
xce_sched_sim_driver_tdriven throughsched_sim_driver_start()against a replay whose snapshot carries a realxce_limit_config_t, after the sim driver's own bootstrap sequence runs, haslimit_get_configured_limits()return a handle that iterates to that real limit — no test-only registration helper needed - Requirements: 1.1–1.7 (makes them exercisable against a real replay, not just via a test-only fixture helper)
- Boundary: Limit Plugin Accessor (factory extraction), new Sim Limit Bootstrap component
- Known deliberate non-goal, tracked separately: this task does NOT wire limit matching (
limit_job_election()) into the sim/replay decision path — that function is only ever reached viaplugin_job_election(), which is normal-mode-only (one caller,scheduler.c) and itself needs a core-safe variant before it could be called fromsim_loop.c. Live usage (Requirements 2.x/3.x) will still show "no active usage instance yet" against a real replay even after this task — only the configured-setting display (1.1-1.7) becomes real. Tracked as a separate follow-up bug, not bundled into this task.
- Extract
-
2. Core: Limit lookup and configured-setting display
-
2.1 Implement name/all resolution with correct absence messaging
- New white-box test file
test_repl_limit_table.ccreated (sibling oftest_repl_resource_table.c, own#includeset coveringplugins/limit.candresource.cfor real — not stubbed — perdesign.md's build-wiring risk note citing bug-1762's precedent) repl_info_limit(sim, arg):arg == "all"iterateslimit_get_configured_limits();NULL/empty prints "no limits configured" (Requirement 1.7);arg != "all"does akey_find()by name (not a scan) on the same handle, printing "no such limit ''" (Requirement 1.3) for aNULLhandle or a miss — aNULLhandle on this path is never treated as "no limits configured," matching that message's ownall-only scoping- Wired as a direct call for now (
repl_dispatch()/repl_cmd_info()wiring itself deferred to task 4); tests in this task callrepl_info_limit()directly, not throughrepl_dispatch() - Observable: REPL output shows "no such limit 'foo'" for an unconfigured name against a fixture with other limits configured, and "no limits configured" for
allagainst a fixture with zero configured limits - Requirements: 1.3, 1.7, 4.1, 4.2
- Depends: 1
- New white-box test file
-
2.2 Display configured setting including scope membership
- Matched limit's name, constrained resource, value/percent, and per-dimension scope print via the existing
fmt_limit()library formatter into a stack buffer, line-prefixed with the existing[sim]convention; all output viafprintf(stdout, ...), never bareprintf()(test_no_bare_printf.cregression-gates this) - Inclusion and exclusion group membership both render (
fmt_group()already handles this, adopted as-is) - Observable: a limit configured with an exclusion group ("all except X") shows both the inclusion and the exclusion in its output;
info limit allagainst a fixture with multiple configured limits displays every one's setting - Requirements: 1.1, 1.2, 1.5
- Matched limit's name, constrained resource, value/percent, and per-dimension scope print via the existing
-
2.3 Display effective host-scope for implicitly host-scoped limits
- After
fmt_limit()'s output, callis_host_based_limit(core->resmgr, limit); if it returns> 0AND the limit's own per-host consumer is not configured, print an explanatory host-scope note - Observable: two REPL fixtures — one percent-based limit on a per-host resource without explicit per-host scoping (note appears), one absolute-value or explicitly-global-scoped limit (note does not appear)
- Requirements: 1.6
- After
-
3. Core: Live accounted-usage display
-
3.1 Display usage for cluster-wide (non-host-scoped) limit instances
- Iterate
limit_get_usage_instances(), filter byvres->limit == matched_limit; for a GLOBAL/SHAREDvres, read capacity fromcore.sharedInst[idx]and accounted usage fromcore.sharedInUse[idx] - Existence-of-instance and zero-usage are distinct: zero matching
xce_vres_t(no capacity entry at all) prints "no active usage instance yet" (Requirement 2.2); a matchedxce_vres_twhosesharedInUse[idx]isNULL/absent instead displays a genuineaccounted=0(it has been evaluated and has a real capacity, just nothing charged against it yet) — existence is gated on the capacity read, never on the in-use read - Observable: a global limit that has matched at least one job shows an accounted/capacity pair reflecting current state; a configured-but-never-evaluated limit shows the explicit no-instance message instead; a matched-but-never-charged limit shows
accounted=0rather than the no-instance message - Requirements: 1.4, 2.1, 2.2, 2.3, 3.3
- Depends: 1
- Iterate
-
3.2 Display per-host usage for host-scoped limit instances
- For a host-scoped
vres(explicit or implicit per 2.3), iteratecore.workers; include a worker only if it has a populated capacity entry (w->resvals[idx]non-NULL) — read capacity fromw->resvals[idx]and accounted usage fromw->resInUse[idx](same existence-vs-zero-usage distinction as 3.1, applied per worker); label each instance with its scope values (user/queue/project from the vresname, plus host) - A limit varying by more than one scope dimension at once (e.g. per-user and per-host together) shows each distinct combination as its own labeled instance
- Observable: a limit matched against jobs on two distinct hosts shows two separate per-host usage lines each labeled with its own scope values, and a third configured-but-untouched host shows no line at all
- Requirements: 1.4, 2.1, 2.2, 2.3, 3.1, 3.2, 3.4
- For a host-scoped
-
4. Register
info limitas a recognizedinfosubcommand- New
else if (strcmp(tokens[1], "limit") == 0)branch inrepl_cmd_info()(repl_info.c) routes torepl_info_limit(); usage-string literal updated to listlimitalongside the existing subcommands test_repl_limit_table.cgains a full dispatch-round-trip test (viarepl_dispatch(), not a direct call) for bothinfo limit <name>andinfo limit all- Observable:
info limit <name>andinfo limit allboth work when typed as real REPL input (throughrepl_dispatch()), andhelp/usage output lists the new subcommand - Requirements: 4.1, 4.2
- Depends: 2.1, 3.1, 3.2
- New
-
5. Verify no mutation, replay determinism, and existing-subcommand regression
- Test: displaying limit information never alters configuration, accounting state, or scheduling decisions (assert config/accounting untouched before vs. after)
- Test: a replay run twice — once with
info limitissued mid-session, once without — produces identical scheduling decisions and identical final accounted state - Full existing
infosubcommand test suite (job/queue/worker/resource/summary) re-run to confirm zero output changes from this addition - Observable: the two new determinism/no-mutation tests pass, and the full pre-existing info-command test suite shows zero diffs
- Requirements: 5.1, 5.2, 5.3
- Depends: 4
-
7. Foundation: Promote human-readable byte-unit formatting into the shared library
- Move the existing byte-unit scaling algorithm and its byte-valued-resource classification out of the scheduler REPL and into the shared library, so any component linking the shared library can reach it, not only the scheduler's REPL
- Add a display-mode-aware value composer to the shared library capturing the existing raw / human-readable / both-together rendering shapes the REPL already uses elsewhere for other byte-valued resources
- The scheduler's own existing byte-formatting functions become thin callers of the newly shared versions — their names, signatures, and observable output stay exactly as they are today
- Add the small scheduler-side bridge between the REPL's own existing display-mode selection and the shared library's newly-introduced mode concept, and retype the REPL's existing shared numeric-value composer (the one piece of formatting logic every byte-valued resource display in the REPL already funnels through, including — but not limited to — limit display) to delegate to the new shared composer through that bridge, so every consumer of that shared composer benefits from one single implementation from this point forward, not just the limit display this requirement set is adding
- Observable: every existing scheduler test that exercises byte-unit formatting continues to pass completely unchanged — including the pre-existing, non-limit displays that already share this exact formatting logic — and new shared-library unit tests directly exercise the promoted scaling, classification, and mode-aware composition logic in isolation
- Requirements: 6.1, 6.3, 6.5, 5.3
- Boundary: Shared Units Formatter, REPL Limit Info Command (the retyped composer and its thin-wrapper callers live in
repl_info.c/repl_fmt.c)
-
8. Core: Human-readable configured-setting display
-
8.1 Add a display-mode-aware way to render a limit's configured setting
- New entry point produces the same configured-setting output the existing formatter already produces, except a byte-valued, non-percentage resource's amount renders in human-readable form according to a supplied display mode
- The existing formatter's own output is provably unchanged for every input it can be given — proven by its complete pre-existing test coverage continuing to pass with zero modification
- Observable: the existing configured-setting formatter's test coverage passes unmodified, and a new test confirms the mode-aware variant renders a byte-valued resource's amount differently across each supported display mode while leaving a percentage or non-byte-valued resource's rendering untouched
- Requirements: 6.1, 6.3, 6.5
- Depends: 7
- Boundary: Shared Units Formatter
-
8.2 Show configured limit amounts in the operator's selected display mode
- The REPL's limit configuration display now renders a byte-valued limit's configured amount using the operator's already-selected display mode, reusing the scheduler-side bridge task 7 introduced, the same mode every other byte-valued resource display in the REPL already honors
- Observable:
info limit <name>against a byte-valued limit shows its configured amount in human-readable form under the REPL's default display mode, and the rendering changes consistently when the operator switches display modes - Requirements: 6.1, 6.2, 6.5
- Depends: 8.1
- Boundary: REPL Limit Info Command
-
9. Core: Correct live usage display's resource classification
- The live usage display currently classifies a limit's amounts using an internal per-instance identifier rather than the limit's own constrained resource, so a byte-valued limit's usage amounts never actually get evaluated for human-readable rendering even though the surrounding display logic (already retyped onto the shared composer by task 7) already supports it
- Classify a limit's live usage amounts by the limit's own constrained resource instead, for both the cluster-wide and the per-host usage display paths
- Observable: a byte-valued limit's accounted and capacity amounts in its live usage display render in human-readable form, matching the same format its configured-setting display already uses; a limit expressed as a percentage of a byte-valued resource shows its resolved absolute amount in human-readable form too
- Requirements: 6.1, 6.3, 6.4, 6.5
- Depends: 7 — task 7 retypes the exact shared formatting call chain this task's fix flows through (the limit usage display's classification feeds into that same composer); sequencing after 7 avoids two tasks editing overlapping logic in an uncoordinated order, not a data dependency on any new symbol this task itself needs
- Boundary: REPL Limit Info Command
-
10. Validation: Confirm correctness, consistency, and no regression
-
10.1 Re-verify existing test coverage against the corrected usage display
- Task 9 found that fixing the resource-classification defect did NOT break any pre-existing assertion outright — instead, several existing assertions on the live usage display's amounts turned out to be loose substring checks against whole-number fixture values, which happen to still match the corrected, human-augmented output by coincidence of formatting (the old expected text is a literal prefix of the new one) rather than by genuinely exercising the fix
- Identify every such loose, coincidentally-still-passing assertion, and strengthen each one to assert the actual corrected output precisely enough that it would fail if the resource-classification fix were reverted
- Observable: each strengthened assertion demonstrably fails when checked against the prior (defective) behavior and passes against the corrected behavior — not merely a new assertion added alongside an untouched, still-coincidentally-passing old one
- Requirements: 6.1, 6.3, 6.4
- Depends: 9
-
10.2 Confirm units consistency, exclusion rules, and zero regression end to end
- A byte-valued limit's configured-setting display and its live usage display agree on units under every display mode, within one request for that limit's information
- A limit whose constrained resource is not byte-valued, or whose own configured value is itself a percentage, is never subject to byte-unit scaling in either display
- The complete pre-existing
info limittest suite, and the complete pre-existing suite for every otherinfosubcommand — including the two pre-existing, non-limit displays that share the same underlying formatting logic this requirement set retyped in task 7 — show zero output differences beyond what this requirement set intends - Observable: a single
info limitinvocation against a byte-valued limit shows the same units convention in its configured-setting line and its usage line(s) in every display mode, a slot-count limit's display is unaffected by display mode in either line, and the full pre-existing scheduler test suite — including the pre-existing non-limit byte-valued-resource displays task 7 touched — passes with no unintended output changes - Requirements: 6.1, 6.2, 6.3, 6.4, 6.5, 5.1, 5.2, 5.3
- Depends: 8.2, 9, 10.1
Parallel Execution Note
No (P) markers are used anywhere in this plan. Tasks 2.1–2.3 and 3.1–3.2 all incrementally edit the same function (repl_info_limit()) in the same file (repl_info.c) and largely share the same new test file (test_repl_limit_table.c) — the parallel-analysis rules' "no conflicting files or shared mutable resources" criterion rules out marking any of them concurrent. Task 1 is the sole Foundation task (rarely (P) by rule, and has no peer to run alongside regardless). Tasks 4 and 5 are Integration/Validation work that structurally depends on Core being complete.
Tasks 7–10 (Requirement 6) follow the same reasoning: task 9 has no genuine data dependency on task 8 (they touch different functions — the configured-setting formatter vs. the live-usage formatter), but both edit repl_info.c and both build on task 7's shared-library symbols and scheduler-side bridge, and task 10's validation work spans all of them — marking any of 8/9 (P) would only invite the same same-file coordination risk this plan has already declined once. Task 7 is the sole new Foundation task, and now also owns retyping the REPL's pre-existing shared numeric-value composer (used by info worker/info resource as well as info limit) onto the new shared formatter — a task-graph sanity review (2026-09-20) caught this as a design.md-specified change with no owning task in an earlier draft, corrected here rather than left implicit. Task 10 is Validation work that structurally depends on Core being complete.
Implementation Notes
- Task 1:
test_limit_plugin.c's pre-existingfixture_init()never populatedcore->plugins(plugin_register()is stubbed to a no-op in that white-box TU). Closed with two new test-local fixture helpers that hand-build a realcore->pluginskvmgr andkey_add()the plugin under its real"xce_sched_limit"name — necessary to exercise the accessors' realkey_find()path, not a workaround.test_repl_limit_table.c(tasks 2.1+) is a different white-box TU with its own include set (per design.md's build-wiring risk note) — it should link realplugin.c/core.cmachinery whereregister_limit()/xce_sched_core_init()actually populatecore->plugins, so it likely does NOT need this same hand-built-kvmgr workaround; confirm this when starting 2.1 rather than assuming the same gap exists there. - Task 2.1: Making
repl_info.cgenuinely calllimit_get_configured_limits()broke the link for ~24 pre-existing white-box test files that#include "../src/repl_info.c"without also linkingplugins/limit.c— this codebase's white-box test convention means any new cross-file call from an included production file ripples to every file that includes the caller but not the callee. Fixed with an identical 5-line link-satisfying stub (return NULL) added to each, matching the established "duplicate, don't share" stub idiom already used throughout this file family; verified behavior-preserving since none of those 24 fixtures register the limit plugin (the real function would also returnNULLthere). Task 3 (which callslimit_get_usage_instances()) will very likely hit the identical ripple across an overlapping or identical file set — expect it, and apply the same fix pattern rather than treating it as a surprise. - Task 3.1: found and fixed a real latent bounds-check imprecision (negative/stale
idxnot safely rejected by a.count-based pre-check) while writing the new GLOBAL/SHARED usage-read code — logged asbug-1824in.wolf/buglog.jsonrather than fixed in the pre-existingrepl_info_resource_one()(out of this task's boundary); the new code uses the corrected pattern instead. Task 3.2 confirmed it did NOT repeat the same mistake in its own per-host reads. - Task 3.2: reviewer approved with one non-blocking suggestion — no test directly exercises Requirement 3.1's "per-user and per-host together" combined-dimension case end-to-end (structurally supported by the code, per independent review inspection, just not covered by a dedicated fixture). Worth adding if this area gets touched again; not required before shipping.
- Task 5 — significant follow-up needed (logged as
bug-1825): all 5 tasks are correctly implemented, thoroughly tested, and genuinely read-only/deterministic — butinfo limitis currently inert in real production usage.register_limit()has exactly one call site in the whole codebase (scheduler.c's normal-modesched_register_plugins()); sim mode's own plugin bootstrap (sim_bootstrap_picker(),sim_picker.c) only registers the picker plugin, never the limit plugin. So a real operator runningxce_scheduler -i/-s <replay>will always see "no limits configured"/"no such limit" frominfo limit, regardless of what the original live recording had configured — every positive-display test in this spec exercises the code via a test-only plugin-registration helper with no production equivalent in sim mode. This is a pre-existing sim-mode driver gap (same class asscheduler-driver-equivalence.md's other documented normal-vs-sim parity gaps), not a defect in this spec's own work, and was out of task 5's test-only boundary to fix. Fixed by task 6. - Task 6 — a real memory-safety lesson, caught by review before it shipped: the first implementation attempt set
plugin->sched = (void*)sim(axce_sched_sim_driver_t*, disguised) before invoking the limit plugin'sconfig_hook, relying on bothxce_scheduler_tandxce_sched_sim_driver_thappening to havexce_sched_core_t coreas their first struct member. An independent reviewer rejected this:core.h's ownxce_core_cache_clear_cb_tdoc comment documents that this EXACT cross-cast pattern already corrupted memory once before in this codebase (cache_clear()reinterpretingxce_sched_sim_driver_t'salt_events_fileas akvtab_t*), and there was no compiler/runtime guard against a future edit to the callee reading past.core. Fixed by addinglimit_seed_config(plugin, xce_sched_core_t* core, config)— an honestly-typed entry point takingcoreas a real field access, no cast — withlimit_update_config()(normal mode) andsim_bootstrap_limit_plugin()(sim mode) both delegating to a shared_implfunction. Lesson for future work in this codebase: when a plugin hook or helper is invoked from BOTH a normal-mode (xce_scheduler_t) and a sim-mode (xce_sched_sim_driver_t) context, never derive one struct type from the other via a first-member cast, even when it "happens to work" today — takexce_sched_core_t*as an honest parameter instead. Two independent reviewers (one per round) verified this by reading the actual struct definitions and the full call chain, not by trusting the report. - Task 7 — a genuine, narrow behavioral corner case surfaced by the promotion, deliberately left as-is: the pre-existing
repl_format_numeric_resource_value()had an asymmetry for"swp"(whose wire unit is megabytes, not bytes): RAW and BOTH modes printed the un-converted raw value, while only HUMAN mode used the MB→bytes-converted value for scaling. The new sharedxce_format_units_value(mode, bytes_value, ...)interface — a single value argument, matching design.md's specified shape exactly — cannot preserve that asymmetry:swpunder RAW/BOTH mode ininfo worker/info resourcenow renders the bytes-converted number instead of the original raw MB number. Confirmed by both the implementer and an independent reviewer: no test in the codebase exercisesswpunder RAW or BOTH mode (all existingswptests use HUMAN only), andswpis not in Requirement 6's byte-valued-resource list (mem/tmp/maxmem/maxtmp/maxswap), so this is untested and out of this requirement's scope — not a regression caught by any existing coverage, but technically in tension with Requirement 5.3's literal "preserve all existing behavior unchanged." Left as-is rather than special-cased, since fixing it would mean giving the shared composer answp-specific parameter it has no other reason to carry. Ifswp's RAW/BOTH display is ever reported as user-visibly wrong, start here. - Task 8.2 — implementer's own stated regression-safety rationale was inaccurate, though its conclusion held: the report claimed the 28 pre-existing
test_repl_limit_table.ctests stay green because they use "slots-based (non-byte-valued)" fixtures unaffected by units-mode. An independent reviewer found this false — every configured-limit fixture in that file hardcodes resource name"mem"(a byte-valued resource);slots-looking names in that file (ug1_slots, etc.) are limit names, not resource names. The real reason those 28 tests stay green is that none of them assert on the formatted"<resname> = <value>"line's exact text at all — they assert only on limit names, group membership/exclusion text, host-scope-note presence, and separate usage/accounting lines. Lesson: when a report explains why an existing test suite is unaffected by a change, verify the stated mechanism against the actual fixtures, not just the pass/fail outcome — a correct outcome can still carry an incorrect explanation, which would mislead the next person who touches this test file expecting aslotsfixture to exist. - Task 9 — design.md/tasks.md's own prediction about which pre-existing assertions would break was wrong, and the fix landed with ZERO pre-existing test failures (not the expected handful): the original design/task-10.1 framing assumed fixing the resname-classification bug would break
test_repl_limit_table.c's existing"accounted=4.00"-style assertions (lines ~1355, 1359, 1455, 1537, 1540, 1599). It didn't — every one of those fixtures uses a whole-number double value (4.0,10.0,0.0,2.0,3.0), andxce_format_units_value(XCE_UNITS_BOTH, ...)'s"%f (%s)"composition renders any whole number's%fexpansion as"N.000000", whose leading"N.00"substring still satisfies the old assertions' barestrstr()checks even though the actual output materially changed (now"accounted=4.000000 (4.00 B)"instead of the old"accounted=4.00"). Independently confirmed by task 9's reviewer, tracing the exact format chain and fixture values, with no counterexample among the 6 citations. Consequence for task 10.1: there is no RED→GREEN "fix a broken assertion" work to do — instead, task 10.1 should STRENGTHEN these 6 loose, coincidentally-passing assertions (e.g. assert the full corrected string, or assert a"B)"/unit-suffix is present) so they actually prove the fix rather than passing by accident of whole-number formatting. General lesson: a substring-only (strstr) assertion with no upper bound or exact-match check is blind to a superset-string regression — the old string being a prefix of the new one is enough to mask a real, materially-different output change.