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}