Skip to main content

kevy_embedded/
ops_snapshot_view.rs

1//! Public point-in-time snapshot view.
2//!
3//! [`Store::snapshot`] freezes a consistent COW view of the WHOLE
4//! keyspace: all shard write locks are taken in shard order (the same
5//! deterministic-order discipline as `atomic_all_shards`, so the two
6//! can't deadlock against each other), every shard's O(n)-shallow
7//! view is collected inside that single window, then the locks drop.
8//! Writers block only for the collection (~8 ns/entry), not for the
9//! caller's subsequent iteration.
10//!
11//! The primary consumer: rebuilding derived state after a
12//! `FeedError::Resync` — freeze a view, note `changes_tail()`, scan
13//! your prefix from the view, resume the feed from the noted cursor.
14
15use kevy_store::SnapshotView;
16
17use crate::store::{Store, lock_write};
18
19/// A frozen, consistent point-in-time view of the whole store.
20///
21/// ```
22/// let s = kevy_embedded::Store::open(kevy_embedded::Config::default())?;
23/// s.set(b"u:1", b"a")?;
24/// let frozen = s.snapshot();
25/// s.set(b"u:2", b"b")?; // lands after the freeze
26/// assert_eq!(frozen.keys_prefix(b"u:").len(), 1);
27/// assert_eq!(s.snapshot().keys_prefix(b"u:").len(), 2);
28/// # Ok::<(), kevy_embedded::KevyError>(())
29/// ```
30#[derive(Debug)]
31pub struct Snapshot {
32    views: Vec<SnapshotView>,
33}
34
35/// One entry from [`Snapshot::each_prefix`] / [`Snapshot::keys_prefix`].
36///
37/// ```
38/// let s = kevy_embedded::Store::open(kevy_embedded::Config::default())?;
39/// s.set(b"u:1", b"a")?;
40/// let entries = s.snapshot().keys_prefix(b"u:");
41/// assert_eq!((entries[0].key.as_slice(), entries[0].ttl_ms), (&b"u:1"[..], None));
42/// # Ok::<(), kevy_embedded::KevyError>(())
43/// ```
44#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
45#[non_exhaustive]
46pub struct SnapshotEntry {
47    /// The key.
48    ///
49    /// ```
50    /// let s = kevy_embedded::Store::open(kevy_embedded::Config::default())?;
51    /// s.set(b"u:1", b"a")?;
52    /// s.set(b"other", b"b")?;
53    /// let keys: Vec<Vec<u8>> = s.snapshot().keys_prefix(b"u:").into_iter().map(|e| e.key).collect();
54    /// assert_eq!(keys, [b"u:1".to_vec()]);
55    /// # Ok::<(), kevy_embedded::KevyError>(())
56    /// ```
57    pub key: Vec<u8>,
58    /// Remaining TTL in ms at freeze time (`None` = no expiry).
59    ///
60    /// ```
61    /// let s = kevy_embedded::Store::open(kevy_embedded::Config::default())?;
62    /// s.set_with_ttl(b"u:1", b"a", std::time::Duration::from_secs(60))?;
63    /// let ttl = s.snapshot().keys_prefix(b"u:")[0].ttl_ms;
64    /// assert!(ttl.is_some_and(|ms| ms > 50_000 && ms <= 60_000));
65    /// # Ok::<(), kevy_embedded::KevyError>(())
66    /// ```
67    pub ttl_ms: Option<u64>,
68}
69
70impl Snapshot {
71    /// Visit every entry under `prefix` as `(key, value, ttl_ms)` —
72    /// the raw [`kevy_store::Value`] borrow, for callers that want the
73    /// typed payload without copies.
74    pub fn each_prefix<F: FnMut(&[u8], &kevy_store::Value, Option<u64>)>(
75        &self,
76        prefix: &[u8],
77        mut f: F,
78    ) {
79        for view in &self.views {
80            view.each(|k, v, ttl| {
81                if k.starts_with(prefix) {
82                    f(k, v, ttl);
83                }
84            });
85        }
86    }
87
88    /// Collect the keys (+ TTLs) under `prefix`, unordered across
89    /// shards.
90    pub fn keys_prefix(&self, prefix: &[u8]) -> Vec<SnapshotEntry> {
91        let mut out = Vec::new();
92        self.each_prefix(prefix, |k, _, ttl| {
93            out.push(SnapshotEntry { key: k.to_vec(), ttl_ms: ttl });
94        });
95        out
96    }
97
98    /// Total entries in the view.
99    pub fn len(&self) -> usize {
100        let mut n = 0;
101        for view in &self.views {
102            view.each(|_, _, _| n += 1);
103        }
104        n
105    }
106
107    /// Whether the view is empty.
108    pub fn is_empty(&self) -> bool {
109        self.len() == 0
110    }
111}
112
113impl Store {
114    /// Freeze a consistent point-in-time [`Snapshot`] of the whole
115    /// keyspace (see the module doc for the locking discipline).
116    pub fn snapshot(&self) -> Snapshot {
117        // Deterministic shard order (same as atomic_all_shards) — hold
118        // ALL write locks across the collection so no write lands
119        // between shard freezes.
120        let guards: Vec<_> = self.shards.iter().map(|s| lock_write(s)).collect();
121        let views = guards.iter().map(|g| g.store.collect_snapshot()).collect();
122        drop(guards);
123        Snapshot { views }
124    }
125}