daemon: sched/scheduler — governor <-> store <-> engine, against an EnginePort seam

The Scheduler that D4 was waiting on. Built against CORE's engine
HEADERS (now in main); the real EnginePort and the veloxd wiring wait
for lane/core's stage-8 bodies to reach main (deferrals.md D4a/D4b) —
core/src/task/ is still .gitkeep there, so linking vdm::Engine now
would be an unresolved symbol.

- sched/engine_port — the abstract seam: start/pause/resume/cancel/
  provide_auth/decide/refresh_url + the ADR 0011 admission config
  (set_task_order / set_max_active_segments / set_host_segment_cap).
  Keeps the Scheduler testable without a live engine and the daemon
  unbound from the concrete vdm::Engine.
- sched/fake_engine_port — a recording impl for tests.
- sched/scheduler:
  * owns the wire-UUID <-> vdm::TaskId map.
  * tick(): snapshot queues (schedule window evaluated with an
    injectable clock) + non-terminal tasks -> governor.evaluate ->
    apply. to_start builds a vdm::task::DownloadSpec from the row and
    calls EnginePort::start; to_resume -> resume(); to_pause ->
    pause() + writes the pause_reason; priority_order -> set_task_order
    over the mapped engine ids. `new` tasks are parked (startMode
    manual) and skipped.
  * on_engine_state(wire_id, state, err): projects an engine
    transition onto the store row (state, pause_reason='auto' when an
    error rides a paused transition per ADR 0013 §2, flattened error
    columns) so the next tick sees ground truth. This is also the hook
    event.task.state will fire from (D5).
  * reconcile_after_restart(): CORE-owned states -> queued, paused
    keeps its reason (ADR 0013 §5).
  * reload_config(): reads connection.maxConcurrentDownloads /
    maxActiveSegments + a daemon-local host-cap map, pushes caps to
    the engine, updates the governor.
  * Deps: injectable local-now clock and a post_to_loop marshaller
    (engine callbacks arrive on engine threads; default runs inline
    for tests).

Test veloxd.sched_scheduler (ASan+UBSan and TSan clean): admission +
ordering, a slot freeing on completion, queue-stop -> pause
(queue_stopped) then queue-restart -> resume (not a fresh start),
engine auto-pause -> pause_reason 'auto' + never auto-resumed,
reconcile_after_restart, reload_config caps push. 35 daemon/cli tests
green.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Upd9WhG9oppieig5nRDLig
This commit is contained in:
2026-09-10 20:29:37 +04:00
co-authored by Claude Sonnet 5
parent 14128c1935
commit d93c8e10a0
8 changed files with 700 additions and 3 deletions
+55
View File
@@ -0,0 +1,55 @@
#pragma once
// A recording EnginePort for Scheduler tests. Every call is logged; start() hands back a
// sequential TaskId. No threads, no real work.
#include <cstdint>
#include <string>
#include <vector>
#include "sched/engine_port.hpp"
namespace velox::daemon::sched {
class FakeEnginePort final : public EnginePort {
public:
struct StartCall {
vdm::TaskId id;
std::string url;
std::string save_path;
vdm::task::DownloadCallbacks callbacks;
};
std::vector<StartCall> starts;
std::vector<vdm::TaskId> paused;
std::vector<vdm::TaskId> resumed;
std::vector<std::pair<vdm::TaskId, bool>> cancelled;
std::vector<std::vector<vdm::TaskId>> orders;
std::vector<std::uint32_t> max_active_segments;
std::vector<std::pair<std::string, std::uint32_t>> host_caps;
vdm::TaskId start(const vdm::task::DownloadSpec& spec,
vdm::task::DownloadCallbacks callbacks) override {
const vdm::TaskId id{next_++};
starts.push_back({id, spec.url, spec.save_path, std::move(callbacks)});
return id;
}
void pause(vdm::TaskId id) override { paused.push_back(id); }
void resume(vdm::TaskId id) override { resumed.push_back(id); }
void cancel(vdm::TaskId id, bool discard) override { cancelled.emplace_back(id, discard); }
void provide_auth(vdm::TaskId, const std::string&, const std::string&, bool) override {}
void decide(vdm::TaskId, vdm::task::Decision) override {}
void refresh_url(vdm::TaskId, const std::string&) override {}
void set_task_order(const std::vector<vdm::TaskId>& order) override { orders.push_back(order); }
void set_max_active_segments(std::uint32_t n) override { max_active_segments.push_back(n); }
void set_host_segment_cap(const std::string& h, std::uint32_t c) override {
host_caps.emplace_back(h, c);
}
const std::vector<vdm::TaskId>& last_order() const { return orders.back(); }
private:
std::uint64_t next_ = 1;
};
} // namespace velox::daemon::sched