use std::collections::VecDeque;
use std::sync::Mutex;
use std::sync::atomic::AtomicBool;
use std::sync::mpsc::{Receiver, Sender};
use crate::fetch::FetchError;
use crate::{
IdMap, ObjectId, ObjectKind, Objects, PeerIdMap, PeerIdSet, ReadLimits, UnverifiedObject,
};
const AHEAD: usize = 256;
const AHEAD_BYTES: usize = 64 * 1024 * 1024;
pub(super) struct Fetched {
pub kind: ObjectKind,
pub length: usize,
pub payload: Option<UnverifiedObject>,
}
type Outcome = Result<Fetched, FetchError>;
type Job = (ObjectId, Option<ObjectKind>);
pub(super) fn read(
store: &Objects,
id: ObjectId,
kind: Option<ObjectKind>,
limits: ReadLimits,
cancel: &AtomicBool,
) -> Outcome {
if kind.is_some_and(|kind| kind != ObjectKind::Blob) {
let object = store
.read_unverified(id, limits, cancel)
.map_err(|source| FetchError::LocalRead { id, source })?
.ok_or(FetchError::Missing(id))?;
Ok(Fetched {
kind: object.kind(),
length: object.data().len(),
payload: Some(object),
})
} else {
let (kind, length, _) = store
.transfer_object(id, limits, cancel)
.map_err(|source| FetchError::LocalRead { id, source })?
.ok_or(FetchError::Missing(id))?;
Ok(Fetched {
kind,
length,
payload: None,
})
}
}
struct Workers {
jobs: Sender<Job>,
done: Receiver<(ObjectId, Outcome)>,
}
pub(super) struct Prefetch<'a> {
store: &'a Objects,
limits: ReadLimits,
cancel: &'a AtomicBool,
workers: Option<Workers>,
submitted: PeerIdSet,
finished: PeerIdMap<Outcome>,
scanned: usize,
in_flight: usize,
depth: usize,
ahead_bytes: usize,
}
pub(super) fn with_prefetch<R>(
store: &Objects,
limits: ReadLimits,
cancel: &AtomicBool,
max_threads: usize,
walk: impl FnOnce(&mut Prefetch<'_>) -> R,
) -> R {
let threads = std::thread::available_parallelism()
.map_or(1, std::num::NonZeroUsize::get)
.min(max_threads);
let mut prefetch = Prefetch {
store,
limits,
cancel,
workers: None,
submitted: PeerIdSet::default(),
finished: PeerIdMap::default(),
scanned: 0,
in_flight: 0,
depth: threads * 2,
ahead_bytes: 0,
};
if threads < 2 {
return walk(&mut prefetch);
}
let (jobs, queue) = std::sync::mpsc::channel::<Job>();
let (results, done) = std::sync::mpsc::channel();
let queue = Mutex::new(queue);
std::thread::scope(|scope| {
for _ in 0..threads {
let results = results.clone();
let queue = &queue;
scope.spawn(move || {
loop {
let job = queue.lock().unwrap_or_else(|e| e.into_inner()).recv();
let Ok((id, kind)) = job else {
return;
};
if results
.send((id, read(store, id, kind, limits, cancel)))
.is_err()
{
return;
}
}
});
}
drop(results);
prefetch.workers = Some(Workers { jobs, done });
let result = walk(&mut prefetch);
prefetch.workers = None;
result
})
}
impl Prefetch<'_> {
pub fn popped(&mut self) {
self.scanned = self.scanned.saturating_sub(1);
}
pub fn take(
&mut self,
id: ObjectId,
kind: Option<ObjectKind>,
pending: &VecDeque<(ObjectId, Option<ObjectKind>)>,
observed: &IdMap<ObjectKind>,
) -> Outcome {
let Some(Workers { jobs, done }) = &self.workers else {
return read(self.store, id, kind, self.limits, self.cancel);
};
if self.submitted.insert(id) {
jobs.send((id, kind))
.map_err(|_| FetchError::Protocol("local read worker stopped"))?;
self.in_flight += 1;
}
while self.submitted.len() < AHEAD
&& self.in_flight < self.depth
&& self.ahead_bytes < AHEAD_BYTES
&& let Some(&(next, next_kind)) = pending.get(self.scanned)
{
self.scanned += 1;
if !observed.contains_key(&next) && self.submitted.insert(next) {
jobs.send((next, next_kind))
.map_err(|_| FetchError::Protocol("local read worker stopped"))?;
self.in_flight += 1;
}
}
loop {
if let Some(outcome) = self.finished.remove(&id) {
self.submitted.remove(&id);
self.ahead_bytes -= payload_bytes(&outcome);
return outcome;
}
let (finished, outcome) = done
.recv()
.map_err(|_| FetchError::Protocol("local read worker stopped"))?;
self.in_flight -= 1;
self.ahead_bytes += payload_bytes(&outcome);
self.finished.insert(finished, outcome);
}
}
}
fn payload_bytes(outcome: &Outcome) -> usize {
outcome
.as_ref()
.ok()
.and_then(|fetched| fetched.payload.as_ref())
.map_or(0, |payload| payload.data().len())
}