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}