Skip to main content

commonware_storage/ordinal/
storage.rs

1use super::{Config, Error};
2use crate::{Context, rmap::RMap};
3use commonware_codec::{CodecFixed, FixedSize, Read, ReadExt, Write as CodecWrite};
4use commonware_cryptography::{Crc32, crc32};
5use commonware_formatting::hex;
6use commonware_runtime::{
7    Blob, Buf, BufMut, Error as RError, WriteOptions,
8    buffer::{Read as ReadBuffer, Write},
9    telemetry::metrics::{Counter, MetricsExt as _},
10};
11use commonware_utils::bitmap::BitMap;
12use futures::future::try_join_all;
13use std::{
14    collections::{BTreeMap, BTreeSet, btree_map::Entry},
15    marker::PhantomData,
16};
17use tracing::{debug, warn};
18
19/// Value stored in the index file.
20#[derive(Debug, Clone)]
21struct Record<V: CodecFixed<Cfg = ()>> {
22    value: V,
23    crc: u32,
24}
25
26impl<V: CodecFixed<Cfg = ()>> Record<V> {
27    /// Serialize `value` followed by the CRC of its serialized bytes.
28    fn encode(value: &V) -> Vec<u8> {
29        let mut buf = Vec::with_capacity(Self::SIZE);
30        value.write(&mut buf);
31        assert_eq!(buf.len(), V::SIZE, "write() did not write expected bytes");
32        let crc = Crc32::checksum(&buf);
33        crc.write(&mut buf);
34        buf
35    }
36
37    /// Deserialize a record, returning the value only if the stored CRC matches the raw
38    /// value bytes.
39    fn decode_valid(mut buf: &[u8]) -> Option<V> {
40        let crc = Crc32::checksum(buf.get(..V::SIZE)?);
41        let record = Self::read(&mut buf).ok()?;
42        (record.crc == crc).then_some(record.value)
43    }
44}
45
46impl<V: CodecFixed<Cfg = ()>> FixedSize for Record<V> {
47    const SIZE: usize = V::SIZE + crc32::Digest::SIZE;
48}
49
50impl<V: CodecFixed<Cfg = ()>> CodecWrite for Record<V> {
51    fn write(&self, buf: &mut impl BufMut) {
52        self.value.write(buf);
53        self.crc.write(buf);
54    }
55}
56
57impl<V: CodecFixed<Cfg = ()>> Read for Record<V> {
58    type Cfg = ();
59
60    fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
61        let value = V::read(buf)?;
62        let crc = u32::read(buf)?;
63
64        Ok(Self { value, crc })
65    }
66}
67
68#[cfg(feature = "arbitrary")]
69impl<V: CodecFixed<Cfg = ()>> arbitrary::Arbitrary<'_> for Record<V>
70where
71    V: for<'a> arbitrary::Arbitrary<'a>,
72{
73    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
74        let value = V::arbitrary(u)?;
75        let mut buf = Vec::with_capacity(V::SIZE);
76        value.write(&mut buf);
77        let crc = Crc32::checksum(&buf);
78        Ok(Self { value, crc })
79    }
80}
81
82/// The store's state, boxed so the public [Ordinal] handle stays pointer-sized.
83struct Inner<E: Context, V: CodecFixed<Cfg = ()>> {
84    // Configuration and context
85    context: E,
86    config: Config,
87
88    // Index blobs for storing key records
89    blobs: BTreeMap<u64, Write<E::Blob>>,
90
91    // RMap for interval tracking
92    intervals: RMap,
93
94    // Pending sections to be synced.
95    pending: BTreeSet<u64>,
96
97    // Metrics
98    puts: Counter,
99    gets: Counter,
100    has: Counter,
101    syncs: Counter,
102    pruned: Counter,
103
104    _phantom: PhantomData<V>,
105}
106
107impl<E: Context, V: CodecFixed<Cfg = ()>> Inner<E, V> {
108    /// See [Ordinal::init].
109    async fn init(
110        context: E,
111        config: Config,
112        bits: Option<BTreeMap<u64, &Option<BitMap>>>,
113    ) -> Result<Self, Error> {
114        // Reset the store unless committed bits are provided to recover from the stored blobs
115        let record_size = Record::<V>::SIZE as u64;
116        let items_per_blob = config.items_per_blob.get();
117        let mut blobs = BTreeMap::new();
118        let stored_blobs = if bits.is_none() {
119            match context.remove(&config.partition, None).await {
120                Ok(()) | Err(RError::PartitionMissing(_)) => Vec::new(),
121                Err(err) => return Err(Error::Runtime(err)),
122            }
123        } else {
124            match context.scan(&config.partition).await {
125                Ok(blobs) => blobs,
126                Err(RError::PartitionMissing(_)) => Vec::new(),
127                Err(err) => return Err(Error::Runtime(err)),
128            }
129        };
130
131        // Open all blobs and check for partial records
132        for name in stored_blobs {
133            let (blob, mut len) = context.open(&config.partition, &name).await?;
134            let index = match name.try_into() {
135                Ok(index) => u64::from_be_bytes(index),
136                Err(nm) => Err(Error::InvalidBlobName(hex(&nm)))?,
137            };
138
139            // Check if blob size is aligned to record size
140            if bits.is_some() && len % record_size != 0 {
141                warn!(
142                    blob = index,
143                    invalid_size = len,
144                    record_size,
145                    "blob size is not a multiple of record size, truncating"
146                );
147                len -= len % record_size;
148                blob.resize(len).await?;
149                blob.sync().await?;
150            }
151
152            debug!(blob = index, len, "found index blob");
153            blobs.insert(index, (blob, len));
154        }
155
156        // Initialize intervals by scanning committed records
157        debug!(
158            blobs = blobs.len(),
159            "rebuilding intervals from existing index"
160        );
161        let start = context.current();
162        let mut items = 0;
163        let mut intervals = RMap::new();
164        if let Some(bits) = &bits {
165            // Drop sections the committed bits do not cover
166            let sections = blobs.keys().copied().collect::<Vec<_>>();
167            for section in sections {
168                let keep = match bits.get(&section) {
169                    Some(Some(bits)) => bits.count_ones() != 0,
170                    Some(None) => true,
171                    None => false,
172                };
173                if !keep {
174                    context
175                        .remove(&config.partition, Some(&section.to_be_bytes()))
176                        .await?;
177                    blobs.remove(&section);
178                }
179            }
180
181            // Replay ignores records outside the committed bits, but recovery clears them so
182            // stored blobs match the checkpointed view
183            let empty = vec![0u8; Record::<V>::SIZE];
184            for (section, (blob, size)) in &blobs {
185                // A section with no bitmap requires every record, so nothing is cleared
186                let Some(Some(bits)) = bits.get(section) else {
187                    continue;
188                };
189                let mut modified = false;
190                for bit_index in 0..(*size / record_size) {
191                    if bit_index >= bits.len() || !bits.get(bit_index) {
192                        blob.write_at(
193                            bit_index * record_size,
194                            empty.clone(),
195                            WriteOptions::default(),
196                        )
197                        .await?;
198                        modified = true;
199                    }
200                }
201                if modified {
202                    blob.sync().await?;
203                }
204            }
205
206            // Rebuild intervals from the committed records
207            for (section, bits) in bits {
208                if let Some(bits) = bits
209                    && bits.count_ones() == 0
210                {
211                    continue;
212                }
213
214                let Some((blob, size)) = blobs.get(section) else {
215                    return Err(Error::MissingRecord(section * items_per_blob));
216                };
217
218                // A section replays every record unless a bitmap restricts replay
219                // to the records it marks
220                let mut set_indices = bits.as_ref().map(|bits| bits.ones_iter());
221                let mut all_indices = 0..items_per_blob;
222
223                // A committed bitmap already proves membership, so marked records are not
224                // re-read and damage surfaces at get. Membership of an unmarked section
225                // comes from record validity, so its records must be read.
226                let mut replay_blob = bits.is_none().then(|| {
227                    ReadBuffer::from_pooler(&context, blob.clone(), *size, config.replay_buffer)
228                });
229                while let Some(bit_index) = set_indices
230                    .as_mut()
231                    .map_or_else(|| all_indices.next(), |indices| indices.next())
232                {
233                    let index = section * items_per_blob + bit_index;
234                    if bit_index >= items_per_blob {
235                        return Err(Error::MissingRecord(index));
236                    }
237                    let offset = bit_index * record_size;
238                    if offset + record_size > *size {
239                        return Err(Error::MissingRecord(index));
240                    }
241
242                    // A committed record that is missing or invalid cannot be recovered
243                    if let Some(replay_blob) = replay_blob.as_mut() {
244                        replay_blob.seek_to(offset)?;
245                        let record_buf = replay_blob.read(Record::<V>::SIZE).await?.coalesce();
246                        if Record::<V>::decode_valid(record_buf.as_ref()).is_none() {
247                            return Err(Error::MissingRecord(index));
248                        }
249                    }
250                    items += 1;
251                    intervals.insert(index);
252                }
253            }
254        }
255        debug!(
256            items,
257            elapsed = ?context.current().duration_since(start).unwrap_or_default(),
258            "rebuilt intervals"
259        );
260
261        // Wrap blobs in write buffers
262        let blobs = blobs
263            .into_iter()
264            .map(|(index, (blob, len))| {
265                (
266                    index,
267                    Write::from_pooler(&context, blob, len, config.write_buffer),
268                )
269            })
270            .collect();
271
272        // Initialize metrics
273        let puts = context.counter("puts", "Number of put calls");
274        let gets = context.counter("gets", "Number of get calls");
275        let has = context.counter("has", "Number of has calls");
276        let syncs = context.counter("syncs", "Number of sync calls");
277        let pruned = context.counter("pruned", "Number of pruned blobs");
278
279        Ok(Self {
280            context,
281            config,
282            blobs,
283            intervals,
284            pending: BTreeSet::new(),
285            puts,
286            gets,
287            has,
288            syncs,
289            pruned,
290            _phantom: PhantomData,
291        })
292    }
293
294    /// See [Ordinal::put].
295    async fn put(&mut self, index: u64, value: V) -> Result<(), Error> {
296        self.puts.inc();
297
298        // Check if blob exists
299        let items_per_blob = self.config.items_per_blob.get();
300        let section = index / items_per_blob;
301        if let Entry::Vacant(entry) = self.blobs.entry(section) {
302            let (blob, len) = self
303                .context
304                .open(&self.config.partition, &section.to_be_bytes())
305                .await?;
306            entry.insert(Write::from_pooler(
307                &self.context,
308                blob,
309                len,
310                self.config.write_buffer,
311            ));
312            debug!(section, "created blob");
313        }
314
315        // Write the value to the blob
316        let blob = self.blobs.get_mut(&section).unwrap();
317        let offset = (index % items_per_blob) * Record::<V>::SIZE as u64;
318        blob.write_at(offset, Record::encode(&value)).await?;
319        self.pending.insert(section);
320
321        // Add to intervals
322        self.intervals.insert(index);
323
324        Ok(())
325    }
326
327    /// See [Ordinal::get].
328    async fn get(&self, index: u64) -> Result<Option<V>, Error> {
329        self.gets.inc();
330
331        // If get isn't in an interval, it doesn't exist and we don't need to access disk
332        if self.intervals.get(&index).is_none() {
333            return Ok(None);
334        }
335
336        // Read from disk
337        let items_per_blob = self.config.items_per_blob.get();
338        let section = index / items_per_blob;
339        let blob = self.blobs.get(&section).unwrap();
340        let offset = (index % items_per_blob) * Record::<V>::SIZE as u64;
341        let read_buf = blob.read_at(offset, Record::<V>::SIZE).await?.coalesce();
342
343        // If record is valid, return it
344        let value =
345            Record::<V>::decode_valid(read_buf.as_ref()).ok_or(Error::InvalidRecord(index))?;
346        Ok(Some(value))
347    }
348
349    /// See [Ordinal::has].
350    fn has(&self, index: u64) -> bool {
351        self.has.inc();
352
353        self.intervals.get(&index).is_some()
354    }
355
356    /// See [Ordinal::next_gap].
357    fn next_gap(&self, index: u64) -> (Option<u64>, Option<u64>) {
358        self.intervals.next_gap(index)
359    }
360
361    /// See [Ordinal::ranges].
362    fn ranges(&self) -> impl Iterator<Item = (u64, u64)> + '_ {
363        self.intervals.iter().map(|(&s, &e)| (s, e))
364    }
365
366    /// See [Ordinal::ranges_from].
367    fn ranges_from(&self, from: u64) -> impl Iterator<Item = (u64, u64)> + '_ {
368        self.intervals.iter_from(from).map(|(&s, &e)| (s, e))
369    }
370
371    /// See [Ordinal::first_index].
372    fn first_index(&self) -> Option<u64> {
373        self.intervals.first_index()
374    }
375
376    /// See [Ordinal::last_index].
377    fn last_index(&self) -> Option<u64> {
378        self.intervals.last_index()
379    }
380
381    /// See [Ordinal::missing_items].
382    fn missing_items(&self, start: u64, max: usize) -> Vec<u64> {
383        self.intervals.missing_items(start, max)
384    }
385
386    /// See [Ordinal::prune].
387    async fn prune(&mut self, min: u64) -> Result<(), Error> {
388        // Collect sections to remove
389        let items_per_blob = self.config.items_per_blob.get();
390        let min_section = min / items_per_blob;
391        let sections_to_remove: Vec<u64> = self
392            .blobs
393            .keys()
394            .filter(|&&section| section < min_section)
395            .copied()
396            .collect();
397
398        // Remove the collected sections
399        for section in sections_to_remove {
400            if let Some(blob) = self.blobs.remove(&section) {
401                drop(blob);
402                self.context
403                    .remove(&self.config.partition, Some(&section.to_be_bytes()))
404                    .await?;
405
406                // Remove the corresponding index range from intervals
407                let start_index = section * items_per_blob;
408                let end_index = (section + 1) * items_per_blob - 1;
409                self.intervals.remove(start_index, end_index);
410                debug!(section, start_index, end_index, "pruned blob");
411            }
412
413            // Update metrics
414            self.pruned.inc();
415        }
416
417        // Clean pending entries that fall into pruned sections.
418        self.pending.retain(|&section| section >= min_section);
419
420        Ok(())
421    }
422
423    /// See [Ordinal::sync].
424    async fn sync(&mut self) -> Result<(), Error> {
425        self.syncs.inc();
426
427        if self.pending.is_empty() {
428            return Ok(());
429        }
430
431        let futures: Vec<_> = self
432            .blobs
433            .iter_mut()
434            .filter(|(section, _)| self.pending.contains(section))
435            .map(|(_, blob)| blob.sync())
436            .collect();
437        try_join_all(futures).await?;
438
439        // Clear pending sections.
440        self.pending.clear();
441
442        Ok(())
443    }
444
445    /// See [Ordinal::destroy].
446    async fn destroy(self) -> Result<(), Error> {
447        for (i, blob) in self.blobs.into_iter() {
448            drop(blob);
449            self.context
450                .remove(&self.config.partition, Some(&i.to_be_bytes()))
451                .await?;
452            debug!(section = i, "destroyed blob");
453        }
454        match self.context.remove(&self.config.partition, None).await {
455            Ok(()) => {}
456            Err(RError::PartitionMissing(_)) => {
457                // Partition already removed or never existed.
458            }
459            Err(err) => return Err(Error::Runtime(err)),
460        }
461        Ok(())
462    }
463}
464
465/// Implementation of [Ordinal].
466///
467/// Mutating functions consume the store and return it only on success: an error (or a dropped
468/// future) destroys the handle.
469pub struct Ordinal<E: Context, V: CodecFixed<Cfg = ()>>(Box<Inner<E, V>>);
470
471impl<E: Context, V: CodecFixed<Cfg = ()>> std::fmt::Debug for Ordinal<E, V> {
472    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
473        f.debug_struct("Ordinal")
474            .field("first_index", &self.0.intervals.first_index())
475            .field("last_index", &self.0.intervals.last_index())
476            .finish_non_exhaustive()
477    }
478}
479
480impl<E: Context, V: CodecFixed<Cfg = ()>> Ordinal<E, V> {
481    /// Initialize a new [Ordinal] instance with a collection of [BitMap]s (indicating which
482    /// records should be considered available).
483    ///
484    /// If a section is not provided in the [BTreeMap], all records in that section are considered
485    /// unavailable. If a [BitMap] is provided for a section, all records in that section are
486    /// considered available if and only if the [BitMap] is set for the record. If a section is provided
487    /// but no [BitMap] is populated, all records in that section are considered available.
488    ///
489    /// Passing `Some(BTreeMap::new())` or `None` removes all stored sections and starts empty.
490    pub async fn init(
491        context: E,
492        config: Config,
493        bits: Option<BTreeMap<u64, &Option<BitMap>>>,
494    ) -> Result<Self, Error> {
495        Ok(Self(Box::new(Inner::init(context, config, bits).await?)))
496    }
497
498    /// Add a value at the specified index (pending until sync).
499    pub async fn put(mut self, index: u64, value: V) -> Result<Self, Error> {
500        self.0.put(index, value).await?;
501        Ok(self)
502    }
503
504    /// Get the value for a given index.
505    pub async fn get(&self, index: u64) -> Result<Option<V>, Error> {
506        self.0.get(index).await
507    }
508
509    /// Check if an index exists.
510    pub fn has(&self, index: u64) -> bool {
511        self.0.has(index)
512    }
513
514    /// Get the next gap information for backfill operations.
515    pub fn next_gap(&self, index: u64) -> (Option<u64>, Option<u64>) {
516        self.0.next_gap(index)
517    }
518
519    /// Get an iterator over all ranges in the [Ordinal].
520    pub fn ranges(&self) -> impl Iterator<Item = (u64, u64)> + '_ {
521        self.0.ranges()
522    }
523
524    /// Get an iterator over ranges that overlap or follow `from`.
525    pub fn ranges_from(&self, from: u64) -> impl Iterator<Item = (u64, u64)> + '_ {
526        self.0.ranges_from(from)
527    }
528
529    /// Retrieve the first index in the [Ordinal].
530    pub fn first_index(&self) -> Option<u64> {
531        self.0.first_index()
532    }
533
534    /// Retrieve the last index in the [Ordinal].
535    pub fn last_index(&self) -> Option<u64> {
536        self.0.last_index()
537    }
538
539    /// Returns up to `max` missing items starting from `start`.
540    ///
541    /// This method iterates through gaps between existing ranges, collecting missing indices
542    /// until either `max` items are found or there are no more gaps to fill.
543    pub fn missing_items(&self, start: u64, max: usize) -> Vec<u64> {
544        self.0.missing_items(start, max)
545    }
546
547    /// Prune indices older than `min` by removing entire blobs.
548    ///
549    /// Pruning is done at blob boundaries to avoid partial deletions. A blob is pruned only if
550    /// all possible indices in that blob are less than `min`.
551    pub async fn prune(mut self, min: u64) -> Result<Self, Error> {
552        self.0.prune(min).await?;
553        Ok(self)
554    }
555
556    /// Write all pending entries and sync all modified [Blob]s.
557    pub async fn sync(mut self) -> Result<Self, Error> {
558        self.0.sync().await?;
559        Ok(self)
560    }
561
562    /// Destroy [Ordinal] and remove all data.
563    pub async fn destroy(self) -> Result<(), Error> {
564        self.0.destroy().await
565    }
566}
567
568#[cfg(all(test, feature = "arbitrary"))]
569mod conformance {
570    use super::*;
571    use commonware_codec::conformance::CodecConformance;
572
573    commonware_conformance::conformance_tests! {
574        CodecConformance<Record<u32>>
575    }
576}
577
578#[cfg(test)]
579mod tests {
580    use super::*;
581    use commonware_runtime::deterministic::Context;
582
583    type TestOrdinal = Ordinal<Context, u64>;
584
585    fn is_send<T: Send>(_: T) {}
586
587    #[allow(dead_code)]
588    fn assert_ordinal_futures_are_send(ordinal: TestOrdinal, key: u64) {
589        is_send(ordinal.get(key));
590        is_send(ordinal.put(key, 0u64));
591    }
592
593    #[allow(dead_code)]
594    fn assert_ordinal_destroy_is_send(ordinal: TestOrdinal) {
595        is_send(ordinal.destroy());
596    }
597}