Skip to main content

commonware_storage/ordinal/
storage.rs

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