Two retrospective design-review findings: - design.md's System Flows sequence diagram showed the controller-layer resval check as an independently-launched client session, implying no dependency on the injecting worker staying connected. As-built (bug-1579), host-attrs resources reset to baseline the instant the worker disconnects, so the check actually runs nested inside the still-open worker session via xce_test's `!` shell-escape. Updated the diagram and prose to show this, and documented the still-open residual risk for custom host-attrs resource names in Error Handling. - Added explicit "do not change this format" warning comments at both trace-log call sites (core_events.c's RESVALS_DELTA case, apiserver's OnResourceLocationUpdated) that xce-test-resources' log-grep depends on -- an unrelated logging cleanup at either site would otherwise silently turn every future verification run into a false UNREACHABLE/timeout. No behavior change in either source file -- comments only.
46 KiB
Design Document
Overview
This feature closes a coverage gap in xce_test's worker-context resupdate command (its resource-name table is missing 7 real dynamic resources) and adds a repeatable, self-contained skill that injects a synthetic resource value through resupdate and confirms it is correctly received and stored at the controller, the live scheduler, and the Go apiserver — the exact three-layer propagation path that took a multi-hour live-deployment investigation to verify manually during bug-1569.
Purpose: Turn a class of bug (resource-value routing/propagation defects) that previously required deploying real worker/controller/scheduler/apiserver daemons and waiting on periodic reporting cycles into a command that reproduces and confirms the fix in seconds.
Users: XCE engineers debugging or regression-testing resource-value propagation across the controller/scheduler/apiserver boundary.
Impact: Extends xce_test (test.c) with a small, backward-compatible change; extends xce-deploy-ha's deploy-ha.sh/SKILL.md with an opt-in, conditionally-generated apiserver deployment step (see Design Decisions below — no change to existing callers that don't need an apiserver); adds two single-line trace-log additions (scheduler, apiserver); adds one new skill. No production runtime behavior changes — the trace-log additions are inert log-only output at debug/trace level, and the xce-deploy-ha extension only activates when the apiserver binary is present, mirroring its existing worker/scheduler conditional-generation pattern.
Goals
resupdateaccepts any resource name the cluster has registered (Requirement 1).- Confirm the existing
join+resupdateworker-context flow needs no real worker daemon (Requirement 2). - A single command deploys controller + scheduler + apiserver, injects one or more resource values, and reports PASS/FAIL per layer (Requirements 3–6).
Non-Goals
- Renaming
xce_test, per-command help, reference documentation, JSON output mode, non-interactive batch mode, argument-convention unification — explicitly deferred to a future "enhance xce_test" project (see requirements.md Future Considerations). - A new live scheduler admin query RPC — rejected in favor of trace-log observation, which needs no new RPC at all (see research.md Design Decisions).
- The record+replay+REPL flow and the apiserver REST/
xcecliquery path — both considered and superseded during design validation in favor of live trace-log observation at each layer's point of application; see research.md Design Decisions for why (this feature's own originating bugs, bug-1568 and bug-1569A, were both storage-layer defects found via exactly this technique, and the REPL's step/continue requirement had already produced one false negative live). - General-purpose apiserver production deployment/HA — the new
xce-deploy-haapiserver step is a minimal single-node instance for local verification only, and no longer needs functional auth/RBAC since no REST query is issued against it. - A new, standalone apiserver deploy script — considered and superseded in favor of extending
xce-deploy-haitself, reusing its already-precedented conditional (binary-present-only) generation pattern instead of re-implementing the same conventions in parallel (see research.md Design Decisions). - CI/automated-pipeline integration.
- Fixing unrelated bugs the verification run happens to surface — those are reported, not silently patched, by this feature.
Boundary Commitments
This Spec Owns
set_resval()/cmd_resupdateinsrc/controller/testsuite/test.c: accepting any registered resource name, with an explicit type-override escape hatch.- A single trace/debug log line in
xce_core_push_events()'sXCE_CORE_EVENT_RESVALS_DELTAcase (src/scheduler/src/core_events.c), emitted at the point the live scheduler daemon applies a resource update — the only scheduler-side change this spec makes. - A single trace/debug log line in
OnResourceLocationUpdated(src/frontend/apiserver/backend/handler.go:92-95), emitted at the point the apiserver stores a received resource update — the only apiserver-side change this spec makes. - An opt-in, conditionally-generated apiserver deployment step added to
xce-deploy-ha'ssrc/tools/deploy-ha.shand documented in itsSKILL.md(Step 4d, alongside the existing Step 4b worker/scheduler steps) — generatesstart-apiserver.shonly when a pre-built apiserver binary already exists at its own Go-build output path (same "caller opts in by having already built it" spirit as the worker/scheduler conditional, though checked at a different path since apiserver is a separate build system — see Components for the precise distinction). Existingxce-deploy-hacallers that never build an apiserver (e.g.xce-test-ha) see zero behavior change. - A new skill (
.claude/skills/xce-test-resources/) that orchestrates deploy + inject + three-layer verify + PASS/FAIL report.
Out of Boundary
resource_sync.c's real, production periodic load-index collection — unchanged.- The controller's resval-forwarding hooks (
do_update_resvals,sched_resource_hook_fun,apiserver_resource_hook_fun) — unchanged; this design only consumes them. - The scheduler's REPL (
repl.c/repl_info.c), recording admin RPCs, and the apiserver's REST API/xcecliquery path — none of these are touched or driven by this design (superseded by the trace-log decision; see research.md Design Decisions). They remain available as a future follow-up if a query-layer-specific check is ever needed. - All other scheduler/apiserver logic beyond the two single-line trace additions listed under "This Spec Owns" above — no behavior change, no new code paths, purely additive log output.
xce-deploy-ha's existing controller/worker/scheduler steps (1–4c) — unmodified; only a new, conditionally-triggered step is added alongside them.- The broader "enhance xce_test" ideas listed in requirements.md's Future Considerations.
Allowed Dependencies
xce-deploy-ha's controller deployment conventions and its existing worker/scheduler conditional-generation pattern (directory layout, TLS cert generation,cluster.binbuild viaxceadmin, Step 3's "generate the extra start script only if the binary exists" rule) — reused directly, by extending the same script, not duplicated in a parallel one (see research.md Design Decisions).xce_test's existing worker context (join,resupdate) and client context (resval) — unmodified except for Requirement 1's table extension.make build-server(src/frontend/Makefile) to build the apiserver binary;make test-integration'sXCE_BIN/SRV_URLconvention as the closest existing precedent for pointing it at a controller.- Each daemon's own existing logging infrastructure (
tinylogin C for the scheduler, zaplogger.Debugin Go for the apiserver — pererror-handling.md) as the channel the new trace lines are written through; no new logging mechanism is introduced.
Revalidation Triggers
- The wire shape of
xce_resval_t/resupdate's CLI flags changes. xce_core_push_events()'sXCE_CORE_EVENT_RESVALS_DELTAhandling, orOnResourceLocationUpdated's call site, is refactored such that the trace log's format/location moves.xce-deploy-ha's Step 3 conditional-generation pattern (or its directory layout) changes shape — this feature's apiserver step is now part of that same script, so a shape change there is a direct, first-party revalidation trigger rather than a drift risk between two separate scripts.- If a future bug turns out to live specifically in the query/REST/serialization path rather than storage, revisit adding a real query-based check for that layer (not precluded by this design, just not built here).
Architecture
Existing Architecture Analysis
A resource value reaches all three layers through one controller-side write, not three independent paths (see research.md): do_update_resvals() applies it to the controller's own w->host.resvals (queried via resval) and fires HOOK_DETAIL_RESOURCE_UPDATE, which fans out to both sched_resource_hook_fun (scheduler) and apiserver_resource_hook_fun (apiserver). This design's job is purely to exercise this existing path with a synthetic worker and observe the result at each of the three existing points of application — it introduces no new production data path, and (per the trace-log decision) reads state at the storage point rather than through a query surface.
Architecture Pattern & Boundary Map
graph TB
Skill[xce-test-resources skill]
WorkerCtx[xce_test worker context]
ClientCtx[xce_test client context]
Controller[xce_controller]
Scheduler[xce_scheduler daemon]
SchedLog[scheduler log]
Apiserver[apiserver]
ApiLog[apiserver log]
Skill --> WorkerCtx
WorkerCtx -->|join and resupdate| Controller
Skill --> ClientCtx
ClientCtx -->|resval query| Controller
Controller -->|resource update notify| Scheduler
Controller -->|resource update push| Apiserver
Scheduler -->|trace log on apply| SchedLog
Apiserver -->|trace log on apply| ApiLog
Skill -->|tail and grep| SchedLog
Skill -->|tail and grep| ApiLog
Architecture Integration:
- Selected pattern: black-box orchestration over existing interfaces plus two minimal, inert trace-log additions at each layer's point of application — this feature adds a coordinating layer (the skill), one small compatibility fix (
resupdate's resource table), and the two log lines; it does not add a new data path or query surface. - Domain boundaries:
resupdatetable fix (C, test.c) is independent of the skill (bash orchestration); thexce-deploy-haapiserver extension is independent of both and reusable by anyxce-deploy-hacaller, not just this skill; the two trace-log additions are independent single-line changes in their respective daemons. - Existing patterns preserved:
xce-deploy-ha's scratch-directory deploy convention and worker/scheduler conditional-generation pattern (extended, not duplicated, for apiserver),xce-test-ha's VERDICT-line reporting convention, each daemon's own existing logging infrastructure (no new logging mechanism). - New components rationale: the
xce-test-resourcesskill is new because no existing tool exercises all three layers together; theresupdatefix is new because the current table is demonstrably incomplete; thexce-deploy-haapiserver step andstart-all.shextend an existing tool rather than introduce a new one, following its own established conditional-generation precedent; the two trace-log lines are new because neither layer currently logs the value it applies (confirmed by direct inspection — see research.md). - Steering compliance: production code changes are two single-line, debug/trace-level log additions with no behavior change, plus
test.c's test-tooling scope; TDD applies to theset_resval()change (see Testing Strategy).
Technology Stack
| Layer | Choice / Version | Role in Feature | Notes |
|---|---|---|---|
| Test client (C, gnu17) | xce_test (existing binary, src/controller/testsuite/test.c) |
Inject via worker context; query controller via client context | Requirements 1, 2, 3 |
| Scheduler (C, gnu17) | Live xce_scheduler daemon (existing, one new trace-log line) |
Applies the resource update and logs it at the point of application | Requirement 4; see research.md Design Decisions |
| API server (Go) | apiserver binary via make build-server (one new trace-log line in backend/handler.go) |
Receives and stores the resource update, logs it at the point of application | Requirement 5; see research.md Design Decisions |
| Orchestration | Bash skill script, xce-deploy-ha/xce-test-ha conventions |
Deploy, inject, tail/grep each layer's log, verify, report, clean up | Requirement 6 |
File Structure Plan
Directory Structure
src/
└── controller/testsuite/
├── resval_typeresolve.c # NEW: set_resval()'s name→Value_u_which resolution, extracted for isolated unit testing
├── resval_typeresolve.h # NEW: its declaration
├── test_resval_typeresolve.c # NEW: Unity unit tests for resval_typeresolve.c (4 cases, see Testing Strategy)
└── test.c # MODIFIED: set_resval() delegates to resval_typeresolve.c; cmd_resupdate() gains -t flag + post-update accept/reject verification
src/controller/meson.build # MODIFIED: add resval_typeresolve.c to xce_test_src (both xce_test and test_resval_typeresolve.c link against it); add test_resval_typeresolve.c + a new controller_unit_test-style test() target — these are hand-maintained explicit file lists, not globs
src/scheduler/src/core_events.c # MODIFIED: one new trace/debug log line in xce_core_push_events()'s XCE_CORE_EVENT_RESVALS_DELTA case
src/frontend/apiserver/backend/handler.go # MODIFIED: one new trace/debug log line in OnResourceLocationUpdated
src/tools/
└── deploy-ha.sh # MODIFIED: Step 3 gains (a) a conditional apiserver-config-generation branch mirroring the existing worker/scheduler conditional, generating start-apiserver.sh (minimal boot-only config + start command — no functional auth/RBAC required, see research.md Design Decisions), and (b) an always-generated start-all.sh that sequences start.sh + health-check + whichever of start-worker.sh/start-scheduler.sh/start-apiserver.sh exist for this deploy — automates the manual recipe SKILL.md's Steps 4/4b/4c already document
.claude/skills/
├── xce-deploy-ha/
│ └── SKILL.md # MODIFIED: new Step 4d documenting the opt-in apiserver step, alongside existing Step 4b (worker/scheduler)
└── xce-test-resources/
├── SKILL.md # NEW: skill description, usage, VERDICT format (xce-test-ha convention)
└── run-resource-verify.sh # NEW: orchestrates deploy + inject + 3-layer verify (log-tail/grep for scheduler+apiserver) + verdict + cleanup
Modified Files
src/controller/testsuite/test.c—set_resval()becomes a thin wrapper around the newresval_typeresolve.cmodule;cmd_resupdate()gains an optional-t <numeric|string>flag per-n/-vpair, and, after sending the update, a follow-up per-name verification query (see Components below — Requirement 1.2/1.3's mechanism).src/controller/meson.build—xce_test_src(line 57) gainstestsuite/resval_typeresolve.cso bothxce_testand the new unit test binary can link it; a new Unity test source list/target (following thecontroller_unit_test/sched_push_unit_testpattern already in this file, e.g. lines 86, 215, 222) gainstestsuite/test_resval_typeresolve.c. Both lists are hand-maintained and explicit, not glob-based — this file must be edited or the new module/test never builds.src/scheduler/src/core_events.c—xce_core_push_events()'sXCE_CORE_EVENT_RESVALS_DELTAcase gains one trace/debug-level log line (via this component's existingtinylog-based logging, e.g.TRACE) emitting the resolved worker host, resource name, and applied value, immediately after the value is written into the worker's live state. Debug/trace level only — never fires at WARN/ERROR, never changes control flow. This is the exact site bug-1568 was root-caused at via ad hoc instrumentation; this spec makes that instrumentation permanent and machine-greppable.src/frontend/apiserver/backend/handler.go—OnResourceLocationUpdated(line 92) gains onelogger.Debug(...)call (zap, per this component's existing convention) logging the resource name, host/location, and stored value, immediately afterstore.ResourceLocations.Put().src/tools/deploy-ha.sh— Step 3 gains two additions:- A new conditional branch, same opt-in-by-presence spirit as the existing worker/scheduler branch (but checking a pre-built binary at the Go build's own output path, e.g.
/tmp/apiserver, rather than alongside$BINARY— a different build system, see Components below): if present, generate$TOP/scripts/start-apiserver.sh(minimal boot-only config) and alogger.yamlwithlogging.level: debug; absent, generates nothing, so existing callers are unaffected. - An always-generated
$TOP/scripts/start-all.sh: runsstart.shfor the controller node(s), waits on Step 5's existing health-check, then runs whichever ofstart-worker.sh/start-scheduler.sh/start-apiserver.shwere actually generated for this deploy (skips any that weren't) — the same backgrounding pattern (&, orsetsid nohup … & disownper SKILL.md's own noted improvement) Steps 4/4b/4c already document doing by hand.
- A new conditional branch, same opt-in-by-presence spirit as the existing worker/scheduler branch (but checking a pre-built binary at the Go build's own output path, e.g.
.claude/skills/xce-deploy-ha/SKILL.md— documents the new opt-in apiserver step as "Step 4d" (following the existing Step 4b worker/scheduler pattern) andstart-all.shas a documented shortcut for Steps 4/4b/4c combined.
New Files
src/controller/testsuite/resval_typeresolve.c/.h—set_resval()'s existingresinfo[]table lookup plus the new numeric-by-default fallback, moved out oftest.cinto a small, dependency-light module (no networking, no REPL state) so it is independently unit-testable.test.c(4298 lines, its ownmain(),libedit/network/REPL dependencies) cannot itself be white-box#included into a Unity test the way smaller single-purpose files in this codebase are — extraction is required, not optional, to satisfy Requirement 1.1/1.4's testability.src/controller/testsuite/test_resval_typeresolve.c— Unity test program forresval_typeresolve.c, registered as its ownmeson.buildtest()target (see Modified Files above); this is the file task 1.1's RED tests and task 2.1's GREEN tests live in.
System Flows
sequenceDiagram
participant Skill as xce-test-resources
participant Worker as xce_test (worker ctx, ONE session)
participant Nested as xce_test (nested ! client ctx)
participant Ctrl as controller
participant Sched as scheduler daemon
participant Api as apiserver
Skill->>Worker: join synthetic-worker
Skill->>Worker: resupdate -n <res> -v <value>
Worker->>Ctrl: xce_update_resvals (wire)
Ctrl->>Ctrl: apply to w.host.resvals
Ctrl->>Sched: notify schedUpdateResvals
Ctrl->>Api: notify schedUpdateResvals
Sched->>Sched: apply + trace-log resource, host, value
Api->>Api: store.ResourceLocations.Put + debug-log resource, host, value
Worker->>Nested: !<shell-cmd> --internal-controller-check (worker STILL connected)
Nested->>Ctrl: resval -n <res> -m synthetic-worker (query)
Ctrl-->>Nested: controller-observed value
Nested-->>Worker: result written to a marker file (nested exit code ignored)
Worker->>Ctrl: quit (worker disconnects HERE -- not before)
Skill->>Skill: parse marker file for controller-observed value
Skill->>Sched: tail/grep scheduler log for expected line (bounded wait)
Sched-->>Skill: scheduler-observed value (or timeout)
Skill->>Api: tail/grep apiserver log for expected line (bounded wait)
Api-->>Skill: apiserver-observed value (or timeout)
Skill->>Skill: compare all three vs injected value, emit verdict
Load-bearing constraint the diagram makes explicit (as-built, bug-1579): the controller-layer check is NOT an independently-launched client session run after the worker disconnects. Any host-attrs-derived resource (type/model/cpuf/maxcpus/maxmem/maxswap/maxtmp/ndisks — worker.c's N_HOST_ATTRS) is reset to baseline by the controller the instant the injecting worker disconnects — so a controller-layer check run as a separate, later session against any host-attrs name would ALWAYS false-MISMATCH, independent of whether the underlying propagation is actually correct (discovered as bug-1579, task 6.2, round-1 REJECTED). The fix nests the controller-layer resval query INSIDE the injecting worker's own still-open session, via xce_test's !<shell-command> escape (preprocess_and_run() — works in any REPL context, including worker, with no leave_ctx()/disconnect) — the query genuinely runs before quit, not merely before some unrelated timeout. The scheduler and apiserver legs have no such constraint: both are pure log-tail/grep against a file, entirely independent of whether the worker connection is still open, so they run after the worker session ends like any other step.
Key simplification from the superseded design: no record/replay/REPL round-trip and no REST/xcecli query are needed — the scheduler and apiserver legs both poll their own live daemon's log file for the trace line the injection is expected to produce, within a bounded wait, exactly like the controller leg's own bounded-wait posture. This removes the REPL's step/continue pitfall entirely (there is no replay step in this flow) and the apiserver's auth/RBAC config from the critical path (see research.md Design Decisions).
Requirements Traceability
| Requirement | Summary | Components | Interfaces | Flows |
|---|---|---|---|---|
| 1.1–1.4 | resupdate accepts any registered resource name |
set_resval()/cmd_resupdate (test.c) |
CLI flags -n/-v/-t |
— |
| 2.1–2.2 | Inject without a real worker daemon | xce_test worker context (join, resupdate, existing) |
CLI | Sequence diagram: join → resupdate → wire update |
| 3.1–3.2 | Controller-side visibility check | xce-test-resources skill, xce_test client context (resval, existing) |
CLI | Sequence diagram: resval query → controller-observed value |
| 4.1–4.3 | Scheduler-side visibility check | xce-test-resources skill, scheduler trace log (core_events.c, new) |
Log line, bounded-wait grep | Sequence diagram: scheduler applies + trace-logs → skill tails/greps scheduler log |
| 5.1, 5.2, 5.4 | Apiserver-side visibility check | xce-test-resources skill, xce-deploy-ha apiserver step, apiserver trace log (handler.go, new) |
Log line, bounded-wait grep | Sequence diagram: apiserver stores + debug-logs → skill tails/greps apiserver log |
| 5.3 | Apiserver local deployment capability | xce-deploy-ha apiserver step + start-all.sh |
Bash script | — |
| 6.1–6.5 | Repeatable run, PASS/FAIL verdict, cleanup | xce-test-resources skill (run-resource-verify.sh) |
CLI, VERDICT line | Sequence diagram: final compare + verdict step |
Components and Interfaces
| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies (P0/P1) | Contracts |
|---|---|---|---|---|---|
set_resval() / cmd_resupdate |
Test Tooling / C | Accept any registered resource name for injection | 1.1–1.4 | xcecli_update_resvals (P0), xcecli_resval (P0) |
Batch |
| Scheduler resval trace log | Scheduler Core / C | Log the resolved host/resource/value at the point the live daemon applies an update | 4.1–4.3 | xce_core_push_events() (P0), tinylog (P0) |
Event |
| Apiserver resval trace log | API Server / Go | Log the resource/host/value at the point the apiserver stores a received update | 5.1, 5.2, 5.4 | OnResourceLocationUpdated (P0), zap logger (P0) |
Event |
xce-deploy-ha apiserver step |
Deployment Tooling / Bash | Extend deploy-ha.sh to conditionally stand up a minimal, boot-only single-node apiserver for local verification |
5.3 | make build-server (P0), a running controller (P0), deploy-ha.sh's existing Step 3 (P0) |
Batch |
xce-test-resources skill |
Test Tooling / Bash | Orchestrate deploy, inject, three-layer verify (log-tail/grep for scheduler+apiserver), report, clean up | 3.1–3.2, 4.1–4.3, 5.1, 5.2, 5.4, 6.1–6.5 | xce-deploy-ha (P0, extended), xce_test (P0), scheduler/apiserver trace logs (P0) |
Batch |
Test Tooling / C
set_resval() / cmd_resupdate
| Field | Detail |
|---|---|
| Intent | Resolve a -n <name> -v <value> pair into a wire xce_resval_t, for any resource the cluster has registered |
| Requirements | 1.1, 1.2, 1.3, 1.4 |
Responsibilities & Constraints
- Resolve a resource name to a
Value_u_whichtag (resval_typeresolve.c): known non-numeric names (type,model) resolve toValue_u_svalue; every other name — including ones not seen before — defaults toValue_u_fvalue, overridable via a new optional-t <numeric|string>flag. - Preserve existing behavior byte-for-byte for every currently-supported resource name (Requirement 1.4) — this is a strictly additive change to the fallback path, not a rewrite of the existing table lookup.
- Report per-name accept/reject outcome when a single invocation carries multiple
-n/-vpairs (Requirement 1.3). Confirmed via direct source inspection: the controller's synchronous RPC reply cannot carry this signal —update_resval_list()(src/controller/worker.c:185-198) unconditionallyreturn 0regardless of any per-item failure, sodo_update_resvals()'sreply.statusis always success even when every submitted name is unregistered.cmd_resupdate()therefore verifies acceptance itself: after thexcecli_update_resvals()call, it issues one follow-upxcecli_resval()query per submitted(resource, worker)pair — the same client-library callcmd_resval/resvalalready uses — and compares the returned value against what was just sent. A match is "accepted"; a not-found result or a mismatched value is "rejected", satisfying Requirement 1.2's clear per-name error without any controller-side change.
Dependencies
- Outbound:
xcecli_update_resvals(src/lib/client.c) — unchanged, existing wire call (P0).xcecli_resval(src/lib/client.c) — unchanged, existing wire call, reused here as the accept/reject verification signal (P0).
Contracts: Service [ ] / API [ ] / Event [ ] / Batch [x] / State [ ]
Batch / Job Contract
- Trigger: operator (or the
xce-test-resourcesskill) runsresupdate -n <name> -v <value> [-t <numeric|string>] [-n <name2> -v <value2> ...]in thexce_testworker context. - Input / validation: resource name (string, required per pair), value (string, parsed per resolved type), optional type override.
- Output / destination: one
xce_update_resvalsRPC to the controller carrying every resolved pair; then onexcecli_resvalquery per pair to confirm it landed; per-pair accept/reject reported to stdout based on that follow-up query, not the update RPC's own reply (which cannot distinguish per-item outcomes — see Responsibilities above). - Idempotency & recovery: re-running with the same name/value is safe (matches existing
resupdatesemantics — a plain overwrite, no accumulation).
Implementation Notes
- Integration: the type-resolution table lookup moves into
resval_typeresolve.cunchanged in behavior;cmd_resupdate()gains the new post-update verification loop described above. - Validation: the follow-up
xcecli_resval()query adds one RPC round-trip per submitted resource name — acceptable for a test/verification tool, not a production hot path. - Risks: a resource that is genuinely registered but whose value legitimately hasn't propagated yet (a race, not a rejection) could be misreported as "rejected" if queried immediately; mitigate with a short bounded retry on the follow-up query before concluding rejection (this codebase's existing resval-propagation timing, confirmed live during the bug-1569 investigation this spec originated from, is sub-second for a controller-local query).
Scheduler Core / C
Scheduler resval trace log
| Field | Detail |
|---|---|
| Intent | Make the live scheduler daemon's application of a resource update observable via its own log, without a query RPC or replay |
| Requirements | 4.1, 4.2, 4.3 |
Responsibilities & Constraints
- Emit one trace/debug-level log line inside
xce_core_push_events()'sXCE_CORE_EVENT_RESVALS_DELTAcase (src/scheduler/src/core_events.c), immediately after the resolved worker's live resource value is written — including the resolved host name, resource name, and applied value, in a fixed, greppable format. - Debug/trace level only (
TRACE, which expands totlog_debug3—src/include/utils.h:93) — never WARN/ERROR, never on any decision path, so normal operation is unaffected; this is purely additive observability. - Fires on every real resource update in production too (not just this feature's synthetic injections) — acceptable, since it is the same log-volume category as this component's other existing
TRACEcalls (e.g.apiserver_worker_change'sTRACEinsrc/controller/apiserver.c:446). - Load-bearing operational requirement:
tlog_debug3/TRACEoutput is gated by the scheduler's own runtime log level (src/scheduler/src/main.c's-vflag, cumulative — confirmed by readingmain.c:arguments->verbose++per-v, thenverbose_to_loglevel(arguments.verbose)). At the daemon's default (unset-v) log level, this line is silently never written, and the verification skill's log-grep would time out on every run — a false FAIL, not a bug in the feature under test. The skill's scheduler-deploy step MUST start the scheduler daemon with-vvv(verbosity 3, matchingtlog_debug3) — the same conventionxce-deploy-ha's generatedstart.shalready uses for the controller (start.sh ctrl-1 -vvv) — see task 5.1.
Dependencies
- Inbound: fires as part of the existing
XCE_CORE_EVENT_RESVALS_DELTAhandling path (P0) — no new caller, no new event type. - Outbound:
tinylog(P0) — existing logging infrastructure, unchanged.
Contracts: Service [ ] / API [ ] / Event [x] / Batch [ ] / State [ ]
Event Contract
- Trigger: the live scheduler daemon processes a
RESVALS_DELTAevent carrying aresupdate-injected (or real) value. - Output / destination: one line to the scheduler daemon's own log file.
- Idempotency & recovery: log-only; no state changes, safe to fire on every event including retries/duplicates.
API Server / Go
Apiserver resval trace log
| Field | Detail |
|---|---|
| Intent | Make the apiserver's storage of a received resource update observable via its own log, without a REST/CLI query |
| Requirements | 5.1, 5.2, 5.4 |
Responsibilities & Constraints
- Add one
logger.Debug(...)call (zap, pererror-handling.md's existing convention) insideOnResourceLocationUpdated(src/frontend/apiserver/backend/handler.go:92-95), immediately afterstore.ResourceLocations.Put(key, *res), logging the resource name, location/host, and stored value. - Debug level only — never
Error/Warn, no behavior change to the store write itself. - Load-bearing operational requirement: the apiserver's log level is config-driven (
config/logger.yaml'slogging.level, loaded byloadLoggerConfig()insrc/frontend/apiserver/utils/logger.go). Zap's default level is not Debug, so unless this config explicitly setslevel: debug,logger.Debug(...)output is silently suppressed and the skill's log-grep would time out on every run — a false FAIL, not a bug in the feature under test.xce-deploy-ha's apiserver step (task 3.1) MUST generate alogger.yamlwithlogging.level: debug.
Dependencies
- Inbound: fires as part of the existing
OnResourceLocationUpdatednotification-handling path (P0) — no new caller. - Outbound: zap
logger(P0) — existing logging infrastructure, unchanged.
Contracts: Service [ ] / API [ ] / Event [x] / Batch [ ] / State [ ]
Event Contract
- Trigger: the live apiserver process receives and stores a resource-location update pushed from the controller.
- Output / destination: one line to the apiserver's own log file.
- Idempotency & recovery: log-only; no state changes.
Deployment Tooling / Bash
xce-deploy-ha apiserver step + start-all.sh
| Field | Detail |
|---|---|
| Intent | Extend deploy-ha.sh to (a) conditionally stand up a minimal, boot-only single-node apiserver against an already-deployed controller, and (b) generate a single start-all.sh that sequences controller → (worker/scheduler/apiserver, whichever were generated) so callers need no manual multi-script sequencing |
| Requirements | 5.3, 6.1 |
Responsibilities & Constraints
- Apiserver step: generate the minimal config needed for the process to boot and connect to the controller, plus a
logger.yamlwithlogging.level: debug(required for task 4.2's trace line to actually be emitted — see that component's Load-bearing operational requirement), modeled onmake test-integration's existingXCE_BIN/SRV_URLconvention. Because verification is trace-log-based (Requirement 5, see research.md Design Decisions), this config no longer needs to produce functional auth/RBAC — no REST query is ever issued against this instance — only enough to start the process and complete the apiserver↔controller connection thatapiserver_resource_hook_fun/OnResourceLocationUpdateddepends on. - Generated only if a pre-built apiserver binary is already present at its conventional output path (e.g.
/tmp/apiserver,make build-server's default). Not an exact mirror of the worker/scheduler conditional: those binaries live in the same meson build directory as the controller and are simple presence-checked; the apiserver is a separate Go build (make build-server,src/frontend/Makefile) with its own output path, sodeploy-ha.shdoes not invokemake build-serveritself — building the apiserver remains the caller's prerequisite, exactly like the existing "run/xce-build controllerfirst if stale" convention in SKILL.md's Prerequisites section, just naming a different build command. This preserves the same "caller opts in by having already built the optional component" semantics the worker/scheduler conditional relies on for backward compatibility — a caller who never runsmake build-serversees no apiserver step generated, same as one who never builds worker/scheduler today. start-all.sh: always generated (independent of which optional components exist for a given deploy). Runsstart.shfor the controller node(s), waits on Step 5's existing health-check, then runs whichever ofstart-worker.sh/start-scheduler.sh/start-apiserver.shwere actually generated — skipping any that weren't — with the scheduler started at-vvv(verbosity 3) specifically so task 4.1'sTRACE/tlog_debug3line is actually emitted (see that component's Load-bearing operational requirement).- Does not attempt HA/multi-instance apiserver deployment — single instance only, sufficient for this feature's verification needs.
Dependencies
- Inbound:
xce-test-resourcesskill (P0) — the primary consumer motivating this extension; also available to any otherxce-deploy-hacaller by construction (same reusability posture as the existing worker/scheduler steps). - Outbound:
make build-server(P0), a running controller (P0),deploy-ha.sh's existing Step 3/Step 5 (P0).
Contracts: Service [ ] / API [ ] / Event [ ] / Batch [x] / State [ ]
Batch / Job Contract
- Trigger:
deploy-ha.shinvoked normally (apiserver/start-all.shgeneration happens automatically as part of Step 3, not a separate opt-in flag the caller must pass);start-all.shthen invoked directly by thexce-test-resourcesskill, or by an operator. - Input / validation: target controller host:port, a deploy/scratch directory (same as existing
xce-deploy-hainputs — no new required parameters). - Output / destination: a running apiserver process connected to the controller, logs under the scratch directory (same convention as
xce-deploy-ha'slogs/) — this is the log task 6.4 greps for Requirement 5's trace line. - Idempotency & recovery: each invocation deploys fresh into a caller-specified scratch directory, matching
xce-deploy-ha's existing "every invocation is a fresh, self-contained deploy" convention — no in-place upgrade semantics.
Implementation Notes
- Integration: exact minimal-boot config shape is confirmed during implementation, not fully pinned here — see research.md Risks; scope is narrower than originally planned since functional auth/RBAC is no longer required.
- Validation:
start-all.sh's own controller health-check gate (reusing Step 5) must complete before it proceeds to start worker/scheduler/apiserver — the skill must additionally confirm the apiserver process has registered with the controller (connection-level, not HTTP-listener) before injecting and grepping for Requirement 5's trace line. - Risks: apiserver startup/config requirements were previously this design's least-verified part; narrowed by dropping the REST/auth requirement, but boot-time config shape is still unconfirmed until implementation.
Test Tooling / Bash
xce-test-resources skill
| Field | Detail |
|---|---|
| Intent | Deploy controller+scheduler+apiserver, inject one or more resource values via resupdate, verify all three layers, report PASS/FAIL, clean up |
| Requirements | 3.1, 3.2, 4.1, 4.2, 4.3, 5.1, 5.2, 5.4, 6.1, 6.2, 6.3, 6.4, 6.5 |
Responsibilities & Constraints
- Deploy controller, scheduler, and apiserver into a fresh scratch directory by invoking
xce-deploy-ha'sdeploy-ha.sh(Step 3) then its generatedstart-all.sh— no manual per-component sequencing lives in this skill;start-all.shalready handles controller→health-check→scheduler/apiserver ordering (see thexce-deploy-haapiserver step +start-all.shcomponent above). - Join a synthetic worker and inject one or more resource values via
resupdate(default set: the 7 names Requirement 1 adds, plus a couple of already-supported names for regression coverage). - Query the controller (
resval), and tail/grep the scheduler daemon's and apiserver's own log files (bounded wait each) for the trace lines their respective resval-update handlers now emit; compare all three against the injected value and attribute any mismatch or timeout to a specific layer (Requirement 4.3's distinct-reporting requirement generalizes to all three layers). - Emit a single machine-parseable VERDICT line, following
xce-test-ha's existing convention (e.g.[xce-test-resources] VERDICT resources=7 layers_checked=3 mismatches=0 result=PASS), plus the usual terminal PASS/FAIL oracle line. - Clean up all deployed processes and runtime/data state (PIDs, sockets, controller/scheduler data directories) unconditionally — on PASS, on FAIL, and on a mid-deployment abort alike — unless the operator passes a "keep running" flag (Requirement 6.5; see Error Handling's Deployment failure entry for why this must cover the abort path too, not just a completed run). Log files under the scratch directory are always preserved, independent of the "keep running" flag — this is a deliberate divergence from
xce-deploy-ha's owncleanup.sh(which wipeslogs/*.log), because the Monitoring section below promises logs survive "for post-run inspection," and this feature's own trace-log verdict evidence (the exact scheduler/apiserver lines a FAIL was based on) would otherwise be unrecoverable after a completed run.
Dependencies
- Inbound: none (top-level entry point, invoked directly by an operator).
- Outbound:
xce-deploy-ha(P0, extended with the apiserver step +start-all.sh),xce_testworker/client contexts (P0), the scheduler daemon's and apiserver's own log files (P0, via the two new trace-log additions).
Contracts: Service [ ] / API [ ] / Event [ ] / Batch [x] / State [ ]
Batch / Job Contract
- Trigger:
/xce-test-resources [resource=value ...](defaults to Requirement 1's gap-resource set if none given). - Input / validation: optional list of
resource=valuepairs; optional scratch directory; optional "keep running" flag. - Output / destination: per-layer pass/fail detail (injected vs. observed value, per resource, per layer) plus one VERDICT line and the standard terminal PASS/FAIL exit code, matching
xce-test-ha's reporting contract closely enough to be immediately familiar to this codebase's engineers. - Idempotency & recovery: safe to re-run — fresh scratch directory per run (
xce-deploy-haconvention), no shared mutable state across runs.
Implementation Notes
- Integration: invokes
deploy-ha.shthen its generatedstart-all.sh, which already gates scheduler/apiserver startup on the controller health-check internally — this skill does not re-implement that sequencing. - Integration: registers cleanup via a
trap(or equivalent) as soon asstart-all.shis invoked, not just at the end of a successful run — deliberately stronger thanxce-deploy-ha's own convention (a separate, manually-invokedcleanup.sh), because Requirement 6.5 requires cleanup-by-default on every path including a mid-deployment abort (see Error Handling). - Validation: the scheduler and apiserver checks are both a bounded-wait poll of a live log file (no replay, no step/continue) — poll with a short interval up to a generous timeout, consistent with
xce-test-ha's existing soak-test conventions; a match found before timeout short-circuits the wait. - Risks: timing-sensitive (bounded waits for controller→scheduler and controller→apiserver propagation); use generous, explicit timeouts, not tight polling that could flake.
Error Handling
Error Strategy
This feature is a verification tool, not a production service — "errors" are primarily reportable verification failures (a layer did not observe the injected value) rather than exceptions to recover from mid-run.
Error Categories and Responses
- Unrecognized resource name (Requirement 1.2):
resupdatereports a clear per-name error, sourced from its own follow-upxcecli_resval()verification query (see Components — the controller's update RPC reply cannot carry this signal); the skill treats this as a run-level failure with the specific rejected name(s) named in its report. - Layer mismatch (Requirements 3.2, 4.2, 5.2): the skill reports which layer(s) failed, with both the injected and observed value, per Requirement 6.3.
- Layer unreachable (e.g. apiserver never came up, or the expected trace line never appears in the scheduler/apiserver log before the bounded wait elapses): treated the same as a mismatch for verdict purposes (FAIL, layer identified, "timeout" distinguished from "mismatch" in the report), not a silent skip.
- Deployment failure (controller/scheduler/apiserver fails to start): the skill aborts before attempting injection, with a clear "deployment failed at step X" message. Cleanup still runs on this path — this is a deliberate strengthening of
xce-deploy-ha's precedent, not a mirror of it:xce-deploy-ha'sdeploy-ha.shhas no trap-based cleanup and leaves already-started processes running on a mid-deploy failure, requiring the caller to run its separatecleanup.shmanually (confirmed againstsrc/tools/deploy-ha.sh). Requirement 6.5 promises cleanup by default regardless of outcome, unless the operator explicitly requested the deployment be left running — a deployment failure is not that request, so the skill installs a trap (or equivalent) covering every deployment step from the first process start onward, not just the post-injection verification path. - Custom host-attrs resource name false-MISMATCH (known residual risk, not fully closed): the default resource set avoids every host-attrs-derived name (
type/model/cpuf/maxcpus/maxmem/maxswap/maxtmp/ndisks—worker.c'sN_HOST_ATTRS) specifically to route around this; the System Flows section above documents the nested-!-escape fix and why it closes the controller-layer check for that default set. If an operator passes a CUSTOMresource=valueargument naming a host-attrs resource, the controller-layer check will still report a false MISMATCH — not because propagation is wrong, but because that resource is reset to baseline the instant the worker session ends, and no code path today re-checks it before that reset for an arbitrary caller-supplied name. This is a known, documented gap (tasks.md task 6.2), not silently swallowed: a genuine fix needs either a C-levelxce_testchange (no REPL path fromworkercontext toclientcontext without an equivalent disconnect) or a bash coprocess restructuring to keep the worker connected across every layer's check — out of scope for the current implementation. The skill should warn (not silently mismatch) when a caller-supplied resource name matches a known host-attrs name.
Monitoring
Not applicable — this is an on-demand, operator-invoked CLI tool, not a running service. Logs are written under the run's scratch directory, following xce-deploy-ha/xce-test-ha's existing logs/ convention, for post-run inspection.
Testing Strategy
- Unit Tests:
resval_typeresolve.c's type-resolution fallback — 4 cases: a currently-known numeric name (e.g.mem) unchanged; a currently-known string name (type) unchanged; an unrecognized name defaults to numeric; an unrecognized name with-t stringoverride resolves to string — directly compilable and testable in isolation since the extracted module has no networking, REPL, ortest.c-global dependency (unliketest.citself, which cannot be white-box included — see File Structure Plan). The skill's verdict-aggregation/comparison step (Requirement 6) is implemented as a self-contained function over (resource, injected value, per-layer observed value) records specifically so its negative path — a genuine per-layer mismatch correctly producing an overall FAIL naming the right layer — can be verified the same way, with synthetic records, without requiring a live deployment. - Integration Tests: the skill's own live run against a real controller+scheduler+apiserver deployment is this feature's primary integration/e2e coverage, following this codebase's established precedent for
xce-test-ha-class tooling (soak/verification tools in this project are validated live, not viameson test()). At minimum, one live run per implementation change verifying: (1) a previously-missing resource name (e.g.r15m) is now accepted and correctly injected, (2) all three layers' trace lines report the correct value within the bounded wait, (3) a genuinely-unregistered resource name included in the same batch is reported as a specific, clearly-identified rejection (not silently dropped) and correctly drives the run's overall verdict to FAIL, per this design's Error Handling. - E2E Path: the full flow in System Flows' sequence diagram, run end-to-end at least once with the default resource set before this feature is considered done.