core: net/http_client — libcurl multi wrapper (stage 2)
One HttpClient owns a small pool of workers, each with its own CURLM; an easy handle lives on one worker for its life. Public start/pause/resume/ cancel enqueue a command + curl_multi_wakeup(); callbacks (on_head / on_data / on_finished) run on the worker thread and return a DataAction (proceed / pause / abort). Covers redirects (final-response head only), ranges (inclusive ByteRange -> CURLOPT_RANGE), proxy/SOCKS5, basic/digest auth, cookies, verbatim headers, stall detection, a coarse recv-rate cap, and a curl_share DNS/TLS cache across workers. CURLcode + HTTP status -> vdm::Error in net/curl_error. A probe is on_head returning abort: it finishes successfully (head_complete), not canceled. Tests drive tools/testserver: full GET, ranged 206, redirect chain, 404 -> not_found, connection refused -> connect_failed, HEAD probe + ranged 0-0 probe (no body), cancel mid-transfer, pause/resume completes. Skip cleanly if testserver isn't in the tree. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
// vdm/net/http_client.hpp — a libcurl-multi wrapper for the download engine.
|
||||
//
|
||||
// One HttpClient owns a small pool of worker threads, each with its own curl_multi
|
||||
// (curl handles are not thread-safe; a handle lives on exactly one worker for its life).
|
||||
// The engine hands it a Request plus callbacks and gets back a Transfer handle it can
|
||||
// pause / resume / cancel from any thread.
|
||||
//
|
||||
// Layering: this is the ONLY core header that pulls in libcurl, and only in its .cpp —
|
||||
// nothing here exposes a curl type. No JSON / SQL / Qt / RPC (CLAUDE.md §3).
|
||||
//
|
||||
// Callbacks run ON THE WORKER THREAD, one transfer at a time for a given Transfer.
|
||||
// They must not block (that stalls every other transfer on that worker) and must not
|
||||
// call back into this Transfer's pause/resume/cancel re-entrantly — post that work
|
||||
// elsewhere. on_data must not allocate on the hot path (AGENT-CORE); the ring buffer it
|
||||
// writes into is preallocated by the caller (stage 4).
|
||||
//
|
||||
// This header compiles standalone.
|
||||
|
||||
#ifndef VDM_NET_HTTP_CLIENT_HPP
|
||||
#define VDM_NET_HTTP_CLIENT_HPP
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "vdm/net/http_types.hpp"
|
||||
#include "vdm/util/bytes.hpp"
|
||||
#include "vdm/util/result.hpp"
|
||||
|
||||
namespace vdm::net {
|
||||
|
||||
// What on_data tells the client to do with the transfer after this chunk.
|
||||
enum class DataAction {
|
||||
proceed, // keep receiving
|
||||
pause, // stop receiving; resumes on Transfer::resume()
|
||||
abort, // end the transfer now (finishes with Error::canceled)
|
||||
};
|
||||
|
||||
struct TransferStats {
|
||||
std::uint64_t bytes_received = 0;
|
||||
long http_status = 0;
|
||||
std::string effective_url;
|
||||
// Timings in milliseconds (from CURLINFO_*_TIME_T), 0 if the phase didn't happen.
|
||||
long namelookup_ms = 0;
|
||||
long connect_ms = 0;
|
||||
long appconnect_ms = 0; // TLS handshake done
|
||||
long starttransfer_ms = 0; // first response byte
|
||||
long total_ms = 0;
|
||||
};
|
||||
|
||||
struct TransferCallbacks {
|
||||
// Response headers are in. Called at most once per transfer (a followed redirect's
|
||||
// intermediate headers are not delivered). If the caller only wanted headers (a
|
||||
// probe), return DataAction::abort here.
|
||||
std::function<DataAction(const ResponseHead &)> on_head;
|
||||
|
||||
// A chunk of body bytes. The span is valid only for the duration of the call.
|
||||
std::function<DataAction(ConstByteSpan)> on_data;
|
||||
|
||||
// The transfer ended — success carries stats, failure carries the mapped error
|
||||
// (ErrorInfo.http_status is set for HTTP-status failures). Always called exactly once,
|
||||
// last.
|
||||
std::function<void(Result<TransferStats>)> on_finished;
|
||||
};
|
||||
|
||||
class HttpClient;
|
||||
|
||||
// Lightweight handle to a running transfer. Copyable (shared state). All methods are
|
||||
// safe to call from any thread; they post to the owning worker and return immediately.
|
||||
// Dropping the last handle does NOT cancel — call cancel() for that.
|
||||
class Transfer {
|
||||
public:
|
||||
Transfer() = default;
|
||||
|
||||
[[nodiscard]] std::uint64_t id() const noexcept;
|
||||
[[nodiscard]] bool valid() const noexcept { return static_cast<bool>(state_); }
|
||||
|
||||
void pause(); // no-op if already paused / finished
|
||||
void resume(); // no-op if not paused / finished
|
||||
void cancel(); // idempotent; on_finished fires with Error::canceled
|
||||
|
||||
private:
|
||||
friend class HttpClient;
|
||||
struct State;
|
||||
explicit Transfer(std::shared_ptr<State> s) : state_(std::move(s)) {}
|
||||
std::shared_ptr<State> state_;
|
||||
};
|
||||
|
||||
class HttpClient {
|
||||
public:
|
||||
struct Options {
|
||||
// Worker threads, each with its own curl_multi. New transfers are assigned
|
||||
// round-robin. 0 => pick from hardware_concurrency (min 1, max 4).
|
||||
unsigned workers = 0;
|
||||
// CURLMOPT_MAX_TOTAL_CONNECTIONS per worker (0 = curl default).
|
||||
long max_connections_per_worker = 0;
|
||||
// Shared DNS + TLS-session cache across this client's workers (curl_share).
|
||||
bool share_dns_and_tls = true;
|
||||
};
|
||||
|
||||
HttpClient(); // default Options
|
||||
explicit HttpClient(Options opts);
|
||||
~HttpClient();
|
||||
|
||||
HttpClient(const HttpClient &) = delete;
|
||||
HttpClient &operator=(const HttpClient &) = delete;
|
||||
|
||||
[[nodiscard]] unsigned worker_count() const noexcept;
|
||||
|
||||
// Start a transfer. The Request is consumed (moved). Returns an invalid Transfer and
|
||||
// never calls the callbacks only if the client is shutting down; every other failure
|
||||
// (bad URL, DNS, ...) is delivered through on_finished.
|
||||
Transfer start(Request req, TransferCallbacks cbs);
|
||||
|
||||
private:
|
||||
friend class Transfer; // Transfer posts pause/resume/cancel commands to Impl
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace vdm::net
|
||||
|
||||
#endif // VDM_NET_HTTP_CLIENT_HPP
|
||||
Reference in New Issue
Block a user