Skip to main content

commonware_storage/archive/immutable/
storage.rs

1use crate::{
2    archive::{immutable::Config, Error, Identifier},
3    freezer::{self, Checkpoint, Cursor, Freezer},
4    metadata::{self, Metadata},
5    ordinal::{self, Ordinal},
6    Context,
7};
8use commonware_codec::{CodecShared, EncodeSize, FixedSize, Read, ReadExt, Write};
9use commonware_macros::boxed;
10use commonware_runtime::{
11    telemetry::metrics::{Counter, MetricsExt as _},
12    Buf, BufMut, BufferPooler,
13};
14use commonware_utils::{bitmap::BitMap, sequence::prefixed_u64::U64, Array};
15use futures::join;
16use std::collections::BTreeMap;
17use tracing::debug;
18
19/// Prefix for [Freezer] records.
20const FREEZER_PREFIX: u8 = 0;
21
22/// Prefix for [Ordinal] records.
23const ORDINAL_PREFIX: u8 = 1;
24
25/// Item stored in [Metadata] to ensure [Freezer] and [Ordinal] remain consistent.
26#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
27enum Record {
28    Freezer(Checkpoint),
29    Ordinal(Option<BitMap>),
30}
31
32impl Record {
33    /// Get the [Freezer] [Checkpoint] from the [Record].
34    fn freezer(&self) -> &Checkpoint {
35        match self {
36            Self::Freezer(checkpoint) => checkpoint,
37            _ => panic!("incorrect record"),
38        }
39    }
40
41    /// Get the [Ordinal] [BitMap] from the [Record].
42    fn ordinal(&self) -> &Option<BitMap> {
43        match self {
44            Self::Ordinal(indices) => indices,
45            _ => panic!("incorrect record"),
46        }
47    }
48}
49
50impl Write for Record {
51    fn write(&self, buf: &mut impl BufMut) {
52        match self {
53            Self::Freezer(checkpoint) => {
54                buf.put_u8(0);
55                checkpoint.write(buf);
56            }
57            Self::Ordinal(indices) => {
58                buf.put_u8(1);
59                indices.write(buf);
60            }
61        }
62    }
63}
64
65impl Read for Record {
66    type Cfg = ();
67    fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
68        let tag = u8::read(buf)?;
69        match tag {
70            0 => Ok(Self::Freezer(Checkpoint::read(buf)?)),
71            1 => Ok(Self::Ordinal(Option::<BitMap>::read_cfg(
72                buf,
73                &(usize::MAX as u64),
74            )?)),
75            _ => Err(commonware_codec::Error::InvalidEnum(tag)),
76        }
77    }
78}
79
80impl EncodeSize for Record {
81    fn encode_size(&self) -> usize {
82        1 + match self {
83            Self::Freezer(_) => Checkpoint::SIZE,
84            Self::Ordinal(indices) => indices.encode_size(),
85        }
86    }
87}
88
89/// An immutable key-value store for ordered data with a minimal memory footprint.
90pub struct Archive<E: BufferPooler + Context, K: Array, V: CodecShared> {
91    /// Number of items per section.
92    items_per_section: u64,
93
94    /// Metadata for the archive.
95    metadata: Metadata<E, U64, Record>,
96
97    /// Freezer for the archive.
98    freezer: Freezer<E, K, V>,
99
100    /// Ordinal for the archive.
101    ordinal: Ordinal<E, Cursor>,
102
103    // Metrics
104    gets: Counter,
105    has: Counter,
106    syncs: Counter,
107}
108
109impl<E: BufferPooler + Context, K: Array, V: CodecShared> Archive<E, K, V> {
110    /// Initialize a new [Archive] with the given [Config].
111    pub async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
112        // Initialize metadata
113        let metadata = Metadata::<E, U64, Record>::init(
114            context.child("metadata"),
115            metadata::Config {
116                partition: cfg.metadata_partition,
117                codec_config: (),
118            },
119        )
120        .await?;
121
122        // Metadata is the commit record for lower-layer storage. If no checkpoint was committed,
123        // Freezer::init treats existing freezer blobs as uncommitted and starts empty.
124        let freezer_key = U64::new(FREEZER_PREFIX, 0);
125        let checkpoint = metadata.get(&freezer_key).map(|freezer| *freezer.freezer());
126
127        // Initialize table
128        //
129        // TODO (#1227): Use sharded metadata to provide consistency
130        let freezer = Freezer::init(
131            context.child("freezer"),
132            freezer::Config {
133                key_partition: cfg.freezer_key_partition,
134                key_write_buffer: cfg.freezer_key_write_buffer,
135                key_page_cache: cfg.freezer_key_page_cache,
136                value_partition: cfg.freezer_value_partition,
137                value_compression: cfg.freezer_value_compression,
138                value_write_buffer: cfg.freezer_value_write_buffer,
139                value_target_size: cfg.freezer_value_target_size,
140                table_partition: cfg.freezer_table_partition,
141                table_initial_size: cfg.freezer_table_initial_size,
142                table_resize_frequency: cfg.freezer_table_resize_frequency,
143                table_resize_chunk_size: cfg.freezer_table_resize_chunk_size,
144                table_replay_buffer: cfg.replay_buffer,
145                codec_config: cfg.codec_config,
146            },
147            checkpoint,
148        )
149        .await?;
150
151        // Collect committed ordinal sections. Ordinal::init removes stored sections that are not
152        // present in this map, so an empty map represents a committed empty ordinal.
153        let sections = metadata
154            .keys()
155            .filter(|k| k.prefix() == ORDINAL_PREFIX)
156            .collect::<Vec<_>>();
157        let mut section_bits = BTreeMap::new();
158        for section in sections {
159            // Get record
160            let bits = metadata.get(section).unwrap().ordinal();
161
162            // Get section
163            let section = section.value();
164            section_bits.insert(section, bits);
165        }
166
167        // Initialize ordinal
168        //
169        // TODO (#1227): Use sharded metadata to provide consistency
170        let ordinal = Ordinal::init(
171            context.child("ordinal"),
172            ordinal::Config {
173                partition: cfg.ordinal_partition,
174                items_per_blob: cfg.items_per_section,
175                write_buffer: cfg.ordinal_write_buffer,
176                replay_buffer: cfg.replay_buffer,
177            },
178            Some(section_bits),
179        )
180        .await?;
181
182        // Initialize metrics
183        let gets = context.counter("gets", "Number of gets performed");
184        let has = context.counter("has", "Number of has performed");
185        let syncs = context.counter("syncs", "Number of syncs called");
186
187        Ok(Self {
188            items_per_section: cfg.items_per_section.get(),
189            metadata,
190            freezer,
191            ordinal,
192            gets,
193            has,
194            syncs,
195        })
196    }
197
198    /// Get the value for the given index.
199    async fn get_index(&self, index: u64) -> Result<Option<V>, Error> {
200        // Get ordinal
201        let Some(cursor) = self.ordinal.get(index).await? else {
202            return Ok(None);
203        };
204
205        // Get journal entry
206        let result = self
207            .freezer
208            .get(freezer::Identifier::Cursor(cursor))
209            .await?;
210
211        // Get value
212        Ok(result)
213    }
214
215    /// Get the value for the given key.
216    async fn get_key(&self, key: &K) -> Result<Option<V>, Error> {
217        // Get table entry
218        let result = self.freezer.get(freezer::Identifier::Key(key)).await?;
219
220        // Get value
221        Ok(result)
222    }
223
224    /// Initialize the section.
225    fn initialize_section(&mut self, section: u64) {
226        // Create active bit vector
227        let bits = BitMap::zeroes(self.items_per_section);
228
229        // Store record
230        let key = U64::new(ORDINAL_PREFIX, section);
231        self.metadata.put(key, Record::Ordinal(Some(bits)));
232        debug!(section, "initialized section");
233    }
234}
235
236impl<E: BufferPooler + Context, K: Array, V: CodecShared> crate::archive::Archive
237    for Archive<E, K, V>
238{
239    type Key = K;
240    type Value = V;
241
242    async fn put(&mut self, index: u64, key: K, data: V) -> Result<(), Error> {
243        // Ignore duplicates
244        if self.ordinal.has(index) {
245            return Ok(());
246        }
247
248        // Initialize section if it doesn't exist
249        let section = index / self.items_per_section;
250        let ordinal_key = U64::new(ORDINAL_PREFIX, section);
251        if self.metadata.get(&ordinal_key).is_none() {
252            self.initialize_section(section);
253        }
254        let record = self.metadata.get_mut(&ordinal_key).unwrap();
255
256        // Update active bits
257        let done = if let Record::Ordinal(Some(bits)) = record {
258            bits.set(index % self.items_per_section, true);
259            bits.count_ones() == self.items_per_section
260        } else {
261            false
262        };
263        if done {
264            *record = Record::Ordinal(None);
265        }
266
267        // Put in table
268        let cursor = self.freezer.put(key, data).await?;
269
270        // Put section and offset in ordinal
271        self.ordinal.put(index, cursor).await?;
272
273        Ok(())
274    }
275
276    async fn get(&self, identifier: Identifier<'_, K>) -> Result<Option<V>, Error> {
277        self.gets.inc();
278
279        match identifier {
280            Identifier::Index(index) => self.get_index(index).await,
281            Identifier::Key(key) => self.get_key(key).await,
282        }
283    }
284
285    async fn has(&self, identifier: Identifier<'_, K>) -> Result<bool, Error> {
286        self.has.inc();
287
288        match identifier {
289            Identifier::Index(index) => Ok(self.ordinal.has(index)),
290            Identifier::Key(key) => Ok(self.freezer.has(key).await?),
291        }
292    }
293
294    async fn sync(&mut self) -> Result<(), Error> {
295        self.syncs.inc();
296
297        // Sync journal and ordinal
298        let (freezer_result, ordinal_result) = join!(self.freezer.sync(), self.ordinal.sync());
299        let checkpoint = freezer_result?;
300        ordinal_result?;
301
302        // Publish the freezer checkpoint with a single metadata sync after the
303        // freezer and ordinal state are durable.
304        let freezer_key = U64::new(FREEZER_PREFIX, 0);
305        self.metadata
306            .put_sync(freezer_key, Record::Freezer(checkpoint))
307            .await?;
308
309        Ok(())
310    }
311
312    fn next_gap(&self, index: u64) -> (Option<u64>, Option<u64>) {
313        self.ordinal.next_gap(index)
314    }
315
316    fn missing_items(&self, index: u64, max: usize) -> Vec<u64> {
317        self.ordinal.missing_items(index, max)
318    }
319
320    fn ranges(&self) -> impl Iterator<Item = (u64, u64)> {
321        self.ordinal.ranges()
322    }
323
324    fn ranges_from(&self, from: u64) -> impl Iterator<Item = (u64, u64)> {
325        self.ordinal.ranges_from(from)
326    }
327
328    fn first_index(&self) -> Option<u64> {
329        self.ordinal.first_index()
330    }
331
332    fn last_index(&self) -> Option<u64> {
333        self.ordinal.last_index()
334    }
335
336    #[boxed]
337    async fn destroy(self) -> Result<(), Error> {
338        // Destroy ordinal
339        self.ordinal.destroy().await?;
340
341        // Destroy freezer
342        self.freezer.destroy().await?;
343
344        // Destroy metadata
345        self.metadata.destroy().await?;
346
347        Ok(())
348    }
349}
350
351#[cfg(all(test, feature = "arbitrary"))]
352mod conformance {
353    use super::*;
354    use commonware_codec::conformance::CodecConformance;
355
356    commonware_conformance::conformance_tests! {
357        CodecConformance<Record>
358    }
359}