Skip to main content

commonware_storage/archive/immutable/
storage.rs

1use crate::{
2    Context,
3    archive::{Error, Identifier, immutable::Config},
4    freezer::{self, Checkpoint, Cursor, Freezer},
5    metadata::{self, Metadata},
6    ordinal::{self, Ordinal},
7};
8use commonware_codec::{Buf, CodecShared, EncodeSize, FixedSize, Read, ReadExt, Write};
9use commonware_runtime::{
10    BufMut,
11    telemetry::metrics::{Counter, MetricsExt as _},
12};
13use commonware_utils::{Array, bitmap::BitMap, sequence::prefixed_u64::U64};
14use futures::{TryFutureExt as _, try_join};
15use std::collections::BTreeMap;
16use tracing::{debug, warn};
17
18/// Prefix for [Freezer] records.
19const FREEZER_PREFIX: u8 = 0;
20
21/// Prefix for [Ordinal] records.
22const ORDINAL_PREFIX: u8 = 1;
23
24/// Item stored in [Metadata] to ensure [Freezer] and [Ordinal] remain consistent.
25#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
26enum Record {
27    Freezer(Checkpoint),
28    Ordinal(Option<BitMap>),
29}
30
31impl Record {
32    /// Get the [Freezer] [Checkpoint] from the [Record].
33    fn freezer(&self) -> &Checkpoint {
34        match self {
35            Self::Freezer(checkpoint) => checkpoint,
36            _ => panic!("incorrect record"),
37        }
38    }
39
40    /// Get the [Ordinal] [BitMap] from the [Record].
41    fn ordinal(&self) -> &Option<BitMap> {
42        match self {
43            Self::Ordinal(indices) => indices,
44            _ => panic!("incorrect record"),
45        }
46    }
47}
48
49impl Write for Record {
50    fn write(&self, buf: &mut impl BufMut) {
51        match self {
52            Self::Freezer(checkpoint) => {
53                buf.put_u8(0);
54                checkpoint.write(buf);
55            }
56            Self::Ordinal(indices) => {
57                buf.put_u8(1);
58                indices.write(buf);
59            }
60        }
61    }
62}
63
64impl Read for Record {
65    type Cfg = ();
66    fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
67        let tag = u8::read(buf)?;
68        match tag {
69            0 => Ok(Self::Freezer(Checkpoint::read(buf)?)),
70            1 => Ok(Self::Ordinal(Option::<BitMap>::read_cfg(
71                buf,
72                &(..=usize::MAX as u64).into(),
73            )?)),
74            _ => Err(commonware_codec::Error::InvalidEnum(tag)),
75        }
76    }
77}
78
79impl EncodeSize for Record {
80    fn encode_size(&self) -> usize {
81        1 + match self {
82            Self::Freezer(_) => Checkpoint::SIZE,
83            Self::Ordinal(indices) => indices.encode_size(),
84        }
85    }
86}
87
88/// The archive's state, boxed so the public [Archive] handle stays pointer-sized.
89struct Inner<E: Context, K: Array, V: CodecShared> {
90    /// Number of items per section.
91    items_per_section: u64,
92
93    /// Metadata for the archive.
94    metadata: Metadata<E, U64, Record>,
95
96    /// Freezer for the archive.
97    freezer: Freezer<E, K, V>,
98
99    /// Ordinal for the archive.
100    ordinal: Ordinal<E, Cursor>,
101
102    // Metrics
103    gets: Counter,
104    has: Counter,
105    syncs: Counter,
106}
107
108impl<E: Context, K: Array, V: CodecShared> Inner<E, K, V> {
109    /// See [Archive::init].
110    async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
111        // Initialize metadata
112        let metadata = Metadata::<E, U64, Record>::init(
113            context.child("metadata"),
114            metadata::Config {
115                partition: cfg.metadata_partition,
116                codec_config: (),
117            },
118        )
119        .await?;
120
121        // Metadata is the commit record for lower-layer storage. If no checkpoint was committed,
122        // Freezer::init treats existing freezer blobs as uncommitted and starts empty.
123        let freezer_key = U64::new(FREEZER_PREFIX, 0);
124        let checkpoint = metadata.get(&freezer_key).map(|freezer| *freezer.freezer());
125
126        // Initialize table
127        //
128        // TODO (#1227): Use sharded metadata to provide consistency
129        let freezer = Freezer::init(
130            context.child("freezer"),
131            freezer::Config {
132                key_partition: cfg.freezer_key_partition,
133                key_write_buffer: cfg.freezer_key_write_buffer,
134                key_page_cache: cfg.freezer_key_page_cache,
135                value_partition: cfg.freezer_value_partition,
136                value_compression: cfg.freezer_value_compression,
137                value_write_buffer: cfg.freezer_value_write_buffer,
138                value_target_size: cfg.freezer_value_target_size,
139                table_partition: cfg.freezer_table_partition,
140                table_initial_size: cfg.freezer_table_initial_size,
141                table_resize_frequency: cfg.freezer_table_resize_frequency,
142                table_resize_chunk_size: cfg.freezer_table_resize_chunk_size,
143                table_replay_buffer: cfg.replay_buffer,
144                codec_config: cfg.codec_config,
145            },
146            checkpoint,
147        )
148        .await?;
149
150        // Collect committed ordinal sections. Ordinal::init removes stored sections that are not
151        // present in this map, so an empty map represents a committed empty ordinal.
152        let sections = metadata
153            .keys()
154            .filter(|k| k.prefix() == ORDINAL_PREFIX)
155            .collect::<Vec<_>>();
156        let mut section_bits = BTreeMap::new();
157        for section in sections {
158            // Get record
159            let bits = metadata.get(section).unwrap().ordinal();
160
161            // A partial section's bitmap spans the whole section, so its length must match the
162            // configured section size. Sections are addressed by that size, so a mismatch would
163            // serve records at the wrong indices.
164            if let Some(bits) = bits
165                && bits.len() != cfg.items_per_section.get()
166            {
167                warn!(
168                    section = section.value(),
169                    bitmap_len = bits.len(),
170                    items_per_section = cfg.items_per_section.get(),
171                    "committed section does not match items_per_section"
172                );
173                return Err(Error::RecordCorrupted);
174            }
175
176            // Get section
177            let section = section.value();
178            section_bits.insert(section, bits);
179        }
180
181        // Initialize ordinal
182        //
183        // TODO (#1227): Use sharded metadata to provide consistency
184        let ordinal = Ordinal::init(
185            context.child("ordinal"),
186            ordinal::Config {
187                partition: cfg.ordinal_partition,
188                items_per_blob: cfg.items_per_section,
189                write_buffer: cfg.ordinal_write_buffer,
190                replay_buffer: cfg.replay_buffer,
191            },
192            Some(section_bits),
193        )
194        .await?;
195
196        // Initialize metrics
197        let gets = context.counter("gets", "Number of gets performed");
198        let has = context.counter("has", "Number of has performed");
199        let syncs = context.counter("syncs", "Number of syncs called");
200
201        Ok(Self {
202            items_per_section: cfg.items_per_section.get(),
203            metadata,
204            freezer,
205            ordinal,
206            gets,
207            has,
208            syncs,
209        })
210    }
211
212    /// Get the value for the given index.
213    async fn get_index(&self, index: u64) -> Result<Option<V>, Error> {
214        // Get ordinal
215        let Some(cursor) = self.ordinal.get(index).await? else {
216            return Ok(None);
217        };
218
219        // Get journal entry
220        let result = self
221            .freezer
222            .get(freezer::Identifier::Cursor(cursor))
223            .await?;
224
225        // Get value
226        Ok(result)
227    }
228
229    /// Get the value for the given key.
230    async fn get_key(&self, key: &K) -> Result<Option<V>, Error> {
231        // Get table entry
232        let result = self.freezer.get(freezer::Identifier::Key(key)).await?;
233
234        // Get value
235        Ok(result)
236    }
237
238    /// Initialize the section.
239    fn initialize_section(&mut self, section: u64) {
240        // Create active bit vector
241        let bits = BitMap::zeroes(self.items_per_section);
242
243        // Store record
244        let key = U64::new(ORDINAL_PREFIX, section);
245        self.metadata.put(key, Record::Ordinal(Some(bits)));
246        debug!(section, "initialized section");
247    }
248
249    /// See [crate::archive::Archive::put].
250    async fn put(mut self: Box<Self>, index: u64, key: K, data: &V) -> Result<Box<Self>, Error> {
251        // Ignore duplicates
252        if self.ordinal.has(index) {
253            return Ok(self);
254        }
255
256        // Initialize section if it doesn't exist
257        let section = index / self.items_per_section;
258        let ordinal_key = U64::new(ORDINAL_PREFIX, section);
259        if self.metadata.get(&ordinal_key).is_none() {
260            self.initialize_section(section);
261        }
262        let record = self.metadata.get_mut(&ordinal_key).unwrap();
263
264        // Update active bits
265        let done = if let Record::Ordinal(Some(bits)) = record {
266            bits.set(index % self.items_per_section, true);
267            bits.count_ones() == self.items_per_section
268        } else {
269            false
270        };
271        if done {
272            *record = Record::Ordinal(None);
273        }
274
275        // Put in table
276        let cursor;
277        (self.freezer, cursor) = self.freezer.put(key, data).await?;
278
279        // Put section and offset in ordinal
280        self.ordinal = self.ordinal.put(index, cursor).await?;
281
282        Ok(self)
283    }
284
285    /// See [crate::archive::Archive::get].
286    async fn get(&self, identifier: Identifier<'_, K>) -> Result<Option<V>, Error> {
287        self.gets.inc();
288
289        match identifier {
290            Identifier::Index(index) => self.get_index(index).await,
291            Identifier::Key(key) => self.get_key(key).await,
292        }
293    }
294
295    /// See [crate::archive::Archive::has].
296    async fn has(&self, identifier: Identifier<'_, K>) -> Result<bool, Error> {
297        self.has.inc();
298
299        match identifier {
300            Identifier::Index(index) => Ok(self.ordinal.has(index)),
301            Identifier::Key(key) => Ok(self.freezer.has(key).await?),
302        }
303    }
304
305    /// See [crate::archive::Archive::sync].
306    async fn sync(mut self: Box<Self>) -> Result<Box<Self>, Error> {
307        self.syncs.inc();
308
309        // Sync journal and ordinal
310        let ((freezer, checkpoint), ordinal) = try_join!(
311            self.freezer.sync().map_err(Error::from),
312            self.ordinal.sync().map_err(Error::from)
313        )?;
314        self.freezer = freezer;
315        self.ordinal = ordinal;
316
317        // Publish the freezer checkpoint with a single metadata sync after the
318        // freezer and ordinal state are durable.
319        let freezer_key = U64::new(FREEZER_PREFIX, 0);
320        self.metadata = self
321            .metadata
322            .put_sync(freezer_key, Record::Freezer(checkpoint))
323            .await?;
324
325        Ok(self)
326    }
327
328    /// See [crate::archive::Archive::next_gap].
329    fn next_gap(&self, index: u64) -> (Option<u64>, Option<u64>) {
330        self.ordinal.next_gap(index)
331    }
332
333    /// See [crate::archive::Archive::missing_items].
334    fn missing_items(&self, index: u64, max: usize) -> Vec<u64> {
335        self.ordinal.missing_items(index, max)
336    }
337
338    /// See [crate::archive::Archive::ranges].
339    fn ranges(&self) -> impl Iterator<Item = (u64, u64)> {
340        self.ordinal.ranges()
341    }
342
343    /// See [crate::archive::Archive::ranges_from].
344    fn ranges_from(&self, from: u64) -> impl Iterator<Item = (u64, u64)> {
345        self.ordinal.ranges_from(from)
346    }
347
348    /// See [crate::archive::Archive::first_index].
349    fn first_index(&self) -> Option<u64> {
350        self.ordinal.first_index()
351    }
352
353    /// See [crate::archive::Archive::last_index].
354    fn last_index(&self) -> Option<u64> {
355        self.ordinal.last_index()
356    }
357
358    /// See [crate::archive::Archive::destroy].
359    async fn destroy(self) -> Result<(), Error> {
360        // Remove the commit record first: whatever an interrupted destroy leaves behind is then
361        // uncommitted relative to any surviving commit record, and the next init discards it.
362        self.metadata.destroy().await?;
363
364        // The stores it named now hold only uncommitted data, so they can be removed in any order.
365        self.ordinal.destroy().await?;
366        self.freezer.destroy().await?;
367        Ok(())
368    }
369}
370
371/// An immutable key-value store for ordered data with a minimal memory footprint.
372///
373/// Mutating functions consume the archive and return it only on success: an error (or a
374/// dropped future) destroys the handle.
375pub struct Archive<E: Context, K: Array, V: CodecShared>(Box<Inner<E, K, V>>);
376
377impl<E: Context, K: Array, V: CodecShared> std::fmt::Debug for Archive<E, K, V> {
378    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
379        f.debug_struct("Archive")
380            .field("first_index", &self.0.first_index())
381            .field("last_index", &self.0.last_index())
382            .finish_non_exhaustive()
383    }
384}
385
386impl<E: Context, K: Array, V: CodecShared> Archive<E, K, V> {
387    /// Initialize a new [Archive] with the given [Config].
388    pub async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
389        Ok(Self(Box::new(Inner::init(context, cfg).await?)))
390    }
391}
392
393impl<E: Context, K: Array, V: CodecShared> crate::archive::Archive for Archive<E, K, V> {
394    type Key = K;
395    type Value = V;
396
397    async fn put(mut self, index: u64, key: K, data: &V) -> Result<Self, Error> {
398        self.0 = self.0.put(index, key, data).await?;
399        Ok(self)
400    }
401
402    async fn get(&self, identifier: Identifier<'_, K>) -> Result<Option<V>, Error> {
403        self.0.get(identifier).await
404    }
405
406    async fn has(&self, identifier: Identifier<'_, K>) -> Result<bool, Error> {
407        self.0.has(identifier).await
408    }
409
410    async fn sync(mut self) -> Result<Self, Error> {
411        self.0 = self.0.sync().await?;
412        Ok(self)
413    }
414
415    fn next_gap(&self, index: u64) -> (Option<u64>, Option<u64>) {
416        self.0.next_gap(index)
417    }
418
419    fn missing_items(&self, index: u64, max: usize) -> Vec<u64> {
420        self.0.missing_items(index, max)
421    }
422
423    fn ranges(&self) -> impl Iterator<Item = (u64, u64)> {
424        self.0.ranges()
425    }
426
427    fn ranges_from(&self, from: u64) -> impl Iterator<Item = (u64, u64)> {
428        self.0.ranges_from(from)
429    }
430
431    fn first_index(&self) -> Option<u64> {
432        self.0.first_index()
433    }
434
435    fn last_index(&self) -> Option<u64> {
436        self.0.last_index()
437    }
438
439    async fn destroy(self) -> Result<(), Error> {
440        self.0.destroy().await
441    }
442}
443
444#[cfg(all(test, feature = "arbitrary"))]
445mod conformance {
446    use super::*;
447    use commonware_codec::conformance::CodecConformance;
448
449    commonware_conformance::conformance_tests! {
450        CodecConformance<Record>
451    }
452}