1mod metrics;
4#[cfg(not(miri))]
6#[cfg(test)]
7mod tests;
8
9use crate::disk_piece_cache::metrics::DiskPieceCacheMetrics;
10use crate::farm;
11use crate::farm::{FarmError, PieceCacheId, PieceCacheOffset};
12use crate::single_disk_farm::direct_io_file_wrapper::DirectIoFileWrapper;
13use crate::utils::AsyncJoinOnDrop;
14use ab_core_primitives::hashes::Blake3Hash;
15use ab_core_primitives::pieces::{Piece, PieceIndex};
16use ab_direct_io_file::AlignedPage;
17use ab_farmer_components::file_ext::FileExt;
18use async_trait::async_trait;
19use bytes::BytesMut;
20use futures::channel::mpsc;
21use futures::{SinkExt, Stream, StreamExt, stream};
22use parking_lot::Mutex;
23use prometheus_client::registry::Registry;
24use std::num::NonZeroU32;
25use std::path::Path;
26use std::sync::atomic::{AtomicU8, Ordering};
27use std::task::Poll;
28use std::{fs, io, iter};
29use thiserror::Error;
30use tokio::runtime::Handle;
31use tokio::task;
32use tracing::{Span, debug, info, warn};
33use triomphe::Arc;
34
35const CONTENTS_READ_SKIP_LIMIT: usize = 3;
38const PIECES_READING_CONCURRENCY: usize = 32;
40const ELEMENT_SIZE: usize =
43 (PieceIndex::SIZE + Piece::SIZE + Blake3Hash::SIZE).next_multiple_of(AlignedPage::SIZE);
44
45#[derive(Debug, Error)]
47pub enum DiskPieceCacheError {
48 #[error("Disk piece cache I/O error: {0}")]
50 Io(#[from] io::Error),
51 #[error("Can't preallocate cache file, probably not enough space on disk: {0}")]
53 CantPreallocateCacheFile(io::Error),
54 #[error("Offset outsize of range: provided {provided}, max {max}")]
56 OffsetOutsideOfRange {
57 provided: u32,
59 max: u32,
61 },
62 #[error("Checksum mismatch")]
64 ChecksumMismatch,
65}
66
67#[derive(Debug)]
68struct FilePool {
69 files: Box<[DirectIoFileWrapper; PIECES_READING_CONCURRENCY]>,
70 cursor: AtomicU8,
71}
72
73impl FilePool {
74 fn open(path: &Path) -> io::Result<Self> {
75 let files = iter::repeat_with(|| DirectIoFileWrapper::open(path))
76 .take(PIECES_READING_CONCURRENCY)
77 .collect::<Result<Box<_>, _>>()?
78 .try_into()
79 .expect("Statically correct length; qed");
80 Ok(Self {
81 files,
82 cursor: AtomicU8::new(0),
83 })
84 }
85
86 fn read(&self) -> &DirectIoFileWrapper {
87 let position = usize::from(self.cursor.fetch_add(1, Ordering::Relaxed));
88 &self.files[position % PIECES_READING_CONCURRENCY]
89 }
90
91 fn write(&self) -> &DirectIoFileWrapper {
92 &self.files[0]
95 }
96}
97
98#[derive(Debug)]
99struct Inner {
100 id: PieceCacheId,
101 files: FilePool,
102 max_num_elements: u32,
103 metrics: Option<DiskPieceCacheMetrics>,
104}
105
106#[derive(Debug, Clone)]
111pub struct DiskPieceCache {
112 inner: Arc<Inner>,
113}
114
115#[async_trait]
116impl farm::PieceCache for DiskPieceCache {
117 fn id(&self) -> &PieceCacheId {
118 &self.inner.id
119 }
120
121 #[inline]
122 fn max_num_elements(&self) -> u32 {
123 self.inner.max_num_elements
124 }
125
126 async fn contents(
127 &self,
128 ) -> Result<
129 Box<
130 dyn Stream<Item = Result<(PieceCacheOffset, Option<PieceIndex>), FarmError>>
131 + Unpin
132 + Send
133 + '_,
134 >,
135 FarmError,
136 > {
137 let this = self.clone();
138 let (mut sender, receiver) = mpsc::channel(100_000);
139 let span = Span::current();
140 let read_contents = task::spawn_blocking(move || {
141 let _guard = span.enter();
142
143 let contents = this.contents();
144 for (piece_cache_offset, maybe_piece) in contents {
145 if let Err(error) =
146 Handle::current().block_on(sender.send(Ok((piece_cache_offset, maybe_piece))))
147 {
148 debug!(%error, "Aborting contents iteration due to receiver dropping");
149 break;
150 }
151 }
152 });
153 let read_contents = Mutex::new(Some(AsyncJoinOnDrop::new(read_contents, false)));
154 let mut receiver = receiver;
156
157 Ok(Box::new(stream::poll_fn(move |ctx| {
158 let poll_result = receiver.poll_next_unpin(ctx);
159
160 if matches!(poll_result, Poll::Ready(None)) {
161 read_contents.lock().take();
162 }
163
164 poll_result
165 })))
166 }
167
168 async fn write_piece(
169 &self,
170 offset: PieceCacheOffset,
171 piece_index: PieceIndex,
172 piece: &Piece,
173 ) -> Result<(), FarmError> {
174 let piece = piece.clone();
175 let piece_cache = self.clone();
176 Ok(AsyncJoinOnDrop::new(
177 task::spawn_blocking(move || piece_cache.write_piece(offset, piece_index, &piece)),
178 false,
179 )
180 .await??)
181 }
182
183 async fn read_piece_index(
184 &self,
185 offset: PieceCacheOffset,
186 ) -> Result<Option<PieceIndex>, FarmError> {
187 let piece_cache = self.clone();
188 let span = Span::current();
189 Ok(AsyncJoinOnDrop::new(
190 task::spawn_blocking(move || {
191 let _guard = span.enter();
192
193 piece_cache.read_piece_index(offset)
194 }),
195 false,
196 )
197 .await??)
198 }
199
200 async fn read_piece(
201 &self,
202 offset: PieceCacheOffset,
203 ) -> Result<Option<(PieceIndex, Piece)>, FarmError> {
204 let span = Span::current();
205
206 if cfg!(windows) {
213 Ok(task::block_in_place(|| {
214 let _guard = span.enter();
215
216 self.read_piece(offset)
217 })?)
218 } else {
219 let piece_cache = self.clone();
220 Ok(AsyncJoinOnDrop::new(
221 task::spawn_blocking(move || {
222 let _guard = span.enter();
223
224 piece_cache.read_piece(offset)
225 }),
226 false,
227 )
228 .await??)
229 }
230 }
231
232 async fn read_pieces(
233 &self,
234 offsets: Box<dyn Iterator<Item = PieceCacheOffset> + Send>,
235 ) -> Result<
236 Box<
237 dyn Stream<Item = Result<(PieceCacheOffset, Option<(PieceIndex, Piece)>), FarmError>>
238 + Send
239 + Unpin
240 + '_,
241 >,
242 FarmError,
243 > {
244 let iter = offsets.map(move |offset| async move {
245 Ok((offset, farm::PieceCache::read_piece(self, offset).await?))
246 });
247 Ok(Box::new(
248 stream::iter(iter).buffer_unordered(PIECES_READING_CONCURRENCY),
251 ))
252 }
253}
254
255impl DiskPieceCache {
256 pub(crate) const FILE_NAME: &'static str = "piece_cache.bin";
257
258 pub fn open(
260 directory: &Path,
261 capacity: NonZeroU32,
262 id: Option<PieceCacheId>,
263 registry: Option<&mut Registry>,
264 ) -> Result<Self, DiskPieceCacheError> {
265 let capacity = capacity.get();
266 let files = FilePool::open(&directory.join(Self::FILE_NAME))?;
267
268 let expected_size = u64::from(Self::element_size()) * u64::from(capacity);
269 let expected_size =
271 expected_size.div_ceil(AlignedPage::SIZE as u64) * AlignedPage::SIZE as u64;
272 {
273 let file = files.write();
274 if file.size()? != expected_size {
275 file.preallocate(expected_size)
278 .map_err(DiskPieceCacheError::CantPreallocateCacheFile)?;
279 file.set_len(expected_size)?;
281 }
282 }
283
284 let id = id.unwrap_or_else(PieceCacheId::new);
286 let metrics = registry.map(|registry| DiskPieceCacheMetrics::new(registry, &id, capacity));
287
288 Ok(Self {
289 inner: Arc::new(Inner {
290 id,
291 files,
292 max_num_elements: capacity,
293 metrics,
294 }),
295 })
296 }
297
298 pub const fn element_size() -> u32 {
300 ELEMENT_SIZE as u32
301 }
302
303 pub(crate) fn contents(
308 &self,
309 ) -> impl ExactSizeIterator<Item = (PieceCacheOffset, Option<PieceIndex>)> + '_ {
310 let mut element = vec![0; ELEMENT_SIZE];
311 let count_total = self.inner.metrics.as_ref().is_some_and(|metrics| {
312 metrics.contents.inc();
313 metrics.capacity_used.get() == 0
314 });
315 let mut current_skip = 0;
316
317 (0..self.inner.max_num_elements).map(move |offset| {
319 if current_skip > CONTENTS_READ_SKIP_LIMIT {
320 return (PieceCacheOffset(offset), None);
321 }
322
323 match self.read_piece_internal(offset, &mut element) {
324 Ok(maybe_piece_index) => {
325 if maybe_piece_index.is_none() {
326 current_skip += 1;
327 } else {
328 if count_total && let Some(metrics) = &self.inner.metrics {
329 metrics.capacity_used.inc();
330 }
331 current_skip = 0;
332 }
333
334 (PieceCacheOffset(offset), maybe_piece_index)
335 }
336 Err(error) => {
337 warn!(%error, %offset, "Failed to read cache element");
338
339 current_skip += 1;
340
341 (PieceCacheOffset(offset), None)
342 }
343 }
344 })
345 }
346
347 pub(crate) fn write_piece(
352 &self,
353 offset: PieceCacheOffset,
354 piece_index: PieceIndex,
355 piece: &Piece,
356 ) -> Result<(), DiskPieceCacheError> {
357 let PieceCacheOffset(offset) = offset;
358 if offset >= self.inner.max_num_elements {
359 return Err(DiskPieceCacheError::OffsetOutsideOfRange {
360 provided: offset,
361 max: self.inner.max_num_elements - 1,
362 });
363 }
364
365 if let Some(metrics) = &self.inner.metrics {
366 metrics.write_piece.inc();
367 let capacity_used = i64::from(offset + 1);
368 if metrics.capacity_used.get() != capacity_used {
369 metrics.capacity_used.set(capacity_used);
370 }
371 }
372 let element_offset = u64::from(offset) * u64::from(Self::element_size());
373
374 let piece_index_bytes = piece_index.to_bytes();
375 let mut bytes = Vec::with_capacity(ELEMENT_SIZE);
377 bytes.extend_from_slice(&piece_index_bytes);
378 bytes.extend_from_slice(piece.as_ref());
379 bytes.extend_from_slice(
380 {
381 let mut hasher = blake3::Hasher::new();
382 hasher.update(&piece_index_bytes);
383 hasher.update(piece.as_ref());
384 hasher.finalize()
385 }
386 .as_bytes(),
387 );
388 bytes.resize(ELEMENT_SIZE, 0);
389 self.inner
390 .files
391 .write()
392 .write_all_at(&bytes, element_offset)?;
393
394 Ok(())
395 }
396
397 pub(crate) fn read_piece_index(
404 &self,
405 offset: PieceCacheOffset,
406 ) -> Result<Option<PieceIndex>, DiskPieceCacheError> {
407 let PieceCacheOffset(offset) = offset;
408 if offset >= self.inner.max_num_elements {
409 warn!(%offset, "Trying to read piece out of range, this must be an implementation bug");
410 return Err(DiskPieceCacheError::OffsetOutsideOfRange {
411 provided: offset,
412 max: self.inner.max_num_elements - 1,
413 });
414 }
415
416 if let Some(metrics) = &self.inner.metrics {
417 metrics.read_piece_index.inc();
418 }
419 self.read_piece_internal(offset, &mut vec![0; ELEMENT_SIZE])
420 }
421
422 pub(crate) fn read_piece(
429 &self,
430 offset: PieceCacheOffset,
431 ) -> Result<Option<(PieceIndex, Piece)>, DiskPieceCacheError> {
432 let PieceCacheOffset(offset) = offset;
433 if offset >= self.inner.max_num_elements {
434 warn!(%offset, "Trying to read piece out of range, this must be an implementation bug");
435 return Err(DiskPieceCacheError::OffsetOutsideOfRange {
436 provided: offset,
437 max: self.inner.max_num_elements - 1,
438 });
439 }
440
441 if let Some(metrics) = &self.inner.metrics {
442 metrics.read_piece.inc();
443 }
444 let mut element = BytesMut::zeroed(ELEMENT_SIZE);
445 if let Some(piece_index) = self.read_piece_internal(offset, &mut element)? {
446 let element = element.freeze();
447 let piece =
448 Piece::try_from(element.slice_ref(&element[PieceIndex::SIZE..][..Piece::SIZE]))
449 .expect("Correct length; qed");
450 Ok(Some((piece_index, piece)))
451 } else {
452 Ok(None)
453 }
454 }
455
456 fn read_piece_internal(
457 &self,
458 offset: u32,
459 element: &mut [u8],
460 ) -> Result<Option<PieceIndex>, DiskPieceCacheError> {
461 self.inner
462 .files
463 .read()
464 .read_exact_at(element, u64::from(offset) * u64::from(Self::element_size()))?;
465
466 let (piece_index_bytes, remaining_bytes) = element.split_at(PieceIndex::SIZE);
467 let (piece_bytes, remaining_bytes) = remaining_bytes.split_at(Piece::SIZE);
468 let (expected_checksum, _padding) = remaining_bytes.split_at(Blake3Hash::SIZE);
470
471 let actual_checksum = {
473 let mut hasher = blake3::Hasher::new();
474 hasher.update(piece_index_bytes);
475 hasher.update(piece_bytes);
476 *hasher.finalize().as_bytes()
477 };
478 if actual_checksum != expected_checksum {
479 if element.iter().all(|&byte| byte == 0) {
480 return Ok(None);
481 }
482
483 debug!(
484 actual_checksum = %hex::encode(actual_checksum),
485 expected_checksum = %hex::encode(expected_checksum),
486 "Hash doesn't match, corrupted piece in cache"
487 );
488
489 return Err(DiskPieceCacheError::ChecksumMismatch);
490 }
491
492 let piece_index = PieceIndex::from_bytes(
493 piece_index_bytes
494 .try_into()
495 .expect("Statically known to have correct size; qed"),
496 );
497 Ok(Some(piece_index))
498 }
499
500 pub(crate) fn wipe(directory: &Path) -> io::Result<()> {
501 let piece_cache = directory.join(Self::FILE_NAME);
502 if !piece_cache.exists() {
503 return Ok(());
504 }
505 info!("Deleting piece cache file at {}", piece_cache.display());
506 fs::remove_file(piece_cache)
507 }
508}