daemon: D4b — download.pause/resume/start/cancel and queue.start/stop drive the scheduler

download.pause/resume/start/cancel and queue.start/stop were stubs; now they call into
the scheduler and take effect immediately, not on the next 1s tick — pausing, resuming
or cancelling a live transfer can't wait, and per ADR 0013 §3 the governor never
touches a user-owned pause on its own.

New rpc::TaskActionPort interface (owned by rpc/, implemented by sched::Scheduler) is
what dispatcher.hpp depends on instead of sched/scheduler.hpp directly. Needed because
veloxd_sched already links veloxd_rpc (for EventHub); dispatcher.hpp pulling in
sched/scheduler.hpp directly would make it a real circular library dependency, breaking
anything that links veloxd_rpc alone (cli's tests, as it turned out — hit and fixed
during this change).

Scheduler::user_pause/user_resume/user_start/user_cancel + pause_queue follow tick()'s
existing to_pause pattern: call the engine (async, no synchronous effect) and transition
the store eagerly so download.get/list are correct the instant the RPC call returns.

Fixed a real bug surfaced while building this: transition() always overwrote
pause_reason to NULL when the engine's own delayed pause-ack callback (on_state to
paused, no error) arrived after whoever actually initiated the pause had already
written the real reason — now it preserves the stored reason when the callback supplies
none, instead of clobbering it. Covered by a regression check in sched_scheduler_test.

store/queues gets get() and set_state() (was list()-only) for queue.start/stop.

Verified against real veloxd + tools/testserver, not just unit tests: pausing a live
single-segment throttled transfer freezes downloadedBytes, resume continues it from
that point, cancel stops it; a bad taskId comes back in BulkTaskResult.failed with
-32010, not a crash; queue.stop(pauseRunning:true) pauses the queue's running task
immediately and queue.start resumes admission.

Known gap: download.start's contract "a task in 'queued' jumps its queue" (priority
bump) is not implemented — admission is still plain FIFO by created_at. Noted in
deferrals.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GRDjHGgpYmMoPE2UFbe7pP
This commit is contained in:
2026-09-11 17:20:58 +04:00
co-authored by Claude Sonnet 5
parent de748cc2fc
commit 55f0c6099d
11 changed files with 478 additions and 45 deletions
+11 -1
View File
@@ -15,6 +15,7 @@
#include <functional>
#include "rpc/event_hub.hpp"
#include "rpc/task_action_port.hpp"
#include "store/sqlite.hpp"
#include "velox_proto.hpp"
@@ -22,7 +23,15 @@ namespace velox::daemon::rpc {
class VeloxDispatcher final : public velox::proto::Dispatcher {
public:
VeloxDispatcher(velox::daemon::store::Db& db, EventHub& hub) : db_(db), hub_(hub) {}
// `actions` drives download.pause/resume/start/cancel and queue.start/stop
// immediately (D4b) — those cannot wait for the next tick(), unlike download.add's
// on_mutation nudge. Optional so existing tests that only exercise download.add/list/
// get keep building with no scheduler at hand; a null actions_ makes those methods
// answer "not implemented" instead of crashing. See rpc/task_action_port.hpp for why
// this is an interface owned by rpc/ rather than a direct sched::Scheduler* (avoids a
// veloxd_rpc <-> veloxd_sched circular library dependency).
VeloxDispatcher(velox::daemon::store::Db& db, EventHub& hub, TaskActionPort* actions = nullptr)
: db_(db), hub_(hub), actions_(actions) {}
// Called after a handler mutates task state (download.add for now). main.cpp wires it
// to nudge the scheduler; unset in tests.
@@ -109,6 +118,7 @@ public:
private:
velox::daemon::store::Db& db_;
EventHub& hub_;
TaskActionPort* actions_;
std::function<void()> on_mutation_;
};