myxce/.kiro/specs/scheduler-core/brief.md
Rongsong Shen dfd8f5b342
docs(scheduler-core): commit accumulated brief/requirements/roadmap edits
These edits (three-thread picker correction in brief.md, record_dir
path-validation and kvmgr-audit requirements in requirements.md,
OPCode_schedAdmin folded into the peer-authentication-hardening
backlog item in roadmap.md) were made during earlier requirements
review and design work but left uncommitted. Reviewed for accuracy
against the completed implementation — all still correct, persisting
as-is.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 03:19:14 +00:00

168 lines
8 KiB
Markdown

# Brief: scheduler-core
## Problem
The XCE scheduler has a plugin architecture but critical foundational gaps:
1. **Job selection policy is hardcoded** — `sched_job_picker()` in `scheduler.c:39` uses the literal string `"xce_sched_fcfs"`. The picker runs in a dedicated thread with `jbuf` cbuf communication. No way to swap policies without recompilation.
2. **Monolithic scheduler tied to controller I/O** — `xce_scheduler_t` holds both scheduling state and controller API context (`apictx`, `cluster_cfg`). No way to replay recorded events for debugging or test scheduling policies without a live cluster.
3. **No picker plugin interface** — the existing plugin framework has `job_elect_fn` and `job_alloc_fn`, but no defined contract for how the picker receives state-change events, manages sessions, or communicates decisions back. The FCFS picker is tightly coupled to the scheduler internals.
4. **No project-level scheduling** — `xce_sched_project_t` is just `{ char* name }`. Projects have no resource ownership, quota, or scheduling policy.
Operators need configurable scheduling policies, and developers need a way to debug/tune scheduling behavior without a live cluster.
## Current State
### What exists
- **Plugin framework** (`plugin.h`, `plugin.c`): `xce_sched_plugin_t` with 9 hooks + `job_elect_fn` / `job_alloc_fn`
- **FCFS job picker** (`fcfs.c`): dedicated `jobpicker` thread. Iterates queues by priority, jobs by (priority desc, queue_time asc). Communicates via `jbuf` cbuf.
- **Scheduling loop** (`scheduler.c`): pick job → allocate → pend or dispatch → next
- **Controller integration** (`handler.c`): receives capnp push/notify → enqueues `xce_sched_update_request_t` → `sched_update()` dequeues. Decisions published via `sched->apictx`.
- **Select expression memoization** (`select_eval_ctx_t`): per-session AST caching
### What's missing
- Pluggable job picker with defined interface contract
- Configurable picker name (no recompilation)
- Core/driver separation: scheduling logic decoupled from controller I/O
- Simulation mode: replay recorded events, virtual clock, deterministic execution
- Runtime recording for debugging/tuning
- State snapshot/restore for replay efficiency
## Desired Outcome
### 1. Pluggable Job Picker
The core scheduling loop calls `picker->next_job()` to get the next schedulable job, with zero knowledge of how jobs are selected. The picker retains today's dedicated thread, communicating with the allocator thread via the existing `jbuf` cbuf (superseded by the full three-thread producer/consumer contract in requirements.md Req 1 AC 3-4 — this brief predates that decision).
Picker plugin interface (`xce_job_picker_iface_t`):
| Category | Hooks | Purpose |
|----------|-------|---------|
| Lifecycle | `init`, `fini` | Setup and teardown |
| State-change | `on_queue_update`, `on_job_push`, `on_job_state_change`, `on_job_update` | Called when events arrive |
| Session | `begin_session`, `next_job`, `end_session` | Scheduling round lifecycle |
| Post-allocation | `on_dispatch`, `on_pend` | After allocation attempt |
| Optional | `on_config`, `on_job_rusage` | Advanced pickers (NULL if unused) |
### 2. Core/Driver Separation
Scheduler core is a passive, event-driven engine with zero controller I/O dependencies. The driver connects it:
| Mode | Driver | Input | Output |
|------|--------|-------|--------|
| Normal | Controller-connected | capnp push/notify → core events | Core decisions → controller |
| Simulation | Replay file | `.replay` capnp binary stream → core events | Core decisions → log |
- **Mode-agnostic core**: no mode flag, no mode-specific code paths
- **Virtualized clock**: `xce_core_now(core)` with two-base + speed formula
- **Discrete event-driven virtual clock**: jumps to next event timestamp
- **Snapshot + delta replay**: load snapshot, replay delta events
- **Deterministic guarantee**: same initial state + same events = same decisions
- **Runtime recording control**: xceadmin `scheduler record start/stop`
### 3. Single Builtin Picker
`builtin_fcfs` internally implements queue-priority + FCFS job ordering — replicates today's behavior.
Per-queue picker overrides supported.
### Queue vs Project — Roles
| | Queue | Project |
|---|---|---|
| **Role** | Scheduling mechanism | Resource ownership |
| **Question** | HOW are jobs ordered? | WHO owns resources? |
| **Key attributes** | Priority, limits, time windows, per-queue picker override, preemption config | Quota, usage, hierarchy (future) |
| **Cardinality** | 1 job → 1 queue | 1 job → 1 project |
## Approach
**Event-Driven Core with Pluggable Picker + Dual-Mode Driver**:
### Core scheduling loop
```c
picker->begin_session(picker);
while ((job = picker->next_job(picker)) != NULL) {
if (try_allocate(job) > 0) {
picker->on_dispatch(picker, job->jobid);
on_decision(driver_ctx, DISPATCH, job);
} else if (can_preempt(job)) {
on_decision(driver_ctx, PREEMPT, job, victims);
picker->on_pend(picker, job->jobid);
} else {
on_decision(driver_ctx, PEND, job);
picker->on_pend(picker, job->jobid);
}
}
picker->end_session(picker);
```
Core knows nothing about project/queue/job ordering. `next_job()` hides all selection.
### Clock virtualization
```c
uint64_t xce_core_now(core) {
wall_ns = clock_gettime(CLOCK_MONOTONIC);
elapsed = wall_ns - core->real_clock_base;
return core->virtual_clock_base + (elapsed * speed_num) / speed_den;
}
```
Normal: bases=0, speed=1/1 → returns wall clock. Simulation discrete: speed=0/1 → frozen at event ts.
### What stays unchanged
- Plugin framework hooks (config, queue, job, worker, state_change, commit, election, prepare_allocation, rusage) — preserved
- Allocation logic — unchanged (in scheduler-allocation sibling spec)
- Controller communication in driver — extended, not replaced
### What's new
- `xce_job_picker_iface_t` — pluggable picker with full hook specification
- `xce_sched_core_t` / `xce_sched_driver_t` / `xce_sched_sim_driver_t`
- `xce_core_event_t` — typed event interface
- `xce_core_decision_cb_t` — decision callback
- `.replay` capnp binary stream format + snapshot/restore
- `xce_core_now()` — virtualized clock accessor
- `builtin_fcfs` — single builtin picker
- `job_pick_plugin` on scheduler + per-queue override
- xceadmin recording commands
## Scope
### In
- Pluggable job picker interface with full hook specification
- Single builtin picker (`builtin_fcfs`)
- Configurable picker plugin with per-queue override
- Core/driver separation: passive core, normal driver, simulation driver
- Virtualized clock (`xce_core_now()` with two-base + speed formula)
- Discrete event-driven virtual clock
- `.replay` capnp binary stream format (HEADER + SNAPSHOT + events + DECISIONS)
- State snapshot/restore
- Deterministic replay guarantee
- Runtime recording control via xceadmin commands
- Data structures: `xce_sched_core_t`, `xce_sched_driver_t`, `xce_sched_sim_driver_t`, `xce_core_event_t`, `xce_job_picker_iface_t`
- Unit tests: picker hook behavior, simulation replay, snapshot/restore, deterministic verification
- HA soak regression for picker changes
### Out
- Allocation iterators and parjob allocator (→ scheduler-allocation)
- Preemption policy and SIGSTP pipeline (→ scheduler-preemption)
- New picker policies beyond builtin_fcfs
- Hierarchical project resource quotas (→ project-resource-ownership)
- Dead field on `xce_sched_job_t`
## Sibling Specs
- **scheduler-allocation**: Iterator-driven allocation, parjob allocator, resreq cache
- **scheduler-preemption**: Preemption policy, victim selection, SIGSTP pipeline
## Constraints
- **C (gnu17)** — no C++, no external libraries beyond existing
- **Cancel-safe** — `next_job()` has pthread_cancel cancellation points
- **Session isolation** — picker state reset on begin_session
- **Backward compatible** — existing FCFS behavior unchanged
- **Deterministic replay** — same state + events → same decisions
- **Mode-agnostic core** — no mode flag, no mode-specific code paths
- **No wall-clock time in core** — all time through xce_core_now()