Skip to main content

commonware_runtime/utils/buffer/
read.rs

1use crate::{Blob, BufferPool, BufferPooler, Error, IoBuf, IoBufs, ReadOptions};
2use std::num::NonZeroUsize;
3
4/// A reader that buffers content from a [Blob] to optimize the performance
5/// of a full scan of contents.
6///
7/// # Allocation Semantics
8///
9/// - The internal read buffer is allocated lazily on the first refill.
10/// - Refills try to reclaim mutable ownership of that same backing allocation.
11/// - If backing is still shared (for example, previously returned slices are alive), a pooled
12///   replacement is allocated and existing backing is left alive until all aliases drop.
13/// - [Self::read] returns zero-copy slices into refill buffers. Holding those slices may
14///   force allocation on subsequent refills.
15///
16/// # Example
17///
18/// ```
19/// use commonware_utils::NZUsize;
20/// use commonware_runtime::{
21///     Blob, BufferPooler, Error, Runner, Storage, WriteOptions, buffer::Read, deterministic,
22/// };
23///
24/// let executor = deterministic::Runner::default();
25/// executor.start(|context| async move {
26///     // Open a blob and add some data (e.g., a journal file)
27///     let (blob, size) = context.open("my_partition", b"my_data").await.expect("unable to open blob");
28///     let data = b"Hello, world! This is a test.".to_vec();
29///     let size = data.len() as u64;
30///     blob.write_at(0, data, WriteOptions::default()).await.expect("unable to write data");
31///
32///     // Create a buffer
33///     let buffer = 64 * 1024;
34///     let mut reader = Read::from_pooler(&context, blob, size, NZUsize!(buffer));
35///
36///     // Read data sequentially
37///     let header = reader.read(16).await.expect("unable to read data");
38///     println!("Read header: {:?}", header.coalesce().as_ref());
39///
40///     // Position is still at 16 (after header)
41///     assert_eq!(reader.position(), 16);
42/// });
43/// ```
44pub struct Read<B: Blob> {
45    /// The underlying blob to read from.
46    blob: B,
47    /// The buffer storing the data read from the blob.
48    buffer: IoBuf,
49    /// The current position in the blob from where the buffer was filled.
50    blob_position: u64,
51    /// The size of the blob.
52    blob_size: u64,
53    /// The current position within the buffer for reading.
54    buffer_position: usize,
55    /// The valid data length in the buffer.
56    buffer_valid_len: usize,
57    /// The maximum size of the buffer.
58    buffer_size: usize,
59    /// Buffer pool used for internal allocations.
60    pool: BufferPool,
61}
62
63impl<B: Blob> Read<B> {
64    /// Creates a new `Read` that reads from the given blob with the specified buffer size.
65    pub fn new(blob: B, blob_size: u64, buffer_size: NonZeroUsize, pool: BufferPool) -> Self {
66        Self {
67            blob,
68            // The first refill allocates the backing buffer.
69            buffer: IoBuf::default(),
70            blob_position: 0,
71            blob_size,
72            buffer_position: 0,
73            buffer_valid_len: 0,
74            buffer_size: buffer_size.get(),
75            pool,
76        }
77    }
78
79    /// Creates a new `Read`, extracting the storage [BufferPool] from a [BufferPooler].
80    pub fn from_pooler(
81        pooler: &impl BufferPooler,
82        blob: B,
83        blob_size: u64,
84        buffer_size: NonZeroUsize,
85    ) -> Self {
86        Self::new(
87            blob,
88            blob_size,
89            buffer_size,
90            pooler.storage_buffer_pool().clone(),
91        )
92    }
93
94    /// Returns how many valid bytes are remaining in the buffer.
95    pub const fn buffer_remaining(&self) -> usize {
96        self.buffer_valid_len - self.buffer_position
97    }
98
99    /// Returns how many bytes remain in the blob from the current position.
100    pub const fn blob_remaining(&self) -> u64 {
101        self.blob_size
102            .saturating_sub(self.blob_position + self.buffer_position as u64)
103    }
104
105    /// Returns the number of bytes in the blob, as provided at construction.
106    pub const fn blob_size(&self) -> u64 {
107        self.blob_size
108    }
109
110    /// Refills the buffer from the blob starting at the current blob position.
111    /// Returns the number of bytes read or an error if the read failed.
112    async fn refill(&mut self) -> Result<usize, Error> {
113        // Update blob position to account for consumed bytes
114        self.blob_position += self.buffer_position as u64;
115        self.buffer_position = 0;
116        self.buffer_valid_len = 0;
117
118        // Calculate how many bytes remain in the blob
119        let blob_remaining = self.blob_size.saturating_sub(self.blob_position);
120        if blob_remaining == 0 {
121            return Err(Error::BlobInsufficientLength);
122        }
123
124        // Calculate how much to read (minimum of buffer size and remaining bytes)
125        let bytes_to_read = std::cmp::min(self.buffer_size as u64, blob_remaining) as usize;
126
127        // Reuse existing allocation when uniquely owned. If readers still hold slices from
128        // previous reads, allocate a pooled replacement and leave old memory alive until dropped.
129        let current = std::mem::take(&mut self.buffer);
130        let buf = match current.try_into_mut() {
131            Ok(mut reusable) if reusable.capacity() >= bytes_to_read => {
132                reusable.clear();
133                reusable
134            }
135            Ok(too_small) => {
136                // Release the undersized buffer before allocating so a tight
137                // pool can reuse its slot for the replacement.
138                drop(too_small);
139                self.pool.alloc(bytes_to_read)
140            }
141            Err(_) => self.pool.alloc(bytes_to_read),
142        };
143        let read_result = self
144            .blob
145            .read_at_buf(
146                self.blob_position,
147                bytes_to_read,
148                buf,
149                ReadOptions::default(),
150            )
151            .await?;
152        self.buffer = read_result.coalesce_with_pool(&self.pool).freeze();
153        self.buffer_valid_len = self.buffer.len();
154
155        Ok(self.buffer_valid_len)
156    }
157
158    /// Reads exactly `len` bytes and returns them as immutable bytes.
159    ///
160    /// Returned bytes are composed of zero-copy slices from the internal read buffer.
161    /// Holding returned slices can keep the current backing shared, which may require
162    /// allocation on later refills.
163    ///
164    /// Returns an error if not enough bytes are available.
165    pub async fn read(&mut self, len: usize) -> Result<IoBufs, Error> {
166        if len == 0 {
167            return Ok(IoBufs::default());
168        }
169
170        // Quick check against total remaining bytes at current position.
171        if self.blob_remaining() < len as u64 {
172            return Err(Error::BlobInsufficientLength);
173        }
174
175        // Read until we have enough bytes
176        let mut remaining = len;
177        let mut out = IoBufs::default();
178        while remaining > 0 {
179            // Check if we need to refill
180            if self.buffer_position >= self.buffer_valid_len {
181                self.refill().await?;
182            }
183
184            // Calculate how many bytes we can take from the buffer
185            let bytes_to_take = std::cmp::min(remaining, self.buffer_remaining());
186
187            // Append bytes from buffer to output
188            out.append(
189                self.buffer
190                    .slice(self.buffer_position..(self.buffer_position + bytes_to_take)),
191            );
192
193            self.buffer_position += bytes_to_take;
194            remaining -= bytes_to_take;
195        }
196
197        Ok(out)
198    }
199
200    /// Returns the current absolute position in the blob.
201    pub const fn position(&self) -> u64 {
202        self.blob_position + self.buffer_position as u64
203    }
204
205    /// Repositions the buffer to read from the specified position in the blob.
206    pub const fn seek_to(&mut self, position: u64) -> Result<(), Error> {
207        // Check if the seek position is valid
208        if position > self.blob_size {
209            return Err(Error::BlobInsufficientLength);
210        }
211
212        // Check if the position is within the current buffer
213        let buffer_start = self.blob_position;
214        let buffer_end = self.blob_position + self.buffer_valid_len as u64;
215
216        if position >= buffer_start && position < buffer_end {
217            // Position is within the current buffer, adjust buffer_position
218            self.buffer_position = (position - self.blob_position) as usize;
219        } else {
220            // Position is outside the current buffer, reset buffer state
221            self.blob_position = position;
222            self.buffer_position = 0;
223            self.buffer_valid_len = 0;
224        }
225
226        Ok(())
227    }
228
229    /// Resizes the blob to the specified len and syncs the blob.
230    ///
231    /// This may be useful if reading some blob after unclean shutdown.
232    pub async fn resize(self, len: u64) -> Result<(), Error> {
233        self.blob.resize(len).await?;
234        self.blob.sync().await
235    }
236}