ab_farmer/single_disk_farm/
plot_cache.rs1#[cfg(not(miri))]
5#[cfg(test)]
6mod tests;
7
8use crate::farm::{FarmError, MaybePieceStoredResult, PlotCache};
9use crate::single_disk_farm::direct_io_file_wrapper::DirectIoFileWrapper;
10use crate::utils::AsyncJoinOnDrop;
11use ab_core_primitives::hashes::Blake3Hash;
12use ab_core_primitives::pieces::{Piece, PieceIndex};
13use ab_farmer_components::file_ext::FileExt;
14use ab_farmer_components::sector::SectorMetadataChecksummed;
15use ab_networking::libp2p::kad::RecordKey;
16use ab_networking::utils::multihash::ToMultihash;
17use async_lock::RwLock as AsyncRwLock;
18use async_trait::async_trait;
19use bytes::BytesMut;
20use parking_lot::RwLock;
21use std::collections::HashMap;
22use std::sync::{Arc as StdArc, Weak};
23use std::{io, mem};
24use thiserror::Error;
25use tokio::task;
26use tracing::{debug, info, trace, warn};
27use triomphe::Arc;
28
29#[derive(Debug, Error)]
31pub enum DiskPlotCacheError {
32 #[error("Plot cache I/O error: {0}")]
34 Io(#[from] io::Error),
35 #[error("Failed to spawn task for blocking thread: {0}")]
37 TokioJoinError(#[from] task::JoinError),
38 #[error("Checksum mismatch")]
40 ChecksumMismatch,
41}
42
43#[derive(Debug)]
44struct CachedPieces {
45 map: HashMap<RecordKey, u32>,
47 next_offset: Option<u32>,
48}
49
50#[derive(Debug, Clone)]
52pub struct DiskPlotCache {
53 file: Weak<DirectIoFileWrapper>,
54 sectors_metadata: Weak<AsyncRwLock<Vec<SectorMetadataChecksummed>>>,
55 cached_pieces: Arc<RwLock<CachedPieces>>,
56 target_sector_count: u16,
57 sector_size: u64,
58}
59
60#[async_trait]
61impl PlotCache for DiskPlotCache {
62 async fn is_piece_maybe_stored(
63 &self,
64 key: &RecordKey,
65 ) -> Result<MaybePieceStoredResult, FarmError> {
66 Ok(self.is_piece_maybe_stored(key))
67 }
68
69 async fn try_store_piece(
72 &self,
73 piece_index: PieceIndex,
74 piece: &Piece,
75 ) -> Result<bool, FarmError> {
76 Ok(self.try_store_piece(piece_index, piece).await?)
77 }
78
79 async fn read_piece(&self, key: &RecordKey) -> Result<Option<Piece>, FarmError> {
80 Ok(self.read_piece(key).await)
81 }
82}
83
84impl DiskPlotCache {
85 pub(crate) fn new(
86 file: &StdArc<DirectIoFileWrapper>,
87 sectors_metadata: &StdArc<AsyncRwLock<Vec<SectorMetadataChecksummed>>>,
88 target_sector_count: u16,
89 sector_size: u64,
90 ) -> Self {
91 info!("Checking plot cache contents, this can take a while");
92 let cached_pieces = {
93 let sectors_metadata = sectors_metadata.read_blocking();
94 let mut element = vec![0; Self::element_size() as usize];
95 let mut map = HashMap::new();
96 let mut next_offset = None;
97
98 let file_size = sector_size * u64::from(target_sector_count);
99 let plotted_size = sector_size * sectors_metadata.len() as u64;
100
101 let from_offset = (plotted_size / u64::from(Self::element_size())) as u32;
103 let to_offset = (file_size / u64::from(Self::element_size())) as u32;
104 for offset in (from_offset..to_offset).rev() {
106 match Self::read_piece_internal(file, offset, &mut element) {
107 Ok(maybe_piece_index) => {
108 if let Some(piece_index) = maybe_piece_index {
109 map.insert(RecordKey::from(piece_index.to_multihash()), offset);
110 } else {
111 next_offset.replace(offset);
112 break;
113 }
114 }
115 Err(DiskPlotCacheError::ChecksumMismatch) => {
116 next_offset.replace(offset);
117 break;
118 }
119 Err(error) => {
120 warn!(%error, %offset, "Failed to read plot cache element");
121 break;
122 }
123 }
124 }
125
126 CachedPieces { map, next_offset }
127 };
128
129 info!("Finished checking plot cache contents");
130
131 Self {
132 file: StdArc::downgrade(file),
133 sectors_metadata: StdArc::downgrade(sectors_metadata),
134 cached_pieces: Arc::new(RwLock::new(cached_pieces)),
135 target_sector_count,
136 sector_size,
137 }
138 }
139
140 pub(crate) const fn element_size() -> u32 {
142 (PieceIndex::SIZE + Piece::SIZE + Blake3Hash::SIZE) as u32
143 }
144
145 pub(crate) fn is_piece_maybe_stored(&self, key: &RecordKey) -> MaybePieceStoredResult {
148 let offset = {
149 let cached_pieces = self.cached_pieces.read();
150
151 let Some(offset) = cached_pieces.map.get(key).copied() else {
152 return if cached_pieces.next_offset.is_some() {
153 MaybePieceStoredResult::Vacant
154 } else {
155 MaybePieceStoredResult::No
156 };
157 };
158
159 offset
160 };
161
162 let Some(sectors_metadata) = self.sectors_metadata.upgrade() else {
163 return MaybePieceStoredResult::No;
164 };
165
166 let element_offset = u64::from(offset) * u64::from(Self::element_size());
167 let plotted_bytes = self.sector_size * sectors_metadata.read_blocking().len() as u64;
169
170 if element_offset < plotted_bytes {
172 self.cached_pieces.write().map.remove(key);
174 MaybePieceStoredResult::No
175 } else {
176 MaybePieceStoredResult::Yes
177 }
178 }
179
180 pub(crate) async fn try_store_piece(
183 &self,
184 piece_index: PieceIndex,
185 piece: &Piece,
186 ) -> Result<bool, DiskPlotCacheError> {
187 let offset = {
188 if self.cached_pieces.read().next_offset.is_none() {
191 return Ok(false);
192 }
193
194 let mut cached_pieces = self.cached_pieces.write();
197 let Some(next_offset) = cached_pieces.next_offset else {
198 return Ok(false);
199 };
200
201 let offset = next_offset;
202 cached_pieces.next_offset = offset.checked_sub(1);
203 offset
204 };
205
206 let Some(sectors_metadata) = self.sectors_metadata.upgrade() else {
207 return Ok(false);
209 };
210
211 let element_offset = u64::from(offset) * u64::from(Self::element_size());
212 let sectors_metadata = sectors_metadata.read().await;
213 let plotted_sectors_count = sectors_metadata.len() as u16;
214 let plotted_bytes = self.sector_size * u64::from(plotted_sectors_count);
215
216 if element_offset < plotted_bytes {
218 drop(sectors_metadata);
220 let mut cached_pieces = self.cached_pieces.write();
221 cached_pieces.next_offset.take();
223 if plotted_sectors_count == self.target_sector_count {
224 mem::take(&mut cached_pieces.map);
226 }
227 return Ok(false);
228 }
229
230 let Some(file) = self.file.upgrade() else {
231 return Ok(false);
233 };
234
235 trace!(
236 %offset,
237 ?piece_index,
238 %plotted_sectors_count,
239 "Found available piece cache free space offset, writing piece",
240 );
241
242 let write_fut = task::spawn_blocking({
243 let piece_index_bytes = piece_index.to_bytes();
244 let mut bytes = Vec::with_capacity(PieceIndex::SIZE + Piece::SIZE + Blake3Hash::SIZE);
247 bytes.extend_from_slice(&piece_index_bytes);
248 bytes.extend_from_slice(piece.as_ref());
249 bytes.extend_from_slice(
250 {
251 let mut hasher = blake3::Hasher::new();
252 hasher.update(&piece_index_bytes);
253 hasher.update(piece.as_ref());
254 hasher.finalize()
255 }
256 .as_bytes(),
257 );
258
259 move || file.write_all_at(&bytes, element_offset)
260 });
261
262 AsyncJoinOnDrop::new(write_fut, false).await??;
263
264 drop(sectors_metadata);
266 self.cached_pieces
268 .write()
269 .map
270 .insert(RecordKey::from(piece_index.to_multihash()), offset);
271
272 Ok(true)
273 }
274
275 pub(crate) async fn read_piece(&self, key: &RecordKey) -> Option<Piece> {
279 let offset = self.cached_pieces.read().map.get(key).copied()?;
280
281 let file = self.file.upgrade()?;
282
283 let read_fn = move || {
284 let mut element = BytesMut::zeroed(Self::element_size() as usize);
285 if let Ok(Some(_piece_index)) = Self::read_piece_internal(&file, offset, &mut element) {
286 let element = element.freeze();
287 let piece =
288 Piece::try_from(element.slice_ref(&element[PieceIndex::SIZE..][..Piece::SIZE]))
289 .expect("Correct length; qed");
290 Some(piece)
291 } else {
292 None
293 }
294 };
295 let maybe_piece = if cfg!(windows) {
302 task::block_in_place(read_fn)
303 } else {
304 let read_fut = task::spawn_blocking(read_fn);
305
306 AsyncJoinOnDrop::new(read_fut, false)
307 .await
308 .unwrap_or_default()
309 };
310
311 if maybe_piece.is_none()
312 && let Some(sectors_metadata) = self.sectors_metadata.upgrade()
313 {
314 let plotted_sectors_count = sectors_metadata.read().await.len() as u16;
315
316 let mut cached_pieces = self.cached_pieces.write();
317 if plotted_sectors_count == self.target_sector_count {
318 mem::take(&mut cached_pieces.map);
320 } else {
321 cached_pieces.map.remove(key);
323 }
324 }
325
326 maybe_piece
327 }
328
329 fn read_piece_internal(
330 file: &DirectIoFileWrapper,
331 offset: u32,
332 element: &mut [u8],
333 ) -> Result<Option<PieceIndex>, DiskPlotCacheError> {
334 file.read_exact_at(element, u64::from(offset) * u64::from(Self::element_size()))?;
335
336 let (piece_index_bytes, remaining_bytes) = element.split_at(PieceIndex::SIZE);
337 let (piece_bytes, expected_checksum) = remaining_bytes.split_at(Piece::SIZE);
338
339 let actual_checksum = {
341 let mut hasher = blake3::Hasher::new();
342 hasher.update(piece_index_bytes);
343 hasher.update(piece_bytes);
344 *hasher.finalize().as_bytes()
345 };
346 if actual_checksum != expected_checksum {
347 if element.iter().all(|&byte| byte == 0) {
348 return Ok(None);
349 }
350
351 debug!(
352 actual_checksum = %hex::encode(actual_checksum),
353 expected_checksum = %hex::encode(expected_checksum),
354 "Hash doesn't match, corrupted or overridden piece in cache"
355 );
356
357 return Err(DiskPlotCacheError::ChecksumMismatch);
358 }
359
360 let piece_index = PieceIndex::from_bytes(
361 piece_index_bytes
362 .try_into()
363 .expect("Statically known to have correct size; qed"),
364 );
365 Ok(Some(piece_index))
366 }
367}