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