commonware_runtime/utils/buffer/write.rs
1use crate::{
2 Blob, Buf, 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 shared 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/// Storage-mutating functions consume the writer and return it only on success: an error (or a
30/// dropped future) destroys the handle.
31///
32/// # Example
33///
34/// ```
35/// use commonware_runtime::{Runner, buffer::{Write, Read}, Blob, Error, Storage, deterministic};
36/// use commonware_utils::NZUsize;
37///
38/// let executor = deterministic::Runner::default();
39/// executor.start(|context| async move {
40/// // Open a blob for writing
41/// let (blob, size) = context.open("my_partition", b"my_data").await.expect("unable to open blob");
42/// assert_eq!(size, 0);
43///
44/// // Create a buffered writer with 16-byte buffer
45/// let blob = Write::new(blob, 0, NZUsize!(16));
46/// let blob = blob.write_at(0, b"hello").await.expect("write failed");
47/// let blob = blob.sync().await.expect("sync failed");
48///
49/// // Write more data in multiple flushes
50/// let blob = blob.write_at(5, b" world").await.expect("write failed");
51/// let blob = blob.write_at(11, b"!").await.expect("write failed");
52/// let blob = blob.sync().await.expect("sync failed");
53///
54/// // Release the writer and read back the persisted data through a new open.
55/// drop(blob);
56/// let (blob, size) = context.open("my_partition", b"my_data").await.expect("unable to reopen blob");
57/// let mut reader = Read::new(blob, size, NZUsize!(8));
58/// let buf = reader.read(size as usize).await.expect("read failed");
59/// assert_eq!(buf.coalesce().as_ref(), b"hello world!");
60/// });
61/// ```
62pub struct Write<B: Blob> {
63 /// The underlying blob to write to.
64 blob: B,
65
66 /// Buffered bytes at the logical tip of the blob.
67 buffer: Buffer,
68
69 /// Durability state for plain writes and range-sync writes.
70 sync_state: SyncState,
71}
72
73impl<B: Blob> Write<B> {
74 /// Creates a new [Write] that buffers up to `capacity` bytes of data to be appended to the tip
75 /// of `blob` with the provided `size`.
76 pub fn new(blob: B, size: u64, capacity: NonZeroUsize) -> Self {
77 Self {
78 blob,
79 buffer: Buffer::new(size, capacity.get()),
80 // Existing blob contents may not be durable yet.
81 sync_state: SyncState::Dirty,
82 }
83 }
84
85 /// Returns the current logical size of the blob including any buffered data.
86 ///
87 /// This represents the total size of data that would be present after flushing.
88 pub const fn size(&self) -> u64 {
89 self.buffer.size()
90 }
91
92 /// Whether [Self::sync] would write buffered bytes, sync the blob, or observe a started sync.
93 pub const fn needs_sync(&self) -> bool {
94 !self.buffer.is_empty() || !self.sync_state.is_clean()
95 }
96
97 /// Read exactly `len` immutable bytes starting at `offset`.
98 pub async fn read_at(&self, offset: u64, len: usize) -> Result<IoBufs, Error> {
99 // Ensure the read doesn't overflow.
100 let end_offset = offset
101 .checked_add(len as u64)
102 .ok_or(Error::OffsetOverflow)?;
103
104 // If the data required is beyond the size of the blob, return an error.
105 if end_offset > self.buffer.size() {
106 return Err(Error::BlobInsufficientLength);
107 }
108
109 // Keep the zero-length fast path after the bounds check so offset > size still preserves
110 // the BlobInsufficientLength contract.
111 if len == 0 {
112 return Ok(IoBufs::default());
113 }
114
115 // Entirely in buffered tip.
116 if offset >= self.buffer.offset {
117 let start = (offset - self.buffer.offset) as usize;
118 let end = start + len;
119 return Ok(self.buffer.slice(start..end).into());
120 }
121
122 // Entirely in blob.
123 if end_offset <= self.buffer.offset {
124 return self.read_blob(offset, len).await;
125 }
126
127 // Overlaps blob and buffered tip.
128 let blob_len = (self.buffer.offset - offset) as usize;
129 let tip_len = len - blob_len;
130 let tip = self.buffer.slice(..tip_len);
131
132 let mut blob = self.read_blob(offset, blob_len).await?;
133 blob.append(tip);
134 Ok(blob)
135 }
136
137 /// Read bytes from the underlying blob.
138 async fn read_blob(&self, offset: u64, len: usize) -> Result<IoBufs, Error> {
139 Ok(self
140 .blob
141 .read_at(offset, len, ReadOptions::default())
142 .await?
143 .freeze())
144 }
145
146 /// Merge `buf` into the in-memory tip buffer at `offset`.
147 ///
148 /// Returns whether `buf` fit. Returns `false` without changing the writer when it does not
149 /// fit. Performs no I/O and does not make the write durable. Callers can fall back to
150 /// [`Self::write_at`] when it does not fit.
151 pub fn try_write_at(&mut self, offset: u64, buf: &[u8]) -> bool {
152 buf.is_empty()
153 || (offset.checked_add(buf.len() as u64).is_some() && self.buffer.merge(buf, offset))
154 }
155
156 /// Write bytes from `buf` at `offset`.
157 ///
158 /// Data is merged into the in-memory tip buffer when possible, otherwise buffered data may be
159 /// flushed and chunks are written directly to the underlying blob.
160 ///
161 /// Returns [Error::OffsetOverflow] when `offset + bufs.len()` overflows.
162 pub async fn write_at(
163 mut self,
164 offset: u64,
165 bufs: impl Into<IoBufs> + Send,
166 ) -> Result<Self, Error> {
167 let mut bufs = bufs.into();
168
169 // Ensure the write doesn't overflow.
170 offset
171 .checked_add(bufs.remaining() as u64)
172 .ok_or(Error::OffsetOverflow)?;
173
174 // Process each chunk of the input buffer, attempting to merge into the tip buffer
175 // or writing directly to the underlying blob.
176 let mut current_offset = offset;
177 while bufs.has_remaining() {
178 let chunk = bufs.chunk();
179 let chunk_len = chunk.len();
180
181 // Chunk falls entirely within the buffer's current range and can be merged.
182 if self.buffer.merge(chunk, current_offset) {
183 bufs.advance(chunk_len);
184 current_offset += chunk_len as u64;
185 continue;
186 }
187
188 // Chunk cannot be merged, so flush the buffer if the range overlaps, and check
189 // if merge is possible after.
190 let chunk_end = current_offset + chunk_len as u64;
191 if self.buffer.offset < chunk_end
192 && let Some((old_buf, old_offset)) = self.buffer.take()
193 {
194 self.sync_state
195 .write_at(&self.blob, old_offset, old_buf, WriteOptions::default())
196 .await?;
197 if self.buffer.merge(chunk, current_offset) {
198 bufs.advance(chunk_len);
199 current_offset += chunk_len as u64;
200 continue;
201 }
202 }
203
204 // Chunk could not be merged (exceeds buffer capacity or outside its range), so
205 // write directly. Note that we may end up writing an intersecting range twice:
206 // once when the buffer is flushed above, then again when we write the chunk
207 // below. Removing this inefficiency may not be worth the additional complexity.
208 let direct = bufs.split_to(chunk_len);
209 self.sync_state
210 .write_at(&self.blob, current_offset, direct, WriteOptions::default())
211 .await?;
212 current_offset += chunk_len as u64;
213
214 // Maintain the "buffer at tip" invariant by advancing offset to the end of this
215 // write if it extended the underlying blob.
216 self.buffer.offset = self.buffer.offset.max(current_offset);
217 }
218
219 Ok(self)
220 }
221
222 /// Resize the logical blob to `len`.
223 ///
224 /// If buffered data exists and the resize extends beyond current size, buffered data is flushed
225 /// before resizing the underlying blob.
226 pub async fn resize(mut self, len: u64) -> Result<Self, Error> {
227 // Flush buffered data to the underlying blob.
228 //
229 // This can only happen if the new size is greater than the current size.
230 if let Some((buf, offset)) = self.buffer.resize(len) {
231 self.sync_state
232 .write_at(&self.blob, offset, buf, WriteOptions::default())
233 .await?;
234 }
235
236 self.sync_state.resize(&self.blob, len).await?;
237
238 Ok(self)
239 }
240
241 /// Flush buffered bytes and durably sync mutations tracked by this writer.
242 pub async fn sync(mut self) -> Result<Self, Error> {
243 match self.buffer.take() {
244 Some((buf, offset)) => self.write_blob_sync(offset, buf).await?,
245 None => self.sync_blob().await?,
246 }
247 Ok(self)
248 }
249
250 /// Flush buffered bytes and begin durably syncing mutations tracked by this writer.
251 ///
252 /// Awaiting the returned [`Handle`] waits for the same durability guarantee as [`Self::sync`]
253 /// for the state flushed by this call. Later calls to [`Self::sync`] and writer methods that
254 /// mutate the blob wait before issuing blob operations. A failure of the started sync is
255 /// reported by the handle and by the next such call. Flush errors are returned directly.
256 pub async fn start_sync(mut self) -> Result<(Self, Handle<()>), Error> {
257 // Complete buffered writes before requesting their durability barrier.
258 if let Some((buf, offset)) = self.buffer.take() {
259 self.sync_state
260 .write_at(&self.blob, offset, buf, WriteOptions::default())
261 .await?;
262 }
263
264 // The runtime runs the sync. The returned handle observes its completion.
265 let handle = self.sync_state.start_sync(&self.blob).await;
266 Ok((self, handle))
267 }
268
269 /// Wait for any started sync to complete without starting a new sync.
270 pub async fn wait_for_sync(mut self) -> Result<Self, Error> {
271 self.sync_state.wait_for_pending().await?;
272 Ok(self)
273 }
274
275 /// Write bytes to the underlying blob and make them durable.
276 ///
277 /// Uses [`Blob::write_at`] with [`WriteOptions::SYNC`] when there are no earlier unsynced
278 /// mutations. Otherwise, writes the bytes and then syncs the blob.
279 async fn write_blob_sync(
280 &mut self,
281 offset: u64,
282 bufs: impl Into<IoBufs> + Send,
283 ) -> Result<(), Error> {
284 self.sync_state
285 .write_at(&self.blob, offset, bufs, WriteOptions::SYNC)
286 .await
287 }
288
289 /// Sync the underlying blob if there are unsynced mutations.
290 async fn sync_blob(&mut self) -> Result<(), Error> {
291 self.sync_state.sync(&self.blob).await
292 }
293}