core: segment/segmenter + segment/budget (stage 6)
vdm/ids.hpp — TaskId, an opaque engine handle (DAEMON keeps the wire UUID <-> TaskId map; the engine never sees the UUID). segment/segmenter — per-download range management (docs/04 §3). Initial lazy split; assign_slot() splits the largest remaining range when the budget grants a slot; on_complete(may_steal) either *steals* the second half of the largest remaining range for the same worker (slot-neutral) or returns nullopt so the caller *yields* the slot (ADR 0011 A1); on_failed() returns requeue only on the 3rd consecutive connection error with a mirror present — the remaining range is orphaned and re-split. Non-resumable or unknown-size => exactly 1 segment; never split below min_segment_bytes (1 MiB). Resume ctor rebuilds from a persisted table (falls back to a fresh layout if it doesn't tile [0,total)). One mutex == "the task lock"; segment fields are std::atomic and the store is a std::deque so a steal's append never moves a worker's record. segment/budget — the global allocator (ADR 0011). Owns exactly one ceiling (maxActiveSegments) and min-1-before-seconds fairness: a two-pass allocation (guarantee pass gives every wanting task 1 slot in DAEMON's priority order, then a growth pass round-robins the rest up to each task's effective cap = min(per_task_cap, host cap, 1 if non-resumable)), recomputed from scratch on every edge so a live set_max_active_segments cut naturally yields the excess lowest-priority-first, never a mid-segment kill. DAEMON-facing surface exactly as promised in daemon/docs/core-requests-m1.md / ADR 0011: budget(), segments_active(), starved_tasks(), starved_since(), set_max_active_segments (drain), set_host_segment_cap, set_task_order, on_budget_changed (a jthread coalesces at <=4 Hz; the tasks_starved 0<->nonzero edge fires immediately). Callbacks are copied out and run after the lock is dropped. Tests: segmenter split/steal/requeue/resume math + a concurrent steal-and-advance run; budget min-1 under a tight budget, round-robin growth, host-cap and non-resumable clamps, live-lower shedding lowest-priority-first, starvation below the task count, starved-edge notification, and a concurrent set_want hammer. Green under ASan/UBSan; the steal path and the budget green under TSan. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
// vdm/segment/budget.cpp
|
||||
|
||||
#include "vdm/segment/budget.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace vdm::segment {
|
||||
|
||||
SegmentBudget::SegmentBudget() : SegmentBudget(Options{}) {}
|
||||
|
||||
SegmentBudget::SegmentBudget(Options opts)
|
||||
: max_active_(opts.max_active_segments ? opts.max_active_segments : 1),
|
||||
notify_period_(opts.notify_period) {
|
||||
notifier_ = std::jthread([this](std::stop_token st) { notifier_loop(st); });
|
||||
}
|
||||
|
||||
SegmentBudget::~SegmentBudget() {
|
||||
notifier_.request_stop();
|
||||
notify_cv_.notify_all();
|
||||
}
|
||||
|
||||
// --- allocation -----------------------------------------------------------------------
|
||||
|
||||
std::uint32_t SegmentBudget::effective_cap_locked(const Task &t) const {
|
||||
std::uint32_t base = t.resumable ? t.per_task_cap : 1;
|
||||
base = std::clamp<std::uint32_t>(base, 1, 32);
|
||||
if (auto it = host_caps_.find(t.host); it != host_caps_.end() && it->second > 0)
|
||||
base = std::min(base, it->second);
|
||||
return base;
|
||||
}
|
||||
|
||||
SegmentBudget::EngineBudget SegmentBudget::snapshot_locked() const {
|
||||
std::uint32_t starved = 0;
|
||||
for (const auto &[id, t] : tasks_)
|
||||
if (t.want >= 1 && t.held == 0)
|
||||
++starved;
|
||||
return EngineBudget{max_active_, active_, starved};
|
||||
}
|
||||
|
||||
// The two-pass fairness allocation. Recomputes every task's target from scratch (so a
|
||||
// live cap cut naturally produces target < held -> yield), diffs against the last
|
||||
// published target, and collects the callbacks to fire once mu_ is released.
|
||||
SegmentBudget::Plan SegmentBudget::reallocate_locked() {
|
||||
// Priority order: DAEMON's list first, then any registered task not in it (defensive;
|
||||
// "a running task absent from the list sorts last").
|
||||
std::vector<TaskId> order;
|
||||
order.reserve(tasks_.size());
|
||||
for (TaskId id : order_)
|
||||
if (tasks_.count(id))
|
||||
order.push_back(id);
|
||||
for (const auto &[id, _] : tasks_)
|
||||
if (std::find(order.begin(), order.end(), id) == order.end())
|
||||
order.push_back(id);
|
||||
|
||||
std::unordered_map<TaskId, std::uint32_t> target;
|
||||
target.reserve(order.size());
|
||||
std::uint32_t pool = max_active_;
|
||||
|
||||
auto capped_want = [&](TaskId id) {
|
||||
const Task &t = tasks_.at(id);
|
||||
return std::min(t.want, effective_cap_locked(t));
|
||||
};
|
||||
|
||||
// Guarantee pass: one slot each, in priority order, to anyone who wants one.
|
||||
for (TaskId id : order) {
|
||||
if (pool == 0)
|
||||
break;
|
||||
if (capped_want(id) >= 1) {
|
||||
target[id] = 1;
|
||||
--pool;
|
||||
}
|
||||
}
|
||||
// Growth pass: round-robin the remainder, up to each task's effective cap.
|
||||
while (pool > 0) {
|
||||
bool granted = false;
|
||||
for (TaskId id : order) {
|
||||
if (pool == 0)
|
||||
break;
|
||||
std::uint32_t &tv = target[id];
|
||||
if (tv < capped_want(id)) {
|
||||
++tv;
|
||||
--pool;
|
||||
granted = true;
|
||||
}
|
||||
}
|
||||
if (!granted)
|
||||
break;
|
||||
}
|
||||
|
||||
const SteadyTime now = std::chrono::steady_clock::now();
|
||||
Plan plan;
|
||||
for (auto &[id, t] : tasks_) {
|
||||
std::uint32_t nt = target.count(id) ? target[id] : 0;
|
||||
if (nt != t.target) {
|
||||
t.target = nt;
|
||||
if (t.on_target)
|
||||
plan.targets.emplace_back(t.on_target, nt);
|
||||
}
|
||||
// starvation timestamp bookkeeping
|
||||
bool starved_now = t.want >= 1 && t.held == 0;
|
||||
if (starved_now && !t.starved_since)
|
||||
t.starved_since = now;
|
||||
if (!starved_now)
|
||||
t.starved_since.reset();
|
||||
}
|
||||
|
||||
EngineBudget eb = snapshot_locked();
|
||||
bool starved_edge = (eb.tasks_starved == 0) != (last_starved_ == 0);
|
||||
if (eb != last_notified_)
|
||||
dirty_ = true;
|
||||
last_starved_ = eb.tasks_starved;
|
||||
|
||||
if (starved_edge && on_changed_) {
|
||||
plan.notify_now = std::make_pair(on_changed_, eb);
|
||||
last_notified_ = eb;
|
||||
dirty_ = false;
|
||||
}
|
||||
if (dirty_)
|
||||
notify_cv_.notify_one();
|
||||
return plan;
|
||||
}
|
||||
|
||||
void SegmentBudget::run(Plan &p) {
|
||||
for (auto &[fn, n] : p.targets)
|
||||
if (fn)
|
||||
fn(n);
|
||||
if (p.notify_now && p.notify_now->first)
|
||||
p.notify_now->first(p.notify_now->second);
|
||||
}
|
||||
|
||||
// --- task-facing --------------------------------------------------------------------
|
||||
|
||||
void SegmentBudget::register_task(TaskId id, const TaskParams ¶ms, SlotTargetFn on_target) {
|
||||
Plan plan;
|
||||
{
|
||||
std::lock_guard lk(mu_);
|
||||
Task t;
|
||||
t.host = params.host;
|
||||
t.per_task_cap = params.per_task_cap ? params.per_task_cap : 1;
|
||||
t.resumable = params.resumable;
|
||||
t.on_target = std::move(on_target);
|
||||
tasks_[id] = std::move(t);
|
||||
plan = reallocate_locked();
|
||||
}
|
||||
run(plan);
|
||||
}
|
||||
|
||||
void SegmentBudget::deregister_task(TaskId id) {
|
||||
Plan plan;
|
||||
{
|
||||
std::lock_guard lk(mu_);
|
||||
auto it = tasks_.find(id);
|
||||
if (it == tasks_.end())
|
||||
return;
|
||||
active_ -= it->second.held;
|
||||
tasks_.erase(it);
|
||||
plan = reallocate_locked();
|
||||
}
|
||||
run(plan);
|
||||
}
|
||||
|
||||
void SegmentBudget::set_want(TaskId id, std::uint32_t want) {
|
||||
Plan plan;
|
||||
{
|
||||
std::lock_guard lk(mu_);
|
||||
auto it = tasks_.find(id);
|
||||
if (it == tasks_.end())
|
||||
return;
|
||||
if (it->second.want == want)
|
||||
return;
|
||||
it->second.want = want;
|
||||
plan = reallocate_locked();
|
||||
}
|
||||
run(plan);
|
||||
}
|
||||
|
||||
bool SegmentBudget::confirm_slot(TaskId id) {
|
||||
std::lock_guard lk(mu_);
|
||||
auto it = tasks_.find(id);
|
||||
if (it == tasks_.end())
|
||||
return false;
|
||||
Task &t = it->second;
|
||||
if (t.held >= t.target)
|
||||
return false; // target was cut in the race
|
||||
++t.held;
|
||||
++active_;
|
||||
if (snapshot_locked() != last_notified_) {
|
||||
dirty_ = true;
|
||||
notify_cv_.notify_one();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SegmentBudget::release_slot(TaskId id) {
|
||||
Plan plan;
|
||||
{
|
||||
std::lock_guard lk(mu_);
|
||||
auto it = tasks_.find(id);
|
||||
if (it == tasks_.end() || it->second.held == 0)
|
||||
return;
|
||||
--it->second.held;
|
||||
--active_;
|
||||
plan = reallocate_locked();
|
||||
}
|
||||
run(plan);
|
||||
}
|
||||
|
||||
// --- DAEMON-facing ---------------------------------------------------------------------
|
||||
|
||||
void SegmentBudget::set_max_active_segments(std::uint32_t n) {
|
||||
Plan plan;
|
||||
{
|
||||
std::lock_guard lk(mu_);
|
||||
n = n ? n : 1;
|
||||
if (n == max_active_)
|
||||
return;
|
||||
max_active_ = n;
|
||||
plan = reallocate_locked();
|
||||
}
|
||||
run(plan);
|
||||
}
|
||||
|
||||
void SegmentBudget::set_host_segment_cap(std::string host, std::uint32_t cap) {
|
||||
Plan plan;
|
||||
{
|
||||
std::lock_guard lk(mu_);
|
||||
if (cap == 0)
|
||||
host_caps_.erase(host);
|
||||
else
|
||||
host_caps_[std::move(host)] = cap;
|
||||
plan = reallocate_locked();
|
||||
}
|
||||
run(plan);
|
||||
}
|
||||
|
||||
void SegmentBudget::set_task_order(std::span<const TaskId> priority_order) {
|
||||
Plan plan;
|
||||
{
|
||||
std::lock_guard lk(mu_);
|
||||
order_.assign(priority_order.begin(), priority_order.end());
|
||||
plan = reallocate_locked();
|
||||
}
|
||||
run(plan);
|
||||
}
|
||||
|
||||
SegmentBudget::EngineBudget SegmentBudget::budget() const {
|
||||
std::lock_guard lk(mu_);
|
||||
return snapshot_locked();
|
||||
}
|
||||
|
||||
std::uint32_t SegmentBudget::segments_active(TaskId id) const {
|
||||
std::lock_guard lk(mu_);
|
||||
auto it = tasks_.find(id);
|
||||
return it == tasks_.end() ? 0 : it->second.held;
|
||||
}
|
||||
|
||||
std::vector<TaskId> SegmentBudget::starved_tasks() const {
|
||||
std::lock_guard lk(mu_);
|
||||
std::vector<TaskId> out;
|
||||
for (const auto &[id, t] : tasks_)
|
||||
if (t.want >= 1 && t.held == 0)
|
||||
out.push_back(id);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<SteadyTime> SegmentBudget::starved_since(TaskId id) const {
|
||||
std::lock_guard lk(mu_);
|
||||
auto it = tasks_.find(id);
|
||||
return it == tasks_.end() ? std::nullopt : it->second.starved_since;
|
||||
}
|
||||
|
||||
void SegmentBudget::on_budget_changed(std::function<void(EngineBudget)> cb) {
|
||||
std::lock_guard lk(mu_);
|
||||
on_changed_ = std::move(cb);
|
||||
}
|
||||
|
||||
// --- notifier thread: coalesced <=4 Hz -----------------------------------------------
|
||||
|
||||
void SegmentBudget::notifier_loop(std::stop_token st) {
|
||||
std::unique_lock lk(mu_);
|
||||
while (!st.stop_requested()) {
|
||||
notify_cv_.wait_for(lk, notify_period_, [&] { return dirty_ || st.stop_requested(); });
|
||||
if (st.stop_requested())
|
||||
break;
|
||||
if (!dirty_)
|
||||
continue;
|
||||
EngineBudget eb = snapshot_locked();
|
||||
auto cb = on_changed_;
|
||||
last_notified_ = eb;
|
||||
last_starved_ = eb.tasks_starved;
|
||||
dirty_ = false;
|
||||
lk.unlock();
|
||||
if (cb)
|
||||
cb(eb);
|
||||
lk.lock();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace vdm::segment
|
||||
@@ -0,0 +1,312 @@
|
||||
// vdm/segment/segmenter.cpp
|
||||
|
||||
#include "vdm/segment/segmenter.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
namespace vdm::segment {
|
||||
namespace {
|
||||
constexpr std::uint32_t kNoIndex = std::numeric_limits<std::uint32_t>::max();
|
||||
constexpr std::uint64_t kU64Max = std::numeric_limits<std::uint64_t>::max();
|
||||
|
||||
bool is_live(SegState s) noexcept {
|
||||
return s == SegState::idle || s == SegState::connecting || s == SegState::downloading ||
|
||||
s == SegState::stalled;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Seg holds std::atomics, so it is neither copyable nor movable — every insertion is an
|
||||
// emplace_back that constructs it in place, followed by stores. This helper centralises
|
||||
// that. Caller holds mu_.
|
||||
std::uint32_t Segmenter::add_seg_locked(std::uint64_t start, std::uint64_t end,
|
||||
std::uint64_t completed, SegState state, bool assigned) {
|
||||
segs_.emplace_back(next_index_++, start, end);
|
||||
Seg &s = segs_.back();
|
||||
s.completed.store(completed);
|
||||
s.state.store(state);
|
||||
s.assigned = assigned;
|
||||
return s.index;
|
||||
}
|
||||
|
||||
// --- construction ---------------------------------------------------------------------
|
||||
|
||||
Segmenter::Segmenter(std::uint64_t total_size, std::uint32_t requested_segments, bool resumable,
|
||||
std::uint64_t min_segment_bytes)
|
||||
: total_size_(total_size),
|
||||
min_seg_(min_segment_bytes ? min_segment_bytes : 1),
|
||||
resumable_(resumable) {
|
||||
compute_target(requested_segments);
|
||||
}
|
||||
|
||||
Segmenter::Segmenter(std::uint64_t total_size, std::uint32_t requested_segments,
|
||||
const std::vector<ResumedRange> &resumed, bool resumable,
|
||||
std::uint64_t min_segment_bytes)
|
||||
: total_size_(total_size),
|
||||
min_seg_(min_segment_bytes ? min_segment_bytes : 1),
|
||||
resumable_(resumable) {
|
||||
compute_target(requested_segments);
|
||||
|
||||
// Validate the resumed table tiles [0, total_size) exactly.
|
||||
bool ok = resumable_ && total_size_ > 0 && !resumed.empty();
|
||||
if (ok) {
|
||||
std::vector<ResumedRange> sorted = resumed;
|
||||
std::sort(sorted.begin(), sorted.end(),
|
||||
[](const auto &a, const auto &b) { return a.start < b.start; });
|
||||
std::uint64_t cursor = 0;
|
||||
for (const auto &r : sorted) {
|
||||
if (r.start != cursor || r.end < r.start || r.completed > r.end - r.start + 1) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
cursor = r.end + 1;
|
||||
}
|
||||
if (ok && cursor != total_size_)
|
||||
ok = false;
|
||||
if (ok) {
|
||||
for (const auto &r : sorted) {
|
||||
bool done = r.completed == r.end - r.start + 1;
|
||||
add_seg_locked(r.start, r.end, r.completed,
|
||||
done ? SegState::complete : SegState::idle, false);
|
||||
}
|
||||
std::uint32_t incomplete = 0;
|
||||
for (const auto &s : segs_)
|
||||
if (s.state.load() != SegState::complete)
|
||||
++incomplete;
|
||||
target_count_ = std::clamp<std::uint32_t>(std::max(incomplete, 1u), 1, kMaxSegments);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Fall back to a fresh single/target layout (segs created lazily by assign_slot()).
|
||||
segs_.clear();
|
||||
}
|
||||
|
||||
void Segmenter::compute_target(std::uint32_t requested) {
|
||||
if (!resumable_ || total_size_ == 0) {
|
||||
target_count_ = 1;
|
||||
return;
|
||||
}
|
||||
std::uint64_t by_size = total_size_ / min_seg_;
|
||||
if (by_size == 0)
|
||||
by_size = 1;
|
||||
std::uint64_t t = requested == 0 ? kDefaultSegments : requested;
|
||||
t = std::min<std::uint64_t>(t, by_size);
|
||||
target_count_ = std::clamp<std::uint32_t>(static_cast<std::uint32_t>(t), 1, kMaxSegments);
|
||||
}
|
||||
|
||||
// --- helpers (mu_ held) --------------------------------------------------------------
|
||||
|
||||
std::uint64_t Segmenter::remaining_of_locked(const Seg &s) const noexcept {
|
||||
std::uint64_t end = s.end.load();
|
||||
std::uint64_t done = s.start + s.completed.load();
|
||||
return done > end ? 0 : end - done + 1;
|
||||
}
|
||||
|
||||
std::uint32_t Segmenter::assigned_count_locked() const noexcept {
|
||||
std::uint32_t n = 0;
|
||||
for (const auto &s : segs_)
|
||||
if (s.assigned)
|
||||
++n;
|
||||
return n;
|
||||
}
|
||||
|
||||
// Split the largest remaining range; hand back its second half as a new segment.
|
||||
std::uint32_t Segmenter::split_largest_remaining_locked() {
|
||||
Seg *victim = nullptr;
|
||||
std::uint64_t best = 0;
|
||||
for (auto &s : segs_) {
|
||||
if (!s.assigned || !is_live(s.state.load()))
|
||||
continue;
|
||||
std::uint64_t rem = remaining_of_locked(s);
|
||||
if (rem > best) {
|
||||
best = rem;
|
||||
victim = &s;
|
||||
}
|
||||
}
|
||||
if (!victim || best < 2 * min_seg_)
|
||||
return kNoIndex;
|
||||
|
||||
const std::uint64_t v_end = victim->end.load();
|
||||
const std::uint64_t half = best / 2; // >= min_seg_ since best >= 2*min
|
||||
const std::uint64_t mid = v_end - half; // victim keeps [start, mid]
|
||||
const std::uint64_t cur = victim->start + victim->completed.load();
|
||||
if (mid < cur || mid - cur + 1 < min_seg_)
|
||||
return kNoIndex; // victim would be too small
|
||||
|
||||
victim->end.store(mid); // the victim's worker reads end before each write and stops here
|
||||
return add_seg_locked(mid + 1, v_end, 0, SegState::idle, false);
|
||||
}
|
||||
|
||||
// --- public: structural (take the lock) --------------------------------------------
|
||||
|
||||
std::optional<std::uint32_t> Segmenter::assign_slot() {
|
||||
std::lock_guard lk(mu_);
|
||||
|
||||
if (!orphans_.empty()) {
|
||||
ResumedRange o = orphans_.front();
|
||||
orphans_.erase(orphans_.begin());
|
||||
return add_seg_locked(o.start, o.end, o.completed, SegState::connecting, true);
|
||||
}
|
||||
|
||||
if (assigned_count_locked() >= target_count_)
|
||||
return std::nullopt;
|
||||
|
||||
if (segs_.empty()) {
|
||||
std::uint64_t end = total_size_ > 0 ? total_size_ - 1 : kU64Max - 1;
|
||||
return add_seg_locked(0, end, 0, SegState::connecting, true);
|
||||
}
|
||||
|
||||
// Some resumed segments may be unassigned idle ranges — hand one out before splitting.
|
||||
for (auto &s : segs_) {
|
||||
if (!s.assigned && s.state.load() == SegState::idle) {
|
||||
s.assigned = true;
|
||||
s.state.store(SegState::connecting);
|
||||
return s.index;
|
||||
}
|
||||
}
|
||||
|
||||
std::uint32_t idx = split_largest_remaining_locked();
|
||||
if (idx == kNoIndex)
|
||||
return std::nullopt;
|
||||
segs_[idx].assigned = true;
|
||||
segs_[idx].state.store(SegState::connecting);
|
||||
return idx;
|
||||
}
|
||||
|
||||
std::optional<std::uint32_t> Segmenter::on_complete(std::uint32_t idx, bool may_steal) {
|
||||
std::lock_guard lk(mu_);
|
||||
if (idx >= segs_.size())
|
||||
return std::nullopt;
|
||||
Seg &seg = segs_[idx];
|
||||
seg.completed.store(seg.end.load() - seg.start + 1);
|
||||
seg.state.store(SegState::complete);
|
||||
seg.assigned = false;
|
||||
|
||||
if (!may_steal)
|
||||
return std::nullopt; // yielding the slot
|
||||
|
||||
if (!orphans_.empty()) {
|
||||
ResumedRange o = orphans_.front();
|
||||
orphans_.erase(orphans_.begin());
|
||||
return add_seg_locked(o.start, o.end, o.completed, SegState::connecting, true);
|
||||
}
|
||||
|
||||
std::uint32_t new_idx = split_largest_remaining_locked();
|
||||
if (new_idx == kNoIndex)
|
||||
return std::nullopt; // nothing to steal -> release the slot
|
||||
segs_[new_idx].assigned = true;
|
||||
segs_[new_idx].state.store(SegState::connecting);
|
||||
return new_idx;
|
||||
}
|
||||
|
||||
FailAction Segmenter::on_failed(std::uint32_t idx, bool connection_error, bool has_mirror) {
|
||||
std::lock_guard lk(mu_);
|
||||
if (idx >= segs_.size())
|
||||
return FailAction::retry;
|
||||
Seg &seg = segs_[idx];
|
||||
++seg.consecutive_failures;
|
||||
|
||||
if (connection_error && seg.consecutive_failures >= 3 && has_mirror) {
|
||||
std::uint64_t cur = seg.start + seg.completed.load();
|
||||
std::uint64_t end = seg.end.load();
|
||||
if (cur <= end)
|
||||
orphans_.push_back({cur, end, 0});
|
||||
seg.state.store(SegState::failed);
|
||||
seg.assigned = false;
|
||||
return FailAction::requeue;
|
||||
}
|
||||
return FailAction::retry;
|
||||
}
|
||||
|
||||
void Segmenter::note_connected(std::uint32_t idx) {
|
||||
std::lock_guard lk(mu_);
|
||||
if (idx < segs_.size())
|
||||
segs_[idx].consecutive_failures = 0;
|
||||
}
|
||||
|
||||
// --- public: per-worker accessors ----------------------------------------------
|
||||
//
|
||||
// These take mu_. They are called from the write path once per buffer flush (a few per
|
||||
// second per segment), not from the curl write callback — the no-lock/no-alloc rule is
|
||||
// about that callback and its ring buffer, not about progress bookkeeping. The segment
|
||||
// fields are still std::atomic so a reader that already holds a stable reference sees a
|
||||
// torn-free value, and so the deque element type is safe to relocate-free.
|
||||
|
||||
void Segmenter::advance(std::uint32_t idx, std::uint64_t bytes) noexcept {
|
||||
std::lock_guard lk(mu_);
|
||||
if (idx >= segs_.size())
|
||||
return;
|
||||
Seg &s = segs_[idx];
|
||||
std::uint64_t len = s.end.load() - s.start + 1;
|
||||
s.completed.store(bytes < len ? bytes : len);
|
||||
}
|
||||
|
||||
std::uint64_t Segmenter::segment_start(std::uint32_t idx) const noexcept {
|
||||
std::lock_guard lk(mu_);
|
||||
return idx < segs_.size() ? segs_[idx].start : 0;
|
||||
}
|
||||
std::uint64_t Segmenter::segment_end(std::uint32_t idx) const noexcept {
|
||||
std::lock_guard lk(mu_);
|
||||
return idx < segs_.size() ? segs_[idx].end.load() : 0;
|
||||
}
|
||||
std::uint64_t Segmenter::segment_completed(std::uint32_t idx) const noexcept {
|
||||
std::lock_guard lk(mu_);
|
||||
return idx < segs_.size() ? segs_[idx].completed.load() : 0;
|
||||
}
|
||||
SegState Segmenter::segment_state(std::uint32_t idx) const noexcept {
|
||||
std::lock_guard lk(mu_);
|
||||
return idx < segs_.size() ? segs_[idx].state.load() : SegState::failed;
|
||||
}
|
||||
void Segmenter::set_segment_state(std::uint32_t idx, SegState st) noexcept {
|
||||
std::lock_guard lk(mu_);
|
||||
if (idx < segs_.size())
|
||||
segs_[idx].state.store(st);
|
||||
}
|
||||
|
||||
// --- public: queries (take the lock) ----------------------------------------------
|
||||
|
||||
std::uint64_t Segmenter::downloaded() const {
|
||||
std::lock_guard lk(mu_);
|
||||
std::uint64_t sum = 0;
|
||||
for (const auto &s : segs_)
|
||||
sum += s.completed.load();
|
||||
return sum;
|
||||
}
|
||||
|
||||
bool Segmenter::all_complete() const {
|
||||
std::lock_guard lk(mu_);
|
||||
if (segs_.empty())
|
||||
return false;
|
||||
if (total_size_ == 0)
|
||||
return segs_.front().state.load() == SegState::complete;
|
||||
if (!orphans_.empty())
|
||||
return false;
|
||||
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> done; // [start, start+completed)
|
||||
for (const auto &s : segs_) {
|
||||
std::uint64_t c = s.completed.load();
|
||||
if (c > 0)
|
||||
done.emplace_back(s.start, s.start + c);
|
||||
}
|
||||
std::sort(done.begin(), done.end());
|
||||
std::uint64_t cursor = 0;
|
||||
for (auto [a, b] : done) {
|
||||
if (a > cursor)
|
||||
return false; // gap
|
||||
if (b > cursor)
|
||||
cursor = b;
|
||||
}
|
||||
return cursor >= total_size_;
|
||||
}
|
||||
|
||||
std::vector<SegmentView> Segmenter::snapshot() const {
|
||||
std::lock_guard lk(mu_);
|
||||
std::vector<SegmentView> out;
|
||||
out.reserve(segs_.size());
|
||||
for (const auto &s : segs_)
|
||||
out.push_back(SegmentView{s.index, s.start, s.end.load(), s.completed.load(),
|
||||
s.state.load(), s.consecutive_failures});
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace vdm::segment
|
||||
Reference in New Issue
Block a user