kevy_store/accounting.rs
1//! Internal accounting helpers on [`Store`]: per-entry weight bookkeeping,
2//! LRU/LFU clock advance, prefetch, and the lazy-expire `live_entry` /
3//! `live_entry_mut` lookups used by every typed accessor.
4//!
5//! Split out of [`crate`] for file-size hygiene. Nothing here is part of
6//! the public surface — all methods are `pub(crate)` and called by sibling
7//! modules (string/hash/list/set/zset/evict/expire/keyspace).
8
9use std::time::Instant;
10
11use kevy_hash::KevyHash;
12
13use crate::value::ENTRY_OVERHEAD;
14use crate::{Entry, SmallBytes, Store, apply_delta, evict, key_heap_bytes_for};
15
16impl Store {
17 /// Insert a fresh entry, replacing any prior. Stamps `entry.weight` from
18 /// the live value and key, then updates `used_memory` for either the
19 /// new-key (charges [`ENTRY_OVERHEAD`]) or overwrite (weight swap) case.
20 pub(crate) fn insert_entry(&mut self, key: SmallBytes, mut entry: Entry) -> Option<Entry> {
21 entry.set_weight(key.heap_bytes() as u64 + entry.value.weight());
22 if self.maxmemory > 0 {
23 self.tick_clock();
24 entry.set_lru_clock(self.clock_counter as u32);
25 }
26 let new_w = entry.weight();
27 let prev = self.map.insert(key, entry);
28 match &prev {
29 Some(old) => {
30 self.used_memory = self
31 .used_memory
32 .saturating_sub(old.weight())
33 .saturating_add(new_w);
34 }
35 None => {
36 self.used_memory = self.used_memory.saturating_add(new_w + ENTRY_OVERHEAD);
37 }
38 }
39 self.update_peak();
40 prev
41 }
42
43 /// Remove a key, returning the displaced entry (`None` if absent).
44 /// Frees the entry's cached weight + [`ENTRY_OVERHEAD`].
45 pub(crate) fn remove_entry(&mut self, key: &[u8]) -> Option<Entry> {
46 let old = self.map.remove(key)?;
47 self.used_memory = self
48 .used_memory
49 .saturating_sub(old.weight() + ENTRY_OVERHEAD);
50 Some(old)
51 }
52
53 /// Apply a signed weight delta to `key`'s cached `Entry::weight` AND to
54 /// the shard-wide `used_memory`. Used by in-place collection mutators
55 /// (HSET adding a field, LPUSH adding an item, …) so we account in O(1)
56 /// without re-walking the container.
57 pub(crate) fn account_delta(&mut self, key: &[u8], delta: i64) {
58 if delta == 0 {
59 return;
60 }
61 if let Some(e) = self.map.get_mut(key) {
62 e.add_to_weight(delta);
63 }
64 apply_delta(&mut self.used_memory, delta);
65 if delta > 0 {
66 self.update_peak();
67 }
68 }
69
70 /// Recompute `weight` for the entry at `key` from its current value +
71 /// key, then propagate the delta to `used_memory`. Use after a wholesale
72 /// in-place value swap (SET / APPEND / INCRBYFLOAT) where the prior
73 /// `Value`'s weight was already cached on the entry.
74 pub(crate) fn reweigh_entry(&mut self, key: &[u8]) {
75 let key_heap = key_heap_bytes_for(key);
76 let Some(e) = self.map.get_mut(key) else {
77 return;
78 };
79 let new_w = key_heap + e.value.weight();
80 let delta = new_w as i64 - e.weight() as i64;
81 e.set_weight(new_w);
82 apply_delta(&mut self.used_memory, delta);
83 if delta > 0 {
84 self.update_peak();
85 }
86 }
87
88 /// Advance the global access ordinal by one tick. Only invoked under
89 /// `maxmemory > 0` so the wrapping_add cost stays out of the unlimited
90 /// fast path.
91 #[inline]
92 pub(crate) fn tick_clock(&mut self) {
93 self.clock_counter = self.clock_counter.wrapping_add(1);
94 }
95
96 #[inline]
97 fn update_peak(&mut self) {
98 if self.used_memory > self.used_memory_peak {
99 self.used_memory_peak = self.used_memory;
100 }
101 }
102
103 /// Hint the CPU to fetch the bucket cache line for `key` into L1. Called
104 /// by the reactor's parse loop on command N+1 while command N is still
105 /// being dispatched — by the time N+1 actually probes the table, the
106 /// metadata line is hot. No-op when the table is empty. Cheap when not.
107 #[inline]
108 pub fn prefetch_for_key(&self, key: &[u8]) {
109 let hash = key.kevy_hash();
110 self.map.prefetch_for_hash(hash);
111 }
112
113 pub(crate) fn expired(&self, key: &[u8], now: Instant) -> bool {
114 match self.map.get(key) {
115 Some(e) => e.is_expired_at(now),
116 None => false,
117 }
118 }
119
120 /// Drop `key` if expired; returns whether it is live afterwards.
121 pub(crate) fn reap(&mut self, key: &[u8], now: Instant) -> bool {
122 if self.expired(key, now) {
123 self.remove_entry(key);
124 self.expired_keys_total = self.expired_keys_total.saturating_add(1);
125 false
126 } else {
127 self.map.contains_key(key)
128 }
129 }
130
131 /// Single-lookup lazy-expiring read: the live `Entry` for `key`, or `None` if
132 /// absent or expired (expired keys are dropped here, as `reap` would).
133 ///
134 /// Two wins over the old `reap(now)`-then-`get` read path: (1) the clock is
135 /// read **only when the entry actually carries a TTL** — most keys don't, so
136 /// the common hit skips `Instant::now()` (~20–40 ns); (2) one fewer keyspace
137 /// lookup on hits (was peek-expiry + `contains_key` + `get` = 3; now peek +
138 /// `get` = 2). The two-phase shape (decide, then mutate/fetch) keeps the
139 /// borrow checker happy without an owning key clone.
140 pub(crate) fn live_entry(&mut self, key: &[u8]) -> Option<&Entry> {
141 let expired = match self.map.get(key) {
142 None => return None,
143 Some(e) => e.is_expired_at(Instant::now()),
144 };
145 if expired {
146 self.remove_entry(key);
147 self.expired_keys_total = self.expired_keys_total.saturating_add(1);
148 return None;
149 }
150 if self.maxmemory > 0 {
151 self.tick_clock();
152 let c = self.clock_counter as u32;
153 let e = self.map.get_mut(key)?;
154 evict::touch_on_access(e, self.eviction_policy, c);
155 return Some(&*e);
156 }
157 self.map.get(key)
158 }
159
160 /// Mutable [`live_entry`](Self::live_entry): the live `Entry` for `key` by
161 /// `&mut`, or `None` if absent/expired (expired dropped). Same wins — clock
162 /// read only on TTL'd keys, one fewer lookup than `reap`-then-`get_mut`.
163 /// Read-modify commands (INCR/APPEND/…) get the entry once and mutate in
164 /// place, preserving any TTL on it.
165 pub(crate) fn live_entry_mut(&mut self, key: &[u8]) -> Option<&mut Entry> {
166 let expired = match self.map.get(key) {
167 None => return None,
168 Some(e) => e.is_expired_at(Instant::now()),
169 };
170 if expired {
171 self.remove_entry(key);
172 self.expired_keys_total = self.expired_keys_total.saturating_add(1);
173 return None;
174 }
175 if self.maxmemory > 0 {
176 self.tick_clock();
177 let c = self.clock_counter as u32;
178 let e = self.map.get_mut(key)?;
179 evict::touch_on_access(e, self.eviction_policy, c);
180 return Some(e);
181 }
182 self.map.get_mut(key)
183 }
184}