daemon: sched/ — the concurrency governor + schedule-window evaluation (build step 4)
The scheduling brain, built against CORE's headers (vdm/engine.hpp,
vdm/segment/budget.hpp) — signatures only; the Engine bodies land in
CORE stage 8 and the Scheduler that wires governor <-> store <-> engine
<-> timer comes after that (daemon/docs/deferrals.md D4).
- sched/governor — a pure decision function. In: a snapshot of every
task's coarse RunState and every queue's state (schedule windows
pre-resolved). Out: {to_start, to_resume, to_pause, pause_reasons,
priority_order}. Enforces, all in TASK units per ADR 0011 §1:
connection.maxConcurrentDownloads; the min(that, maxActiveSegments)
clamp (§2); Queue.maxConcurrent; the per-host task cap (§4); and a
stopped queue / closed window runs nothing. Never touches a task
paused for `user` or CORE's `auto` (ADR 0013 §3) — only Schedule /
QueueStopped / AdmissionReconcile are auto-resumable. Deterministic:
main-list before queued-in-queue, then queue order, then FIFO, then
task_id.
- sched/schedule_window — window_open(Schedule, local tm): disabled =>
always open; `once` => date + time match; `periodic` => weekday in
daysOfWeek (empty = every day) + time in [start, stop); null start =>
midnight, null stop => end of day, stop < start => overnight window.
Pure; re-evaluated every tick, no cached instants.
- veloxd_sched static lib; veloxd links it (nothing calls it yet).
Tests (ASan+UBSan and TSan clean): veloxd.sched_window (10 window
cases incl. overnight, once, null bounds), veloxd.sched_governor
(global/clamp/per-queue/per-host caps, stop vs window pause reasons,
resume-only-governor-reasons, auth-pause untouched, admission
reconcile, determinism under shuffled input). 32 daemon/cli tests
green; full tree green.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Upd9WhG9oppieig5nRDLig
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
// The concurrency governor — a pure decision function. Given a snapshot of every task's
|
||||
// coarse run-state and every queue's state, it decides which tasks should start, which
|
||||
// should resume, which should pause, and the priority order to push to CORE's segment
|
||||
// budget. No I/O, no store, no engine, no clock — the caller supplies an already-evaluated
|
||||
// world (schedule windows resolved via sched/schedule_window.hpp) and applies the result.
|
||||
//
|
||||
// Axes it enforces, all in TASK units (ADR 0011 §1: DAEMON counts tasks, CORE counts
|
||||
// segments):
|
||||
// * connection.maxConcurrentDownloads — global running-task ceiling
|
||||
// * min(that, connection.maxActiveSegments) — the one ADR 0011 §2 clamp
|
||||
// * Queue.maxConcurrent — per-queue running-task ceiling
|
||||
// * per-host task cap (== that host's segment cap) — ADR 0011 §4
|
||||
// * queue running state + schedule window — a stopped/closed queue runs nothing
|
||||
//
|
||||
// What it never does: touch a task paused for a reason it does not own (user, or CORE's
|
||||
// auto-pause for auth_required / server_file_changed / disk_full) — ADR 0013 §3.
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace velox::daemon::sched {
|
||||
|
||||
// The governor's coarse view of a task. Derived from the SQLite row + the engine's last
|
||||
// reported state; the governor does not need the fine-grained EngineState.
|
||||
enum class RunState {
|
||||
Queued, // wants a slot; eligible for admission
|
||||
Running, // probing / connecting / downloading / retry_wait / assembling / verifying
|
||||
Paused, // see pause_reason for whether the governor may resume it
|
||||
Terminal, // complete / failed / cancelled — ignored
|
||||
};
|
||||
|
||||
// Why a paused task is paused. Only the first three are governor-owned and auto-resumable;
|
||||
// `User` and `Auto` (CORE's auth/decision/disk auto-pause) are never touched here.
|
||||
enum class PauseReason { User, Schedule, QueueStopped, AdmissionReconcile, Auto };
|
||||
|
||||
struct TaskView {
|
||||
std::string task_id; // wire UUID
|
||||
RunState run_state = RunState::Queued;
|
||||
std::optional<PauseReason> pause_reason; // set iff run_state == Paused
|
||||
std::optional<std::string> queue_id; // nullopt => the main list (no queue)
|
||||
std::int64_t queue_position = 0; // order within the queue
|
||||
std::string host; // for the per-host cap; "" opts out
|
||||
std::int64_t admit_rank = 0; // FIFO tiebreak (created_at, then rowid)
|
||||
};
|
||||
|
||||
struct QueueView {
|
||||
std::string queue_id;
|
||||
bool running = false; // Queue.state == "running"
|
||||
std::int64_t max_concurrent = 1;
|
||||
bool window_open = true; // sched/schedule_window.hpp result for right now
|
||||
};
|
||||
|
||||
struct GovernorConfig {
|
||||
std::int64_t max_concurrent_downloads = 5; // connection.maxConcurrentDownloads
|
||||
std::int64_t max_active_segments = 32; // connection.maxActiveSegments (§2 clamp)
|
||||
std::map<std::string, std::int64_t> host_caps{}; // host -> max running tasks; absent => unlimited
|
||||
};
|
||||
|
||||
struct Decision {
|
||||
std::vector<std::string> to_start; // Queued -> admit: caller invokes Engine::start()
|
||||
std::vector<std::string> to_resume; // Paused (governor-owned reason) -> Engine::resume()
|
||||
std::vector<std::string> to_pause; // Running -> Engine::pause(); reason in pause_reasons
|
||||
std::map<std::string, PauseReason> pause_reasons; // task_id -> why, for the DB column
|
||||
std::vector<std::string> priority_order; // running + starting + resuming, for set_task_order()
|
||||
};
|
||||
|
||||
class Governor {
|
||||
public:
|
||||
Governor() = default;
|
||||
explicit Governor(GovernorConfig cfg) : cfg_(std::move(cfg)) {}
|
||||
|
||||
void set_config(GovernorConfig cfg) { cfg_ = std::move(cfg); }
|
||||
const GovernorConfig& config() const noexcept { return cfg_; }
|
||||
|
||||
// Pure: same inputs -> same Decision. `tasks` and `queues` are snapshots; order within
|
||||
// them does not matter (the governor sorts by its own keys).
|
||||
Decision evaluate(const std::vector<TaskView>& tasks,
|
||||
const std::vector<QueueView>& queues) const;
|
||||
|
||||
private:
|
||||
GovernorConfig cfg_;
|
||||
};
|
||||
|
||||
} // namespace velox::daemon::sched
|
||||
Reference in New Issue
Block a user