use crate::cache::ChunkCache;
use crate::retry;
use anyhow::{bail, Result};
use cavs_hash::{hash_chunk, to_hex, ChunkHash};
use cavs_proto::errors::ErrorCode;
use std::io::Read as _;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Mutex;
pub const ACCEPT_STORED_HEADER: &str = "x-cavs-accept-stored";
pub const COMPRESSION_HEADER: &str = "x-cavs-compression";
pub const RAW_LEN_HEADER: &str = "x-cavs-raw-len";
pub const DEFAULT_CONNECTIONS: usize = 8;
#[derive(Debug, Default, Clone, Copy)]
pub struct ParallelStats {
pub wire_bytes: u64,
pub raw_bytes: u64,
pub count: u64,
}
pub fn fetch_chunks_parallel(
agent: &ureq::Agent,
server: &str,
asset: &str,
hashes: &[ChunkHash],
cache: &ChunkCache,
connections: usize,
) -> Result<ParallelStats> {
if hashes.is_empty() {
return Ok(ParallelStats::default());
}
let workers = connections.max(1).min(hashes.len());
let next = AtomicUsize::new(0);
let failed = AtomicBool::new(false);
let first_error: Mutex<Option<anyhow::Error>> = Mutex::new(None);
let wire = AtomicUsize::new(0);
let raw = AtomicUsize::new(0);
let count = AtomicUsize::new(0);
std::thread::scope(|scope| {
for _ in 0..workers {
let agent = agent.clone();
scope.spawn(|| {
let agent = agent;
loop {
if failed.load(Ordering::Relaxed) {
return;
}
let idx = next.fetch_add(1, Ordering::Relaxed);
if idx >= hashes.len() {
return;
}
let hash = hashes[idx];
match fetch_one(&agent, server, asset, &hash) {
Ok((raw_bytes, wire_len)) => {
if let Err(e) = cache.put(&hash, &raw_bytes) {
record_error(&failed, &first_error, e);
return;
}
wire.fetch_add(wire_len, Ordering::Relaxed);
raw.fetch_add(raw_bytes.len(), Ordering::Relaxed);
count.fetch_add(1, Ordering::Relaxed);
}
Err(e) => {
record_error(&failed, &first_error, e);
return;
}
}
}
});
}
});
if let Some(e) = first_error.into_inner().unwrap() {
return Err(e);
}
Ok(ParallelStats {
wire_bytes: wire.load(Ordering::Relaxed) as u64,
raw_bytes: raw.load(Ordering::Relaxed) as u64,
count: count.load(Ordering::Relaxed) as u64,
})
}
fn record_error(failed: &AtomicBool, slot: &Mutex<Option<anyhow::Error>>, e: anyhow::Error) {
failed.store(true, Ordering::Relaxed);
let mut guard = slot.lock().unwrap();
if guard.is_none() {
*guard = Some(e);
}
}
fn fetch_one(
agent: &ureq::Agent,
server: &str,
asset: &str,
hash: &ChunkHash,
) -> Result<(Vec<u8>, usize)> {
let hex = to_hex(hash);
let url = format!("{server}/api/assets/{asset}/chunks/{hex}");
let resp = retry::with_retry(&format!("GET {url}"), || {
agent.get(&url).set(ACCEPT_STORED_HEADER, "1").call()
})?;
let compression: u8 = resp
.header(COMPRESSION_HEADER)
.and_then(|s| s.parse().ok())
.unwrap_or(cavs_proto::WIRE_COMPRESSION_NONE);
let len_raw: Option<usize> = resp.header(RAW_LEN_HEADER).and_then(|s| s.parse().ok());
let mut wire = Vec::new();
resp.into_reader().read_to_end(&mut wire)?;
let wire_len = wire.len();
let raw = match compression {
cavs_proto::WIRE_COMPRESSION_NONE => wire,
cavs_proto::WIRE_COMPRESSION_ZSTD => {
let cap = len_raw.unwrap_or(16 * 1024 * 1024);
zstd::bulk::decompress(&wire, cap)
.map_err(|e| anyhow::anyhow!("decompressing chunk {hex}: {e}"))?
}
other => bail!("unknown wire compression {other} for chunk {hex}"),
};
if let Some(n) = len_raw {
if raw.len() != n {
bail!(
"{}",
ErrorCode::ChunkHashMismatch.msg(format!(
"chunk {hex}: raw length {} != declared {n}",
raw.len()
))
);
}
}
if hash_chunk(&raw) != *hash {
bail!(
"{}",
ErrorCode::ChunkHashMismatch.msg(format!("chunk {hex} failed hash verification"))
);
}
Ok((raw, wire_len))
}