core: add the download engine — task machine, DownloadHandle, Engine

Stage 8 of the CORE build order: the bodies behind the DownloadSpec /
callback API reviewed in core/docs/engine-api-m1.md. Wires probe -> segment
workers -> WriteBuffer -> SparseFile -> .veloxpart.meta -> retry/backoff ->
SegmentBudget -> RateLimiter -> callbacks into one event-driven machine.

- Engine (src/engine.cpp): owns HttpClient, Prober, SegmentBudget,
  RateLimiter and one timer jthread (min-heap of scheduled fns). start()
  builds a task and returns a DownloadHandle; ~Impl quiesces every task
  before joining the timer so no callback fires during teardown.

- DownloadTaskState (src/task/download_task.cpp): one `mu` task lock; a
  shared_mutex over the worker map for the curl write path; callbacks
  collected under `mu` and fired after release via a separate deferred
  queue; weak_from_this() in every async hop. State machine over the
  CORE-owned EngineState subset, auto-pause on 401/407 and on a 200 where
  206 was expected, validated resume via If-Range.

- digest (src/task/digest.cpp): OpenSSL EVP hash_file() for the optional
  post-download checksum; links OpenSSL::Crypto PRIVATE.

- Segmenter::release_segment(): hand a paused segment back to the pool
  unassigned so resume's assign_slot() picks it up instead of splitting a
  still-"assigned" range and orphaning its front half.

- DownloadHandle now names the real control block (vdm::task::
  DownloadTaskState, defined only in the engine TU) via a namespace-scope
  fwd decl and a public-but-effectively-engine-only ctor, replacing the
  nested State/friend pair. Every public signature is unchanged; DAEMON
  (vdm-79) confirmed sched/ names only the public API.

Fixes found while building the end-to-end suite (tests/task/engine_test.cpp,
9 cases against tools/testserver, green under ASan/UBSan and TSan):
- a dropped connection lost its unflushed WriteBuffer tail while advance()
  had already counted those bytes as done -> a retry resumed past an
  unwritten hole. Flush on the failure path.
- when the byte counters hit total while other workers were still live,
  teardown dropped their buffered tails. Now: cancel them and let each
  worker's own seg_finished drain it (the `assembling` state), last one
  starts verification -- no cross-thread buffer access.
- seg_head() let a 401 with credentials present abort before libcurl's
  resend; now it proceeds once and acts on the final status.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
This commit is contained in:
2026-09-10 20:19:31 +04:00
co-authored by Claude Sonnet 5
parent efbf366c18
commit 91636f8a4d
11 changed files with 1876 additions and 5 deletions
+4
View File
@@ -131,6 +131,10 @@ class Segmenter {
[[nodiscard]] SegState segment_state(std::uint32_t idx) const noexcept;
void set_segment_state(std::uint32_t idx, SegState s) noexcept;
// Hand a segment back to the pool without touching its `completed`: it becomes an
// unassigned idle range that the next assign_slot() picks up (used on pause).
void release_segment(std::uint32_t idx) noexcept;
// Sum of bytes done across every segment (active + already complete). Locks.
[[nodiscard]] std::uint64_t downloaded() const;
[[nodiscard]] bool all_complete() const;
+9 -4
View File
@@ -31,6 +31,11 @@ class Engine; // owns and fills DownloadHandle (see vdm/engine.hpp)
namespace vdm::task {
// The task control block. Opaque: defined only in the engine's translation unit. A handle
// holds a shared_ptr to one; the engine keeps its own copy so the task outlives a caller
// that drops its handle.
struct DownloadTaskState;
// --- input ---------------------------------------------------------------------------
struct Checksum {
@@ -177,6 +182,9 @@ struct DownloadCallbacks {
class DownloadHandle {
public:
DownloadHandle() = default;
// The engine builds handles; `DownloadTaskState` is incomplete everywhere else, so
// this is effectively engine-only without a friend declaration.
explicit DownloadHandle(std::shared_ptr<DownloadTaskState> s) : state_(std::move(s)) {}
[[nodiscard]] TaskId id() const noexcept;
[[nodiscard]] bool valid() const noexcept { return static_cast<bool>(state_); }
@@ -207,10 +215,7 @@ class DownloadHandle {
[[nodiscard]] Progress progress() const;
private:
friend class vdm::Engine;
struct State;
explicit DownloadHandle(std::shared_ptr<State> s) : state_(std::move(s)) {}
std::shared_ptr<State> state_;
std::shared_ptr<DownloadTaskState> state_;
};
} // namespace vdm::task