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}