commonware_runtime/utils/buffer/write.rs
1use crate::{
2 Blob, Buf, BufferPool, BufferPooler, Error, Handle, IoBufs, ReadOptions, WriteOptions,
3 buffer::{SyncState, tip::Buffer},
4};
5use std::num::NonZeroUsize;
6
7/// A writer that buffers the raw content of a [Blob] to optimize the performance of appending or
8/// updating data.
9///
10/// # Allocation Semantics
11///
12/// - [Self::new] starts with a detached tip buffer and allocates backing on first buffered write.
13/// - Subsequent writes reuse that backing, copy-on-write allocation only occurs when buffered data
14/// is shared (for example, after handing out immutable views) or a merge needs more capacity.
15/// - Sparse writes merged into tip extend logical length and zero-fill any gap in-buffer.
16/// - Flush paths ([Self::sync], [Self::resize], overlap flushes in [Self::write_at]) hand drained
17/// bytes to the blob and leave the tip detached until the next buffered write.
18///
19/// # Access
20///
21/// [Write] is a single-owner buffered handle that owns mutation ordering and durability
22/// bookkeeping for the wrapped [Blob]. Raw [Blob] handles cloned before wrapping observe only
23/// flushed data and may not see the latest buffered writes until [Self::sync], [Self::resize], or
24/// an overlapping [Self::write_at] flushes them. Those raw handles must not be used to write,
25/// resize, or otherwise mutate the blob while a [Write] exists. External mutations bypass the
26/// buffer state and [Self::sync] may use [Blob::write_at] with [WriteOptions::SYNC], which is
27/// not a durability barrier for those external mutations.
28///
29/// # Example
30///
31/// ```
32/// use commonware_runtime::{Runner, BufferPooler, buffer::{Write, Read}, Blob, Error, Storage, deterministic};
33/// use commonware_utils::NZUsize;
34///
35/// let executor = deterministic::Runner::default();
36/// executor.start(|context| async move {
37/// // Open a blob for writing
38/// let (blob, size) = context.open("my_partition", b"my_data").await.expect("unable to open blob");
39/// assert_eq!(size, 0);
40///
41/// // Create a buffered writer with 16-byte buffer
42/// let mut blob = Write::from_pooler(&context, blob, 0, NZUsize!(16));
43/// blob.write_at(0, b"hello").await.expect("write failed");
44/// blob.sync().await.expect("sync failed");
45///
46/// // Write more data in multiple flushes
47/// blob.write_at(5, b" world").await.expect("write failed");
48/// blob.write_at(11, b"!").await.expect("write failed");
49/// blob.sync().await.expect("sync failed");
50///
51/// // Read back the data to verify
52/// let (blob, size) = context.open("my_partition", b"my_data").await.expect("unable to reopen blob");
53/// let mut reader = Read::from_pooler(&context, blob, size, NZUsize!(8));
54/// let buf = reader.read(size as usize).await.expect("read failed");
55/// assert_eq!(buf.coalesce().as_ref(), b"hello world!");
56/// });
57/// ```
58pub struct Write<B: Blob> {
59 /// The underlying blob to write to.
60 blob: B,
61
62 /// Buffered bytes at the logical tip of the blob.
63 buffer: Buffer,
64
65 /// Durability state for plain writes and range-sync writes.
66 sync_state: SyncState,
67}
68
69impl<B: Blob> Write<B> {
70 /// Creates a new [Write] that buffers up to `capacity` bytes of data to be appended to the tip
71 /// of `blob` with the provided `size`.
72 pub fn new(blob: B, size: u64, capacity: NonZeroUsize, pool: BufferPool) -> Self {
73 Self {
74 blob,
75 buffer: Buffer::new(size, capacity.get(), pool),
76 // Existing blob contents may not be durable yet.
77 sync_state: SyncState::Dirty,
78 }
79 }
80
81 /// Creates a new [Write], extracting the storage [BufferPool] from a [BufferPooler].
82 pub fn from_pooler(
83 pooler: &impl BufferPooler,
84 blob: B,
85 size: u64,
86 capacity: NonZeroUsize,
87 ) -> Self {
88 Self::new(blob, size, capacity, pooler.storage_buffer_pool().clone())
89 }
90
91 /// Returns the current logical size of the blob including any buffered data.
92 ///
93 /// This represents the total size of data that would be present after flushing.
94 pub const fn size(&self) -> u64 {
95 self.buffer.size()
96 }
97
98 /// Read exactly `len` immutable bytes starting at `offset`.
99 pub async fn read_at(&self, offset: u64, len: usize) -> Result<IoBufs, Error> {
100 // Ensure the read doesn't overflow.
101 let end_offset = offset
102 .checked_add(len as u64)
103 .ok_or(Error::OffsetOverflow)?;
104
105 // If the data required is beyond the size of the blob, return an error.
106 if end_offset > self.buffer.size() {
107 return Err(Error::BlobInsufficientLength);
108 }
109
110 // Keep the zero-length fast path after the bounds check so offset > size still preserves
111 // the BlobInsufficientLength contract.
112 if len == 0 {
113 return Ok(IoBufs::default());
114 }
115
116 // Entirely in buffered tip.
117 if offset >= self.buffer.offset {
118 let start = (offset - self.buffer.offset) as usize;
119 let end = start + len;
120 return Ok(self.buffer.slice(start..end).into());
121 }
122
123 // Entirely in blob.
124 if end_offset <= self.buffer.offset {
125 return self.read_blob(offset, len).await;
126 }
127
128 // Overlaps blob and buffered tip.
129 let blob_len = (self.buffer.offset - offset) as usize;
130 let tip_len = len - blob_len;
131 let tip = self.buffer.slice(..tip_len);
132
133 let mut blob = self.read_blob(offset, blob_len).await?;
134 blob.append(tip);
135 Ok(blob)
136 }
137
138 /// Read bytes from the underlying blob.
139 async fn read_blob(&self, offset: u64, len: usize) -> Result<IoBufs, Error> {
140 Ok(self
141 .blob
142 .read_at(offset, len, ReadOptions::default())
143 .await?
144 .freeze())
145 }
146
147 /// Write bytes from `buf` at `offset`.
148 ///
149 /// Data is merged into the in-memory tip buffer when possible, otherwise buffered data may be
150 /// flushed and chunks are written directly to the underlying blob.
151 ///
152 /// Returns [Error::OffsetOverflow] when `offset + bufs.len()` overflows.
153 pub async fn write_at(
154 &mut self,
155 offset: u64,
156 bufs: impl Into<IoBufs> + Send,
157 ) -> Result<(), Error> {
158 let mut bufs = bufs.into();
159
160 // Ensure the write doesn't overflow.
161 offset
162 .checked_add(bufs.remaining() as u64)
163 .ok_or(Error::OffsetOverflow)?;
164
165 // Process each chunk of the input buffer, attempting to merge into the tip buffer
166 // or writing directly to the underlying blob.
167 let mut current_offset = offset;
168 while bufs.has_remaining() {
169 let chunk = bufs.chunk();
170 let chunk_len = chunk.len();
171
172 // Chunk falls entirely within the buffer's current range and can be merged.
173 if self.buffer.merge(chunk, current_offset) {
174 bufs.advance(chunk_len);
175 current_offset += chunk_len as u64;
176 continue;
177 }
178
179 // Chunk cannot be merged, so flush the buffer if the range overlaps, and check
180 // if merge is possible after.
181 let chunk_end = current_offset + chunk_len as u64;
182 if self.buffer.offset < chunk_end
183 && let Some((old_buf, old_offset)) = self.buffer.take()
184 {
185 self.sync_state
186 .write_at(&self.blob, old_offset, old_buf, WriteOptions::default())
187 .await?;
188 if self.buffer.merge(chunk, current_offset) {
189 bufs.advance(chunk_len);
190 current_offset += chunk_len as u64;
191 continue;
192 }
193 }
194
195 // Chunk could not be merged (exceeds buffer capacity or outside its range), so
196 // write directly. Note that we may end up writing an intersecting range twice:
197 // once when the buffer is flushed above, then again when we write the chunk
198 // below. Removing this inefficiency may not be worth the additional complexity.
199 let direct = bufs.split_to(chunk_len);
200 self.sync_state
201 .write_at(&self.blob, current_offset, direct, WriteOptions::default())
202 .await?;
203 current_offset += chunk_len as u64;
204
205 // Maintain the "buffer at tip" invariant by advancing offset to the end of this
206 // write if it extended the underlying blob.
207 self.buffer.offset = self.buffer.offset.max(current_offset);
208 }
209
210 Ok(())
211 }
212
213 /// Resize the logical blob to `len`.
214 ///
215 /// If buffered data exists and the resize extends beyond current size, buffered data is flushed
216 /// before resizing the underlying blob.
217 pub async fn resize(&mut self, len: u64) -> Result<(), Error> {
218 // Flush buffered data to the underlying blob.
219 //
220 // This can only happen if the new size is greater than the current size.
221 if let Some((buf, offset)) = self.buffer.resize(len) {
222 self.sync_state
223 .write_at(&self.blob, offset, buf, WriteOptions::default())
224 .await?;
225 }
226
227 self.sync_state.resize(&self.blob, len).await?;
228
229 Ok(())
230 }
231
232 /// Flush buffered bytes and durably sync mutations tracked by this writer.
233 pub async fn sync(&mut self) -> Result<(), Error> {
234 if let Some((buf, offset)) = self.buffer.take() {
235 return self.write_blob_sync(offset, buf).await;
236 }
237
238 self.sync_blob().await
239 }
240
241 /// Flush buffered bytes and begin durably syncing mutations tracked by this writer.
242 ///
243 /// Awaiting the returned [`Handle`] waits for the same durability guarantee as [`Self::sync`]
244 /// for the state flushed by this call. Later calls to [`Self::sync`] and writer methods that
245 /// mutate the blob wait before issuing blob operations.
246 pub async fn start_sync(&mut self) -> Handle<()> {
247 if let Some((buf, offset)) = self.buffer.take()
248 && let Err(err) = self
249 .sync_state
250 .write_at(&self.blob, offset, buf, WriteOptions::default())
251 .await
252 {
253 return Handle::ready(Err(err));
254 }
255
256 self.sync_state.start_sync(&self.blob).await
257 }
258
259 /// Wait for any started sync to complete without starting a new sync.
260 pub async fn wait_for_sync(&mut self) -> Result<(), Error> {
261 self.sync_state.wait_for_pending().await
262 }
263
264 /// Write bytes to the underlying blob and make them durable.
265 ///
266 /// Uses [`Blob::write_at`] with [`WriteOptions::SYNC`] when there are no earlier unsynced
267 /// mutations. Otherwise, writes the bytes and then syncs the blob.
268 async fn write_blob_sync(
269 &mut self,
270 offset: u64,
271 bufs: impl Into<IoBufs> + Send,
272 ) -> Result<(), Error> {
273 self.sync_state
274 .write_at(&self.blob, offset, bufs, WriteOptions::SYNC)
275 .await
276 }
277
278 /// Sync the underlying blob if there are unsynced mutations.
279 async fn sync_blob(&mut self) -> Result<(), Error> {
280 self.sync_state.sync(&self.blob).await
281 }
282}