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>
8 KiB
Brief: scheduler-core
Problem
The XCE scheduler has a plugin architecture but critical foundational gaps:
-
Job selection policy is hardcoded —
sched_job_picker()inscheduler.c:39uses the literal string"xce_sched_fcfs". The picker runs in a dedicated thread withjbufcbuf communication. No way to swap policies without recompilation. -
Monolithic scheduler tied to controller I/O —
xce_scheduler_tholds 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. -
No picker plugin interface — the existing plugin framework has
job_elect_fnandjob_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. -
No project-level scheduling —
xce_sched_project_tis 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_twith 9 hooks +job_elect_fn/job_alloc_fn - FCFS job picker (
fcfs.c): dedicatedjobpickerthread. Iterates queues by priority, jobs by (priority desc, queue_time asc). Communicates viajbufcbuf. - Scheduling loop (
scheduler.c): pick job → allocate → pend or dispatch → next - Controller integration (
handler.c): receives capnp push/notify → enqueuesxce_sched_update_request_t→sched_update()dequeues. Decisions published viasched->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
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
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 specificationxce_sched_core_t/xce_sched_driver_t/xce_sched_sim_driver_txce_core_event_t— typed event interfacexce_core_decision_cb_t— decision callback.replaycapnp binary stream format + snapshot/restorexce_core_now()— virtualized clock accessorbuiltin_fcfs— single builtin pickerjob_pick_pluginon 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
.replaycapnp 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()