Skip to main content

commonware_storage/archive/prunable/
storage.rs

1use super::{Config, Translator};
2use crate::{
3    archive::{Error, Identifier},
4    index::{unordered::Index, Unordered},
5    journal::segmented::oversized::{
6        Config as OversizedConfig, Oversized, Record as OversizedRecord,
7    },
8    rmap::RMap,
9};
10use commonware_codec::{CodecShared, FixedSize, Read, ReadExt, Write};
11use commonware_macros::boxed;
12use commonware_runtime::{
13    telemetry::metrics::{Counter, Gauge, GaugeExt, MetricsExt as _},
14    Buf, BufMut, BufferPooler, Handle, Metrics, Storage,
15};
16use commonware_utils::Array;
17use futures::{pin_mut, StreamExt};
18use std::collections::{btree_map, BTreeMap, BTreeSet};
19use tracing::debug;
20
21/// Index entry for the archive.
22#[derive(Debug, Clone, PartialEq)]
23struct Record<K: Array> {
24    /// The index for this entry.
25    index: u64,
26    /// The key for this entry.
27    key: K,
28    /// Byte offset in value journal (same section).
29    value_offset: u64,
30    /// Size of value data in the value journal.
31    value_size: u32,
32}
33
34impl<K: Array> Record<K> {
35    /// Create a new [Record].
36    const fn new(index: u64, key: K, value_offset: u64, value_size: u32) -> Self {
37        Self {
38            index,
39            key,
40            value_offset,
41            value_size,
42        }
43    }
44}
45
46impl<K: Array> Write for Record<K> {
47    fn write(&self, buf: &mut impl BufMut) {
48        self.index.write(buf);
49        self.key.write(buf);
50        self.value_offset.write(buf);
51        self.value_size.write(buf);
52    }
53}
54
55impl<K: Array> Read for Record<K> {
56    type Cfg = ();
57
58    fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
59        let index = u64::read(buf)?;
60        let key = K::read(buf)?;
61        let value_offset = u64::read(buf)?;
62        let value_size = u32::read(buf)?;
63        Ok(Self {
64            index,
65            key,
66            value_offset,
67            value_size,
68        })
69    }
70}
71
72impl<K: Array> FixedSize for Record<K> {
73    // index + key + value_offset + value_size
74    const SIZE: usize = u64::SIZE + K::SIZE + u64::SIZE + u32::SIZE;
75}
76
77impl<K: Array> OversizedRecord for Record<K> {
78    fn value_location(&self) -> (u64, u32) {
79        (self.value_offset, self.value_size)
80    }
81
82    fn with_location(mut self, offset: u64, size: u32) -> Self {
83        self.value_offset = offset;
84        self.value_size = size;
85        self
86    }
87}
88
89#[cfg(feature = "arbitrary")]
90impl<K: Array> arbitrary::Arbitrary<'_> for Record<K>
91where
92    K: for<'a> arbitrary::Arbitrary<'a>,
93{
94    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
95        Ok(Self {
96            index: u64::arbitrary(u)?,
97            key: K::arbitrary(u)?,
98            value_offset: u64::arbitrary(u)?,
99            value_size: u32::arbitrary(u)?,
100        })
101    }
102}
103
104/// Implementation of `Archive` storage.
105pub struct Archive<T: Translator, E: BufferPooler + Storage + Metrics, K: Array, V: CodecShared> {
106    items_per_section: u64,
107
108    /// Combined index + value storage with crash recovery.
109    oversized: Oversized<E, Record<K>, V>,
110
111    /// Sections with writes not yet included in any sync request. Moved into `requested` when a
112    /// sync is requested; the `syncs` metric counts only this set, so each section of writes is
113    /// counted once per request.
114    pending: BTreeSet<u64>,
115
116    /// Sections included in a sync request by [crate::archive::Archive::start_sync], retained
117    /// until a full sync completes.
118    ///
119    /// Retention is load-bearing: a [crate::archive::Archive::start_sync] handle must cover
120    /// every previously accepted write, even when the call itself wrote nothing (e.g. a
121    /// duplicate put). Re-requesting these sections makes their buffers return the in-flight
122    /// sync's handle (a completed sync resolves immediately; no new I/O is issued). Pruned
123    /// sections must be removed from this set, or a later request would trip the journal's
124    /// prune guard.
125    requested: BTreeSet<u64>,
126
127    /// Oldest allowed section to read from. Updated when `prune` is called.
128    oldest_allowed: Option<u64>,
129
130    /// Maps translated key representation to its corresponding index.
131    keys: Index<T, u64>,
132
133    /// Maps index to its first position in the index journal.
134    indices: BTreeMap<u64, u64>,
135
136    /// Additional positions for indices that have more than one entry.
137    /// Only populated when used via [crate::archive::MultiArchive::put_multi].
138    extra_indices: BTreeMap<u64, Vec<u64>>,
139
140    /// Interval tracking for gap detection.
141    intervals: RMap,
142
143    // Metrics
144    items_tracked: Gauge,
145    indices_pruned: Counter,
146    unnecessary_reads: Counter,
147    gets: Counter,
148    has: Counter,
149    syncs: Counter,
150}
151
152impl<T: Translator, E: BufferPooler + Storage + Metrics, K: Array, V: CodecShared>
153    Archive<T, E, K, V>
154{
155    /// Calculate the section for a given index.
156    const fn section(&self, index: u64) -> u64 {
157        (index / self.items_per_section) * self.items_per_section
158    }
159
160    /// Returns true when `index` is below the prune floor.
161    const fn pruned(&self, index: u64) -> bool {
162        match self.oldest_allowed {
163            Some(oldest_allowed) => index < oldest_allowed,
164            None => false,
165        }
166    }
167
168    /// Iterate over all positions for a given index (first + extras).
169    fn iter_positions(&self, index: u64) -> impl Iterator<Item = u64> + '_ {
170        self.indices.get(&index).into_iter().copied().chain(
171            self.extra_indices
172                .get(&index)
173                .into_iter()
174                .flat_map(|v| v.iter().copied()),
175        )
176    }
177
178    /// Initialize a new `Archive` instance.
179    ///
180    /// The in-memory index for `Archive` is populated during this call
181    /// by replaying only the index journal (no values are read).
182    pub async fn init(context: E, cfg: Config<T, V::Cfg>) -> Result<Self, Error> {
183        // Initialize oversized journal
184        let oversized_cfg = OversizedConfig {
185            index_partition: cfg.key_partition,
186            value_partition: cfg.value_partition,
187            index_page_cache: cfg.key_page_cache,
188            index_write_buffer: cfg.key_write_buffer,
189            value_write_buffer: cfg.value_write_buffer,
190            compression: cfg.compression,
191            codec_config: cfg.codec_config,
192        };
193        let mut oversized: Oversized<E, Record<K>, V> =
194            Oversized::init(context.child("oversized"), oversized_cfg).await?;
195
196        // Initialize keys and replay index journal (no values read!)
197        let mut indices: BTreeMap<u64, u64> = BTreeMap::new();
198        let mut extra_indices: BTreeMap<u64, Vec<u64>> = BTreeMap::new();
199        let mut keys = Index::new(context.child("index"), cfg.translator.clone());
200        let mut intervals = RMap::new();
201        {
202            debug!("initializing archive from index journal");
203            let stream = oversized.replay(0, 0, cfg.replay_buffer).await?;
204            pin_mut!(stream);
205            while let Some(result) = stream.next().await {
206                let (_section, position, entry) = result?;
207
208                // Store index location (position in index journal)
209                match indices.entry(entry.index) {
210                    btree_map::Entry::Vacant(e) => {
211                        e.insert(position);
212                    }
213                    btree_map::Entry::Occupied(_) => {
214                        extra_indices.entry(entry.index).or_default().push(position);
215                    }
216                }
217
218                // Store index in keys
219                keys.insert(&entry.key, entry.index);
220
221                // Store index in intervals
222                intervals.insert(entry.index);
223            }
224            debug!("archive initialized");
225        }
226
227        // Initialize metrics
228        let items_tracked = context.gauge("items_tracked", "Number of items tracked");
229        let indices_pruned = context.counter("indices_pruned", "Number of indices pruned");
230        let unnecessary_reads = context.counter(
231            "unnecessary_reads",
232            "Number of unnecessary reads performed during key lookups",
233        );
234        let gets = context.counter("gets", "Number of gets performed");
235        let has = context.counter("has", "Number of has performed");
236        let syncs = context.counter("syncs", "Number of syncs called");
237        let _ = items_tracked.try_set(indices.len());
238
239        // Return populated archive
240        Ok(Self {
241            items_per_section: cfg.items_per_section.get(),
242            oversized,
243            pending: BTreeSet::new(),
244            requested: BTreeSet::new(),
245            oldest_allowed: None,
246            indices,
247            extra_indices,
248            intervals,
249            keys,
250            items_tracked,
251            indices_pruned,
252            unnecessary_reads,
253            gets,
254            has,
255            syncs,
256        })
257    }
258
259    async fn get_index(&self, index: u64) -> Result<Option<V>, Error> {
260        // Update metrics
261        self.gets.inc();
262
263        // Get first position at this index
264        let position = match self.indices.get(&index) {
265            Some(&position) => position,
266            None => return Ok(None),
267        };
268
269        // Fetch index entry to get value location
270        let section = self.section(index);
271        let entry = self.oversized.get(section, position).await?;
272        let (value_offset, value_size) = entry.value_location();
273
274        // Fetch value directly from blob storage (bypasses page cache)
275        let value = self
276            .oversized
277            .get_value(section, value_offset, value_size)
278            .await?;
279        Ok(Some(value))
280    }
281
282    async fn get_key(&self, key: &K) -> Result<Option<V>, Error> {
283        // Update metrics
284        self.gets.inc();
285
286        // Fetch index
287        let iter = self.keys.get(key);
288        for index in iter {
289            // Continue if index is no longer allowed due to pruning.
290            if self.pruned(*index) {
291                continue;
292            }
293
294            // Get all positions at this index
295            if !self.indices.contains_key(index) {
296                return Err(Error::RecordCorrupted);
297            }
298            let section = self.section(*index);
299
300            for position in self.iter_positions(*index) {
301                // Fetch index entry from index journal to verify key
302                let entry = self.oversized.get(section, position).await?;
303
304                // Verify key matches
305                if entry.key.as_ref() == key.as_ref() {
306                    // Fetch value directly from blob storage (bypasses page cache)
307                    let (value_offset, value_size) = entry.value_location();
308                    let value = self
309                        .oversized
310                        .get_value(section, value_offset, value_size)
311                        .await?;
312                    return Ok(Some(value));
313                }
314                self.unnecessary_reads.inc();
315            }
316        }
317
318        Ok(None)
319    }
320
321    /// Check whether any retained index stores `key`.
322    ///
323    /// Confirms translated-key candidates against index journal entries,
324    /// never reading values.
325    async fn has_key(&self, key: &K) -> Result<bool, Error> {
326        for index in self.keys.get(key) {
327            // Continue if index is no longer allowed due to pruning.
328            if self.pruned(*index) {
329                continue;
330            }
331
332            // Get all positions at this index
333            if !self.indices.contains_key(index) {
334                return Err(Error::RecordCorrupted);
335            }
336            let section = self.section(*index);
337
338            for position in self.iter_positions(*index) {
339                // Fetch index entry from index journal to verify key
340                let entry = self.oversized.get(section, position).await?;
341                if entry.key.as_ref() == key.as_ref() {
342                    return Ok(true);
343                }
344                self.unnecessary_reads.inc();
345            }
346        }
347
348        Ok(false)
349    }
350
351    fn has_index(&self, index: u64) -> bool {
352        // Check if index exists
353        self.indices.contains_key(&index)
354    }
355
356    async fn put_internal(
357        &mut self,
358        index: u64,
359        key: K,
360        data: V,
361        skip_if_index_exists: bool,
362    ) -> Result<(), Error> {
363        // Check last pruned
364        let oldest_allowed = self.oldest_allowed.unwrap_or(0);
365        if index < oldest_allowed {
366            return Err(Error::AlreadyPrunedTo(oldest_allowed));
367        }
368
369        // Check for existing index when enforcing single-item semantics.
370        if skip_if_index_exists && self.indices.contains_key(&index) {
371            return Ok(());
372        }
373
374        // Write value and index entry atomically (glob first, then index)
375        let section = self.section(index);
376        let entry = Record::new(index, key.clone(), 0, 0);
377        let (position, _, _) = self.oversized.append(section, entry, &data).await?;
378
379        // Store index location
380        match self.indices.entry(index) {
381            btree_map::Entry::Vacant(e) => {
382                e.insert(position);
383            }
384            btree_map::Entry::Occupied(_) => {
385                self.extra_indices.entry(index).or_default().push(position);
386            }
387        }
388
389        // Store interval
390        self.intervals.insert(index);
391
392        // Insert and prune any useless keys
393        self.keys
394            .insert_and_retain(&key, index, |v| *v >= oldest_allowed);
395
396        // Add section to pending
397        self.pending.insert(section);
398
399        // Update metrics
400        let _ = self.items_tracked.try_set(self.indices.len());
401        Ok(())
402    }
403
404    /// Prune `Archive` to the provided `min` (masked by the configured
405    /// section mask).
406    ///
407    /// If this is called with a min lower than the last pruned, nothing
408    /// will happen.
409    pub async fn prune(&mut self, min: u64) -> Result<(), Error> {
410        // Update `min` to reflect section mask
411        let min = self.section(min);
412
413        // Check if min is less than last pruned
414        if let Some(oldest_allowed) = self.oldest_allowed {
415            if min <= oldest_allowed {
416                // We don't return an error in this case because the caller
417                // shouldn't be burdened with converting `min` to some section.
418                return Ok(());
419            }
420        }
421        debug!(min, "pruning archive");
422
423        // Prune oversized journal (handles both index and values)
424        self.oversized.prune(min).await?;
425
426        // Remove pending and requested sync work (no need to call `sync` as we are pruning)
427        self.pending = self.pending.split_off(&min);
428        self.requested = self.requested.split_off(&min);
429
430        // Remove all indices that are less than min
431        loop {
432            let next = match self.indices.first_key_value() {
433                Some((index, _)) if *index < min => *index,
434                _ => break,
435            };
436            self.indices.remove(&next).unwrap();
437            self.extra_indices.remove(&next);
438            self.indices_pruned.inc();
439        }
440
441        // Remove all keys from interval tree less than min
442        if min > 0 {
443            self.intervals.remove(0, min - 1);
444        }
445
446        // Update last pruned (to prevent reads from pruned sections)
447        self.oldest_allowed = Some(min);
448        let _ = self.items_tracked.try_set(self.indices.len());
449        Ok(())
450    }
451}
452
453impl<T: Translator, E: BufferPooler + Storage + Metrics, K: Array, V: CodecShared>
454    crate::archive::Archive for Archive<T, E, K, V>
455{
456    type Key = K;
457    type Value = V;
458
459    async fn put(&mut self, index: u64, key: K, data: V) -> Result<(), Error> {
460        self.put_internal(index, key, data, true).await
461    }
462
463    async fn get(&self, identifier: Identifier<'_, K>) -> Result<Option<V>, Error> {
464        match identifier {
465            Identifier::Index(index) => self.get_index(index).await,
466            Identifier::Key(key) => self.get_key(key).await,
467        }
468    }
469
470    async fn has(&self, identifier: Identifier<'_, K>) -> Result<bool, Error> {
471        self.has.inc();
472        match identifier {
473            Identifier::Index(index) => Ok(self.has_index(index)),
474            Identifier::Key(key) => self.has_key(key).await,
475        }
476    }
477
478    async fn sync(&mut self) -> Result<(), Error> {
479        // Update metrics (`requested` sections were already counted by `start_sync`)
480        self.syncs.inc_by(self.pending.len() as u64);
481        self.requested.append(&mut self.pending);
482
483        // Sync oversized journal (handles both index and values). Re-syncing `requested` sections
484        // also waits for any of their syncs still in flight.
485        self.oversized.sync(&self.requested).await?;
486
487        self.requested.clear();
488        Ok(())
489    }
490
491    async fn start_sync(&mut self) -> Result<Handle<()>, Error> {
492        // Update metrics
493        self.syncs.inc_by(self.pending.len() as u64);
494
495        // Move sections into `requested` rather than dropping them: section buffers reuse
496        // in-flight syncs, so re-requesting a section makes this handle observe outstanding work
497        // without issuing a new sync.
498        self.requested.append(&mut self.pending);
499        Ok(self.oversized.start_sync(&self.requested).await?)
500    }
501
502    fn next_gap(&self, index: u64) -> (Option<u64>, Option<u64>) {
503        self.intervals.next_gap(index)
504    }
505
506    fn missing_items(&self, index: u64, max: usize) -> Vec<u64> {
507        self.intervals.missing_items(index, max)
508    }
509
510    fn ranges(&self) -> impl Iterator<Item = (u64, u64)> {
511        self.intervals.iter().map(|(&s, &e)| (s, e))
512    }
513
514    fn ranges_from(&self, from: u64) -> impl Iterator<Item = (u64, u64)> {
515        self.intervals.iter_from(from).map(|(&s, &e)| (s, e))
516    }
517
518    fn first_index(&self) -> Option<u64> {
519        self.intervals.first_index()
520    }
521
522    fn last_index(&self) -> Option<u64> {
523        self.intervals.last_index()
524    }
525
526    #[boxed]
527    async fn destroy(self) -> Result<(), Error> {
528        Ok(self.oversized.destroy().await?)
529    }
530}
531
532impl<T: Translator, E: BufferPooler + Storage + Metrics, K: Array, V: CodecShared>
533    crate::archive::MultiArchive for Archive<T, E, K, V>
534{
535    async fn get_all(&self, index: u64) -> Result<Option<Vec<V>>, Error> {
536        // Update metrics
537        self.gets.inc();
538
539        // Check if the index exists.
540        if !self.indices.contains_key(&index) {
541            return Ok(None);
542        }
543
544        // Get all positions at this index
545        let section = self.section(index);
546        let extra_count = self.extra_indices.get(&index).map_or(0, Vec::len);
547
548        let mut values = Vec::with_capacity(1 + extra_count);
549        for position in self.iter_positions(index) {
550            // Fetch index entry from index journal to verify key
551            let entry = self.oversized.get(section, position).await?;
552
553            // Fetch value directly from blob storage (bypasses page cache)
554            let (value_offset, value_size) = entry.value_location();
555            let value = self
556                .oversized
557                .get_value(section, value_offset, value_size)
558                .await?;
559            values.push(value);
560        }
561        Ok(Some(values))
562    }
563
564    async fn put_multi(&mut self, index: u64, key: K, data: V) -> Result<(), Error> {
565        self.put_internal(index, key, data, false).await
566    }
567
568    async fn has_at(&self, index: u64, key: &K) -> Result<bool, Error> {
569        self.has.inc();
570
571        // Ignore pruned indices.
572        if self.pruned(index) {
573            return Ok(false);
574        }
575
576        // A key absent from the in-memory index is not stored anywhere, so
577        // absence is decided without touching disk. A translated-key hit may
578        // be a collision, so confirm against the stored keys at `index`
579        // (reads index journal entries, never values).
580        if !self.keys.get(key).any(|candidate| *candidate == index) {
581            return Ok(false);
582        }
583        let section = self.section(index);
584        for position in self.iter_positions(index) {
585            let entry = self.oversized.get(section, position).await?;
586            if entry.key.as_ref() == key.as_ref() {
587                return Ok(true);
588            }
589            self.unnecessary_reads.inc();
590        }
591        Ok(false)
592    }
593}
594
595#[cfg(all(test, feature = "arbitrary"))]
596mod conformance {
597    use super::*;
598    use commonware_codec::conformance::CodecConformance;
599    use commonware_utils::sequence::U64;
600
601    commonware_conformance::conformance_tests! {
602        CodecConformance<Record<U64>>
603    }
604}