Skip to main content

ab_farmer/single_disk_farm/
plot_cache.rs

1//! Plot cache for single disk farm
2
3// TODO: Not supported under Miri: https://github.com/rust-lang/miri/issues/5137
4#[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
30/// Size of a cache element: piece index, piece and checksum, followed by zero padding to whole
31/// disk pages, such that elements can be written without read-modify-write
32const ELEMENT_SIZE: usize =
33    (PieceIndex::SIZE + Piece::SIZE + Blake3Hash::SIZE).next_multiple_of(AlignedPage::SIZE);
34
35/// Disk plot cache open error
36#[derive(Debug, Error)]
37pub enum DiskPlotCacheError {
38    /// I/O error occurred
39    #[error("Plot cache I/O error: {0}")]
40    Io(#[from] io::Error),
41    /// Failed to spawn task for blocking thread
42    #[error("Failed to spawn task for blocking thread: {0}")]
43    TokioJoinError(#[from] task::JoinError),
44    /// Checksum mismatch
45    #[error("Checksum mismatch")]
46    ChecksumMismatch,
47}
48
49#[derive(Debug)]
50struct CachedPieces {
51    /// Map of piece index into offset
52    map: HashMap<RecordKey, u32>,
53    next_offset: Option<u32>,
54}
55
56/// Additional piece cache that exploit part of the plot that does not contain sectors yet
57#[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    /// Store piece in cache if there is free space, and return `Ok(true)`.
76    /// Returns `Ok(false)` if there is no free space, or the farm or process is shutting down.
77    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            // Step over all free potential offsets for pieces that could have been cached
108            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            // TODO: Parallelize or read in larger batches
111            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    /// Size of a single plot cache element
147    pub(crate) const fn element_size() -> u32 {
148        ELEMENT_SIZE as u32
149    }
150
151    /// Check if piece is potentially stored in this cache (not guaranteed to be because it might be
152    /// overridden with sector any time)
153    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        // Blocking read is fine because writes in farmer are very rare and very brief
174        let plotted_bytes = self.sector_size * sectors_metadata.read_blocking().len() as u64;
175
176        // Make sure offset is after anything that is already plotted
177        if element_offset < plotted_bytes {
178            // Remove entry since it was overwritten with a sector already
179            self.cached_pieces.write().map.remove(key);
180            MaybePieceStoredResult::No
181        } else {
182            MaybePieceStoredResult::Yes
183        }
184    }
185
186    /// Store piece in cache if there is free space, and return `Ok(true)`.
187    /// Returns `Ok(false)` if there is no free space, or the farm or process is shutting down.
188    pub(crate) async fn try_store_piece(
189        &self,
190        piece_index: PieceIndex,
191        piece: &Piece,
192    ) -> Result<bool, DiskPlotCacheError> {
193        let offset = {
194            // First, do a quick concurrent check for free space with a read lock, dropping it
195            // immediately.
196            if self.cached_pieces.read().next_offset.is_none() {
197                return Ok(false);
198            }
199
200            // Then, if there was free space, acquire a write lock, and check for intervening
201            // writes.
202            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            // Metadata has been dropped, farm or process is shutting down
214            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        // Make sure offset is after anything that is already plotted
223        if element_offset < plotted_bytes {
224            // Just to be safe, avoid any overlap of read and write locks
225            drop(sectors_metadata);
226            let mut cached_pieces = self.cached_pieces.write();
227            // No space to store more pieces anymore
228            cached_pieces.next_offset.take();
229            if plotted_sectors_count == self.target_sector_count {
230                // Free allocated memory once fully plotted
231                mem::take(&mut cached_pieces.map);
232            }
233            return Ok(false);
234        }
235
236        let Some(file) = self.file.upgrade() else {
237            // File has been dropped, farm or process is shutting down
238            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            // Combine all data with padding here, such that the whole element is written at once
251            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        // Just to be safe, avoid any overlap of read and write locks
271        drop(sectors_metadata);
272        // Store newly written piece in the map
273        self.cached_pieces
274            .write()
275            .map
276            .insert(RecordKey::from(piece_index.to_multihash()), offset);
277
278        Ok(true)
279    }
280
281    /// Read piece from cache.
282    ///
283    /// Returns `None` if not cached.
284    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        // TODO: On Windows spawning blocking task that allows concurrent reads causes huge memory
302        //  usage. No idea why it happens, but not spawning anything at all helps for some reason.
303        //  Someone at some point should figure it out and fix, but it will probably be not me
304        //  (Nazar).
305        //  See https://github.com/autonomys/subspace/issues/2813 and linked forum post for details.
306        //  This TODO exists in multiple files
307        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                // Free allocated memory once fully plotted
325                mem::take(&mut cached_pieces.map);
326            } else {
327                // Remove entry just in case it was overridden with a sector already
328                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        // The rest of the element after the checksum is zero padding
345        let (expected_checksum, _padding) = remaining_bytes.split_at(Blake3Hash::SIZE);
346
347        // Verify checksum
348        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}