Skip to main content

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}