Skip to main content

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}