design.md's own "Open Questions/Risks" list (items 5 and 6) still described the OPCode_schedAdmin admission gap and the tlog activation gap as open/ deferred, even though both were fixed earlier in this session -- marked FIXED with pointers to the actual change. research.md's schedAdmin decision record and its Risks & Mitigations bullet got the same treatment (SUPERSEDES note, historical record preserved rather than rewritten). tasks.md's 2026-08-07 GO/NO-GO follow-up list gained a status-update line: follow-up 1 (tlog) is now fixed, follow-up 2 (fcfs.c/builtin_fcfs.c traceability) is only partially closed (design.md text was already correct; tasks 2.1-2.6 still lack _Verified: lines -- flagged, not fixed here since I have no fresh verification evidence for those tasks). roadmap.md's scheduler-core summary line corrected to attribute each gap to the right source (2026-08-07 validate-impl vs today's separate design-review pass) instead of conflating them.
794 lines
68 KiB
Markdown
794 lines
68 KiB
Markdown
# Technical Design: scheduler-core
|
||
|
||
## Overview
|
||
|
||
**Purpose**: This feature replaces the scheduler's hardcoded FCFS job-selection logic and its hard-wired coupling to the live controller with a pluggable picker interface and a passive, mode-agnostic scheduling core. The core is driven by either a normal-mode driver (live controller) or a simulation-mode driver (recorded `.replay` file), and both modes are required to produce byte-identical scheduling decisions given identical input — this is the spec's central, testable invariant.
|
||
|
||
**Users**: Cluster operators configure job-picking policy without a rebuild (Req 2) and gain a `xceadmin scheduler record`/`dump` workflow. Scheduler developers gain a `--sim`/`--interactive` simulation driver to debug and tune scheduling policy against recorded production traces without a live cluster (Req 5).
|
||
|
||
**Impact**: `xce_scheduler_t` (today: scheduling state + `apictx` + `cluster_cfg` in one struct, `scheduler.c`) splits into `xce_sched_core_t` (pure scheduling state, zero I/O) plus `xce_sched_driver_t` (normal, controller-attached) and `xce_sched_sim_driver_t` (simulation, replay-attached). The hardcoded `"xce_sched_fcfs"` string at `scheduler.c:43` is replaced by a configurable, plugin-registered picker. A new controller opcode (`OPCode_schedAdmin`) and a new xceadmin command group (`scheduler record`/`scheduler dump`) are added as pass-through admin plumbing.
|
||
|
||
### Goals
|
||
- Job-picking policy configurable per-scheduler and per-queue without recompilation, with `builtin_fcfs` shipping as the zero-behavior-change default (Req 1-4).
|
||
- Scheduling core with zero controller I/O, wall-clock, or filesystem dependencies, driven identically by a normal or simulation driver (Req 5 AC 1-8).
|
||
- Runtime recording of live scheduling sessions to a self-contained `.replay` file, and a simulation driver + interactive REPL that replays it deterministically (Req 5 AC 9-22).
|
||
- Normal↔sim equivalence as a verifiable regression test: record a live session, replay it, diff the decisions — zero divergence (Req 5 AC 7.1).
|
||
|
||
### Non-Goals
|
||
- New job-picking policies beyond `builtin_fcfs` (interface only; sibling work).
|
||
- Allocation iterators / `try_allocate()` internals (`scheduler-allocation`).
|
||
- Preemption policy / victim selection (`scheduler-preemption`) — this design defines `PreemptDecision`'s **wire shape** only (needed for `Req 5 AC 5`'s decision table to compile), not preemption logic.
|
||
- Hierarchical project resource quotas (`project-resource-ownership`).
|
||
- REPL state mutation, reverse execution, or scripting (explicitly deferred in Req 5 AC 22).
|
||
|
||
## Boundary Commitments
|
||
|
||
### This Spec Owns
|
||
- `xce_job_picker_iface_t` and the `builtin_fcfs` implementation. As-built: a new `src/scheduler/src/plugins/builtin_fcfs.c` implements the interface; `plugins/fcfs.c` was deliberately left unchanged (still `job_elect_fn`-only) as the live exercise target for `plugin.c`'s legacy compat wrapper (see Components — BuiltinFcfsPicker).
|
||
- `xce_sched_core_t`: all scheduling state, the picker/allocator thread pair, the driver↔core protocol (`core_push_events`, `core_go`, `on_decision`), and `xce_core_now()` clock virtualization.
|
||
- `xce_sched_driver_t` (normal-mode): controller event translation, decision publishing, and recording control (start/stop/status, periodic snapshot).
|
||
- `xce_sched_sim_driver_t` (simulation-mode): `.replay` file loading, discrete virtual clock, `--sim-verify`/`--sim-alt-events`/`--sim-output`, and the interactive REPL.
|
||
- The `.replay` capnp wire format (`ReplayHeader`, `SnapshotEntry`, `ReplayEvent`, `ReplaySession`, `ReplayDecision`, and their payload sub-structs) in `src/schema/controller.capnp`.
|
||
- The `OPCode_schedAdmin` request/reply pair and its **pass-through** dispatch case in the controller's `myreq_handler` (`src/controller/cluster.c`), plus its accompanying `src/controller/testsuite/` coverage — this spec's only footprint outside `src/scheduler/`.
|
||
- `xceadmin scheduler record start|stop|status` and `xceadmin scheduler dump <FILE>` (new `src/xceadmin/src/xceadmin/scheduler.py`).
|
||
- `job_pick_plugin` (global + per-queue) and `project_name` (per-queue) fields; `xce_sched_project_t` scaffolding (name + priority only — no quota logic).
|
||
|
||
### Out of Boundary
|
||
- `try_allocate()` and the allocation iterator (`scheduler-allocation` owns the implementation; this spec only calls it from the allocator thread, unchanged signature).
|
||
- `can_preempt()` and victim-selection semantics (`scheduler-preemption` owns the algorithm; this spec owns only `PreemptDecision`'s wire shape, see Data Models).
|
||
- Hierarchical project quota, `resource_quota`/`resource_used`/`parent_project` on `xce_sched_project_t` (`project-resource-ownership`).
|
||
- Any controller state or persistence for recording (`OPCode_schedAdmin` is explicitly a stateless pass-through — the controller commits nothing, owns no WAL entry for it).
|
||
- REPL write-side mutation, semantic snapshot diff in xceadmin (both explicitly deferred, Req 5 AC 22).
|
||
|
||
### Allowed Dependencies
|
||
- Existing plugin framework (`plugin.h`/`plugin.c`) — `xce_job_picker_iface_t` is a new plugin category registered through the same `plugin_register`/`key_find` mechanism, not a parallel system.
|
||
- Existing select-expression evaluator (`resreq.h`, `select_eval_ctx_t`) — used as-is by the allocator thread; the core does not re-implement or wrap it.
|
||
- libxce data-structure subset only inside the core: `ordq`, `dataq`, `cbuf`, `xlist`, `zarray`, `kvmgr`, `rtti` (Req 5 AC 3). The core must **not** link `netio`, `delay`, `xceapi`, or `idgen`.
|
||
- `xcecli_ctx_t` API (`xcecli_jobdispatch`, `xcecli_set_pend_reason`, `xcecli_preempt`) — normal-mode driver only, never referenced from `xce_sched_core_t` or `xce_sched_sim_driver_t`.
|
||
- `xcecommon.wire`/`xcecommon.schema`/`xceadmin.discovery` (Python) — the new `xceadmin/scheduler.py` module reuses these exactly as `push.py` does; it does not open its own socket or reload the schema.
|
||
|
||
### Revalidation Triggers
|
||
- Any change to `xce_job_picker_iface_t`'s hook signatures — every picker plugin (present and future) must be re-verified.
|
||
- Any change to `jbuf` cbuf capacity away from 1 — invalidates the determinism argument in Req 5 AC 7; `scheduler-allocation` and `scheduler-preemption` must both re-verify ordering assumptions.
|
||
- Any change to the `.replay` capnp schema's message set or `ReplayDecision.body` union — `scheduler-preemption`'s `PreemptDecision` consumer and any external tooling reading `.replay` files must re-check compatibility.
|
||
- Any change to `OPCode_schedAdmin`'s request/reply shape — `xceadmin.scheduler` module must be updated in lockstep (same repo, same PR).
|
||
|
||
## Architecture
|
||
|
||
### Existing Architecture Analysis
|
||
|
||
Today, `xce_scheduler_t` (`src/scheduler/include/scheduler.h`) owns scheduling state (`jobs`, `queues`, `workers`, `plugins` kvmgrs), controller I/O (`apictx: xcecli_ctx_t`, `cluster_cfg: xce_cluster_config_t`), and three threads (`jobpicker`, `allocator`, main) wired through three `cbuf`s (`wbuf` cap 1, `jbuf` cap `njobs`≡1 today, `decisions` cap `ndecisions`≡5). `main.c`'s loop calls `sched_update` → `sched_begin_session` → `sched_go` → drains `decisions` cbuf with a hardcoded `switch (decision->op)` → `sched_end_session`. The picker plugin (`fcfs.c`) is selected by a literal string (`scheduler.c:43`) and implements the old `xce_sched_plugin_t->job_elect_fn` hook, tightly coupled to `sched->queues`/`sched->jobs` kvmgrs directly.
|
||
|
||
**Preserved without change**: the three-thread/three-cbuf pattern (`wbuf`→picker, `jbuf`→allocator, `decisions`→driver); `plugin_register`/`key_find` plugin lookup; `select_eval_ctx_t` per-session memoization; `sched_queue_is_ready()`/`sched_job_is_ready()` (currently stub `return 1;` in `scheduler.c`, called as-is by both today's `fcfs_elect_job` and the new `builtin_fcfs`).
|
||
|
||
**What changes**: `xce_scheduler_t` splits along the controller-I/O seam into `xce_sched_core_t` (no `apictx`/`cluster_cfg`) and `xce_sched_driver_t` (holds them). The picker plugin category becomes `xce_job_picker_iface_t`, replacing `job_elect_fn` (deprecated with a compatibility wrapper, Req 5 AC "Migration from old job_elect_fn").
|
||
|
||
### Architecture Pattern & Boundary Map
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph DriverLayer[Driver Layer]
|
||
NormalDriver[Normal Driver]
|
||
SimDriver[Simulation Driver]
|
||
Controller[Live Controller]
|
||
ReplayFile[Replay File]
|
||
Controller --> NormalDriver
|
||
ReplayFile --> SimDriver
|
||
end
|
||
|
||
subgraph CoreLayer[Scheduler Core]
|
||
Core[Scheduler Core State]
|
||
Picker[Job Picker Plugin]
|
||
Allocator[Allocator Thread]
|
||
Core --> Picker
|
||
Picker --> Allocator
|
||
end
|
||
|
||
NormalDriver -->|core_push_events core_go| Core
|
||
SimDriver -->|core_push_events core_go| Core
|
||
Core -->|on_decision| NormalDriver
|
||
Core -->|on_decision| SimDriver
|
||
|
||
NormalDriver -->|xcecli| Controller
|
||
SimDriver -->|write| RecordFile[Sim Output Replay File]
|
||
|
||
subgraph AdminPath[Admin Control Path]
|
||
XceAdmin[xceadmin scheduler CLI]
|
||
SchedAdminOp[OPCode_schedAdmin dispatch]
|
||
XceAdmin --> SchedAdminOp
|
||
SchedAdminOp --> Controller
|
||
end
|
||
Controller -->|forward request-reply| NormalDriver
|
||
```
|
||
|
||
**Architecture Integration**:
|
||
- **Selected pattern**: Event-sourced core with a swappable driver (ports-and-adapters, applied narrowly — the core is the hexagon, the two drivers are the only adapters). Selected in requirements.md as the only pattern that makes normal↔sim equivalence a structural property rather than a discipline (see `research.md`, Architecture Pattern Evaluation).
|
||
- **Domain boundaries**: Core owns scheduling *decisions*; drivers own I/O translation *only* — no scheduling logic in either driver. `OPCode_schedAdmin` dispatch is a third, thin adapter (controller-side) that never touches core or driver state directly, only forwards bytes.
|
||
- **Existing patterns preserved**: three-thread/cbuf pipeline, plugin registration, select-expression memoization.
|
||
- **New components rationale**: `SimDriver` and the `.replay` format are net-new (no existing analog); `SchedAdminOpcode` is new but structurally mirrors the existing `OPCode_upgradeConfig` case (see `research.md`).
|
||
- **Steering compliance**: no new wall-clock/network dependency inside `src/scheduler/`'s core files (`ha-async-io.md`'s async-I/O discipline extended to a synchronous core by design, not violated — the core has no I/O at all); `.capnp` used for the new wire format per `structure.md`'s "Cap'n Proto for wire formats" convention.
|
||
|
||
### Technology Stack
|
||
|
||
| Layer | Choice / Version | Role in Feature | Notes |
|
||
|-------|------------------|------------------|-------|
|
||
| Scheduler core / drivers | C (gnu17) | `xce_sched_core_t`, `xce_sched_driver_t`, `xce_sched_sim_driver_t` | No new language/runtime; existing `src/scheduler/` toolchain |
|
||
| Wire format | Cap'n Proto (existing `controller.capnp`) | `.replay` stream, `OPCode_schedAdmin` request/reply | Extends the existing schema file, not a new one |
|
||
| Admin CLI | Python / `click` (existing xceadmin stack) | `scheduler record`/`scheduler dump` commands | New module (`xceadmin/scheduler.py`) in the existing package, same `pycapnp` schema loader |
|
||
| Build | Meson ≥ 1.5 (existing) | New `.c` files added to `src/scheduler/meson.build`; testsuite entries | No new build system |
|
||
|
||
### Dependency Direction
|
||
|
||
`Picker Plugin Interface → Scheduler Core → {Normal Driver, Simulation Driver} → {Controller (xcecli), Replay File I/O}`. Controller-side `SchedAdminOpcode` dispatch depends only on the capnp schema (shared, not owned by the core) and forwards to the driver without depending on `xce_sched_core_t` internals. xceadmin's `scheduler.py` depends on `xcecommon`/`xceadmin.discovery` only — never on any C header. Each layer imports only from layers to its left; the core never imports driver headers (enforced by the "Allowed Dependencies" list above — no `xcecli_ctx_t`/`netio` symbol may appear in `core.c`).
|
||
|
||
## File Structure Plan
|
||
|
||
### Directory Structure
|
||
|
||
**As-built** (updated 2026-08-07 after implementation; see tasks.md's `## Implementation Notes` for the full rationale behind each split — all were 800-line-file-cap splits made during implementation, not scope changes):
|
||
|
||
```
|
||
src/scheduler/
|
||
├── include/
|
||
│ ├── scheduler.h # unchanged Phase 1; Phase 2 shrinks to xce_scheduler_t compat shim
|
||
│ ├── plugin.h # + xce_job_picker_iface_t (Req 1 AC 2), deprecation of job_elect_fn
|
||
│ ├── core.h # xce_sched_core_t, xce_core_event_t, decision cb type
|
||
│ ├── driver.h # xce_sched_driver_t (normal-mode)
|
||
│ ├── sim.h # xce_sched_sim_driver_t -- built as sim.h, not sim_driver.h (task 11.1, documented deviation)
|
||
│ ├── recording.h # record start/stop/status types, ReplayEvent/ReplaySession writer contract
|
||
│ ├── repl.h # REPL hook wrappers consumed by sim_loop.c -- no-op unless sim->interactive
|
||
│ ├── decision.h # pre-existing; extended with XCE_DECISION_PREEMPT (task 4.4)
|
||
│ └── sched/types.h # + job_pick_plugin, project_name on xce_sched_queue_t (Req 2)
|
||
├── src/
|
||
│ ├── scheduler.c # sched_job_picker(): job_elect_fn != NULL delegates to the legacy loop, NULL runs the generic xce_job_picker_iface_t loop
|
||
│ ├── core.c # xce_sched_core_init/fini, jbuf(cap=1)/decisions cbuf plumbing
|
||
│ ├── core_events.c # xce_core_push_events() -- split out of core.c (800-line cap; task 4.3)
|
||
│ ├── core_session.c # xce_core_begin_session/xce_core_end_session -- split out of core.c (task 4.4)
|
||
│ ├── core_clock.c # xce_core_now(), tlog format integration -- the ONE file allowed to call clock_gettime() (task 4.5)
|
||
│ ├── driver.c # normal-mode driver: capnp<->core event translation, decision publishing, deferred admin-request handling
|
||
│ ├── recording.c # record start/stop/status, header/snapshot writes
|
||
│ ├── recording_events.c # ReplayEvent writes -- split out of recording.c (task 9.2/9.3)
|
||
│ ├── recording_session.c # ReplaySession begin/end writes -- split out of recording.c (task 9.2/9.3)
|
||
│ ├── repl.c # REPL command loop core (execution control, Req 5 AC 22)
|
||
│ ├── repl_break.c # REPL breakpoints + event/decision hook wrappers -- split out of repl.c (task 12.2)
|
||
│ ├── repl_info.c # REPL state-inspection commands -- split out of repl.c (task 12.2)
|
||
│ ├── repl_params.c # REPL snapshot/simulation-parameter commands -- split out of repl.c (task 12.3)
|
||
│ ├── repl_internal.h # shared repl_*.c internal declarations (task 12.2)
|
||
│ ├── snapshot.c # save_snapshot()/load_snapshot() orchestration, picker rebuild
|
||
│ ├── snapshot_wire.c # SnapshotData capnp encode/decode -- split out of snapshot.c (task 8.1)
|
||
│ ├── snapshot_load.c # snapshot restore application -- split out of snapshot.c (task 8.1)
|
||
│ ├── sim.c # simulation startup (Req 5 AC 13): replay header/snapshot load, virtual clock seed
|
||
│ ├── sim_loop.c # simulation event loop + REPL hook call sites (Req 5 AC 14/18)
|
||
│ ├── sim_record.c # --sim-output ReplayEvent/ReplaySession writes -- split out of sim_loop.c (task 11.5)
|
||
│ ├── sim_verify.c # --sim-verify decision comparator -- split out of sim_loop.c (task 11.4)
|
||
│ ├── handler.c # unchanged — capnp push/notify enqueue into pend_updates; scheduler_req_handler dispatches SchedAdminRequest
|
||
│ ├── plugin.c # + picker plugin registration category, + job_elect_fn compat
|
||
# wrapper (adapts old-style job_elect_fn-only plugins to
|
||
# xce_job_picker_iface_t, logs a deprecation warning at register time)
|
||
│ └── plugins/
|
||
│ ├── builtin_fcfs.c # the native xce_job_picker_iface_t default picker (Req 3) -- NEW file, see note below
|
||
│ └── fcfs.c # UNCHANGED -- still registers job_elect_fn only; kept as the live exercise target for plugin.c's compat wrapper (task 1.2). NOT rewritten as originally planned here; see BuiltinFcfsPicker component notes.
|
||
├── testsuite/
|
||
│ ├── test_picker_iface.c, test_picker_fcfs.c, test_picker_fcfs_session.c, test_picker_wiring.c,
|
||
│ │ test_picker_config_fields.c, test_picker_core_loop_boundary.c, test_picker_pendreason_factory_audit.c,
|
||
│ │ test_plugin_picker_registration.c # picker interface + builtin_fcfs + registration/compat-wrapper coverage
|
||
│ ├── test_core_events.c, test_core_session.c, test_core_full_cycle.c, test_core_isolation.c,
|
||
│ │ test_core_jbuf_capacity_pin.c # core contract + determinism-pin coverage
|
||
│ ├── test_clock.c # xce_core_now() two-base+speed formula
|
||
│ ├── test_driver_translate.c, test_driver_decision_cb.c, test_driver_session_ordering.c
|
||
│ ├── test_recording_path_validation.c, test_record_lifecycle.c, test_admin_request.c
|
||
│ ├── test_snapshot_save_load.c
|
||
│ ├── test_sim_startup.c, test_sim_event_loop.c, test_sim_isolation.c, test_sim_alt_events.c,
|
||
│ │ test_sim_verify.c, test_sim_record_output.c, test_sim_equivalence.c
|
||
│ └── test_repl_execution_control.c, test_repl_breakpoints_inspection.c, test_repl_snapshot_params.c,
|
||
│ test_repl_equivalence.c
|
||
src/controller/
|
||
├── cluster.c # + case OPCode_schedAdmin (Req 5 AC 10)
|
||
└── testsuite/
|
||
└── test_sched_admin_dispatch.c # standby rejection, disconnected-scheduler, timeout, success round-trip
|
||
# (name differs from this plan's original test_sched_admin.c; same coverage,
|
||
# registered in controller_test.c same as existing opcode tests)
|
||
src/schema/
|
||
└── controller.capnp # + SchedAdminRequest/Reply, ReplayHeader/.../ReplayDecision + payload structs (see Data Models);
|
||
# + WalCatchupEntry.payload/batchPayloads switched Data->List(UInt8) (bug-1505, follow-up
|
||
# outside this spec's boundary, same codec-crash class as SnapshotData.internalState/bug-1504)
|
||
src/xceadmin/src/xceadmin/
|
||
├── cli.py # + @cli.group() scheduler; nested @scheduler.group() record with
|
||
# start/stop/status commands, plus @scheduler.command() dump (thin layer only)
|
||
└── scheduler.py # record_start/stop/status(), dump() — mirrors push.py's pattern
|
||
src/xceadmin/tests/
|
||
├── test_scheduler.py # mock-TLS-server tests for record start/stop/status + dump parsing
|
||
└── test_cli_scheduler_mock.py # additional CLI-level mock-server coverage (task 10.6, beyond this plan's original file list)
|
||
```
|
||
|
||
### Modified Files
|
||
- `src/scheduler/src/scheduler.c` — Phase 1: hardcoded picker string removed; Phase 2: `sched_job_picker()` branches `job_elect_fn != NULL` (legacy plugins, delegates to their own self-contained loop) vs `NULL` (native `xce_job_picker_iface_t` pickers, generic per-session loop) — `scheduler.c` was not retired; it remains the picker-dispatch and allocator-loop home.
|
||
- `src/scheduler/src/plugins/fcfs.c` — **as-built: left unchanged**, not rewritten. `plugins/builtin_fcfs.c` (new file) implements `xce_job_picker_iface_t` and is the registered default (`schedobj.c`); `fcfs.c` keeps registering `job_elect_fn` only, deliberately, as the live exercise target for `plugin.c`'s legacy compat wrapper (task 1.2) — see BuiltinFcfsPicker component notes for the full rationale.
|
||
- `src/scheduler/src/main.c` — decision-drain `switch` replaced by `on_decision` callback registration (Req 5 AC 6); also gained the `--sim`/`--interactive`/`--record-dir`/`--sim-*` CLI surface (tasks 9.1, 11.1).
|
||
- `src/controller/cluster.c` — one new `case OPCode_schedAdmin:` block, structurally mirroring `OPCode_upgradeConfig` (primary-gate, delegate, explicit reply every path).
|
||
- `src/schema/controller.capnp` — additive only: new `OPCode_schedAdmin` enum value, new structs, two new union members on existing `Request.body`/`RequestReply.data` unions (ordinals `@31`/`@19` respectively — next available, verified against current schema in `research.md`).
|
||
- `src/xceadmin/src/xceadmin/cli.py` — one new `@cli.group() scheduler`, containing a nested `@scheduler.group() record` (with `start`/`stop`/`status` commands) plus a sibling `@scheduler.command() dump` — matching the literal CLI syntax `xceadmin scheduler record start`/`xceadmin scheduler dump`, not 4 flat commands.
|
||
|
||
## System Flows
|
||
|
||
### Normal-Mode Scheduling Round (driver ↔ core)
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant Ctrl as Controller
|
||
participant Drv as Normal Driver
|
||
participant Core as Scheduler Core
|
||
participant Pick as Picker Thread
|
||
participant Alloc as Allocator Thread
|
||
|
||
Ctrl->>Drv: capnp push or notify
|
||
Drv->>Drv: buffer into pend_updates
|
||
Drv->>Core: core_push_events
|
||
Core->>Pick: state change hooks, synchronous, picker idle
|
||
Drv->>Core: core_go, non blocking
|
||
Core->>Pick: signal via wbuf
|
||
Pick->>Pick: begin_session then next_job loop
|
||
Pick->>Alloc: send jobid via jbuf per job
|
||
Alloc->>Alloc: try_allocate then can_preempt
|
||
Alloc->>Drv: send decision via decisions cbuf
|
||
Drv->>Ctrl: on_decision publishes dispatch, pend reason, or preempt
|
||
Alloc->>Drv: end of session marker via decisions cbuf
|
||
Drv->>Core: core_end_session
|
||
```
|
||
|
||
Key decision not obvious from the diagram: decisions publish to the controller **immediately per-decision**, not batched at session end (Req 5, "Decision publishing") — the drain loop in the driver thread interleaves `cbuf_get`/`on_decision` one at a time.
|
||
|
||
### `OPCode_schedAdmin` Recording Control (admin path)
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant CLI as XceAdmin SchedulerModule
|
||
participant Ctrl as Controller ClusterDispatch
|
||
participant Sched as Scheduler NormalDriver
|
||
|
||
CLI->>Ctrl: schedAdmin request, action plus recordFile
|
||
Ctrl->>Ctrl: check ha_is_primary, reject standby
|
||
Ctrl->>Sched: forward via scheduler endpoint, five second timeout
|
||
Sched->>Sched: validate recordFile, reject or resolve under record_dir
|
||
Sched->>Sched: save_snapshot then write header and snapshot entry
|
||
Sched-->>Ctrl: schedAdmin reply, ok plus recording state and counters
|
||
Ctrl-->>CLI: relay reply to caller
|
||
```
|
||
|
||
Key decision not obvious from the diagram: the controller is a pure pass-through — it does not interpret `SchedAdminRequest` fields beyond the opcode dispatch, and owns no recording state (Boundary Commitments, "This Spec Owns" — recording logic lives entirely in the scheduler driver).
|
||
|
||
## Requirements Traceability
|
||
|
||
| Requirement | Summary | Components | Interfaces | Flows |
|
||
|---|---|---|---|---|
|
||
| 1.1 | Core loop calls `next_job()` only, no ordering logic | SchedulerCore, JobPicker | `xce_job_picker_iface_t.next_job` | Normal-Mode Round |
|
||
| 1.2 | Picker hook categories (lifecycle/state/session/post-alloc/optional) | JobPicker | `xce_job_picker_iface_t` | — |
|
||
| 1.3 | Three-thread cbuf model | SchedulerCore | `wbuf`/`jbuf`/`decisions` cbufs | Normal-Mode Round |
|
||
| 1.4 | Picker/allocator/main-loop pseudocode contract | SchedulerCore, JobPicker | `core_push_events`/`core_go` | Normal-Mode Round |
|
||
| 1.5 | Cancel-safe, session-aware picker | JobPicker | `begin_session`/`end_session`/`next_job` | — |
|
||
| 1.6 | Replace hardcoded picker string | SchedulerCore | `plugin_register`/`key_find` | — |
|
||
| 1.7 | Preserve thread architecture, decision publish via callback | SchedulerCore | `on_decision` cb | Normal-Mode Round |
|
||
| 2.1 | `job_pick_plugin` on `xce_scheduler_t`, default `builtin_fcfs` | SchedulerCore | — | — |
|
||
| 2.2 | `job_pick_plugin` per-queue override | JobPicker | `xce_sched_queue_t.job_pick_plugin` | — |
|
||
| 2.3 | `project_name` on queue | SchedulerCore | `xce_sched_queue_t.project_name` | — |
|
||
| 2.4 | Config-driven picker name, no rebuild | SchedulerCore | scheduler config | — |
|
||
| 2.5 | Unregistered plugin fails loud | SchedulerCore | `plugin_register`/`key_find` | — |
|
||
| 2.6 | Picker contract documented (cancel-safe, session-aware) | JobPicker | `xce_job_picker_iface_t` | — |
|
||
| 3.1 | `builtin_fcfs` auto-registered as default | BuiltinFcfsPicker | plugin registration | — |
|
||
| 3.2 | Hook-by-hook FCFS implementation | BuiltinFcfsPicker | `xce_job_picker_iface_t` | — |
|
||
| 3.3 | Private `ordq_t` state, no core queries in `next_job` | BuiltinFcfsPicker | — | — |
|
||
| 3.4 | Ordering equivalence with today's `fcfs_elect_job` | BuiltinFcfsPicker | — | — |
|
||
| 3.5 | Dedicated picker thread retained, cancel points | BuiltinFcfsPicker, SchedulerCore | `next_job`/`cbuf_get(wbuf)` | Normal-Mode Round |
|
||
| 3.6 | `end_session` no-op, `pendJobs` carries over | BuiltinFcfsPicker | — | — |
|
||
| 4.1 | Job-picking order regression check | BuiltinFcfsPicker | — | test_picker_fcfs.c |
|
||
| 4.2 | Existing scheduler unit tests pass unmodified | SchedulerCore | — | testsuite/ |
|
||
| 4.3 | HA soak — no job loss/divergence | SchedulerCore | — | `/xce-test-ha` |
|
||
| 5.1 | Mode-agnostic core, no mode flag | SchedulerCore | `xce_sched_core_init()` | — |
|
||
| 5.2 | Passive event-driven interface, zero I/O deps | SchedulerCore | `core_push_events`/`on_decision` | — |
|
||
| 5.3 | Virtualized clock, two-base+speed formula | SchedulerCore | `xce_core_now()` | — |
|
||
| 5.4 | Core event types (typed payloads) | SchedulerCore | `xce_core_event_t` | — |
|
||
| 5.5 | Decision output types (DISPATCH/PEND/PREEMPT) | SchedulerCore | `xce_decision_t` | — |
|
||
| 5.6 | Decision output callback | SchedulerCore, NormalDriver | `xce_core_decision_cb_t` | Normal-Mode Round |
|
||
| 5.7 | Deterministic guarantee + normal↔sim equivalence | SchedulerCore | `jbuf` cap=1, hash-iteration audit, PRNG seed | test_sim_equivalence.c |
|
||
| 5.8 | Driver pending-event queue | NormalDriver | `pend_updates` dataq | Normal-Mode Round |
|
||
| 5.9 | Controller integration (capnp translate, publish decisions) | NormalDriver | `xcecli_ctx_t` | Normal-Mode Round |
|
||
| 5.10 | Runtime recording control (`OPCode_schedAdmin`) | NormalDriver, SchedAdminOpcode, XceAdminSchedulerCLI | `SchedAdminRequest`/`Reply` | Admin Path |
|
||
| 5.11 | Periodic snapshot during recording | NormalDriver | `record_snapshot_interval` | — |
|
||
| 5.12 | Session response time metrics | SchedulerCore | `SessionSummary` | — |
|
||
| 5.13 | Simulation mode startup (load header+snapshot) | SimDriver | `.replay` load sequence | — |
|
||
| 5.14 | Discrete event-driven virtual clock | SimDriver | `--sim-speed` | — |
|
||
| 5.15 | `--sim-verify` deterministic replay verification | SimDriver | `decision_eq` | — |
|
||
| 5.16 | `--sim-alt-events` what-if branching | SimDriver | alt-events file | — |
|
||
| 5.17 | Simulation isolation (no controller deps) | SimDriver | `xce_sched_sim_driver_t` | — |
|
||
| 5.18 | `ReplaySession` as session boundary | SimDriver | event loop | — |
|
||
| 5.19 | `--sim-output` simulation record file | SimDriver | `.replay` writer | — |
|
||
| 5.20 | `.replay` capnp stream format | ReplayFormat | schema structs | — |
|
||
| 5.21 | Snapshot serialization / picker rebuild | SchedulerCore, ReplayFormat | `save_snapshot`/`load_snapshot` | — |
|
||
| 5.22 | Interactive REPL | SimDriver | command loop | — |
|
||
| 6.1 | Resreq immutability | SchedulerCore | — | — |
|
||
| 6.2 | Pend reason factory (`make_pendreason()`) | SchedulerCore | `lib/preason.c` | — |
|
||
| 6.3 | Pointer-identity fast path | SchedulerCore | — | — |
|
||
| 6.4 | Select expression memoization preserved | SchedulerCore | `select_eval_ctx_t` | — |
|
||
| 6.5 | Snapshot forward-compatible with object sharing | ReplayFormat | `SnapshotData` | — |
|
||
|
||
## Components and Interfaces
|
||
|
||
| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies (P0/P1) | Contracts |
|
||
|-----------|--------------|--------|---------------|---------------------------|-----------|
|
||
| JobPicker (interface) | Scheduler Core | Defines the pluggable job-selection contract | 1.1, 1.2, 1.4, 1.5, 2.2, 2.6 | plugin.h (P0) | Service |
|
||
| BuiltinFcfsPicker | Scheduler Core | Default picker replicating today's FCFS order | 3.1-3.6, 4.1 | JobPicker (P0) | Service |
|
||
| SchedulerCore | Scheduler Core | Passive, mode-agnostic scheduling engine | 1.1, 1.3-1.4, 1.6-1.7, 2.1, 2.3-2.5, 3.5, 4.2-4.3, 5.1-5.7, 5.12, 5.21, 6.1-6.4 | JobPicker (P0), allocation/preemption (P1, sibling specs) | Service, State |
|
||
| NormalDriver | Scheduler Driver | Controller-attached driver, recording control | 5.6, 5.8-5.11 | SchedulerCore (P0), `xcecli_ctx_t` (P0) | Service, Event |
|
||
| SimDriver | Scheduler Driver | Replay-attached driver, virtual clock, REPL | 5.13-5.19, 5.22 | SchedulerCore (P0), ReplayFormat (P0) | Service, State |
|
||
| ReplayFormat | Wire Protocol | `.replay` capnp schema, `SnapshotData` | 5.20, 5.21, 6.5 | `controller.capnp` (P0) | Batch |
|
||
| SchedAdminOpcode | Controller | Pass-through dispatch for `OPCode_schedAdmin` | 5.10 | `cluster.c` `myreq_handler` (P0) | API |
|
||
| XceAdminSchedulerCLI | Tooling | `scheduler record`/`scheduler dump` commands | 5.10 | `xcecommon.wire`/`discovery` (P0) | API |
|
||
|
||
### Scheduler Core
|
||
|
||
#### JobPicker
|
||
|
||
| Field | Detail |
|
||
|-------|--------|
|
||
| Intent | Contract every job-selection strategy implements; hides all ordering logic from the core loop |
|
||
| Requirements | 1.1, 1.2, 1.4, 1.5, 2.2, 2.6 |
|
||
|
||
**Responsibilities & Constraints**
|
||
- Owns iteration order over pending jobs; the core never inspects queue/job internals for ordering.
|
||
- Cancel-safe (`pthread_cancel` points in `next_job()`) and session-scoped (`begin_session` resets iteration state).
|
||
- State-change hooks run synchronously in the driver thread during `core_push_events()`; session hooks run in the picker thread; post-allocation hooks run in the allocator thread (Req 1 AC 3).
|
||
|
||
**Dependencies**
|
||
- Inbound: SchedulerCore — invokes every hook (P0)
|
||
- Outbound: none — picker implementations must not call back into core internals outside the hook parameters they're given
|
||
|
||
**Contracts**: Service [x] / API [ ] / Event [ ] / Batch [ ] / State [ ]
|
||
|
||
##### Service Interface
|
||
```c
|
||
typedef struct {
|
||
int (*init)(void* picker, xce_sched_core_t* core);
|
||
void (*fini)(void* picker);
|
||
int (*begin_session)(void* picker);
|
||
xce_sched_job_t* (*next_job)(void* picker);
|
||
int (*end_session)(void* picker);
|
||
int (*on_dispatch)(void* picker, uint64_t jobid);
|
||
int (*on_pend)(void* picker, uint64_t jobid);
|
||
int (*on_queue_update)(void* picker, xce_sched_queue_t* queue);
|
||
int (*on_job_push)(void* picker, xce_sched_job_t* job);
|
||
int (*on_job_state_change)(void* picker, xce_sched_job_t* job, uint32_t state);
|
||
int (*on_job_update)(void* picker, xce_sched_job_t* job);
|
||
int (*on_config)(void* picker, xce_conf4sched_t* config); /* MAY be NULL */
|
||
int (*on_job_rusage)(void* picker, xce_sched_job_t* job, xce_job_rusages_t* rusages); /* MAY be NULL */
|
||
} xce_job_picker_iface_t;
|
||
```
|
||
- Preconditions: `init()` called exactly once before any other hook; `begin_session()` precedes every `next_job()` sequence.
|
||
- Postconditions: `next_job()` returns NULL exactly when no ready job remains for the current session.
|
||
- Invariants: hook implementations never block indefinitely (picker thread holds no lock the allocator needs); `next_job()` never allocates a job the same session has already returned.
|
||
|
||
**Implementation Notes**
|
||
- Integration: registered via `plugin_register`/`key_find` under the picker plugin category (new `plugin.c` registration table entry).
|
||
- Validation: `test_picker_fcfs.c` feeds hooks directly and asserts `next_job()` order — no live core needed (Req 3 AC 3's testability rationale).
|
||
- Risks: a picker implementation that queries core state directly during `next_job()` (bypassing the hook-fed private state pattern) would still compile but silently reintroduce lock contention — not enforceable at the interface level, called out for code review.
|
||
|
||
#### BuiltinFcfsPicker
|
||
|
||
| Field | Detail |
|
||
|-------|--------|
|
||
| Intent | Default `xce_job_picker_iface_t` implementation; replicates today's `fcfs_elect_job` ordering exactly through the new interface |
|
||
| Requirements | 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 4.1 |
|
||
|
||
**Responsibilities & Constraints**
|
||
- Maintains private state only — `ordq_t queues` (ordered by `queue->priority desc`) and, per queue, an `ordq_t pendJobs` (ordered by `priority desc, queue_time asc`). Never queries `sched->queues`/`sched->jobs` during `next_job()` (Req 3 AC 3).
|
||
- Every hook's behavior is fully specified in requirements.md Req 3 AC 2's hook-by-hook table — not repeated here; this block covers integration and testability, not the algorithm.
|
||
- Auto-registered as `"builtin_fcfs"` and set as the default `job_pick_plugin` at scheduler startup (Req 3 AC 1).
|
||
- `end_session()` is a no-op; undispatched jobs carry over in `pendJobs` to the next round unchanged (Req 3 AC 6).
|
||
|
||
**Dependencies**
|
||
- Inbound: SchedulerCore — invokes every `xce_job_picker_iface_t` hook, same as any picker (P0)
|
||
- Outbound: none — implements the interface, depends on nothing beyond it
|
||
|
||
**Contracts**: Service [x] / API [ ] / Event [ ] / Batch [ ] / State [ ]
|
||
|
||
##### Service Interface
|
||
Implements `xce_job_picker_iface_t` (struct shown under JobPicker, above) with the `fcfs_plugin_data_t` private state described in Responsibilities. No additional public surface beyond the interface itself.
|
||
- Preconditions/Postconditions/Invariants: inherits JobPicker's contract in full; adds no relaxations or extensions.
|
||
|
||
**Implementation Notes**
|
||
- Integration, as-built: implemented in a **new** file, `plugins/builtin_fcfs.c`, registered as `"builtin_fcfs"` and set as the default `job_pick_plugin` (`schedobj.c`) — NOT a rewrite of `plugins/fcfs.c` in place, as originally planned above. `fcfs.c` was deliberately left untouched (still `job_elect_fn`-only, unregistered as default) because task 1.2's `job_elect_fn` compat wrapper needs a real legacy-style plugin to exercise; `fcfs.c` fills that role, and `scheduler.c`'s `sched_job_picker()` dispatch (`job_elect_fn != NULL` vs `NULL`) depends on both variants existing side by side. Functionally equivalent to the original plan (one default picker implementing the new interface, ordering-equivalent to legacy `fcfs_elect_job`); only the file-layout mechanics differ.
|
||
- Validation: `test_picker_fcfs.c` is this component's primary test — feeds `on_queue_update`/`on_job_push`/`on_job_state_change` hooks directly and asserts `next_job()` returns jobs in `(queue priority desc, job priority desc, queue_time asc)` order, matching today's `fcfs_elect_job` (Req 4.1's regression check).
|
||
- Risks: ordering-equivalence regression is the main risk (any drift from today's `fcfs_elect_job` output is a behavior change operators didn't opt into) — mitigated by `test_picker_fcfs.c` asserting against the documented ordering rule directly, plus the HA soak in Req 4.3.
|
||
|
||
#### SchedulerCore
|
||
|
||
| Field | Detail |
|
||
|-------|--------|
|
||
| Intent | Owns all scheduling state; drives the picker/allocator threads; exposes the driver↔core protocol |
|
||
| Requirements | 1.1, 1.3-1.4, 1.6-1.7, 2.1, 2.3-2.5, 3.5, 4.2-4.3, 5.1-5.7, 5.12, 5.21, 6.1-6.4 |
|
||
|
||
**Responsibilities & Constraints**
|
||
- Zero direct dependency on controller communication, capnp, network I/O, filesystem, or wall-clock time (Req 5 AC 2) — enforced by the "Allowed Dependencies" link list, not just convention.
|
||
- `jbuf` cbuf capacity is hardcoded to 1 inside `xce_sched_core_init()` — never a parameter (Req 5 AC 7, pinned against the existing `njobs` FIXME in `scheduler.c:384`).
|
||
- All internal time via `xce_core_now()`; no direct `time()`/`clock_gettime()`/`gettimeofday()` calls anywhere in `core.c`.
|
||
|
||
**Dependencies**
|
||
- Inbound: NormalDriver, SimDriver — both call `core_push_events`/`core_go`, register `on_decision` (P0)
|
||
- Outbound: JobPicker (P0); `try_allocate()`/`can_preempt()` from sibling specs (P1 — signature-stable dependency, no behavior assumption)
|
||
- External: none
|
||
|
||
**Contracts**: Service [x] / State [x]
|
||
|
||
##### Service Interface
|
||
```c
|
||
int xce_sched_core_init(xce_sched_core_t*, xce_core_decision_cb_t, void* driver_ctx, uint64_t prng_seed);
|
||
int xce_core_push_events(xce_sched_core_t*, xce_core_event_t* events, int nevents);
|
||
int xce_core_go(xce_sched_core_t*);
|
||
int xce_core_end_session(xce_sched_core_t*);
|
||
uint64_t xce_core_now(xce_sched_core_t*);
|
||
int xce_sched_core_save_snapshot(xce_sched_core_t*, SnapshotData* out);
|
||
int xce_sched_core_load_snapshot(xce_sched_core_t*, const SnapshotData* in);
|
||
void xce_sched_core_fini(xce_sched_core_t*);
|
||
```
|
||
- Preconditions: `core_push_events()` only valid when no session is active (`session_active == 0`); `core_go()` returns `-EINVAL` if a session is already active. `xce_sched_core_save_snapshot()`/`xce_sched_core_load_snapshot()` are likewise only valid when `session_active == 0` — both return `-EBUSY` if called mid-session, rather than silently capturing or restoring torn state. Every caller in this design (record start, periodic checkpoint, simulation startup) already invokes them between sessions by construction; the `-EBUSY` return is a defensive contract check, not a path any correct caller should hit.
|
||
- Postconditions: after `on_decision` delivers the end-of-session `NULL` marker, `session_active` is guaranteed to transition to 0 on the subsequent `core_end_session()` call.
|
||
- Invariants: given identical `(config, event sequence)`, `core_push_events`+`core_go` produce an identical decision sequence (Req 5 AC 7) — the whole point of the design.
|
||
|
||
##### State Management
|
||
- State model: scheduling state (jobs/queues/workers/resources/resvals/users/projects) held in `kvmgr`s inside `xce_sched_core_t`, mirroring today's `xce_scheduler_t` layout minus `apictx`/`cluster_cfg`.
|
||
- Persistence & consistency: no persistence inside the core itself — `save_snapshot()`/`load_snapshot()` are the only serialization boundary, and only the driver decides when to invoke them.
|
||
- Concurrency strategy: three-thread cbuf pipeline unchanged from today; clock atoms (`virtual_clock_base`, `real_clock_base`, `speed_num`, `speed_den`) are lock-free `_Atomic uint64_t`, updated by the driver as a group before each event dispatch.
|
||
|
||
**Implementation Notes**
|
||
- Integration: `core_push_events`/`core_go`/`on_decision` are the only surface either driver touches — no driver reaches into `xce_sched_core_t` internals.
|
||
- Validation: `test_core_events.c` (contract-level), `test_sim_equivalence.c` (record→replay→diff, Req 5 AC 7.1 — the spec's central regression test).
|
||
- Risks: the `kvmgr` iteration-order audit (Req 5 AC 7, "Stabilized hash table iteration") is a Phase 2 deliverable with real but bounded scope — jobs/queues/users/projects only (per `research.md` and requirements.md's explicit Phase 2 task).
|
||
|
||
### Scheduler Driver
|
||
|
||
#### NormalDriver
|
||
|
||
| Field | Detail |
|
||
|-------|--------|
|
||
| Intent | Connects the core to a live controller; owns recording control |
|
||
| Requirements | 5.6, 5.8, 5.9, 5.10, 5.11 |
|
||
|
||
**Responsibilities & Constraints**
|
||
- Translates capnp push/notify into `xce_core_event_t`, buffers between sessions in `pend_updates` (existing dataq swap mechanism).
|
||
- Publishes each decision immediately via `xcecli_jobdispatch`/`xcecli_set_pend_reason`/`xcecli_preempt` as it drains from `decisions` cbuf — not batched at session end.
|
||
- Recording is boolean-gated: zero file I/O, zero extra allocation when `recording == false` (Req 5 AC 10, Performance).
|
||
- Validates `recordFile` before ever calling `fopen()`: reject `/` or `..`, resolve under `record_dir`, confirm containment via `realpath()` (Req 5 AC 10, "Record file path validation" — this design inherits, not re-derives, that requirement).
|
||
- `record_dir` is provisioned exactly one way: a `--record-dir DIR` scheduler startup CLI flag (default `/var/log/xce/`), parsed alongside the existing normal-mode arguments in `main.c` — never a capnp field on any request path (`SchedAdminRequest` included), so nothing reachable over the wire can redirect where recordings are written. Matches the existing `--sim-*` flag family's provisioning style (CLI-only, not config-pushed).
|
||
|
||
**Dependencies**
|
||
- Inbound: `main.c` (process entry point) (P0)
|
||
- Outbound: SchedulerCore (P0), `xcecli_ctx_t` (P0)
|
||
- External: `SchedAdminOpcode` dispatch (P1 — receives forwarded requests, not a caller relationship)
|
||
|
||
**Contracts**: Service [x] / Event [x]
|
||
|
||
##### Service Interface
|
||
```c
|
||
typedef struct {
|
||
xce_sched_core_t core;
|
||
xcecli_ctx_t apictx;
|
||
xce_cluster_config_t cluster_cfg;
|
||
dataq_t* pend_updates[2];
|
||
char* record_dir; /* from --record-dir CLI flag at startup; default "/var/log/xce/";
|
||
never settable via any request-path field */
|
||
char* record_file_path;
|
||
FILE* record_file;
|
||
bool recording;
|
||
uint64_t record_event_count;
|
||
uint64_t record_snapshot_interval;
|
||
uint64_t record_start_ts;
|
||
} xce_sched_driver_t;
|
||
|
||
int sched_driver_run(xce_sched_driver_t*, xce_conf4sched_t*, xce_cluster_config_t*, void* handler);
|
||
int sched_driver_record_start(xce_sched_driver_t*, const char* record_file /* bare filename or NULL */);
|
||
int sched_driver_record_stop(xce_sched_driver_t*);
|
||
int sched_driver_record_status(xce_sched_driver_t*, SchedAdminReply* out);
|
||
```
|
||
- Preconditions: `record_start` fails with `-EALREADY`-equivalent if already recording; `record_stop` fails if not recording.
|
||
- Postconditions: on `record_start` success, `ReplayHeader` + initial `SnapshotEntry` are durably on disk before the reply is sent.
|
||
- Invariants: `recording == true` iff `record_file != NULL`.
|
||
|
||
##### Event Contract
|
||
- Subscribed events: capnp push/notify from the controller (existing handler.c path, unchanged).
|
||
- Published events: decisions to the controller via `xcecli_ctx_t`; `SchedAdminReply` back through the forwarding path on recording-control requests.
|
||
- Ordering / delivery guarantees: decisions publish in allocator output order; recording writes events/decisions in the same order they cross the driver thread.
|
||
|
||
**Implementation Notes**
|
||
- Integration: `main.c`'s mode-selection branch calls `sched_driver_run()` when `--sim` is absent.
|
||
- Validation: recording lifecycle tested against a real filesystem (temp dir) — start/stop/status round trip, path-validation rejection cases, disk-full simulated via a read-only mount point.
|
||
- Risks: `OPCode_schedAdmin` ships with the same primary/standby gate as `OPCode_upgradeConfig` and no additional RBAC/auth check — a deliberate scope decision (project owner, 2026-08-02), not an oversight. Strengthening authentication for this opcode (and `config push`, if it also lacks one) is deferred to a follow-up spec. See Open Questions/Risks.
|
||
|
||
#### SimDriver
|
||
|
||
| Field | Detail |
|
||
|-------|--------|
|
||
| Intent | Connects the core to a `.replay` file; owns virtual clock, verification, and the REPL |
|
||
| Requirements | 5.13-5.19, 5.22 |
|
||
|
||
**Responsibilities & Constraints**
|
||
- Contains no `xcecli_ctx_t`, `xce_cluster_config_t`, capnp socket, or controller RPC of any kind (Req 5 AC 17) — enforced by the struct definition itself having no such fields.
|
||
- Discrete virtual clock: jumps directly to the next event's timestamp, never a fixed tick (Req 5 AC 14).
|
||
- REPL is a single `fgets` loop in the same thread as the simulation loop — no REPL thread (Req 5 AC 22, Implementation note).
|
||
|
||
**Dependencies**
|
||
- Inbound: `main.c` (P0, when `--sim` is present)
|
||
- Outbound: SchedulerCore (P0), ReplayFormat (P0)
|
||
- External: none
|
||
|
||
**Contracts**: Service [x] / State [x]
|
||
|
||
##### Service Interface
|
||
```c
|
||
typedef struct {
|
||
xce_sched_core_t core;
|
||
FILE *replay_file, *alt_events_file, *record_file;
|
||
uint64_t sim_speed_num, sim_speed_den;
|
||
bool verify;
|
||
bool interactive;
|
||
} xce_sched_sim_driver_t;
|
||
|
||
int sched_sim_driver_run(xce_sched_sim_driver_t*, /* CLI arguments struct */ const void* args);
|
||
```
|
||
- Preconditions: replay file's second message must be a `SnapshotEntry` (rejected otherwise, Req 5 AC 20).
|
||
- Postconditions: with `--sim-output`, every session cycle appends exactly one `ReplaySession` to the output file before the next `ReplayEvent` batch begins.
|
||
- Invariants: REPL `step`/`continue` to completion produces byte-identical output to batch `--sim` (Req 5 AC 22, "Deterministic replay is preserved").
|
||
|
||
##### State Management
|
||
- State model: same `xce_sched_core_t` as normal mode, loaded via `load_snapshot()` instead of live events.
|
||
- Persistence & consistency: `--sim-output` writes a new independent `.replay` file; never mutates the input file.
|
||
- Concurrency strategy: single-threaded driver loop around the same core thread pair; REPL blocks the driver loop intentionally (no wall-clock deadline in simulation).
|
||
|
||
**Implementation Notes**
|
||
- Integration: `main.c`'s mode-selection branch calls `sched_sim_driver_run()` when `--sim` is present.
|
||
- Validation: `test_sim_equivalence.c` drives this component directly (record via NormalDriver's test harness, replay via this one, diff).
|
||
- Risks: none beyond what's already captured under SchedulerCore (the determinism guarantee is a core property; SimDriver only needs to not violate it, e.g. by never bypassing `core_push_events`/`core_go`).
|
||
|
||
### Wire Protocol
|
||
|
||
#### ReplayFormat
|
||
|
||
| Field | Detail |
|
||
|-------|--------|
|
||
| Intent | `.replay` capnp stream schema and `SnapshotData` |
|
||
| Requirements | 5.20, 5.21, 6.5 |
|
||
|
||
**Responsibilities & Constraints**
|
||
- Additive-only changes to `controller.capnp` — reuses existing structs (`Conf4Sched`, `SchedJobData`, `WorkerData`, `ResourceData`, `ResValList`, `QueueConf`, `SchedJobRusages`) wherever they already exist (verified in `research.md`); only defines new structs where none exist.
|
||
- `SnapshotData` field shapes track the *actual current* C struct field sets (e.g. `xce_sched_user_t` is `{name}` only today, `xce_sched_project_t` is `{name}` only today) — the schema does not invent fields the C structs don't have yet.
|
||
|
||
**Dependencies**
|
||
- Inbound: SchedulerCore (`save_snapshot`/`load_snapshot`), NormalDriver (recording writer), SimDriver (reader/writer) (all P0)
|
||
- Outbound: none
|
||
- External: none
|
||
|
||
**Contracts**: Batch [x]
|
||
|
||
##### Batch / Job Contract
|
||
- Trigger: `record start` (normal mode) or `--sim-output` (simulation mode) write a `.replay` stream incrementally; `--sim`/`xceadmin scheduler dump` read one.
|
||
- Input / validation: first message must be `ReplayHeader`; second must be `SnapshotEntry` (Req 5 AC 20) — both enforced at read time, not just documented.
|
||
- Output / destination: append-only file; `record stop` / clean simulation completion writes `ReplayEvent{endSession}`.
|
||
- Idempotency & recovery: a write failure mid-message truncates cleanly to the last complete message (Req 5 AC 10, error handling) — no partial-message corruption possible because each message is a full capnp frame written atomically at the `fwrite` level.
|
||
|
||
**New capnp structs** (additive to `controller.capnp`; `ReplayHeader`/`ReplayEvent`/`ReplaySession`/`ReplayDecision`/`SessionSummary` wrapper structs already fully specified in requirements.md Req 5 AC 20 — not repeated here):
|
||
|
||
```capnp
|
||
# New event payloads (JOBS_PUSH/WORKERS_PUSH/etc reuse existing Conf4Sched,
|
||
# SchedJobData, WorkerData, ResourceData, ResValList, QueueConf, SchedJobRusages —
|
||
# verified present in controller.capnp, research.md).
|
||
|
||
struct JobStateChangeEvent $C.mapname("job_state_change_event_t") {
|
||
jobid @0: UInt64;
|
||
oldState @1: UInt32;
|
||
newState @2: UInt32;
|
||
exitCode @3: Int32;
|
||
}
|
||
|
||
struct JobUpdateEvent $C.mapname("job_update_event_t") {
|
||
jobid @0: UInt64;
|
||
changedFields @1: UInt32; # bitmask: JOB_F_PRIORITY, JOB_F_QUEUE, JOB_F_RESREQ
|
||
job @2: Job; # full updated record; reuses existing Job struct
|
||
}
|
||
|
||
# New decision payloads for ReplayDecision.body (Req 5 AC 5).
|
||
# DISPATCH/PEND reuse existing JobAlloc/JobPendReason; PREEMPT is new
|
||
# (wire shape owned here per Boundary Commitments; victim-selection
|
||
# semantics owned by scheduler-preemption — see research.md).
|
||
#
|
||
# AC 5's field list for DISPATCH is "jobid, workerid, allocations[]"
|
||
# (singular workerid). JobAlloc already generalizes this correctly for
|
||
# multi-host jobs: JobAlloc.jobid covers jobid, JobAlloc.allocs is
|
||
# List(HostAlloc) with each HostAlloc.host as that allocation's worker
|
||
# id and HostAlloc.allocs: List(ResAlloc) as its resource/amount pairs
|
||
# -- verified against controller.capnp, no new fields needed. jobid
|
||
# lives ONLY in JobAlloc.jobid (not duplicated at DispatchDecision
|
||
# level) to avoid two unsynchronized sources of truth for the same job.
|
||
|
||
struct DispatchDecision $C.mapname("sched_dispatch_decision_t") {
|
||
alloc @0: JobAlloc;
|
||
}
|
||
|
||
struct PendDecision $C.mapname("sched_pend_decision_t") {
|
||
jobid @0: UInt64;
|
||
reason @1: JobPendReason;
|
||
}
|
||
|
||
struct PreemptVictim $C.mapname("preempt_victim_t") {
|
||
jobid @0: UInt64;
|
||
workerid @1: Text;
|
||
}
|
||
|
||
struct PreemptDecision $C.mapname("sched_preempt_decision_t") {
|
||
jobid @0: UInt64;
|
||
victims @1: List(PreemptVictim);
|
||
reason @2: JobPendReason;
|
||
}
|
||
|
||
# SnapshotData — full core-state checklist from requirements.md Req 5 AC 21,
|
||
# given a concrete wire shape. Reuses existing List(...) payload structs
|
||
# for domain objects already carried elsewhere on the wire; the two
|
||
# scheduler-internal-only caches (shared-resource tracking, unsatisfiable-
|
||
# resreq cache) and session/PRNG state have no existing external contract,
|
||
# so they are versioned opaque blobs the core alone reads/writes -- no
|
||
# consumer outside save_snapshot()/load_snapshot() ever inspects their
|
||
# internal shape (WHAT not HOW: the boundary that matters is "round-trips
|
||
# through the core, opaque to everyone else").
|
||
|
||
struct SnapshotData $C.mapname("sched_snapshot_data_t") {
|
||
config @0: Conf4Sched;
|
||
jobs @1: SchedJobData;
|
||
queues @2: List(QueueConf);
|
||
workers @3: WorkerData;
|
||
resources @4: ResourceData;
|
||
resvals @5: ResValList;
|
||
users @6: List(SchedUser);
|
||
projects @7: List(SchedProject);
|
||
prngSeed @8: UInt64;
|
||
internalState @9: Data; # shared-resource tracking + unsatisfiable-resreq
|
||
# cache + session/iterator state, core-opaque
|
||
internalVersion @10: UInt32; # format tag for internalState, so a future
|
||
# incompatible internal layout fails loud
|
||
# instead of misinterpreting old bytes
|
||
}
|
||
|
||
struct SchedUser $C.mapname("xce_sched_user_t") {
|
||
name @0: Text; # matches xce_sched_user_t today (name only) --
|
||
# extend in lockstep if xce_sched_user_t gains fields
|
||
}
|
||
|
||
struct SchedProject $C.mapname("xce_sched_project_t") {
|
||
name @0: Text;
|
||
priority @1: Int32;
|
||
}
|
||
```
|
||
|
||
**Implementation Notes**
|
||
- Integration: `save_snapshot()`/`load_snapshot()` are the only functions that construct/consume `SnapshotData`; no other component touches `internalState`'s bytes.
|
||
- Validation: as-built, round-trip coverage for every new struct lives in `src/controller/testsuite/test_e2e_capnp.c` (task 7.5), not a dedicated `test_replay_format.c` — this repo's existing convention is one shared capnp round-trip test file per schema, and this spec's new structs were added to it rather than starting a second one; `internalVersion` mismatch is a dedicated negative test within it.
|
||
- Risks: `internalState`'s opaque-blob design trades wire-level inspectability for implementation flexibility — acceptable because AC 21 only requires the *core* to round-trip correctly, never that `xceadmin scheduler dump` render its internals meaningfully (dump can print `internalState`'s byte length and `internalVersion` only).
|
||
|
||
### Controller
|
||
|
||
#### SchedAdminOpcode
|
||
|
||
| Field | Detail |
|
||
|-------|--------|
|
||
| Intent | Pass-through dispatch for `OPCode_schedAdmin` in the controller's request handler |
|
||
| Requirements | 5.10 |
|
||
|
||
**Responsibilities & Constraints**
|
||
- Structurally mirrors the existing `OPCode_upgradeConfig` case (`cluster.c:670-705`) for the primary-gate/delegate/explicit-reply shape: primary-only gate via `ha_is_primary()`, explicit reply on every path, 5s timeout.
|
||
- Owns zero recording state — every field of `SchedAdminRequest`/`Reply` passes through untouched.
|
||
- **Blocking-wait admission (hardened 2026-08-19, design review follow-up)**: like `OPCode_upgradeConfig` (in-process commit-barrier wait, `COMMIT_WAIT_RESERVED`-admitted), this case now also admission-gates its blocking wait. Originally (accepted, project owner, 2026-08-02) this case blocked its `vtask` handler thread for up to 5s waiting on a cross-process reply from the scheduler daemon while holding `c->sched.lock`, with no reservation — a boundary case reviewed against `ha-async-io.md`'s vtaskmgr admission rule and accepted on the grounds that `xceadmin scheduler` commands are cluster-administrator-triggered, low-frequency, human-paced traffic, not expected to burst concurrently the way heartbeat/replication traffic does. A retrospective design review flagged this as a real (if low-probability) starvation risk under a burst of concurrent/scripted requests, since each blocked vtask thread occupies a worker for up to 5s and `c->sched.lock` already serializes them one at a time — N concurrent requests could occupy N threads for up to N×5s. Fix: `vtaskmgr_blocking_acquire(&c->vtaskmgr)`/`vtaskmgr_blocking_release(&c->vtaskmgr)` now wrap the ENTIRE forward+wait region (acquired before `mylock_lock(&c->sched.lock)`, released on every exit path) — the same shared budget `commit.c`'s `wait_admit_locked()`/`wait_release_locked()` already gate on (wired via `commit_set_pool(&c->commit, &c->vtaskmgr)`). Admission-exhausted requests fast-reject with `{ok=false, error="server busy, try again"}` before ever touching `c->sched.lock` — never queued. See requirements.md Req 5 AC 10's own Amendment for the full rationale.
|
||
|
||
**Dependencies**
|
||
- Inbound: XceAdminSchedulerCLI, via TLS+capnp (P0)
|
||
- Outbound: NormalDriver (P0, forwards the request)
|
||
- External: none
|
||
|
||
**Contracts**: API [x]
|
||
|
||
##### API Contract
|
||
| Method | Endpoint (opcode) | Request | Response | Errors |
|
||
|--------|--------------------|---------|----------|--------|
|
||
| Request/Reply | `OPCode_schedAdmin` | `SchedAdminRequest` | `SchedAdminReply` | `-EAGAIN` (not primary), `"scheduler not connected"`, `"request timeout"`, `"invalid record file name"`, `"already recording"`, `"not recording"`, `"server busy, try again"` (admission exhausted) (full table: requirements.md Req 5 AC 10) |
|
||
|
||
**Implementation Notes**
|
||
- Integration: one new `case` in `myreq_handler`'s switch (`cluster.c`); no new file.
|
||
- Validation: `test_sched_admin_dispatch.c` (`src/controller/testsuite/`; named differently from this plan's original `test_sched_admin.c`, same coverage) asserting the reply-on-every-path property (standby rejection, disconnected-scheduler rejection, timeout) — mirrors existing `OPCode_upgradeConfig` test coverage style.
|
||
- Risks: no RBAC/auth check is visible for the comparable `OPCode_upgradeConfig` case in `cluster.c` (see `research.md`), and `OPCode_schedAdmin` inherits that same gap by deliberate decision — auth hardening for this opcode is deferred to a follow-up spec, not silently accepted as permanent.
|
||
|
||
### Tooling
|
||
|
||
#### XceAdminSchedulerCLI
|
||
|
||
| Field | Detail |
|
||
|-------|--------|
|
||
| Intent | `xceadmin scheduler record start/stop/status` and `xceadmin scheduler dump <FILE>` |
|
||
| Requirements | 5.10 |
|
||
|
||
**Responsibilities & Constraints**
|
||
- `cli.py` stays a thin `click` layer; all logic lives in the new `scheduler.py` module, mirroring `push.py`'s split.
|
||
- `record_start/stop/status()` go through `xceadmin.discovery.follow_redirects` + `decode_reply` (request/reply opcode, needs redirect-following, unlike `probe_config()`).
|
||
- `dump()` has no network dependency — decodes a local `.replay` file only.
|
||
|
||
**Dependencies**
|
||
- Inbound: operator CLI invocation (P0)
|
||
- Outbound: `xcecommon.wire`/`schema`, `xceadmin.discovery` (P0); SchedAdminOpcode via the wire (P0, for record/status only)
|
||
- External: none
|
||
|
||
**Contracts**: API [x]
|
||
|
||
##### API Contract
|
||
| Method | Endpoint | Request | Response | Errors |
|
||
|--------|----------|---------|----------|--------|
|
||
| CLI | `scheduler record start [FILE]` | `SchedAdminRequest{recordStart}` | `SchedAdminReply` | `XceAdminError` subclasses (connection, redirect, decode) |
|
||
| CLI | `scheduler record stop` | `SchedAdminRequest{recordStop}` | `SchedAdminReply` | same |
|
||
| CLI | `scheduler record status` | `SchedAdminRequest{recordStatus}` | `SchedAdminReply` | same |
|
||
| CLI | `scheduler dump <FILE>` | (local file) | formatted text to stdout | `SchemaNotFoundError`, file-not-found |
|
||
|
||
**Implementation Notes**
|
||
- Integration: `cli.py` imports `record_start`/`record_stop`/`record_status`/`dump` from `scheduler.py`, same import shape as today's `build_cluster_config`/`push_config`.
|
||
- Validation: `test_scheduler.py` — mock-TLS-server tests for the three record commands (mirrors `test_config_push_mock.py`), plus golden-file tests for `dump()` against a small fixture `.replay` file.
|
||
- Risks: none beyond ReplayFormat's (this component only formats/transmits, never interprets scheduling semantics).
|
||
|
||
## Data Models
|
||
|
||
Covered in full under **Components and Interfaces → ReplayFormat** above (new capnp structs) — not duplicated here per the dedup guidance in design-principles.md. `SnapshotData`'s reuse of existing `Conf4Sched`/`SchedJobData`/`WorkerData`/`ResourceData`/`ResValList`/`QueueConf` is the only cross-cutting data-model decision; see `research.md`, "Reusable capnp payload types," for the verification trail.
|
||
|
||
## Error Handling
|
||
|
||
### Error Strategy
|
||
Fail-loud, no silent fallback — consistent with the controller's existing "fail loud, stay alive" design invariant (`src/controller/CLAUDE.md`), applied to the scheduler process at the scope this feature touches.
|
||
|
||
### Error Categories and Responses
|
||
- **Configuration errors** (unregistered picker plugin, Req 2 AC 5): scheduler startup fails with a clear error — no silent fallback to `builtin_fcfs`.
|
||
- **Recording errors** (disk full, permission error, invalid `recordFile`): logged, recording state cleanly torn down (`recording=false`, file closed, `record_file_path` freed), scheduling continues unaffected (Req 5 AC 10, error handling) — recording failure is never fatal to the scheduler process.
|
||
- **Simulation errors** (missing/corrupt replay file, missing initial `SnapshotEntry`): simulation driver fails at startup with a clear message — no partial/best-effort simulation.
|
||
- **Admin-path errors** (`OPCode_schedAdmin`): every failure mode gets an explicit `{ok=false, error=...}` reply — never a dropped request (mirrors the "no silent drop" precedent already established for replication-class messages, `src/controller/CLAUDE.md`'s protocol checklist).
|
||
|
||
### Monitoring
|
||
No new metrics infrastructure — `SessionSummary` (Req 5 AC 12) provides session-duration diagnostics through the existing `.replay` recording path; no separate telemetry system is introduced.
|
||
|
||
## Testing Strategy
|
||
|
||
- **Unit — Picker contract** (`test_picker_fcfs.c`): feed `on_queue_update`/`on_job_push`/`on_job_state_change` hooks directly, assert `next_job()` returns jobs in `(queue priority desc, job priority desc, queue_time asc)` order — verifies Req 3.2-3.4 without a live core.
|
||
- **Unit — Core event contract** (`test_core_events.c`): `core_push_events()` followed by `core_go()` with a stub picker asserts state-change hooks fire exactly once per event, synchronously, before `core_go()` returns — verifies Req 5.1-5.8.
|
||
- **Unit — Clock virtualization** (`test_clock.c`): `xce_core_now()` against the four mode rows in Req 5 AC 3's table (normal, discrete, real-time, fast/slow) — asserts the two-base+speed formula directly, no wall-clock flakiness (fixed `real_clock_base`/mocked `clock_gettime` delta).
|
||
- **Unit — Replay format round-trip** (as-built: `src/controller/testsuite/test_e2e_capnp.c`, not a dedicated `test_replay_format.c` — added to this repo's existing shared capnp round-trip test file instead): encode/decode every new capnp struct (`JobStateChangeEvent`, `JobUpdateEvent`, `DispatchDecision`, `PendDecision`, `PreemptDecision`, `SnapshotData`) — field-for-field equality after round-trip.
|
||
- **Integration — Deterministic replay** (`test_sim_equivalence.c`, Req 5.7/5.7.1): record a synthetic scheduling session through `NormalDriver`'s test harness → replay through `SimDriver` → assert `ReplayDecision` sequences match exactly. This is the design's highest-value test — it directly exercises the central invariant.
|
||
- **Integration — Recording lifecycle** (driver-level, Req 5.10-5.11): `record start` → submit synthetic events → `record stop` → verify file structure (`ReplayHeader`, `SnapshotEntry`, alternating `ReplayEvent`/`ReplaySession`, periodic checkpoints at the configured interval, final `endSession`).
|
||
- **Integration — `OPCode_schedAdmin` dispatch** (`test_sched_admin_dispatch.c`, controller-level; named differently from this plan's original `test_sched_admin.c`): standby rejection, disconnected-scheduler rejection, timeout, and successful forward/reply round trip — mirrors existing `OPCode_upgradeConfig` test coverage.
|
||
- **Integration — xceadmin scheduler CLI** (`test_scheduler.py`): mock-TLS-server tests for `record start/stop/status` (redirect-following, reply decoding) plus golden-file tests for `dump()` against a fixture `.replay` file.
|
||
- **E2E — HA soak regression** (Req 4.3, Req 5 per-phase verification): `/xce-test-ha` sequential and chaotic rounds after each phase, asserting zero new job loss/divergence attributable to this feature — required before each phase merges, not a one-time gate at the end.
|
||
- **E2E — REPL equivalence** (Req 5.22): `--interactive` with `step`/`continue` to completion produces the same decision sequence as batch `--sim` on the same replay file.
|
||
|
||
## Security Considerations
|
||
|
||
_Feature-specific decisions only; baseline RBAC/auth practices are in `.kiro/steering/security.md`._
|
||
|
||
- **`recordFile` path validation** is already fully specified at the requirements level (Req 5 AC 10, "Record file path validation" — reject `/`/`..`, resolve under configured `record_dir`, `realpath()` containment check before `fopen()`). This design's `NormalDriver` and `SchedAdminOpcode` components inherit that requirement directly. `record_dir` itself is provisioned only via a `--record-dir` scheduler startup CLI flag — never from `SchedAdminRequest` or any other request-path field — so the validation boundary can't be redirected by a caller.
|
||
- **`OPCode_schedAdmin` ships with no RBAC/auth check beyond the primary/standby gate — accepted scope decision, deferred hardening.** The comparable `OPCode_upgradeConfig` case has no visible RBAC/auth check in `cluster.c` either (see `research.md`). Project owner decision (2026-08-02): authentication for this opcode is enhanced in a follow-up spec, not blocking this one. Phase 3 ships `OPCode_schedAdmin` at parity with `OPCode_upgradeConfig`'s current (unauthenticated-beyond-primary-gate) posture — this is a known, accepted gap, not a silent one.
|
||
- **`.replay` files may contain sensitive cluster state** (full job/user/resource snapshots) — file permissions on `record_dir` are an operational concern (steering-level, not new to this feature) but worth calling out since this is the first feature to write full scheduler snapshots to disk in a human-decodable format (`xceadmin scheduler dump`).
|
||
|
||
## Performance & Scalability
|
||
|
||
_Feature-specific targets only; general practices are in steering docs._
|
||
|
||
- **Recording overhead**: zero file I/O, zero extra allocation when `recording == false` — a single boolean branch per session (Req 5 AC 10, explicit requirement, inherited here as a design constraint on `NormalDriver`).
|
||
- **Determinism vs throughput**: `jbuf` cbuf capacity is permanently 1 (rendezvous, not a buffer) — this bounds picker→allocator throughput to strictly sequential handoff. This is an intentional trade-off (determinism over pipelining) already made at the requirements level (Req 5 AC 7) and is not revisited here; any future throughput work must find a different mechanism that preserves the ordering guarantee (e.g. deterministic batching with an explicit order tag), not simply raise the cbuf capacity.
|
||
- **Simulation fast mode**: `speed_num=0` means the virtual clock never advances between events at wall-clock cost — `--sim` on a large `.replay` file runs at CPU-bound speed, not wall-clock speed. No specific throughput target is set here (none given in requirements); implementation should avoid introducing any per-event syscall beyond what decoding the capnp stream requires.
|
||
- **`OPCode_schedAdmin` vtask occupancy (hardened 2026-08-19)**: each request blocks its vtask handler thread up to 5s (cross-process wait on the scheduler's reply) while holding `c->sched.lock` — now gated by the same `vtaskmgr_blocking_acquire`/`_release` admission reservation `commit.c`'s commit-wait barrier shares (see `SchedAdminOpcode` component above). Admission-exhausted bursts fast-reject instead of occupying vtask threads; not separately load-tested against a real concurrent burst (unit-tested only, `test_schedadmin_fast_rejects_when_admission_exhausted`), but the mechanism itself is the same one already proven under `test_vtask_admission.c` and production commit-wait traffic.
|
||
|
||
## Open Questions / Risks
|
||
|
||
1. **`OPCode_schedAdmin` authorization mechanism** (P2 — accepted gap, deferred). Ships at parity with `OPCode_upgradeConfig`'s current posture (primary/standby gate only, no RBAC/auth check). Project owner decision (2026-08-02): auth hardening for this opcode is a follow-up spec, not a Phase 3 blocker. Tracked in `.kiro/steering/roadmap.md`'s existing `peer-authentication-hardening` backlog item (updated 2026-08-02 to cover `OPCode_schedAdmin` alongside `OP_upgradeConfig` — same `validate_host()` gate closes both). See Security Considerations and `research.md`.
|
||
2. **`PreemptDecision`'s wire shape is pre-committed ahead of `scheduler-preemption`'s own design** (P1). Flagged as a revalidation trigger in Boundary Commitments; `scheduler-preemption`'s design should explicitly confirm or extend this shape early, not discover it mid-implementation.
|
||
3. **`SnapshotData.internalState` opaque-blob scope** (P2). If a future spec needs `xceadmin scheduler dump` to render shared-resource-tracking or unsatisfiable-resreq-cache contents meaningfully (not just byte length), `internalState` will need to become a structured capnp field instead of opaque `Data` — deferred here because no current requirement asks for it (AC 20's dump description only requires human-readable output for what dump already needs to show, and internals were never asked to be inspectable).
|
||
4. **`xce_sched_core_t`'s `config`/`plugins`/`jobs`/`queues`/`workers`/`users`/`projects` field block is left as `var ...` in requirements.md's data-structure summary** (not fully typed) — Phase 2 implementation must finalize these as concrete `kvmgr`/pointer types when `core.c` is written; this design does not re-type them since requirements.md already commits to "moved from `xce_scheduler_t`" as the authoritative source of truth for their shapes.
|
||
5. ~~**`OPCode_schedAdmin` blocks a vtask handler thread up to 5s per request with no admission reservation**~~ **FIXED 2026-08-19** (design-review follow-up). Now gated by `vtaskmgr_blocking_acquire`/`_release` against the same `c->vtaskmgr` reservation `commit.c`'s commit-wait barrier shares; admission-exhausted requests fast-reject with `{ok=false, error="server busy, try again"}` instead of blocking. See `SchedAdminOpcode` component above and `research.md`'s "SUPERSEDES" note.
|
||
6. ~~**Req 5 AC 3's tlog virtual-clock logging integration is built but never activated**~~ **FIXED 2026-08-19** (design-review follow-up). `sched_sim_driver_start()` (`sim.c`) now calls `xce_core_register_tlog_format()`/`xce_core_set_active_for_logging(&sim->core)` right after `sim_set_virtual_clock()`, exactly at the site this entry identified; `sched_sim_driver_run()` (`sim_loop.c`) deactivates (`xce_core_set_active_for_logging(NULL)`) on all 7 of its return paths to avoid the ASAN stack-use-after-scope hazard this entry flagged — confirmed by fixing a real instance of exactly that hazard, caught live when the new test was added (bug-1598, `test_sim_event_loop.c`'s fixtures switched to `static` storage). No sim-driver fini function was added; the per-return-path deactivation is the shipped mitigation instead.
|
||
7. **`plugins/fcfs.c` vs `plugins/builtin_fcfs.c` split was undocumented in this design until 2026-08-07** (P3 — documentation gap only, now fixed here). Found by `/kiro-validate-impl`: the file-structure and BuiltinFcfsPicker sections above originally said `fcfs.c` would be rewritten; the actual implementation added a new `builtin_fcfs.c` and deliberately left `fcfs.c` untouched (task 1.2's compat-wrapper exercise target). Functionally correct and confirmed live (`builtin_fcfs` is the registered default), just wasn't reflected here until this update. Tasks 2.1-2.6 in tasks.md still carry no `_Verified:` annotations (unlike every other task group) — worth backfilling for traceability.
|