Skip to main content

kevy_store/
store_admin.rs

1//! Store administration: the coarse cached clock, memory accounting
2//! and eviction entrypoints, and the WATCH version ledger. Split from
3//! `lib.rs` to keep that file under the 500-LOC house rule.
4
5use crate::{ENTRY_OVERHEAD, EvictionPolicy, Store, StoreError, evict, now_ns};
6
7impl Store {
8    /// An empty store with default settings: no maxmemory bound, no
9    /// tiering budget, and no persistence attached — the caller wires
10    /// those on afterwards.
11    pub fn new() -> Self {
12        Store::default()
13    }
14
15    /// Refresh the coarse cached clock (`Self::cached_ns`) from a single
16    /// `Instant::now()`. Call once per reactor-loop batch / reaper tick; the
17    /// per-access read path then skips its own clock read. Lazy expiry is
18    /// coarse to this cadence (a key expires ≤ one refresh-interval late,
19    /// never early — writes stamp deadlines from a fresh clock).
20    #[inline]
21    pub fn refresh_clock(&mut self) {
22        self.cached_ns = now_ns();
23    }
24
25    /// Enable/disable trusting the cached clock for lazy expiry (see
26    /// `Self::cached_ns`). Call with `true` only when something refreshes the
27    /// clock regularly (the server reactor per batch, the embedded background
28    /// reaper per tick); leave `false` for manual-reaper mode. Seeds the cache
29    /// when enabling so the first access is accurate.
30    #[inline]
31    pub fn set_cached_clock(&mut self, on: bool) {
32        self.cached_clock = on;
33        if on {
34            self.refresh_clock();
35        }
36    }
37
38    /// Install (or clear, with `maxmemory == 0`) the eviction limit and
39    /// policy. Cheap; safe to call repeatedly (e.g. on `CONFIG SET`).
40    #[inline]
41    pub fn set_max_memory(&mut self, maxmemory: u64, policy: EvictionPolicy) {
42        self.maxmemory = maxmemory;
43        self.eviction_policy = policy;
44    }
45
46    /// Live byte estimate (see field doc).
47    #[inline]
48    pub fn used_memory(&self) -> u64 {
49        self.used_memory
50    }
51
52    /// `used_memory` high-water mark since startup.
53    #[inline]
54    pub fn used_memory_peak(&self) -> u64 {
55        self.used_memory_peak
56    }
57
58    /// Configured `maxmemory` (0 = unlimited).
59    #[inline]
60    pub fn maxmemory(&self) -> u64 {
61        self.maxmemory
62    }
63
64    /// Configured eviction policy.
65    #[inline]
66    pub fn eviction_policy(&self) -> EvictionPolicy {
67        self.eviction_policy
68    }
69
70    /// Total keys evicted since startup.
71    #[inline]
72    pub fn evictions_total(&self) -> u64 {
73        self.evictions_total
74    }
75
76    /// Live keys carrying a TTL (`INFO keyspace`'s `expires=`). O(1) — reads
77    /// the maintained counter, not an O(n) scan (cf. [`Self::ttl_pending_count`]).
78    #[inline]
79    pub fn expires_count(&self) -> usize {
80        self.expires as usize
81    }
82
83    /// Apply a signed delta to the [`Self::expires`] counter, clamped at 0.
84    /// Centralises the saturating arithmetic for every TTL-transition site.
85    #[inline]
86    pub(crate) fn adjust_expires(&mut self, delta: i64) {
87        if delta != 0 {
88            self.expires = (self.expires as i64 + delta).max(0) as u64;
89        }
90    }
91
92    /// `WATCH` — record this key in the version tracker and return its
93    /// current version. Subsequent writes on this shard bump the version
94    /// via [`Self::bump_if_watched`]. Caller (the conn's origin shard)
95    /// stores the returned version; `EXEC` later asks every owning shard
96    /// "is the version still N?" via [`Self::key_version`].
97    ///
98    /// Keys that have never been written stay at version 0 — the first
99    /// write after a `WATCH` bumps to 1, which is what makes the "dirty"
100    /// comparison work (stored 0 ≠ current 1 ⇒ abort EXEC).
101    pub fn record_watch(&mut self, key: &[u8]) -> u64 {
102        #[cfg(feature = "std")]
103        {
104            *self.watch_versions.entry(key.to_vec()).or_insert(0)
105        }
106        #[cfg(not(feature = "std"))]
107        {
108            // KevyMap has no entry API — insert-if-absent, then read.
109            if self.watch_versions.get(key).is_none() {
110                self.watch_versions.insert(key.to_vec(), 0);
111            }
112            self.watch_versions.get(key).copied().unwrap_or(0)
113        }
114    }
115
116    /// Read-only version lookup used by `EXEC`'s pre-execution check.
117    /// Returns `0` for keys never `WATCH`-ed (matches the initial value
118    /// `record_watch` would have inserted, so a `WATCH` → no-write →
119    /// `EXEC` sequence sees the stored 0 == current 0 and proceeds).
120    #[inline]
121    pub fn key_version(&self, key: &[u8]) -> u64 {
122        self.watch_versions.get(key).copied().unwrap_or(0)
123    }
124
125    /// Bump the version of `key` if (and only if) it has been `WATCH`-ed at
126    /// least once. Write-side call after every mutation. The empty check
127    /// runs BEFORE the key is hashed — the common nothing-watched case
128    /// pays one branch, not a guaranteed-miss probe.
129    #[inline]
130    pub fn bump_if_watched(&mut self, key: &[u8]) {
131        if self.watch_versions.is_empty() {
132            return;
133        }
134        if let Some(v) = self.watch_versions.get_mut(key) {
135            *v = v.wrapping_add(1);
136        }
137    }
138
139    /// Invalidate every watched key in one shot. Called from `FLUSHDB`
140    /// / `FLUSHALL` execution paths — every WATCH against this shard
141    /// must invalidate so a pending `EXEC` aborts.
142    pub fn bump_all_watched(&mut self) {
143        #[cfg(feature = "std")]
144        for v in self.watch_versions.values_mut() {
145            *v = v.wrapping_add(1);
146        }
147        #[cfg(not(feature = "std"))]
148        for (_, v) in self.watch_versions.iter_mut() {
149            *v = v.wrapping_add(1);
150        }
151    }
152
153    /// Cached weight of `key` (dynamic part + [`ENTRY_OVERHEAD`]). Returns
154    /// `None` when the key is absent or expired (no implicit reap).
155    pub fn estimate_key_bytes(&self, key: &[u8]) -> Option<u64> {
156        self.map.get(key).map(|e| e.weight() + ENTRY_OVERHEAD)
157    }
158
159    /// O(1) precondition check the dispatch layer calls before every write
160    /// command. Returns `Err(OutOfMemory)` only when `maxmemory > 0`, the
161    /// budget is already over, AND the policy is `NoEviction` (Redis
162    /// behaviour). All other policies let the write proceed and recover via
163    /// [`Self::try_evict_after_write`].
164    #[inline]
165    pub fn precheck_for_write(&self) -> Result<(), StoreError> {
166        if self.maxmemory == 0 || self.used_memory <= self.maxmemory {
167            return Ok(());
168        }
169        if self.eviction_policy == EvictionPolicy::NoEviction {
170            return Err(StoreError::OutOfMemory);
171        }
172        Ok(())
173    }
174
175    /// Run after every write command. No-op when disabled or under budget;
176    /// otherwise samples per [`Self::eviction_policy`] and removes keys until
177    /// back under `maxmemory` or no eligible candidate remains. Returns the
178    /// number of keys evicted (0 on the common fast path).
179    #[inline]
180    pub fn try_evict_after_write(&mut self) -> usize {
181        if self.maxmemory == 0 || self.used_memory <= self.maxmemory {
182            return 0;
183        }
184        evict::evict_until_under_limit(self)
185    }
186}