commonware_storage/journal/contiguous/mod.rs
1//! Contiguous journals with position-based access.
2//!
3//! This module provides position-based journal implementations where items are stored
4//! contiguously and can be accessed by their position (0-indexed). Both [fixed]-size and
5//! [variable]-size item journals are supported.
6//!
7//! # Ownership
8//!
9//! Mutating methods take the journal by value and return it on success. If a mutating
10//! method returns an error, or its future is dropped before it finishes, the journal is
11//! gone: state that was not yet durable is discarded, but everything already on disk stays
12//! recoverable.
13
14use super::Error;
15use commonware_runtime::{Handle, ReadOptions};
16use futures::{Stream, stream};
17use std::{future::Future, num::NonZeroUsize, ops::Range};
18
19mod blobs;
20mod checkpoint;
21pub mod fixed;
22mod metrics;
23pub mod variable;
24
25#[cfg(test)]
26pub(crate) mod tests;
27
28/// Return the number of items that can be written before crossing the current blob boundary.
29///
30/// `position` is the next logical item position and `remaining` is the number of items left in the
31/// append batch. The result is always at least one when `remaining > 0`.
32fn batch_count_to_blob_boundary(position: u64, remaining: usize, items_per_blob: u64) -> usize {
33 let pos_in_blob = position % items_per_blob;
34 let remaining_space = items_per_blob - pos_in_blob;
35
36 // Keep the min in u64 so a 2^32-item blob space does not truncate to zero on 32-bit targets.
37 remaining_space.min(remaining as u64) as usize
38}
39
40/// Return the blob containing `position`.
41///
42/// # Examples
43///
44/// ```ignore
45/// // With 10 items per blob:
46/// assert_eq!(position_to_blob(0, 10), 0); // position 0 -> blob 0
47/// assert_eq!(position_to_blob(9, 10), 0); // position 9 -> blob 0
48/// assert_eq!(position_to_blob(10, 10), 1); // position 10 -> blob 1
49/// assert_eq!(position_to_blob(25, 10), 2); // position 25 -> blob 2
50/// assert_eq!(position_to_blob(30, 10), 3); // position 30 -> blob 3
51/// ```
52const fn position_to_blob(position: u64, items_per_blob: u64) -> u64 {
53 position / items_per_blob
54}
55
56/// Return the first position stored in `blob`.
57fn blob_first_position(blob: u64, items_per_blob: u64) -> Result<u64, Error> {
58 blob.checked_mul(items_per_blob)
59 .ok_or(Error::OffsetOverflow)
60}
61
62/// Return the exclusive logical end for `blob`, clamped to `end`.
63const fn blob_end_position(blob: u64, items_per_blob: u64, end: u64) -> u64 {
64 // No positions exist, so `end - 1` would underflow
65 if end == 0 {
66 return 0;
67 }
68
69 // This blob contains `end - 1`, so clamp to the journal end
70 let end_blob = (end - 1) / items_per_blob;
71 if blob >= end_blob {
72 return end;
73 }
74
75 // Earlier blobs have a representable natural boundary
76 (blob + 1) * items_per_blob
77}
78
79/// The batch decoded by one [BatchDecoder::next] call.
80struct Decoded<T> {
81 /// The items decoded, possibly none.
82 batch: Batch<T>,
83 /// The error that ended this blob's replay after `batch`, if any.
84 error: Option<Error>,
85}
86
87impl<T> Decoded<T> {
88 /// The `items` decoded before `error` ended the blob's replay.
89 const fn failed(start: u64, items: Vec<T>, error: Error) -> Self {
90 Self {
91 batch: Batch { start, items },
92 error: Some(error),
93 }
94 }
95}
96
97/// Per-blob replay state that yields decoded batches.
98trait BatchDecoder: Sized {
99 /// The decoded item type.
100 type Item;
101
102 /// Decode the next batch from this blob state, or return `None` once it is exhausted.
103 ///
104 /// An error ends the blob's replay after the items decoded before it, and the state is not
105 /// polled again.
106 fn next(self) -> impl Future<Output = Option<(Decoded<Self::Item>, Self)>> + Send;
107}
108
109/// Stream driver over per-blob replay states.
110struct ReplayStreamState<S: BatchDecoder> {
111 /// Remaining blob states, in ascending blob order.
112 states: std::vec::IntoIter<S>,
113 /// State currently being drained.
114 current: Option<S>,
115 /// Error to yield after the batch decoded before it.
116 pending: Option<Error>,
117}
118
119impl<S: BatchDecoder + Send> ReplayStreamState<S>
120where
121 S::Item: Send,
122{
123 /// Yield the next decoded batch, or the error that ends the stream.
124 async fn next(mut self) -> Option<(Result<Batch<S::Item>, Error>, Self)> {
125 loop {
126 if let Some(err) = self.pending.take() {
127 return Some((Err(err), self));
128 }
129
130 let state = match self.current.take().or_else(|| self.states.next()) {
131 Some(state) => state,
132 None => return None,
133 };
134
135 match state.next().await {
136 Some((decoded, state)) => {
137 // A failed blob is dropped, and its error ends the stream before any later
138 // blob is read.
139 match decoded.error {
140 Some(err) => {
141 self.pending = Some(err);
142 self.states = Vec::new().into_iter();
143 }
144 None => self.current = Some(state),
145 }
146 if !decoded.batch.items.is_empty() {
147 return Some((Ok(decoded.batch), self));
148 }
149 }
150 None => {
151 self.current = None;
152 }
153 }
154 }
155 }
156}
157
158/// Build a batch stream from per-blob replay states.
159fn replay_stream_from_states<S>(
160 states: Vec<S>,
161) -> impl Stream<Item = Result<Batch<S::Item>, Error>> + Send
162where
163 S: BatchDecoder + Send,
164 S::Item: Send,
165{
166 stream::unfold(
167 ReplayStreamState {
168 states: states.into_iter(),
169 current: None,
170 pending: None,
171 },
172 ReplayStreamState::next,
173 )
174}
175
176/// A batch of consecutive items yielded by [`Contiguous::replay`]. `items[i]` is the item at
177/// position `start + i`, so `start + items.len()` must not exceed `u64::MAX`.
178#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct Batch<T> {
180 /// Position of the first item.
181 pub start: u64,
182 /// The items at `start..start + items.len()`.
183 pub items: Vec<T>,
184}
185
186impl<T> IntoIterator for Batch<T> {
187 type Item = (u64, T);
188 type IntoIter = std::iter::Zip<Range<u64>, std::vec::IntoIter<T>>;
189
190 /// Iterate the items paired with their positions.
191 fn into_iter(self) -> Self::IntoIter {
192 let end = self.start + self.items.len() as u64;
193 (self.start..end).zip(self.items)
194 }
195}
196
197/// A read-only, position-based view of a contiguous journal.
198///
199/// Maintains a monotonically increasing position counter where each appended item receives a unique
200/// position starting from 0.
201///
202/// Returned items may retain shared backing allocations larger than their byte fields.
203pub trait Contiguous: Send + Sync {
204 /// The type of items stored in the journal.
205 type Item: Send + Sync;
206
207 /// Returns [start, end) with a guaranteed stable pruning boundary.
208 fn bounds(&self) -> Range<u64>;
209
210 /// Read the item at the given position.
211 ///
212 /// Guaranteed not to return [Error::ItemPruned] for positions within `bounds()`.
213 fn read(&self, position: u64) -> impl Future<Output = Result<Self::Item, Error>> + Send;
214
215 /// Read multiple items at the given positions, which must be strictly increasing.
216 ///
217 /// Equivalent to serving every position [`try_read_many_sync`](Self::try_read_many_sync)
218 /// declines with one batched read. Implementations may fuse the two passes.
219 fn read_many(
220 &self,
221 positions: &[u64],
222 ) -> impl Future<Output = Result<Vec<Self::Item>, Error>> + Send;
223
224 /// Read an item if it can be done synchronously (e.g. without I/O), returning `None`
225 /// otherwise. Decode failures surface as `None` and the async read path reports the error.
226 fn try_read_sync(&self, position: u64) -> Option<Self::Item>;
227
228 /// Probe multiple strictly increasing positions, serving those that can be read
229 /// synchronously (e.g. from a page cache) and returning one slot per position. Positions
230 /// that require I/O, fail to decode, or fall outside `bounds()` decline to `None`. The
231 /// async read paths are the sole error authority for declined positions.
232 fn try_read_many_sync(&self, positions: &[u64]) -> Vec<Option<Self::Item>>;
233
234 /// Return a stream of the items in `range` as [`Batch`]es of consecutive items, in position
235 /// order. `range` must fall within `bounds()`. An empty range within `bounds()` yields an
236 /// empty stream.
237 ///
238 /// Every batch holds at least one item, consecutive batches are contiguous, and no batch
239 /// crosses a backing blob boundary. A batch's items exceed `buffer` bytes as stored, framing
240 /// and compression included, by less than one item.
241 ///
242 /// Backing reads of item data carry `read_options`, except reads served through the page
243 /// cache, which applies its own policy.
244 ///
245 /// # Errors
246 ///
247 /// Returns [Error::ItemPruned] if `range.start` precedes `bounds().start`, or
248 /// [Error::ItemOutOfRange] if `range.end` exceeds `bounds().end` or the range is inverted
249 /// (`start > end`).
250 fn replay(
251 &self,
252 range: Range<u64>,
253 buffer: NonZeroUsize,
254 read_options: ReadOptions,
255 ) -> impl Future<
256 Output = Result<impl Stream<Item = Result<Batch<Self::Item>, Error>> + Send, Error>,
257 > + Send;
258}
259
260/// Items to append via [`Mutable::append_many`].
261///
262/// `Flat` wraps a single contiguous slice; `Nested` wraps multiple slices appended in order.
263pub enum Many<'a, T> {
264 /// A single contiguous slice of items.
265 Flat(&'a [T]),
266 /// Multiple slices of items, appended in order.
267 Nested(&'a [&'a [T]]),
268}
269
270impl<T> Many<'_, T> {
271 /// Returns the total number of items across all segments.
272 pub fn len(&self) -> usize {
273 match self {
274 Self::Flat(items) => items.len(),
275 Self::Nested(nested_items) => nested_items.iter().map(|items| items.len()).sum(),
276 }
277 }
278
279 /// Returns `true` if there are no items across all segments.
280 pub fn is_empty(&self) -> bool {
281 match self {
282 Self::Flat(items) => items.is_empty(),
283 Self::Nested(nested_items) => nested_items.iter().all(|items| items.is_empty()),
284 }
285 }
286}
287
288/// A [Contiguous] journal that supports appending and prefix pruning.
289pub trait Mutable: Contiguous + Sized {
290 /// Append a new item to the journal, returning its position.
291 ///
292 /// Positions are consecutively increasing starting from 0. The position of each item
293 /// is stable across pruning (i.e., if item X has position 5, it will always have
294 /// position 5 even if earlier items are pruned).
295 ///
296 /// # Errors
297 ///
298 /// Returns an error if the underlying storage operation fails or if the item cannot
299 /// be encoded.
300 fn append(
301 self,
302 item: &Self::Item,
303 ) -> impl std::future::Future<Output = Result<(Self, u64), Error>> + Send;
304
305 /// Append items to the journal, returning the position of the last item appended.
306 ///
307 /// Returns [Error::EmptyAppend] if items is empty.
308 fn append_many(
309 self,
310 items: Many<'_, Self::Item>,
311 ) -> impl std::future::Future<Output = Result<(Self, u64), Error>> + Send;
312
313 /// Prune items at positions strictly less than `min_position`.
314 ///
315 /// Returns `true` if any data was pruned, `false` otherwise.
316 ///
317 /// # Behavior
318 ///
319 /// - If `min_position > bounds.end`, the prune is capped to `bounds.end` (no error is returned)
320 /// - Some items with positions less than `min_position` may be retained due to
321 /// section/blob alignment
322 /// - This operation is not atomic, but implementations guarantee the journal is left in a
323 /// recoverable state if a crash occurs during pruning
324 ///
325 /// # Errors
326 ///
327 /// Returns an error if the underlying storage operation fails.
328 fn prune(
329 self,
330 min_position: u64,
331 ) -> impl std::future::Future<Output = Result<(Self, bool), Error>> + Send;
332
333 /// Begin durably persisting the current state of the journal.
334 ///
335 /// Awaiting the returned [Handle] provides the same durability guarantee as [Self::commit]
336 /// for the state present when the call begins (later appends need their own sync). Also
337 /// tries to advance the recovery watermark to the previous proven durable size, bounding
338 /// startup recovery. Use [Self::sync] to guarantee no recovery is needed. Dropping the
339 /// handle does not cancel the sync or lose its failure, which resurfaces at latest on the
340 /// next [Self::sync].
341 fn start_sync(
342 self,
343 ) -> impl std::future::Future<Output = Result<(Self, Handle<()>), Error>> + Send;
344
345 /// Durably persist the journal, guaranteeing the current state will survive a crash.
346 ///
347 /// For a stronger guarantee that eliminates potential recovery, use [Self::sync] instead.
348 fn commit(self) -> impl std::future::Future<Output = Result<Self, Error>> + Send;
349
350 /// Durably persist the journal, guaranteeing the current state will survive a crash, and that
351 /// no recovery will be needed on startup.
352 ///
353 /// This provides a stronger guarantee than [Self::commit] but may be slower.
354 fn sync(self) -> impl std::future::Future<Output = Result<Self, Error>> + Send;
355
356 /// Destroy the journal, removing all associated storage.
357 ///
358 /// This method consumes the journal and deletes all persisted data, leaving behind no storage
359 /// artifacts. This can be used to clean up disk resources in tests.
360 ///
361 /// # Crash Safety
362 ///
363 /// This operation is intended for final teardown and is not crash-safe. If interrupted,
364 /// reopening the same storage may observe partially removed state. Use a reset operation
365 /// provided by the concrete type when the journal must remain recoverable.
366 fn destroy(self) -> impl std::future::Future<Output = Result<(), Error>> + Send;
367}