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#[derive(Debug)]
21pub struct Snapshot {
22 views: Vec<SnapshotView>,
23}
24
25/// One entry from [`Snapshot::each_prefix`] / [`Snapshot::keys_prefix`].
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct SnapshotEntry {
28 /// The key.
29 pub key: Vec<u8>,
30 /// Remaining TTL in ms at freeze time (`None` = no expiry).
31 pub ttl_ms: Option<u64>,
32}
33
34impl Snapshot {
35 /// Visit every entry under `prefix` as `(key, value, ttl_ms)` —
36 /// the raw [`kevy_store::Value`] borrow, for callers that want the
37 /// typed payload without copies.
38 pub fn each_prefix<F: FnMut(&[u8], &kevy_store::Value, Option<u64>)>(
39 &self,
40 prefix: &[u8],
41 mut f: F,
42 ) {
43 for view in &self.views {
44 view.each(|k, v, ttl| {
45 if k.starts_with(prefix) {
46 f(k, v, ttl);
47 }
48 });
49 }
50 }
51
52 /// Collect the keys (+ TTLs) under `prefix`, unordered across
53 /// shards.
54 pub fn keys_prefix(&self, prefix: &[u8]) -> Vec<SnapshotEntry> {
55 let mut out = Vec::new();
56 self.each_prefix(prefix, |k, _, ttl| {
57 out.push(SnapshotEntry { key: k.to_vec(), ttl_ms: ttl });
58 });
59 out
60 }
61
62 /// Total entries in the view.
63 pub fn len(&self) -> usize {
64 let mut n = 0;
65 for view in &self.views {
66 view.each(|_, _, _| n += 1);
67 }
68 n
69 }
70
71 /// Whether the view is empty.
72 pub fn is_empty(&self) -> bool {
73 self.len() == 0
74 }
75}
76
77impl Store {
78 /// Freeze a consistent point-in-time [`Snapshot`] of the whole
79 /// keyspace (see the module doc for the locking discipline).
80 pub fn snapshot(&self) -> Snapshot {
81 // Deterministic shard order (same as atomic_all_shards) — hold
82 // ALL write locks across the collection so no write lands
83 // between shard freezes.
84 let guards: Vec<_> = self.shards.iter().map(|s| lock_write(s)).collect();
85 let views = guards.iter().map(|g| g.store.collect_snapshot()).collect();
86 drop(guards);
87 Snapshot { views }
88 }
89}