Files
vdm/daemon/src/rpc/task_action_port.hpp
T
samiandClaude Sonnet 5 4f6c0cc9d2 daemon: D3's remainder — rules.*, queue.reorder, schedule.*, limiter.*, download.update/refreshUrl
rules.list/rules.upsert: new store/rules.{hpp,cpp} (list in priority order; apply()
upserts+removes atomically, generating an id when absent, per the schema's own "never
leaves the table in a half-valid state"). Found and fixed along the way: migration
0001's rules table had no column for Rule.name at all — every rules.list/.upsert call
failed outright ("no such column: name"), unit tests included, since :memory: migrates
through the same path. Migration 0004 adds it.

queue.reorder: new store::Queues::reorder — taskIds must be an exact permutation of
the queue's current membership (compared as sorted sets) or nothing is written and
-32602 names the queue; a valid permutation rewrites every member's queue_position in
one transaction.

schedule.get/schedule.set: a thin wrapper over the queues.schedule column (already
read since D3b, never independently settable). nextRunAt is deliberately left unset —
computing it needs the same local-time, DST-aware window logic
sched/schedule_window.hpp's window_open() only has half of; called out rather than
approximated, and the field is optional.

limiter.get/limiter.set: backed by the same downloads.speedLimitEnabled/
downloads.speedLimitBps settings keys D9 already wired — one bag of truth, not two.
The new part is reaching the engine: EnginePort/TaskActionPort gain
set_global_speed_limit(bps) (0 = unlimited, TokenBucket's own convention), wired to
Engine::rate_limiter().set_global_limit(). Pushed live on every limiter.set *and* on
Scheduler::reload_config() so a limit from a previous run isn't silently unlimited
again after a restart. applyToRunning is accepted but has no lever to pull
differently — a single shared global bucket has no "next task only" variant. Also
found, not chased further: the schema's "globalBps:0 with enabled:true means 'stop
everything'" is the opposite of what TokenBucket does with rate_bps==0 (unlimited) —
a real discrepancy, but the schema says the GUI must not offer that combination.

download.update: "moving saveDir or filename moves the file on disk in the same
operation" — resolved and root-checked like download.add's destination, then the
.veloxpart/.veloxpart.meta pair (or the finished file, if complete) is moved via
rename, falling back to copy+remove across filesystems, only when the resolved
location actually differs. categoryId/queueId(appended to the new queue's run
order)/description/segments/bufferBytes/checksum apply through new
store::Tasks::apply_update.

download.refreshUrl: same async server-layer special-case as download.probe (a real
network round trip, same 30s deadline). Re-probes, flags contentChanged only when
size or validator are both known and actually differ, persists the new URL and probe
result, and swaps the URL on a live engine handle via a newly-widened
EnginePort::refresh_url (now takes headers too, matching DownloadHandle's real
signature — the seam had silently dropped them).

Found and documented, not fixed: the generated parser collapses "field absent" and
"field explicitly null" to the same nullopt for every optional<T> patch field
(DownloadUpdateParamsPatch, Settings) — both schemas document "an explicit null
clears the field" but neither handler can act on it because the wire distinction is
already gone by the time either sees the parsed struct. A generator-level gap
(PROTO's), not something to hand-route around locally.

Verified against real veloxd + tools/testserver: rules create/list, limiter.set
takes effect and reads back, schedule.set/get round-trips, queue.reorder against real
membership (and rejects a non-permutation), download.update renames+recategorizes a
task, download.refreshUrl swaps a paused task's URL and reports contentChanged
correctly. Full ctest: 55/55 (excluding the pre-existing, unrelated conformance
failure noted two commits back).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GRDjHGgpYmMoPE2UFbe7pP
2026-09-12 14:26:18 +04:00

97 lines
5.3 KiB
C++

#pragma once
// The seam between the dispatcher and the scheduler for user-initiated task/queue actions
// (download.pause/resume/start/cancel, queue.stop's pauseRunning) — owned by rpc/ so
// dispatcher.hpp (part of veloxd_rpc) never has to include sched/scheduler.hpp, which
// would make veloxd_rpc depend on veloxd_sched at compile time. veloxd_sched already
// depends on veloxd_rpc (for EventHub); the other direction too would be a real circular
// library dependency, not just an inconvenience — anything linking veloxd_rpc alone (e.g.
// the CLI's tests) would fail to link over symbols it never calls.
//
// sched::Scheduler implements this directly (it already lives in a library that depends on
// rpc/, so adding an rpc-defined base costs nothing new); main.cpp hands the dispatcher a
// `TaskActionPort*` pointing at the same Scheduler it constructs.
#include <functional>
#include <string>
#include <vector>
#include "velox_proto.hpp"
namespace velox::daemon::rpc {
class TaskActionPort {
public:
virtual ~TaskActionPort() = default;
// Mirrors sched::Scheduler::UserActionResult: whether the task was found at all,
// whether it actually changed state (a task already in the target/a terminal state is
// reported found=true, changed=false — BulkTaskResult's own "not an error" contract),
// and its resulting/current state spelling either way.
struct Result {
bool found = false;
bool changed = false;
std::string state;
};
virtual Result user_pause(const std::string& wire_id) = 0;
virtual Result user_resume(const std::string& wire_id) = 0;
virtual Result user_start(const std::string& wire_id) = 0;
virtual Result user_cancel(const std::string& wire_id, bool discard_partial) = 0;
// queue.stop(pauseRunning=true): pause every currently-running task in `queue_id` now.
// Returns the wire ids actually paused.
virtual std::vector<std::string> pause_queue(const std::string& queue_id) = 0;
// download.provideAuth: answers a task auto-paused on a 401/407. false if the task
// isn't currently holding a live engine handle (nothing waiting on credentials).
// `remember` is accepted but not yet acted on — persisting to the Secret Service isn't
// wired anywhere in this build yet (CLAUDE.md §4: never SQLite, never logs); this
// always does the "this retry only" half. Noted in deferrals.md.
virtual bool provide_auth(const std::string& wire_id, const std::string& username,
const std::string& password, bool remember) = 0;
// download.probe (D2), the File Info dialog's own network round trip — no task row
// involved. Genuinely async (the engine's probe pool; up to the schema's 30s
// x-deadlineMs) and so cannot fit VeloxDispatcher's synchronous on_download_probe:
// the RPC server layer (uds_server.cpp / ws_server.cpp) special-cases "download.probe"
// before the generic dispatch(), the same way it already special-cases session.hello,
// calls this, and queues the reply whenever `done` fires — on an engine thread, so the
// implementation must marshal back to the loop before calling it, the same as every
// other EnginePort callback. Kept in std::string/proto terms (not vdm::net::*) so this
// header — included by dispatcher.hpp, part of veloxd_rpc — never needs core/include's
// vdm headers; the vdm::net::ProbeRequest/ProbeResult conversion lives in sched/, which
// already depends on vdm.
virtual void probe_now(
const velox::proto::DownloadProbeParams& params,
std::function<void(velox::proto::HandlerResult<velox::proto::DownloadProbeResult>)>
done) = 0;
// download.refreshUrl ("IDM's 'Refresh Download Address'"): same async reasoning and
// the same server-layer special-case as probe_now — a real network round trip, up to
// the schema's own 30s x-deadlineMs. Re-probes the new URL, compares size/validator
// against what the task already has on record (contentChanged), persists the new URL
// and probe result, and — if the task holds a live engine handle — swaps its URL
// in-flight without losing progress.
virtual void refresh_url(
const std::string& wire_id, const std::string& url,
const std::optional<velox::proto::Headers>& headers,
const std::optional<std::vector<velox::proto::Cookie>>& cookies,
std::function<void(velox::proto::HandlerResult<velox::proto::DownloadRefreshUrlResult>)>
done) = 0;
// settings.set of a connection.* key: re-read connection.maxConcurrentDownloads /
// maxActiveSegments and the per-host cap table, push the new caps to the engine and
// the governor (Scheduler::reload_config's own doc comment names this exact trigger).
// Named apply_settings_reload rather than reload_config to avoid colliding with
// sched::Scheduler's own already-public reload_config() (returns DbResult<void>,
// consumed by main.cpp and a unit test — kept as-is rather than reshaped to fit here).
virtual void apply_settings_reload() = 0;
// limiter.set: pushed straight to the engine's shared global token bucket. 0 means
// unlimited (the bucket's own convention).
virtual void set_global_speed_limit(std::uint64_t bps) = 0;
};
} // namespace velox::daemon::rpc