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}