use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use reqwest::Client;
use tracing::{debug, warn};
use crate::error::TransferError;
use crate::file::DestFile;
use crate::limit::RateLimiter;
use crate::mirror::SourceSet;
use crate::progress::{Event, Reporter};
use crate::retry::{Decision, RetryPolicy};
use crate::scheduler::{Lease, Scheduler};
use crate::shutdown::Cancel;
use crate::storage::OPEN_END;
pub struct WorkerCtx {
pub client: Client,
pub sources: Arc<SourceSet>,
pub file: Arc<DestFile>,
pub scheduler: Arc<Scheduler>,
pub reporter: Reporter,
pub retry: RetryPolicy,
pub limiter: Option<Arc<RateLimiter>>,
pub read_timeout: Duration,
pub expected_total: Option<u64>,
pub ranges_supported: bool,
pub discovered_size: Arc<AtomicU64>,
pub cancel: Cancel,
pub primed: Mutex<Option<PrimedBody>>,
}
pub struct PrimedBody {
pub url: url::Url,
pub response: reqwest::Response,
}
impl WorkerCtx {
fn take_primed(&self, url: &url::Url, start: u64) -> Option<reqwest::Response> {
if start != 0 {
return None;
}
let mut slot = self.primed.lock().unwrap_or_else(|e| e.into_inner());
match slot.as_ref() {
Some(primed) if &primed.url == url => slot.take().map(|p| p.response),
_ => None,
}
}
}
pub enum WorkerOutcome {
Finished,
Fatal(TransferError),
}
pub async fn run(ctx: Arc<WorkerCtx>, worker_id: usize) -> WorkerOutcome {
loop {
if ctx.cancel.is_cancelled() {
return WorkerOutcome::Finished;
}
let Some((lease, split)) = ctx.scheduler.acquire() else {
if ctx.scheduler.is_finished() || ctx.scheduler.has_failure() {
return WorkerOutcome::Finished;
}
tokio::select! {
_ = ctx.scheduler.wait_for_change(Duration::from_millis(250)) => {}
_ = ctx.cancel.cancelled() => return WorkerOutcome::Finished,
}
continue;
};
if let Some(split) = split {
ctx.reporter.emit(Event::RangeSplit {
index: split.shrunk.idx,
new_index: split.added.idx,
at: split.added.start,
});
debug!(
worker = worker_id,
victim = split.shrunk.idx,
new_range = split.added.idx,
at = split.added.start,
"split a slow range"
);
}
ctx.reporter.emit(Event::RangeStarted {
index: lease.idx,
start: lease.cursor(),
end: lease.end(),
});
match transfer(&ctx, &lease, worker_id).await {
Ok(()) => {
ctx.scheduler.complete(lease.idx);
ctx.reporter
.emit(Event::RangeCompleted { index: lease.idx });
let (done, total) = ctx.scheduler.counts();
ctx.reporter.stats.set_ranges_complete(done);
ctx.reporter.stats.set_ranges_total(total);
}
Err(TransferError::Cancelled) => {
ctx.scheduler.release(lease.idx);
return WorkerOutcome::Finished;
}
Err(err) => {
ctx.scheduler.fail(lease.idx);
warn!(worker = worker_id, range = lease.idx, %err, "range failed permanently");
return WorkerOutcome::Fatal(err);
}
}
}
}
async fn transfer(ctx: &WorkerCtx, lease: &Lease, worker_id: usize) -> Result<(), TransferError> {
let mut attempts = 0u32;
loop {
if ctx.cancel.is_cancelled() {
return Err(TransferError::Cancelled);
}
if lease.remaining() == 0 {
return Ok(());
}
let (source_idx, source) = ctx.sources.pick();
let url = source.url.clone();
let validator = source.validator.clone();
let before = lease.progress();
match attempt(ctx, lease, &url, validator.as_deref()).await {
Ok(()) => {
ctx.sources.reward(source_idx);
return Ok(());
}
Err(err) => {
if lease.progress() > before {
attempts = 0;
}
attempts += 1;
ctx.sources.penalise(source_idx);
match ctx.retry.decide(&err, attempts) {
Decision::Retry { delay, attempt } => {
ctx.reporter.stats.record_retry();
ctx.reporter.emit(Event::RetryScheduled {
index: Some(lease.idx),
attempt,
delay_ms: delay.as_millis() as u64,
reason: err.to_string(),
});
debug!(
worker = worker_id,
range = lease.idx,
attempt,
?delay,
%err,
"retrying range"
);
tokio::select! {
_ = tokio::time::sleep(delay) => {}
_ = ctx.cancel.cancelled() => return Err(TransferError::Cancelled),
}
}
Decision::GiveUp => return Err(err),
}
}
}
}
}
async fn attempt(
ctx: &WorkerCtx,
lease: &Lease,
url: &url::Url,
validator: Option<&str>,
) -> Result<(), TransferError> {
let start_cursor = lease.cursor();
let open_ended = lease.is_open_ended();
let (req_start, req_end) = if ctx.ranges_supported {
(
start_cursor,
if open_ended { None } else { Some(lease.end()) },
)
} else {
if start_cursor != 0 {
return Err(TransferError::Protocol(
"cannot resume from the middle: this server does not support range requests".into(),
));
}
(0, None)
};
let mut resp = match ctx.take_primed(url, req_start) {
Some(primed) => primed,
None => {
crate::http::get_range(
&ctx.client,
url,
req_start,
req_end,
validator,
ctx.expected_total,
)
.await?
}
};
ctx.reporter.stats.connection_opened();
let _conn = ConnectionGuard(&ctx.reporter);
let mut cursor = start_cursor;
let mut pending_event_bytes = 0u64;
let mut last_event = Instant::now();
loop {
if ctx.cancel.is_cancelled() {
return Err(TransferError::Cancelled);
}
let chunk = tokio::select! {
biased;
_ = ctx.cancel.cancelled() => return Err(TransferError::Cancelled),
read = tokio::time::timeout(ctx.read_timeout, resp.chunk()) => match read {
Err(_) => return Err(TransferError::Timeout(ctx.read_timeout)),
Ok(Err(e)) => return Err(TransferError::from_reqwest(&e)),
Ok(Ok(None)) => break,
Ok(Ok(Some(chunk))) => chunk,
},
};
if chunk.is_empty() {
continue;
}
let end = lease.end();
let writable = if open_ended && end >= OPEN_END {
chunk.len() as u64
} else {
let room = end.saturating_sub(cursor).saturating_add(1);
(chunk.len() as u64).min(room)
};
if writable == 0 {
break;
}
if let Some(limiter) = &ctx.limiter {
limiter.acquire(writable).await;
}
let slice = &chunk[..writable as usize];
ctx.file
.write_at(slice, cursor)
.map_err(|e| TransferError::Io(e.to_string()))?;
cursor += writable;
lease.publish_progress(cursor - lease.start);
ctx.reporter.stats.add_downloaded(writable);
pending_event_bytes += writable;
if last_event.elapsed() >= Duration::from_millis(100) {
ctx.reporter.emit(Event::BytesWritten {
index: lease.idx,
bytes: pending_event_bytes,
});
pending_event_bytes = 0;
last_event = Instant::now();
}
if !open_ended && cursor > end {
break;
}
}
if pending_event_bytes > 0 {
ctx.reporter.emit(Event::BytesWritten {
index: lease.idx,
bytes: pending_event_bytes,
});
}
if open_ended && lease.end() >= OPEN_END {
ctx.discovered_size.store(cursor, Ordering::Release);
ctx.scheduler
.set_end(lease.idx, cursor.saturating_sub(1).max(lease.start));
return Ok(());
}
if cursor > lease.end() {
return Ok(());
}
Err(TransferError::Network(format!(
"connection closed with {} bytes of the range still missing",
lease.end() + 1 - cursor
)))
}
struct ConnectionGuard<'a>(&'a Reporter);
impl Drop for ConnectionGuard<'_> {
fn drop(&mut self) {
self.0.stats.connection_closed();
}
}