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