#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 #include #include #include #include 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 pause_reason; // set iff run_state == Paused std::optional 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 host_caps{}; // host -> max running tasks; absent => unlimited }; struct Decision { std::vector to_start; // Queued -> admit: caller invokes Engine::start() std::vector to_resume; // Paused (governor-owned reason) -> Engine::resume() std::vector to_pause; // Running -> Engine::pause(); reason in pause_reasons std::map pause_reasons; // task_id -> why, for the DB column std::vector 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& tasks, const std::vector& queues) const; private: GovernorConfig cfg_; }; } // namespace velox::daemon::sched