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