Skip to main content

commonware_storage/cache/
storage.rs

1use super::Config;
2use crate::{
3    journal::{
4        Error,
5        segmented::variable::{Config as JConfig, Journal},
6    },
7    rmap::RMap,
8};
9use commonware_codec::{CodecShared, EncodeSize, Read, ReadExt, Write, varint::UInt};
10use commonware_runtime::{
11    Buf, BufMut, Metrics, ReadOptions, Storage,
12    telemetry::metrics::{Counter, Gauge, GaugeExt, MetricsExt as _},
13};
14use std::collections::{BTreeMap, BTreeSet};
15use tracing::debug;
16
17/// Record stored in the `Cache`.
18struct Record<V: CodecShared> {
19    index: u64,
20    value: V,
21}
22
23impl<V: CodecShared> Record<V> {
24    /// Create a new `Record`.
25    const fn new(index: u64, value: V) -> Self {
26        Self { index, value }
27    }
28}
29
30impl<V: CodecShared> Write for Record<V> {
31    fn write(&self, buf: &mut impl BufMut) {
32        UInt(self.index).write(buf);
33        self.value.write(buf);
34    }
35}
36
37impl<V: CodecShared> Read for Record<V> {
38    type Cfg = V::Cfg;
39
40    fn read_cfg(buf: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
41        let index = UInt::read(buf)?.into();
42        let value = V::read_cfg(buf, cfg)?;
43        Ok(Self { index, value })
44    }
45}
46
47impl<V: CodecShared> EncodeSize for Record<V> {
48    fn encode_size(&self) -> usize {
49        UInt(self.index).encode_size() + self.value.encode_size()
50    }
51}
52
53#[cfg(feature = "arbitrary")]
54impl<V: CodecShared> arbitrary::Arbitrary<'_> for Record<V>
55where
56    V: for<'a> arbitrary::Arbitrary<'a>,
57{
58    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
59        Ok(Self::new(u.arbitrary()?, u.arbitrary()?))
60    }
61}
62
63/// The cache's state, boxed so the public [Cache] handle stays pointer-sized.
64struct Inner<E: Storage + Metrics, V: CodecShared> {
65    items_per_blob: u64,
66    journal: Journal<E, Record<V>>,
67    pending: BTreeSet<u64>,
68
69    // Oldest allowed section to read from. This is updated when `prune` is called.
70    oldest_allowed: Option<u64>,
71    indices: BTreeMap<u64, u64>,
72    intervals: RMap,
73
74    items_tracked: Gauge,
75    gets: Counter,
76    has: Counter,
77    syncs: Counter,
78}
79
80impl<E: Storage + Metrics, V: CodecShared> Inner<E, V> {
81    /// Calculate the section for a given index.
82    const fn section(&self, index: u64) -> u64 {
83        (index / self.items_per_blob) * self.items_per_blob
84    }
85
86    /// See [Cache::init].
87    async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
88        // Initialize journal
89        let journal = Journal::<E, Record<V>>::init(
90            context.child("journal"),
91            JConfig {
92                partition: cfg.partition,
93                compression: cfg.compression,
94                codec_config: cfg.codec_config,
95                page_cache: cfg.page_cache,
96                write_buffer: cfg.write_buffer,
97            },
98        )
99        .await?;
100
101        // Initialize keys and run corruption check
102        let mut indices = BTreeMap::new();
103        let mut intervals = RMap::new();
104        let journal = {
105            debug!("initializing cache");
106            let mut replay = journal
107                .replay(0, 0, cfg.replay_buffer, ReadOptions::default())
108                .await?;
109            while let Some(result) = replay.next().await {
110                // Extract key from record
111                let (_, offset, _, data) = result?;
112
113                // Store index
114                indices.insert(data.index, offset);
115
116                // Store index in intervals
117                intervals.insert(data.index);
118            }
119            debug!(items = indices.len(), "cache initialized");
120            replay.finish()?
121        };
122
123        // Initialize metrics
124        let items_tracked = context.gauge("items_tracked", "Number of items tracked");
125        let gets = context.counter("gets", "Number of gets performed");
126        let has = context.counter("has", "Number of has performed");
127        let syncs = context.counter("syncs", "Number of syncs called");
128        let _ = items_tracked.try_set(indices.len());
129
130        // Return populated cache
131        Ok(Self {
132            items_per_blob: cfg.items_per_blob.get(),
133            journal,
134            pending: BTreeSet::new(),
135            oldest_allowed: None,
136            indices,
137            intervals,
138            items_tracked,
139            gets,
140            has,
141            syncs,
142        })
143    }
144
145    /// See [Cache::get].
146    async fn get(&self, index: u64) -> Result<Option<V>, Error> {
147        // Update metrics
148        self.gets.inc();
149
150        // Get index location
151        let offset = match self.indices.get(&index) {
152            Some(offset) => *offset,
153            None => return Ok(None),
154        };
155
156        // Fetch item from disk
157        let section = self.section(index);
158        let record = self.journal.get(section, offset).await?;
159        Ok(Some(record.value))
160    }
161
162    /// See [Cache::next_gap].
163    fn next_gap(&self, index: u64) -> (Option<u64>, Option<u64>) {
164        self.intervals.next_gap(index)
165    }
166
167    /// See [Cache::first].
168    fn first(&self) -> Option<u64> {
169        self.intervals.iter().next().map(|(&start, _)| start)
170    }
171
172    /// See [Cache::missing_items].
173    fn missing_items(&self, start: u64, max: usize) -> Vec<u64> {
174        self.intervals.missing_items(start, max)
175    }
176
177    /// See [Cache::has].
178    fn has(&self, index: u64) -> bool {
179        // Update metrics
180        self.has.inc();
181
182        // Check if index exists
183        self.indices.contains_key(&index)
184    }
185
186    /// See [Cache::prune].
187    async fn prune(mut self: Box<Self>, min: u64) -> Result<Box<Self>, Error> {
188        // Update `min` to reflect section mask
189        let min = self.section(min);
190
191        // Check if min is less than last pruned
192        if let Some(oldest_allowed) = self.oldest_allowed
193            && min <= oldest_allowed
194        {
195            // We don't return an error in this case because the caller
196            // shouldn't be burdened with converting `min` to some section.
197            return Ok(self);
198        }
199        debug!(min, "pruning cache");
200
201        // Prune journal
202        (self.journal, _) = self.journal.prune(min).await?;
203
204        // Remove pending writes (no need to call `sync` as we are pruning)
205        loop {
206            let next = match self.pending.iter().next() {
207                Some(section) if *section < min => *section,
208                _ => break,
209            };
210            self.pending.remove(&next);
211        }
212
213        // Remove all indices that are less than min
214        loop {
215            let next = match self.indices.first_key_value() {
216                Some((index, _)) if *index < min => *index,
217                _ => break,
218            };
219            self.indices.remove(&next).unwrap();
220        }
221
222        // Remove all intervals that are less than min
223        if min > 0 {
224            self.intervals.remove(0, min - 1);
225        }
226
227        // Update last pruned (to prevent reads from
228        // pruned sections)
229        self.oldest_allowed = Some(min);
230        let _ = self.items_tracked.try_set(self.indices.len());
231        Ok(self)
232    }
233
234    /// See [Cache::put].
235    async fn put(mut self: Box<Self>, index: u64, value: V) -> Result<(Box<Self>, bool), Error> {
236        // A put below the prune floor is satisfied without storing
237        let oldest_allowed = self.oldest_allowed.unwrap_or(0);
238        if index < oldest_allowed {
239            debug!(index, oldest_allowed, "ignoring put below prune floor");
240            return Ok((self, false));
241        }
242
243        // Check for existing index
244        if self.indices.contains_key(&index) {
245            return Ok((self, true));
246        }
247
248        // Store item in journal
249        let record = Record::new(index, value);
250        let section = self.section(index);
251        let offset;
252        (self.journal, offset, _) = self.journal.append(section, &record).await?;
253
254        // Store index
255        self.indices.insert(index, offset);
256
257        // Add index to intervals
258        self.intervals.insert(index);
259
260        // Add section to pending
261        self.pending.insert(section);
262
263        // Update metrics
264        let _ = self.items_tracked.try_set(self.indices.len());
265        Ok((self, true))
266    }
267
268    /// See [Cache::sync].
269    async fn sync(mut self: Box<Self>) -> Result<Box<Self>, Error> {
270        self.syncs.inc_by(self.pending.len() as u64);
271        self.journal = self.journal.sync(&self.pending).await?;
272        self.pending.clear();
273        Ok(self)
274    }
275
276    /// See [Cache::destroy].
277    async fn destroy(self) -> Result<(), Error> {
278        self.journal.destroy().await
279    }
280}
281
282/// Implementation of `Cache` storage.
283///
284/// Mutating functions consume the cache and return it only on success: an error (or a dropped
285/// future) destroys the handle.
286pub struct Cache<E: Storage + Metrics, V: CodecShared>(Box<Inner<E, V>>);
287
288impl<E: Storage + Metrics, V: CodecShared> std::fmt::Debug for Cache<E, V> {
289    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290        f.debug_struct("Cache")
291            .field("first_index", &self.0.intervals.first_index())
292            .field("last_index", &self.0.intervals.last_index())
293            .finish_non_exhaustive()
294    }
295}
296
297impl<E: Storage + Metrics, V: CodecShared> Cache<E, V> {
298    /// Initialize a new `Cache` instance.
299    ///
300    /// The in-memory index for `Cache` is populated during this call
301    /// by replaying the journal.
302    pub async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
303        Ok(Self(Box::new(Inner::init(context, cfg).await?)))
304    }
305
306    /// Retrieve an item from the [Cache].
307    pub async fn get(&self, index: u64) -> Result<Option<V>, Error> {
308        self.0.get(index).await
309    }
310
311    /// Retrieve the next gap in the [Cache].
312    pub fn next_gap(&self, index: u64) -> (Option<u64>, Option<u64>) {
313        self.0.next_gap(index)
314    }
315
316    /// Returns the first index in the [Cache].
317    pub fn first(&self) -> Option<u64> {
318        self.0.first()
319    }
320
321    /// Returns up to `max` missing items starting from `start`.
322    ///
323    /// This method iterates through gaps between existing ranges, collecting missing indices
324    /// until either `max` items are found or there are no more gaps to fill.
325    pub fn missing_items(&self, start: u64, max: usize) -> Vec<u64> {
326        self.0.missing_items(start, max)
327    }
328
329    /// Check if an item exists in the [Cache].
330    pub fn has(&self, index: u64) -> bool {
331        self.0.has(index)
332    }
333
334    /// Prune [Cache] to the provided `min`.
335    ///
336    /// If this is called with a min lower than the last pruned, nothing
337    /// will happen.
338    pub async fn prune(mut self, min: u64) -> Result<Self, Error> {
339        self.0 = self.0.prune(min).await?;
340        Ok(self)
341    }
342
343    /// Store an item in the [Cache].
344    ///
345    /// If the index already exists, put does nothing and returns. A put below the prune
346    /// floor is satisfied without storing: pruning declared that range obsolete, so nothing
347    /// is mutated and nothing below the floor is ever readable.
348    pub async fn put(mut self, index: u64, value: V) -> Result<Self, Error> {
349        (self.0, _) = self.0.put(index, value).await?;
350        Ok(self)
351    }
352
353    /// Sync all pending writes.
354    pub async fn sync(mut self) -> Result<Self, Error> {
355        self.0 = self.0.sync().await?;
356        Ok(self)
357    }
358
359    /// Stores an item in the [Cache] and syncs it, plus any other pending writes, to disk.
360    ///
361    /// If the index already exists, the cache is just synced. A put satisfied below the
362    /// prune floor stored nothing, so it skips the sync.
363    pub async fn put_sync(mut self, index: u64, value: V) -> Result<Self, Error> {
364        let stored;
365        (self.0, stored) = self.0.put(index, value).await?;
366        if !stored {
367            return Ok(self);
368        }
369        self.sync().await
370    }
371
372    /// Remove all persistent data created by this [Cache].
373    pub async fn destroy(self) -> Result<(), Error> {
374        self.0.destroy().await
375    }
376}
377
378#[cfg(all(test, feature = "arbitrary"))]
379mod conformance {
380    use super::*;
381    use commonware_codec::conformance::CodecConformance;
382
383    commonware_conformance::conformance_tests! {
384        CodecConformance<Record<u64>>,
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391    use commonware_runtime::deterministic::Context;
392
393    type TestCache = Cache<Context, u64>;
394
395    fn is_send<T: Send>(_: T) {}
396
397    #[allow(dead_code)]
398    fn assert_cache_futures_are_send(cache: &TestCache, key: &u64) {
399        is_send(cache.get(*key));
400    }
401
402    #[allow(dead_code)]
403    fn assert_cache_destroy_is_send(cache: TestCache) {
404        is_send(cache.destroy());
405    }
406}