Skip to main content

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}