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}