Skip to main content

yo_kv/
evict.rs

1//! Choosing which key to throw away.
2//!
3//! Eviction is the one place in the server where being approximately right is
4//! the correct engineering answer. A server that has run out of room has to give
5//! some memory back before it can answer the write in front of it, and the
6//! client is waiting. Finding the genuinely least recently used key out of forty
7//! million of them means an ordering over all of them, which is a structure to
8//! maintain on every read of every key forever, to make a decision that is only
9//! ever a guess about the future anyway. Redis decided in 3.0 that it would
10//! rather sample a few keys and take the worst of them, and it was right.
11//!
12//! So this samples. [`Policy`] says which keys are eligible and how to score
13//! them, [`score`] turns one record into a number where larger means a better
14//! victim, and the caller takes the largest number it saw.
15//!
16//! # What the number means
17//!
18//! Not much on its own, and that is deliberate. The four scoring rules produce
19//! numbers on four different scales: seconds of idleness under the clock
20//! policies, a countdown from 255 under LFU, a subtraction from the top of the
21//! range under `volatile-ttl`, and a constant under the random ones. They are
22//! never compared across policies, because a policy does not change halfway
23//! through a round of sampling, so the only thing the scale has to support is
24//! the comparison of two candidates under the same rule.
25//!
26//! The one property they all share is the direction. Bigger is more disposable.
27//! Getting that backwards would build a cache that keeps exactly the keys nobody
28//! wants, and it would still pass a test that only checked the pick was eligible,
29//! which is why the tests here check which of two keys comes out and not just
30//! that one did.
31//!
32//! # Why the good ones are kept
33//!
34//! A round of five samples throws four keys away, and one of them is often
35//! better than anything the next round turns up. [`Pool`] keeps sixteen of them
36//! between rounds, which is Redis 3.0's idea and is most of what separates its
37//! eviction from a plain random sample. Every round adds to the same pool, so
38//! the candidate that eventually goes is the worst key seen across all of them
39//! rather than the worst key seen in the last five.
40//!
41//! A pool that outlives a command cannot hold addresses, because the next write
42//! moves them, so it holds key bytes. Each of the sixteen slots owns its buffer
43//! and reuses it, so a pool that has been through one round of eviction does not
44//! allocate again unless a longer key than it has ever seen turns up.
45//!
46//! Holding keys rather than addresses also means a candidate can go stale: the
47//! key can be deleted, expire, or lose its deadline under a `volatile` policy
48//! between the round that spotted it and the round that takes it. So a candidate
49//! is looked up and rechecked on the way out, and one that no longer qualifies
50//! is dropped and the next one tried. That costs at most sixteen failed lookups
51//! in the worst case, which is bounded and rare, against a decision that is
52//! measurably closer to the right one every time it is not.
53
54use yo_common::Addr;
55
56use crate::access::{Lfu, Policy};
57use crate::value;
58
59/// How many keys a round of sampling looks at, which is `maxmemory-samples`.
60///
61/// Five, which is Redis's default and is a better number than it sounds. The
62/// published curve for it flattens hard: five samples already picks a key from
63/// close to the true tail, ten is visibly better, and everything past that is
64/// paying for a decision that a guess about the future does not deserve.
65pub const SAMPLES: usize = 5;
66
67/// The largest score, used by the policies that do not really have one.
68///
69/// Under `allkeys-random` and `volatile-random` every eligible key is as good a
70/// victim as every other, so they all score the same and the first one sampled
71/// wins. It is the top of the range rather than the bottom so that a caller
72/// comparing against a starting score of zero does not have to special case it.
73pub const ANY: u64 = u64::MAX;
74
75/// Whether a policy would ever consider this record.
76///
77/// The only rule is the deadline: a `volatile` policy will not touch a key that
78/// has no expiry, whatever else is true of it. That is the rule behind the
79/// classic surprise, which is that `volatile-lru` on a database where nothing
80/// has a TTL evicts nothing at all and starts refusing writes, and it is worth
81/// having in one place rather than inline at the sampling loop.
82#[must_use]
83pub fn eligible(rec: &[u8], policy: Policy) -> bool {
84    !policy.volatile_only() || value::expire_at(rec).is_some()
85}
86
87/// How disposable this record is under this policy. Larger goes first.
88///
89/// The four rules, in the order the match takes them:
90///
91/// Under `volatile-ttl` the key that expires soonest goes first, so the score
92/// counts down from the top of the range as the deadline moves out. A record
93/// with no deadline cannot reach here, because [`eligible`] refused it, and if
94/// one somehow did it would score zero and lose to everything.
95///
96/// Under the random pair every eligible key scores the same, which makes the
97/// pick the first one sampled. That is a fair draw and not a biased one, because
98/// the sample itself is what did the choosing.
99///
100/// Under an LFU policy the counter is read with the decay applied, and the score
101/// is what is left of the range above it. The counter saturates at 255, so a key
102/// that has been hammered scores zero and is the last thing to go.
103///
104/// Under everything else the field is a clock and the score is seconds of
105/// idleness. That covers the LRU pair, the LRM pair, and `noeviction`, which
106/// scores keys it will never evict because `OBJECT IDLETIME` asks the same
107/// question and a server that will never evict still has to answer it.
108#[must_use]
109pub fn score(rec: &[u8], policy: Policy, now_ms: u64, lfu: Lfu) -> u64 {
110    if matches!(policy, Policy::VolatileTtl) {
111        return value::expire_at(rec).map_or(0, |at| u64::MAX - at);
112    }
113    if policy.is_random() {
114        return ANY;
115    }
116    let access = value::access(rec).unwrap_or_default();
117    if policy.is_lfu() {
118        return u64::from(u8::MAX - access.freq(now_ms, lfu));
119    }
120    access.idle_secs(now_ms)
121}
122
123/// The best victim seen so far in one round of sampling.
124///
125/// It holds an address rather than a key, which is what confines it to a single
126/// round: an address is only good until the next write, and the caller deletes
127/// the winner before it writes anything. [`Pool`] is the version that survives a
128/// round, and it pays for that by holding bytes.
129///
130/// This is what the random policies use, because they have no ordering for a
131/// pool to approximate and every eligible key is already the answer.
132#[derive(Debug, Clone, Copy)]
133pub struct Best {
134    /// Where the winner is, or [`Addr::NONE`] if nothing eligible turned up.
135    pub addr: Addr,
136    /// Its score, meaningful only against another score under the same policy.
137    pub score: u64,
138}
139
140impl Best {
141    /// Nothing yet.
142    pub const EMPTY: Best = Best {
143        addr: Addr::NONE,
144        score: 0,
145    };
146
147    /// Whether anything eligible has been seen.
148    #[must_use]
149    pub const fn is_empty(self) -> bool {
150        self.addr.is_none()
151    }
152
153    /// Take this candidate if it beats what is held.
154    ///
155    /// Strictly better and not as good, so a tie leaves the earlier one in
156    /// place. That is what makes the random policies pick the first key sampled
157    /// rather than the last, and under the other policies it means the pick does
158    /// not wander between keys that are equally stale.
159    pub fn offer(&mut self, addr: Addr, score: u64) {
160        if self.is_empty() || score > self.score {
161            *self = Best { addr, score };
162        }
163    }
164}
165
166/// How many candidates survive between rounds of sampling.
167///
168/// Sixteen, which is Redis's `EVPOOL_SIZE`. It is three rounds of sampling at
169/// the default of five, so a pool holds roughly the last three rounds worth of
170/// the keys worth remembering and forgets the rest.
171pub const CANDIDATES: usize = 16;
172
173/// One candidate, and the buffer its key is kept in between rounds.
174#[derive(Debug, Default, Clone)]
175struct Slot {
176    /// Its score when it was last offered, on the scale [`score`] was using.
177    score: u64,
178    /// The key, copied because an address would not survive the next write.
179    key: Vec<u8>,
180}
181
182impl Slot {
183    /// Become this candidate, keeping whatever buffer was already here.
184    fn fill(&mut self, key: &[u8], score: u64) {
185        self.score = score;
186        self.key.clear();
187        self.key.extend_from_slice(key);
188    }
189}
190
191/// The best candidates seen across rounds, worst victim last.
192///
193/// Sorted by score ascending, so [`Pool::take`] pops the end and the weakest
194/// candidate is always at the front where a better one displaces it. Sixteen
195/// entries is small enough that a sorted array beats anything with a shape, and
196/// the shifting is a `rotate` over at most fifteen `Vec` headers.
197///
198/// The array is allocated on the first offer rather than on construction,
199/// because a database that never evicts anything is the common one and it does
200/// not deserve sixteen anythings.
201#[derive(Debug, Default, Clone)]
202pub struct Pool {
203    at: Vec<Slot>,
204    len: usize,
205}
206
207impl Pool {
208    /// An empty pool that has not allocated anything.
209    #[must_use]
210    pub const fn new() -> Pool {
211        Pool {
212            at: Vec::new(),
213            len: 0,
214        }
215    }
216
217    /// How many candidates are held.
218    #[inline]
219    #[must_use]
220    pub const fn len(&self) -> usize {
221        self.len
222    }
223
224    /// Whether there is nothing to take.
225    #[inline]
226    #[must_use]
227    pub const fn is_empty(&self) -> bool {
228        self.len == 0
229    }
230
231    /// Forget every candidate and keep the buffers.
232    ///
233    /// The caller runs this when the answers stop meaning anything, which is a
234    /// policy change and a flush. Both leave a pool full of scores on a scale
235    /// nothing uses any more or keys that are not there, and while the recheck
236    /// on the way out would survive either, a stale pool is sixteen wasted
237    /// lookups in front of the next eviction.
238    #[inline]
239    pub fn clear(&mut self) {
240        self.len = 0;
241    }
242
243    /// What the buffers cost.
244    #[must_use]
245    pub fn memory_bytes(&self) -> usize {
246        self.at.capacity() * size_of::<Slot>()
247            + self.at.iter().map(|s| s.key.capacity()).sum::<usize>()
248    }
249
250    /// Put a candidate in the running.
251    ///
252    /// A key already held is re-scored rather than held twice, because the same
253    /// key turning up in two rounds is ordinary and two entries for it would be
254    /// one wasted slot and one guaranteed miss on the way out.
255    ///
256    /// A key worse than everything held is dropped when the pool is full, which
257    /// is the common case once it has warmed up and is the reason this is cheap.
258    pub fn offer(&mut self, key: &[u8], score: u64) {
259        if self.at.is_empty() {
260            self.at.resize_with(CANDIDATES, Slot::default);
261        }
262        if let Some(i) = self.at[..self.len].iter().position(|s| s.key == key) {
263            if self.at[i].score == score {
264                return;
265            }
266            // Out of the sorted run and into the free space past it, which keeps
267            // its buffer where the insert below can pick it up again.
268            self.at[i..self.len].rotate_left(1);
269            self.len -= 1;
270        } else if self.len == CANDIDATES && score <= self.at[0].score {
271            return;
272        }
273        let i = self.at[..self.len].partition_point(|s| s.score <= score);
274        let at = if self.len < CANDIDATES {
275            // The free slot at `len` comes back to `i` and everything from `i`
276            // moves up one.
277            self.at[i..=self.len].rotate_right(1);
278            self.len += 1;
279            i
280        } else {
281            // The pool is full and this beats the front of it, so the front goes
282            // and everything below `i` moves down one. `i` is at least one here
283            // because the check above sent back anything the front could beat.
284            self.at[..i].rotate_left(1);
285            i - 1
286        };
287        self.at[at].fill(key, score);
288    }
289
290    /// The worst key held, removed from the pool.
291    ///
292    /// It is removed whether or not the caller can use it, because a candidate
293    /// the caller looked at and rejected is a candidate that will be rejected
294    /// again next time, and the point of a pool is to stop paying for the same
295    /// answer twice.
296    pub fn take(&mut self) -> Option<&[u8]> {
297        if self.len == 0 {
298            return None;
299        }
300        self.len -= 1;
301        Some(&self.at[self.len].key)
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    /// What the pool holds, worst victim first, so a test can read it.
310    fn held(p: &Pool) -> Vec<(&[u8], u64)> {
311        p.at[..p.len]
312            .iter()
313            .rev()
314            .map(|s| (&s.key[..], s.score))
315            .collect()
316    }
317
318    #[test]
319    fn the_worst_candidate_comes_out_first() {
320        let mut p = Pool::new();
321        p.offer(b"middling", 50);
322        p.offer(b"terrible", 90);
323        p.offer(b"fine", 10);
324
325        assert_eq!(p.len(), 3);
326        assert_eq!(p.take(), Some(&b"terrible"[..]));
327        assert_eq!(p.take(), Some(&b"middling"[..]));
328        assert_eq!(p.take(), Some(&b"fine"[..]));
329        assert_eq!(p.take(), None);
330        assert!(p.is_empty());
331    }
332
333    #[test]
334    fn a_full_pool_keeps_the_worst_sixteen_and_nothing_else() {
335        let mut p = Pool::new();
336        // Thirty two keys offered worst first, so every one after the first
337        // sixteen is better than everything held and should be turned away.
338        for i in 0..32u64 {
339            p.offer(format!("key-{i}").as_bytes(), 1000 - i);
340        }
341        assert_eq!(p.len(), CANDIDATES);
342        let names: Vec<_> = held(&p)
343            .into_iter()
344            .map(|(k, _)| String::from_utf8(k.to_vec()).expect("ascii"))
345            .collect();
346        assert_eq!(names[0], "key-0", "the worst key offered");
347        assert_eq!(names[15], "key-15");
348
349        // And the other way round, where every one displaces the front.
350        let mut q = Pool::new();
351        for i in 0..32u64 {
352            q.offer(format!("key-{i}").as_bytes(), i);
353        }
354        assert_eq!(q.len(), CANDIDATES);
355        assert_eq!(q.take(), Some(&b"key-31"[..]), "the worst key offered");
356    }
357
358    #[test]
359    fn a_key_offered_twice_is_held_once_at_its_new_score() {
360        let mut p = Pool::new();
361        p.offer(b"a", 10);
362        p.offer(b"b", 20);
363        p.offer(b"c", 30);
364        // The same key again, now the worst thing in the pool rather than the
365        // best. One entry, in its new place.
366        p.offer(b"a", 40);
367
368        assert_eq!(p.len(), 3);
369        assert_eq!(
370            held(&p),
371            vec![(&b"a"[..], 40), (&b"c"[..], 30), (&b"b"[..], 20)]
372        );
373    }
374
375    #[test]
376    fn a_key_offered_twice_at_the_same_score_changes_nothing() {
377        let mut p = Pool::new();
378        p.offer(b"a", 10);
379        p.offer(b"b", 20);
380        p.offer(b"a", 10);
381
382        assert_eq!(p.len(), 2);
383        assert_eq!(held(&p), vec![(&b"b"[..], 20), (&b"a"[..], 10)]);
384    }
385
386    #[test]
387    fn a_key_offered_twice_into_a_full_pool_still_leaves_room() {
388        let mut p = Pool::new();
389        for i in 0..CANDIDATES as u64 {
390            p.offer(format!("key-{i}").as_bytes(), 100 + i);
391        }
392        // Worse than everything held, and already held, so the pool has to drop
393        // it out of the middle before it puts it back on the end rather than
394        // evicting its own front to make room for a key it already has.
395        p.offer(b"key-3", 999);
396
397        assert_eq!(p.len(), CANDIDATES);
398        assert_eq!(p.take(), Some(&b"key-3"[..]));
399        assert_eq!(
400            p.take(),
401            Some(&b"key-15"[..]),
402            "the front was not thrown away"
403        );
404    }
405
406    #[test]
407    fn clearing_forgets_the_candidates_and_keeps_the_buffers() {
408        let mut p = Pool::new();
409        for i in 0..CANDIDATES as u64 {
410            p.offer(format!("a rather long key name number {i}").as_bytes(), i);
411        }
412        let held = p.memory_bytes();
413        p.clear();
414
415        assert!(p.is_empty());
416        assert_eq!(p.take(), None);
417        assert_eq!(p.memory_bytes(), held, "the buffers went with the scores");
418    }
419
420    #[test]
421    fn a_warm_pool_does_not_allocate_again() {
422        let mut p = Pool::new();
423        for i in 0..64u64 {
424            p.offer(format!("key-{i:0>6}").as_bytes(), i % 17);
425        }
426        let settled = p.memory_bytes();
427        for i in 0..1000u64 {
428            p.offer(format!("key-{i:0>6}").as_bytes(), i % 17);
429        }
430        assert_eq!(
431            p.memory_bytes(),
432            settled,
433            "a key no longer than any it has seen cost it an allocation"
434        );
435    }
436
437    #[test]
438    fn an_untouched_pool_costs_nothing() {
439        let p = Pool::new();
440        assert_eq!(p.memory_bytes(), 0);
441        assert!(p.is_empty());
442    }
443}