daemon: D2 — download.probe, genuinely async on both transports
download.probe was a stub (-32603). It needs an HTTP round trip on the engine's probe
pool (up to the schema's 30s x-deadlineMs), which cannot fit VeloxDispatcher's
synchronous on_download_probe -> HandlerResult<T> return without blocking the RPC
loop for the duration — a hard no per CLAUDE.md ("never block the RPC loop") and
AGENT-DAEMON.md build step 1.
uds_server.cpp and ws_server.cpp special-case "download.probe" before the generic
dispatch(), exactly the way they already special-case session.hello/session.subscribe:
parse the params, call the port, and queue the reply whenever the callback fires
(dropped silently if the connection is gone by then).
rpc::TaskActionPort gains probe_now(DownloadProbeParams, callback) — kept in proto/std
terms, no vdm::net::* in the signature, so veloxd_rpc never needs core/include's vdm
headers just to declare this. sched::Scheduler::probe_now is the implementation:
builds a vdm::net::ProbeRequest, runs it on the engine's probe pool, marshals the
engine-thread callback back onto the loop (deps_.post_to_loop, same as every other
engine callback here), maps a probe failure to -32013 ProbeFailed (data.httpStatus set
when there was an HTTP response), and fills suggestedCategoryId/suggestedSaveDir with a
plain extension match against the categories table — not the real rules engine, which
is still D3; noted in a comment.
Verified against real veloxd + tools/testserver, not just unit tests: a real probe
answers in ~5ms with size/resumable/etag/redirect chain; a bad host maps to -32013;
and — the actual point of the async design — a connection running a 10s slow-loris
probe does not block a second connection's download.list, which answers in ~1ms while
the probe is still outstanding.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GRDjHGgpYmMoPE2UFbe7pP
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "sched/schedule_window.hpp"
|
||||
#include "store/categories.hpp"
|
||||
#include "store/segments.hpp"
|
||||
#include "store/settings.hpp"
|
||||
#include "store/tasks.hpp"
|
||||
@@ -570,9 +571,82 @@ std::vector<std::string> Scheduler::pause_queue(const std::string& queue_id) {
|
||||
return paused;
|
||||
}
|
||||
|
||||
void Scheduler::probe_now(const vdm::net::ProbeRequest& req,
|
||||
std::function<void(vdm::Result<vdm::net::ProbeResult>)> done) {
|
||||
engine_.probe(req, std::move(done));
|
||||
namespace {
|
||||
|
||||
// Extension match against the categories table (categories.extensions, per category.list),
|
||||
// the same table download.add would consult once rules.* actually exists (D3). Not the
|
||||
// real rules engine — no host/mime/size clauses — just enough that the File Info dialog's
|
||||
// preselect isn't always "general".
|
||||
std::string guess_category_id(store::Db& db, const std::string& filename) {
|
||||
const auto dot = filename.find_last_of('.');
|
||||
if (dot == std::string::npos || dot + 1 >= filename.size()) return "general";
|
||||
std::string ext = filename.substr(dot + 1);
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
|
||||
store::Categories categories(db);
|
||||
auto cats = categories.list();
|
||||
if (!cats) return "general";
|
||||
for (const auto& c : *cats) {
|
||||
for (const auto& e : c.extensions)
|
||||
if (e == ext) return c.categoryId;
|
||||
}
|
||||
return "general";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Scheduler::probe_now(
|
||||
const proto::DownloadProbeParams& params,
|
||||
std::function<void(proto::HandlerResult<proto::DownloadProbeResult>)> done) {
|
||||
vdm::net::ProbeRequest req;
|
||||
req.url = params.url;
|
||||
if (params.headers)
|
||||
for (const auto& [k, v] : *params.headers) req.headers.push_back({k, v});
|
||||
if (params.cookies)
|
||||
for (const auto& c : *params.cookies) req.cookies.push_back({c.name, c.value});
|
||||
if (params.referrer) req.referrer = *params.referrer;
|
||||
if (params.userAgent) req.user_agent = *params.userAgent;
|
||||
|
||||
engine_.probe(req, [this, params, done](vdm::Result<vdm::net::ProbeResult> pr) {
|
||||
deps_.post_to_loop([this, params, done, pr]() {
|
||||
if (!pr) {
|
||||
nlohmann::json data;
|
||||
if (pr.error().http_status != 0) data["httpStatus"] = pr.error().http_status;
|
||||
done(std::unexpected(proto::HandlerError{
|
||||
proto::ErrorCode::ProbeFailed, pr.error().context, data}));
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string filename = vdm::net::suggest_filename(*pr);
|
||||
|
||||
proto::DownloadProbeResult r;
|
||||
r.filename = filename.empty() ? "download.bin" : filename;
|
||||
if (pr->total_size) r.sizeBytes = static_cast<std::int64_t>(*pr->total_size);
|
||||
r.mime = pr->mime;
|
||||
r.resumable = pr->resumable;
|
||||
r.effectiveUrl = pr->effective_url.empty() ? params.url : pr->effective_url;
|
||||
r.suggestedCategoryId = guess_category_id(db_, r.filename);
|
||||
if (!pr->etag.empty()) r.etag = pr->etag;
|
||||
if (!pr->last_modified.empty()) r.lastModified = pr->last_modified;
|
||||
r.acceptRanges = pr->accept_ranges;
|
||||
if (!pr->redirect_chain.empty()) r.redirectChain = pr->redirect_chain;
|
||||
if (pr->requires_auth) r.requiresAuth = true;
|
||||
|
||||
store::Settings settings(db_);
|
||||
store::Categories categories(db_);
|
||||
if (auto cats = categories.list()) {
|
||||
for (const auto& c : *cats)
|
||||
if (c.categoryId == r.suggestedCategoryId) {
|
||||
r.suggestedSaveDir = c.saveDir;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!r.suggestedSaveDir) r.suggestedSaveDir = settings.get_string("saveTo.defaultDir");
|
||||
|
||||
done(r);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace velox::daemon::sched
|
||||
|
||||
@@ -132,13 +132,17 @@ public:
|
||||
// instead of User. Returns the wire ids actually paused.
|
||||
std::vector<std::string> pause_queue(const std::string& queue_id) override;
|
||||
|
||||
// download.probe's standalone use (File Info dialog, no task row involved): a thin
|
||||
// passthrough to the engine's own probe pool, outside the segment budget (ADR 0011
|
||||
// §5). Never blocks — `done` arrives on an engine thread like every other EnginePort
|
||||
// callback; the caller (the RPC server layer, not this synchronous dispatcher — see
|
||||
// rpc/dispatcher.hpp's top comment) is responsible for marshalling the reply back.
|
||||
void probe_now(const vdm::net::ProbeRequest& req,
|
||||
std::function<void(vdm::Result<vdm::net::ProbeResult>)> done);
|
||||
// rpc::TaskActionPort. Builds a vdm::net::ProbeRequest from `params`, runs it on the
|
||||
// engine's probe pool (outside the segment budget, ADR 0011 §5), and converts the
|
||||
// result back to proto terms — including the suggestedCategoryId/-SaveDir guess (a
|
||||
// plain extension match against the categories table; the real rules engine is D3).
|
||||
// `done` is called already marshalled onto the loop thread via post_to_loop, same as
|
||||
// every other engine callback here — the caller never has to know it started on an
|
||||
// engine thread.
|
||||
void probe_now(
|
||||
const velox::proto::DownloadProbeParams& params,
|
||||
std::function<void(velox::proto::HandlerResult<velox::proto::DownloadProbeResult>)>
|
||||
done) override;
|
||||
|
||||
// Diagnostics / tests.
|
||||
std::optional<std::string> wire_id_of(vdm::TaskId id) const;
|
||||
|
||||
Reference in New Issue
Block a user