Skip to main content

kevy_store/
snapshot.rs

1//! Point-in-time snapshot views — the freeze half of COW serialization.
2//!
3//! [`Store::collect_snapshot`] walks the keyspace once and shallow-clones
4//! every live entry: keys and string values copy their bytes (≤22 B inline
5//! = a 24 B memcpy), collection values bump an `Arc` refcount. The pause is
6//! O(n) at nanoseconds per entry — independent of collection sizes and of
7//! disk speed. The returned [`SnapshotView`] is `Send`: hand it to a
8//! background thread and serialize at leisure while the store keeps
9//! mutating (writes copy-on-write via `Arc::make_mut`, deletions just drop
10//! one strong ref — the view's data stays alive until it is dropped).
11//!
12//! TTLs are resolved to remaining-milliseconds at collect time, so the view
13//! is a consistent instant: an entry that expires *after* the collect still
14//! appears with the remaining TTL it had at that instant.
15
16use crate::value::Value;
17use crate::{SmallBytes, Store, now_ns, remaining_ms};
18
19/// A frozen, `Send` view of one store's live entries at a single instant.
20pub struct SnapshotView {
21    entries: Vec<(SmallBytes, Value, Option<u64>)>,
22    /// v2.4: hash field TTLs frozen with the view.
23    hfttl: Vec<(SmallBytes, SmallBytes, u64)>,
24}
25
26// Compile-time guarantee that a view can cross to a serializer thread.
27const _: () = {
28    const fn assert_send<T: Send>() {}
29    assert_send::<SnapshotView>();
30};
31
32impl SnapshotView {
33    /// Visit every entry as `(key, &value, ttl_ms)` — the same shape as
34    /// [`Store::snapshot_each`], so serializers take either source.
35    pub fn each<F: FnMut(&[u8], &Value, Option<u64>)>(&self, mut f: F) {
36        for (k, v, ttl) in &self.entries {
37            f(k.as_slice(), v, *ttl);
38        }
39    }
40
41    /// v2.4: visit the frozen hash field TTLs.
42    pub fn each_hash_ttl<F: FnMut(&[u8], &[u8], u64)>(&self, mut f: F) {
43        for (k, field, d) in &self.hfttl {
44            f(k.as_slice(), field.as_slice(), *d);
45        }
46    }
47
48    /// Number of entries frozen in the view.
49    pub fn len(&self) -> usize {
50        self.entries.len()
51    }
52
53    /// Whether the view holds zero entries.
54    pub fn is_empty(&self) -> bool {
55        self.entries.is_empty()
56    }
57}
58
59impl Store {
60    /// Freeze a point-in-time [`SnapshotView`] of every live entry.
61    ///
62    /// O(n) shallow: per entry one key clone + one [`Value`] clone (string
63    /// bytes copied, collections refcount-bumped) + the TTL resolved to
64    /// remaining millis. Expired-but-unreaped entries are skipped, matching
65    /// [`Store::snapshot_each`].
66    pub fn collect_snapshot(&self) -> SnapshotView {
67        let now = now_ns();
68        let mut entries = Vec::with_capacity(self.map.len());
69        for (k, e) in &self.map {
70            if e.is_expired_at(now) {
71                continue;
72            }
73            let ttl = e.expire_at_ns.map(|ns| remaining_ms(ns, now));
74            entries.push((k.clone(), e.value.clone(), ttl));
75        }
76        let mut hfttl = Vec::new();
77        self.hash_ttl_each(|k, f, d| {
78            hfttl.push((
79                crate::SmallBytes::from_slice(k),
80                crate::SmallBytes::from_slice(f),
81                d,
82            ));
83        });
84        SnapshotView { entries, hfttl }
85    }
86}