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 pub fn new() -> Self {
9 Store::default()
10 }
11
12 /// Refresh the coarse cached clock (`Self::cached_ns`) from a single
13 /// `Instant::now()`. Call once per reactor-loop batch / reaper tick; the
14 /// per-access read path then skips its own clock read. Lazy expiry is
15 /// coarse to this cadence (a key expires ≤ one refresh-interval late,
16 /// never early — writes stamp deadlines from a fresh clock).
17 #[inline]
18 pub fn refresh_clock(&mut self) {
19 self.cached_ns = now_ns();
20 }
21
22 /// Enable/disable trusting the cached clock for lazy expiry (see
23 /// `Self::cached_ns`). Call with `true` only when something refreshes the
24 /// clock regularly (the server reactor per batch, the embedded background
25 /// reaper per tick); leave `false` for manual-reaper mode. Seeds the cache
26 /// when enabling so the first access is accurate.
27 #[inline]
28 pub fn set_cached_clock(&mut self, on: bool) {
29 self.cached_clock = on;
30 if on {
31 self.refresh_clock();
32 }
33 }
34
35 /// Install (or clear, with `maxmemory == 0`) the eviction limit and
36 /// policy. Cheap; safe to call repeatedly (e.g. on `CONFIG SET`).
37 #[inline]
38 pub fn set_max_memory(&mut self, maxmemory: u64, policy: EvictionPolicy) {
39 self.maxmemory = maxmemory;
40 self.eviction_policy = policy;
41 }
42
43 /// Live byte estimate (see field doc).
44 #[inline]
45 pub fn used_memory(&self) -> u64 {
46 self.used_memory
47 }
48
49 /// `used_memory` high-water mark since startup.
50 #[inline]
51 pub fn used_memory_peak(&self) -> u64 {
52 self.used_memory_peak
53 }
54
55 /// Configured `maxmemory` (0 = unlimited).
56 #[inline]
57 pub fn maxmemory(&self) -> u64 {
58 self.maxmemory
59 }
60
61 /// Configured eviction policy.
62 #[inline]
63 pub fn eviction_policy(&self) -> EvictionPolicy {
64 self.eviction_policy
65 }
66
67 /// Total keys evicted since startup.
68 #[inline]
69 pub fn evictions_total(&self) -> u64 {
70 self.evictions_total
71 }
72
73 /// Live keys carrying a TTL (`INFO keyspace`'s `expires=`). O(1) — reads
74 /// the maintained counter, not an O(n) scan (cf. [`Self::ttl_pending_count`]).
75 #[inline]
76 pub fn expires_count(&self) -> usize {
77 self.expires as usize
78 }
79
80 /// Apply a signed delta to the [`Self::expires`] counter, clamped at 0.
81 /// Centralises the saturating arithmetic for every TTL-transition site.
82 #[inline]
83 pub(crate) fn adjust_expires(&mut self, delta: i64) {
84 if delta != 0 {
85 self.expires = (self.expires as i64 + delta).max(0) as u64;
86 }
87 }
88
89 /// `WATCH` — record this key in the version tracker and return its
90 /// current version. Subsequent writes on this shard bump the version
91 /// via [`Self::bump_if_watched`]. Caller (the conn's origin shard)
92 /// stores the returned version; `EXEC` later asks every owning shard
93 /// "is the version still N?" via [`Self::key_version`].
94 ///
95 /// Keys that have never been written stay at version 0 — the first
96 /// write after a `WATCH` bumps to 1, which is what makes the "dirty"
97 /// comparison work (stored 0 ≠ current 1 ⇒ abort EXEC).
98 pub fn record_watch(&mut self, key: &[u8]) -> u64 {
99 #[cfg(feature = "std")]
100 {
101 *self
102 .watch_versions
103 .entry(key.to_vec())
104 .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
187}