yo_kv/access.rs
1//! How recently a key was used, and how often, in twenty four bits.
2//!
3//! Eviction has to pick a victim, and the ten policies pick one by asking one of
4//! three questions about every candidate: when was this last read, when was it
5//! last written, or how often is it read. Redis answers all three out of a single
6//! twenty four bit field on the object, reading it one way under an LFU policy
7//! and the other way under everything else, and this is that field.
8//!
9//! ```text
10//! clock +-------------------------------------------+
11//! | seconds since the epoch, low 24 bits |
12//! +-------------------------------------------+
13//!
14//! LFU +---------------------------+---------------+
15//! | minutes, low 16 bits | counter, u8 |
16//! +---------------------------+---------------+
17//! ```
18//!
19//! The two readings share the field because a key is only ever under one policy
20//! at a time, and a server that switches policy at runtime is one where the old
21//! reading is garbage under the new one. Redis says so in the error text on
22//! `OBJECT FREQ`, which tells the operator that switching will take some time to
23//! adjust, and that sentence is a description of exactly this.
24//!
25//! # Least recently modified
26//!
27//! There are ten policies rather than the eight most people can name.
28//! `volatile-lrm` and `allkeys-lrm` arrived in 8.8 and they are the reason the
29//! top box above is labelled clock rather than LRU. They store the same seconds
30//! in the same bits and are read by the same subtraction. The only difference is
31//! when the field is written: least recently used stamps it on every lookup,
32//! least recently modified stamps it only when the value changes, so a key that
33//! is read a million times and written once is a good victim under LRM and a bad
34//! one under LRU.
35//!
36//! The thing worth writing down is that the clock is kept under all eight of the
37//! non LFU policies and not just under the two LRU ones. Redis stamps it on every
38//! lookup under `noeviction` and under the random policies too, which is why
39//! `OBJECT IDLETIME` gives a real answer on a default server that is never going
40//! to evict anything. Assuming otherwise is easy and it makes `OBJECT IDLETIME`
41//! answer zero forever.
42//!
43//! # Why the arithmetic is copied rather than improved
44//!
45//! Both readings wrap, and both wrap in ways that a fresh design would not
46//! choose. The LRU clock is twenty four bits of seconds, so it goes round every
47//! hundred and ninety four days, and the idle time calculation has a branch in
48//! it whose whole job is to give a sane answer across that wrap. The LFU clock
49//! is sixteen bits of minutes and goes round every forty five days.
50//!
51//! None of that is copied out of admiration. `OBJECT IDLETIME` and `OBJECT FREQ`
52//! are in the Redis test suite, the numbers they return are asserted on, and a
53//! counter that is off by one against Redis's is a compatibility bug rather than
54//! a rounding difference. So the constants here are Redis's constants and the
55//! branches here are Redis's branches, and the places where that produces an odd
56//! answer are marked as such rather than fixed.
57//!
58//! # What is different
59//!
60//! The randomness is passed in rather than drawn here. Redis calls `rand()`
61//! inside its increment, which makes the function untestable and makes two
62//! servers replaying the same commands disagree. Here the caller hands over the
63//! generator it already owns, which is the shard's, so the whole thing is a pure
64//! function of its inputs and a test can assert on a specific counter after a
65//! specific number of accesses.
66
67use yo_common::rng::Rng;
68
69/// Bits in the field. Everything above these is not ours to write.
70const BITS: u32 = 24;
71
72/// The largest value the field holds, which is what the LRU clock wraps at.
73const MAX: u32 = (1 << BITS) - 1;
74
75/// Milliseconds per tick of the LRU clock.
76///
77/// A second, which is the resolution `OBJECT IDLETIME` reports in anyway. It is
78/// also what makes the twenty four bits last a hundred and ninety four days
79/// instead of four and a half hours.
80const LRU_RESOLUTION_MS: u64 = 1000;
81
82/// What a key's counter starts at under an LFU policy.
83///
84/// Five, and it is not zero for a reason worth writing down. A key that has just
85/// been created has been accessed once, and starting it at zero would make it
86/// the most attractive victim in the database at the moment it was written,
87/// which means a fresh write could be evicted before the client that made it
88/// ever read it back. Five gives it enough of a floor to survive the next few
89/// sampling rounds and be judged on its actual traffic.
90pub const LFU_INIT: u8 = 5;
91
92/// How sharply the counter's growth flattens out.
93///
94/// The default is ten, which is Redis's. A larger number means more accesses are
95/// needed to move the counter, so the counter covers a wider range of traffic in
96/// the same eight bits.
97pub const LFU_LOG_FACTOR: u32 = 10;
98
99/// Minutes without an access before the counter comes down by one.
100///
101/// One, which is Redis's default. Zero turns decay off entirely and makes the
102/// counter a lifetime total, which sounds appealing and is not: a key that was
103/// hot last Tuesday would outrank one that is hot now, forever.
104pub const LFU_DECAY_MINUTES: u32 = 1;
105
106/// The low sixteen bits, which is where the LFU clock lives.
107const LFU_TIME_MAX: u32 = 0xffff;
108
109/// The two knobs an LFU policy has, which are `lfu-log-factor` and
110/// `lfu-decay-time`.
111///
112/// They travel together because neither means anything on its own. The factor
113/// decides how many accesses it takes to climb and the decay decides how fast
114/// the climb is given back, so it is the pair that sets what the counter
115/// measures, and passing one without the other is how they drift apart.
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub struct Lfu {
118 /// How sharply the counter's growth flattens out. See [`LFU_LOG_FACTOR`].
119 pub log_factor: u32,
120 /// Minutes of quiet per step down. See [`LFU_DECAY_MINUTES`].
121 pub decay_minutes: u32,
122}
123
124impl Lfu {
125 /// Redis's defaults, which are a factor of ten and a decay of one minute.
126 pub const DEFAULT: Lfu = Lfu {
127 log_factor: LFU_LOG_FACTOR,
128 decay_minutes: LFU_DECAY_MINUTES,
129 };
130}
131
132impl Default for Lfu {
133 fn default() -> Lfu {
134 Lfu::DEFAULT
135 }
136}
137
138/// What a server does when it runs out of room, and therefore which reading of
139/// [`Access`] is the live one.
140///
141/// The ten are Redis's ten and the names are the strings `maxmemory-policy`
142/// takes. They vary along two axes that are worth separating, because most of
143/// the code downstream only cares about one of them: which keys are eligible,
144/// and how a victim is chosen from among them.
145///
146/// The `volatile` half only considers keys that have a deadline, which is the
147/// setting for a server holding a cache and a working set in the same database.
148/// The trap in it is that a `volatile` policy on a database where nothing has a
149/// TTL cannot evict anything at all, so it behaves as [`Policy::NoEviction`] and
150/// starts refusing writes, and that surprises people often enough that it is
151/// worth saying here.
152///
153/// Ten and not the eight everyone knows. `volatile-lrm` and `allkeys-lrm` are
154/// least recently modified, they are in 8.8 and therefore in the version we
155/// claim to be, and they are easy to miss because most of what is written about
156/// Redis eviction predates them. They share the clock with the LRU pair and
157/// differ in one rule: a read does not move it. See [`Policy::stamps_on_read`].
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
159pub enum Policy {
160 /// Evict nothing and refuse the write instead. Redis's default, and this
161 /// crate's, because losing data silently is not a default anyone should get
162 /// without asking.
163 #[default]
164 NoEviction,
165 /// Any key, least recently used first.
166 AllKeysLru,
167 /// Any key, least frequently used first.
168 AllKeysLfu,
169 /// Any key, chosen at random.
170 AllKeysRandom,
171 /// Any key, least recently modified first.
172 AllKeysLrm,
173 /// Keys with a deadline, least recently used first.
174 VolatileLru,
175 /// Keys with a deadline, least frequently used first.
176 VolatileLfu,
177 /// Keys with a deadline, chosen at random.
178 VolatileRandom,
179 /// Keys with a deadline, soonest to expire first.
180 VolatileTtl,
181 /// Keys with a deadline, least recently modified first.
182 VolatileLrm,
183}
184
185impl Policy {
186 /// The string `CONFIG GET maxmemory-policy` reports.
187 #[must_use]
188 pub const fn name(self) -> &'static str {
189 match self {
190 Policy::NoEviction => "noeviction",
191 Policy::AllKeysLru => "allkeys-lru",
192 Policy::AllKeysLfu => "allkeys-lfu",
193 Policy::AllKeysRandom => "allkeys-random",
194 Policy::AllKeysLrm => "allkeys-lrm",
195 Policy::VolatileLru => "volatile-lru",
196 Policy::VolatileLfu => "volatile-lfu",
197 Policy::VolatileRandom => "volatile-random",
198 Policy::VolatileTtl => "volatile-ttl",
199 Policy::VolatileLrm => "volatile-lrm",
200 }
201 }
202
203 /// Every policy, in the order Redis lists them.
204 ///
205 /// The order is not ours to pick. `CONFIG SET maxmemory-policy garbage`
206 /// fails with a message that names the legal values, and a client comparing
207 /// that message against a real server compares the whole string, so the
208 /// order in `config.c` is the order here.
209 pub const ALL: [Policy; 10] = [
210 Policy::VolatileLru,
211 Policy::VolatileLfu,
212 Policy::VolatileRandom,
213 Policy::VolatileTtl,
214 Policy::VolatileLrm,
215 Policy::AllKeysLru,
216 Policy::AllKeysLfu,
217 Policy::AllKeysRandom,
218 Policy::AllKeysLrm,
219 Policy::NoEviction,
220 ];
221
222 /// The policy a `CONFIG SET maxmemory-policy` argument names.
223 ///
224 /// Case insensitive, because `CONFIG SET` is everywhere else and a client
225 /// that sends `ALLKEYS-LRU` is not wrong.
226 #[must_use]
227 pub fn parse(s: &[u8]) -> Option<Policy> {
228 Policy::ALL
229 .into_iter()
230 .find(|p| s.eq_ignore_ascii_case(p.name().as_bytes()))
231 }
232
233 /// Whether only keys with a deadline are eligible.
234 #[must_use]
235 pub const fn volatile_only(self) -> bool {
236 matches!(
237 self,
238 Policy::VolatileLru
239 | Policy::VolatileLfu
240 | Policy::VolatileRandom
241 | Policy::VolatileTtl
242 | Policy::VolatileLrm
243 )
244 }
245
246 /// Whether the access field is being read as a frequency counter.
247 ///
248 /// This is the question `OBJECT FREQ` asks before it answers, because under
249 /// any other policy the bits hold something else and reporting them as a
250 /// frequency would be reporting a number that means nothing.
251 #[must_use]
252 pub const fn is_lfu(self) -> bool {
253 matches!(self, Policy::AllKeysLfu | Policy::VolatileLfu)
254 }
255
256 /// Whether a victim is picked by how recently the key was used.
257 #[must_use]
258 pub const fn is_lru(self) -> bool {
259 matches!(self, Policy::AllKeysLru | Policy::VolatileLru)
260 }
261
262 /// Whether a victim is picked by how recently the key was written.
263 ///
264 /// The same clock as [`Policy::is_lru`] read the same way. The pair differ
265 /// only in when the clock is set, which is [`Policy::stamps_on_read`].
266 #[must_use]
267 pub const fn is_lrm(self) -> bool {
268 matches!(self, Policy::AllKeysLrm | Policy::VolatileLrm)
269 }
270
271 /// Whether a victim is picked by a fair draw and nothing else.
272 ///
273 /// The pair that has no ordering to approximate, which is why the eviction
274 /// pool skips them: keeping candidates between rounds is how a sampled
275 /// policy gets closer to the true worst key, and under these two every
276 /// eligible key already is the answer.
277 #[must_use]
278 pub const fn is_random(self) -> bool {
279 matches!(self, Policy::AllKeysRandom | Policy::VolatileRandom)
280 }
281
282 /// Whether the access field holds a clock, which is the question `OBJECT
283 /// IDLETIME` asks before it answers.
284 ///
285 /// True for eight of the ten. Only an LFU policy packs something else in
286 /// there, and reporting those bits as an idle time would be reporting a
287 /// number that means nothing.
288 #[must_use]
289 pub const fn is_clock(self) -> bool {
290 !self.is_lfu()
291 }
292
293 /// Whether reading a key writes the access field back to it.
294 ///
295 /// True for eight of the ten, which is not the answer this had before and
296 /// is the answer Redis gives. It is tempting to think `noeviction` and the
297 /// random policies have nothing to maintain on a read, and that is true of
298 /// eviction and false of the field: Redis stamps the clock on every lookup
299 /// under all of them, which is why `OBJECT IDLETIME` tells the truth on a
300 /// default server that will never evict anything.
301 ///
302 /// The two LRM policies are the exception, and they are the whole reason
303 /// they exist. Least recently modified wants the clock to say when the value
304 /// was last written, so a read that moved it would erase the only thing the
305 /// policy is measuring.
306 #[must_use]
307 pub const fn stamps_on_read(self) -> bool {
308 !self.is_lrm()
309 }
310
311 /// Whether writing a key writes the access field back to it.
312 ///
313 /// True for all ten, by two different routes. Under LRM it is the point.
314 /// Under the other eight a write resolves the key first and that resolve is
315 /// a read like any other, so the stamp has already happened by the time the
316 /// value changes.
317 #[must_use]
318 pub const fn stamps_on_write(self) -> bool {
319 true
320 }
321}
322
323/// How recently and how often one key has been used.
324///
325/// Which of the two it is depends on the [`Policy`] in force, and this type does
326/// not know which that is. The reader picks.
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
328pub struct Access(u32);
329
330impl Access {
331 /// The field as stored, which is always inside twenty four bits.
332 #[inline]
333 #[must_use]
334 pub const fn bits(self) -> u32 {
335 self.0 & MAX
336 }
337
338 /// Rebuild one from bits that came out of a record.
339 ///
340 /// Anything above the low twenty four is dropped rather than trusted,
341 /// because those bits belong to whatever is packed alongside.
342 #[inline]
343 #[must_use]
344 pub const fn from_bits(bits: u32) -> Access {
345 Access(bits & MAX)
346 }
347
348 /// Whether this key has never been stamped.
349 ///
350 /// All zeroes, which is what a record carries from the moment it is written
351 /// until something reads it under a policy that cares. It is a sentinel and
352 /// not a reading, and both readings answer for it as though the key had just
353 /// been used: zero seconds idle, and the starting frequency. That is the
354 /// safe direction. The other one would make every key in the database the
355 /// most attractive victim available for as long as it went unread, which
356 /// would evict the working set the moment a policy was switched on.
357 ///
358 /// It is a sentinel rather than a flag bit because it costs nothing and
359 /// because it means a record can be created without anybody deciding what to
360 /// put here. The writers do not know the clock or the policy, and having
361 /// them ask would have put both through every call site that makes a record.
362 ///
363 /// Zero is very nearly unreachable as a real reading. An LRU stamp is zero
364 /// only in the first second of 1970. An LFU stamp is zero only for a key
365 /// already decayed to nothing that is touched during the one minute in every
366 /// forty five days when the LFU clock wraps, and the cost of the collision
367 /// is that the key looks freshly used for a moment instead of unused. That
368 /// is a rounding error in a heuristic, and it is worth it to keep the write
369 /// path from having to care.
370 #[inline]
371 #[must_use]
372 pub const fn is_unset(self) -> bool {
373 self.bits() == 0
374 }
375
376 /// The reading for a key touched at `now_ms` under an LRU policy.
377 #[inline]
378 #[must_use]
379 pub const fn lru(now_ms: u64) -> Access {
380 Access(clock_at(now_ms))
381 }
382
383 /// The reading for a key created at `now_ms` under an LFU policy.
384 #[inline]
385 #[must_use]
386 pub const fn lfu(now_ms: u64) -> Access {
387 Access::pack(minutes_at(now_ms), LFU_INIT)
388 }
389
390 /// How long ago this key was read, in seconds, under an LRU policy.
391 ///
392 /// This is what `OBJECT IDLETIME` returns. The branch is the wrap: once the
393 /// clock has gone round, a key stamped before the wrap holds a number larger
394 /// than the clock does, and subtracting the wrong way round would report a
395 /// key that was read a second ago as a hundred and ninety four days idle,
396 /// which under `allkeys-lru` would evict the hottest key in the database.
397 ///
398 /// The wrapped arm is short by one second, because the period is `MAX + 1`
399 /// and Redis subtracts from `MAX`. That is not a mistake here, it is Redis's
400 /// mistake reproduced on purpose, and it is worth being clear about because
401 /// it looks exactly like the kind of thing somebody would tidy up. Fixing it
402 /// would make `OBJECT IDLETIME` disagree with Redis by a second for the keys
403 /// that were stamped before a wrap, once every hundred and ninety four days.
404 ///
405 /// It is also self consistent over there, which is the part that settles it.
406 /// `RESTORE` takes an idle time and turns it back into a stamp, and it adds
407 /// `MAX` where this subtracts `MAX`, so a value that goes out through
408 /// `OBJECT IDLETIME` and comes back in through `RESTORE` lands on the number
409 /// it started from. Correcting one end here would break that round trip
410 /// against a real Redis without making any single answer more true.
411 #[inline]
412 #[must_use]
413 pub const fn idle_secs(self, now_ms: u64) -> u64 {
414 if self.is_unset() {
415 return 0;
416 }
417 let now = clock_at(now_ms);
418 let then = self.bits();
419 let ticks = if now >= then {
420 now - then
421 } else {
422 now + (MAX - then)
423 };
424 ticks as u64
425 }
426
427 /// The frequency counter, with the decay since the last access applied.
428 ///
429 /// This is what `OBJECT FREQ` returns and what eviction compares. The decay
430 /// is applied on read rather than on a timer, which is what makes the whole
431 /// thing free when nobody is asking: there is no sweep that walks every key
432 /// once a minute to bring counters down, and a key nobody looks at costs
433 /// nothing to not look at.
434 #[inline]
435 #[must_use]
436 pub const fn freq(self, now_ms: u64, lfu: Lfu) -> u8 {
437 if self.is_unset() {
438 return LFU_INIT;
439 }
440 let counter = self.counter();
441 if lfu.decay_minutes == 0 {
442 return counter;
443 }
444 let periods = self.elapsed_minutes(now_ms) / lfu.decay_minutes;
445 if periods >= counter as u32 {
446 0
447 } else {
448 counter - periods as u8
449 }
450 }
451
452 /// The field after one access under an LFU policy.
453 ///
454 /// Decay first and then increment, in that order, because the other order
455 /// would let a key that is read once a minute climb forever: the increment
456 /// would land before the decay took it off again and the counter would
457 /// ratchet up on traffic that is not actually heavy.
458 ///
459 /// The increment is probabilistic and that is the whole trick. Eight bits
460 /// cannot count to a million, so the counter does not count accesses, it
461 /// samples them, at odds that fall as the counter rises. A key at 5 moves on
462 /// the next access, a key at 100 moves on about one access in a thousand,
463 /// and the result is a number that orders keys by traffic across several
464 /// orders of magnitude without ever needing a ninth bit.
465 #[must_use]
466 pub fn touched(self, now_ms: u64, lfu: Lfu, rng: &mut Rng) -> Access {
467 let counter = self.freq(now_ms, lfu);
468 Access::pack(minutes_at(now_ms), incr(counter, lfu.log_factor, rng))
469 }
470
471 /// The counter as stored, with no decay applied.
472 ///
473 /// Only the decay in [`Access::freq`] should be reading this. It is the raw
474 /// low byte and it is an overestimate of the key's frequency by however long
475 /// it has been since the key was last touched.
476 #[inline]
477 const fn counter(self) -> u8 {
478 (self.0 & 0xff) as u8
479 }
480
481 /// Minutes since the counter was last brought up to date.
482 ///
483 /// Wraps the same way the LRU clock does and for the same reason, except
484 /// that sixteen bits of minutes goes round every forty five days rather than
485 /// every hundred and ninety four. It is short by one minute across the wrap
486 /// for the same reason [`Access::idle_secs`] is short by one second, and it
487 /// is kept for the same reason.
488 #[inline]
489 const fn elapsed_minutes(self, now_ms: u64) -> u32 {
490 let now = minutes_at(now_ms);
491 let then = (self.0 >> 8) & LFU_TIME_MAX;
492 if now >= then {
493 now - then
494 } else {
495 LFU_TIME_MAX - then + now
496 }
497 }
498
499 /// Put a clock reading and a counter together into the field.
500 #[inline]
501 const fn pack(minutes: u32, counter: u8) -> Access {
502 Access(((minutes & LFU_TIME_MAX) << 8) | counter as u32)
503 }
504}
505
506/// The LRU clock at `now_ms`, which is seconds truncated to twenty four bits.
507#[inline]
508const fn clock_at(now_ms: u64) -> u32 {
509 ((now_ms / LRU_RESOLUTION_MS) & MAX as u64) as u32
510}
511
512/// The LFU clock at `now_ms`, which is minutes truncated to sixteen bits.
513#[inline]
514const fn minutes_at(now_ms: u64) -> u32 {
515 ((now_ms / 60_000) & LFU_TIME_MAX as u64) as u32
516}
517
518/// One probabilistic step up the counter.
519///
520/// Saturates at 255 rather than wrapping, which matters more than it looks: a
521/// counter that wrapped would turn the hottest key in the database into the
522/// coldest one, and it would do it silently.
523#[inline]
524fn incr(counter: u8, log_factor: u32, rng: &mut Rng) -> u8 {
525 if counter == u8::MAX {
526 return u8::MAX;
527 }
528 // Below the starting value the odds are even, so a brand new key and a key
529 // that has decayed to nothing both climb on their next access rather than
530 // being stuck at the bottom.
531 let base = counter.saturating_sub(LFU_INIT) as u32;
532 if rng.chance(1, base * log_factor + 1) {
533 counter + 1
534 } else {
535 counter
536 }
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542
543 /// A millisecond reading `secs` seconds after the epoch.
544 const fn at(secs: u64) -> u64 {
545 secs * 1000
546 }
547
548 /// Decay turned off, so a test can hold a counter still.
549 const NO_DECAY: Lfu = Lfu {
550 decay_minutes: 0,
551 ..Lfu::DEFAULT
552 };
553
554 /// A log factor of zero, which makes every increment certain rather than
555 /// probabilistic. It is how a test walks the counter somewhere on purpose
556 /// instead of waiting for the odds.
557 const EVEN: Lfu = Lfu {
558 log_factor: 0,
559 ..Lfu::DEFAULT
560 };
561
562 #[test]
563 fn idle_time_is_seconds_since_the_key_was_touched() {
564 let a = Access::lru(at(1_000));
565 assert_eq!(a.idle_secs(at(1_000)), 0);
566 assert_eq!(a.idle_secs(at(1_030)), 30);
567 assert_eq!(a.idle_secs(at(1_000 + 86_400)), 86_400);
568 }
569
570 #[test]
571 fn idle_time_ignores_the_part_of_a_second_that_has_not_finished() {
572 // The clock is seconds, so a key touched at 1500 ms and read at 1900 ms
573 // is on the same tick and is zero seconds idle. Redis reports the same,
574 // and a test suite that touches a key and immediately asks for its idle
575 // time depends on it.
576 let a = Access::lru(1_500);
577 assert_eq!(a.idle_secs(1_900), 0);
578 assert_eq!(a.idle_secs(2_100), 1);
579 }
580
581 #[test]
582 fn idle_time_survives_the_clock_going_round() {
583 // The clock wraps at 2^24 seconds, which is about a hundred and ninety
584 // four days. A key touched just before the wrap and read just after it
585 // has to come back as seconds old and not as most of a year old, because
586 // under allkeys-lru the second answer evicts the hottest key there is.
587 let wrap = at(MAX as u64);
588 let a = Access::lru(wrap - at(5));
589 assert_eq!(a.idle_secs(wrap - at(5)), 0);
590 // Five seconds before the wrap and three after is eight, and Redis says
591 // seven, because its wrapped arm subtracts from `MAX` when the period is
592 // `MAX + 1`. Seven is the answer this has to give. See `idle_secs`.
593 assert_eq!(a.idle_secs(wrap + at(3)), 7);
594 }
595
596 #[test]
597 fn a_new_key_starts_at_the_initial_frequency() {
598 let a = Access::lfu(at(0));
599 assert_eq!(a.freq(at(0), Lfu::DEFAULT), LFU_INIT);
600 }
601
602 #[test]
603 fn the_counter_decays_one_step_per_decay_period() {
604 let a = Access::lfu(at(0));
605 assert_eq!(a.freq(at(60), Lfu::DEFAULT), LFU_INIT - 1);
606 assert_eq!(a.freq(at(180), Lfu::DEFAULT), LFU_INIT - 3);
607 assert_eq!(a.freq(at(3_600), Lfu::DEFAULT), 0, "and stops at zero");
608 }
609
610 #[test]
611 fn a_decay_time_of_zero_turns_decay_off() {
612 let a = Access::lfu(at(0));
613 assert_eq!(a.freq(at(86_400 * 30), NO_DECAY), LFU_INIT);
614 }
615
616 #[test]
617 fn the_counter_decays_across_its_own_wrap() {
618 // The LFU clock is sixteen bits of minutes, so it goes round every forty
619 // five days. Reading across the wrap the wrong way round would report a
620 // gap of most of the period, which would decay every counter to zero and
621 // make the policy pick a victim at random.
622 let a = Access::lfu(at(60 * (LFU_TIME_MAX as u64 - 2)));
623 assert_eq!(
624 a.freq(at(60 * (LFU_TIME_MAX as u64 - 1)), Lfu::DEFAULT),
625 LFU_INIT - 1
626 );
627 // Three minutes later, and two decays rather than three, which is the
628 // same one Redis is short by across a wrap. See `elapsed_minutes`.
629 assert_eq!(
630 a.freq(at(60 * (LFU_TIME_MAX as u64 + 1)), Lfu::DEFAULT),
631 LFU_INIT - 2
632 );
633 }
634
635 #[test]
636 fn a_hot_key_climbs_and_a_cold_one_does_not() {
637 // The point of the whole counter in one assertion. Same elapsed time,
638 // different traffic, and the busy key has to come out ahead.
639 let mut rng = Rng::new(1);
640 let mut hot = Access::lfu(at(0));
641 for i in 0..10_000u64 {
642 hot = hot.touched(at(i / 100), Lfu::DEFAULT, &mut rng);
643 }
644 let cold = Access::lfu(at(0));
645 assert!(
646 hot.freq(at(100), Lfu::DEFAULT) > cold.freq(at(100), Lfu::DEFAULT),
647 "hot {} cold {}",
648 hot.freq(at(100), Lfu::DEFAULT),
649 cold.freq(at(100), Lfu::DEFAULT)
650 );
651 }
652
653 #[test]
654 fn the_counter_flattens_out_rather_than_running_away() {
655 // Ten thousand accesses in the same minute, so no decay, and the counter
656 // has to be well short of ten thousand. If it were linear this would
657 // saturate in two hundred and fifty accesses and every busy key in the
658 // database would be pinned at 255 and indistinguishable from every other
659 // busy key, which is the failure the logarithm exists to avoid.
660 let mut rng = Rng::new(7);
661 let mut a = Access::lfu(at(0));
662 for _ in 0..10_000 {
663 a = a.touched(at(0), Lfu::DEFAULT, &mut rng);
664 }
665 let f = a.freq(at(0), Lfu::DEFAULT);
666 assert!((30..=90).contains(&f), "ten thousand accesses reached {f}");
667 }
668
669 #[test]
670 fn the_counter_saturates_instead_of_wrapping() {
671 // A counter that wrapped would turn the hottest key in the database into
672 // the coldest, so this walks it to the top with the odds forced even and
673 // checks that it stays there.
674 let mut rng = Rng::new(3);
675 let mut a = Access::lfu(at(0));
676 for _ in 0..100_000 {
677 a = a.touched(at(0), EVEN, &mut rng);
678 }
679 assert_eq!(a.freq(at(0), Lfu::DEFAULT), u8::MAX);
680 }
681
682 #[test]
683 fn a_decayed_key_climbs_again_at_even_odds() {
684 // A key that has decayed to zero is at the bottom, and the odds of
685 // moving are meant to be even there rather than one in one. Two
686 // accesses is enough to leave zero behind.
687 let mut rng = Rng::new(11);
688 let mut a = Access::lfu(at(0));
689 assert_eq!(a.freq(at(3_600), Lfu::DEFAULT), 0);
690 a = a.touched(at(3_600), Lfu::DEFAULT, &mut rng);
691 assert_eq!(a.freq(at(3_600), Lfu::DEFAULT), 1);
692 }
693
694 #[test]
695 fn the_field_is_twenty_four_bits_and_survives_a_round_trip() {
696 // It has to fit alongside whatever it is packed with, so nothing here
697 // may write above bit 23.
698 let mut rng = Rng::new(5);
699 let mut a = Access::lfu(at(0));
700 for i in 0..1_000u64 {
701 a = a.touched(at(i * 37), Lfu::DEFAULT, &mut rng);
702 assert_eq!(a.bits() >> BITS, 0, "wrote above bit 23");
703 assert_eq!(Access::from_bits(a.bits()), a);
704 }
705 for i in 0..1_000u64 {
706 let l = Access::lru(at(i * 100_003));
707 assert_eq!(l.bits() >> BITS, 0);
708 assert_eq!(Access::from_bits(l.bits()), l);
709 }
710 }
711
712 #[test]
713 fn bits_above_the_field_are_dropped_rather_than_trusted() {
714 assert_eq!(Access::from_bits(0xffff_ffff).bits(), MAX);
715 assert_eq!(Access::from_bits(0xff00_0000).bits(), 0);
716 }
717
718 /// Every policy name round trips, in either case, and nothing else parses.
719 ///
720 /// The names are wire strings. A typo in one of them is a `CONFIG SET` that
721 /// a client thinks worked and a `CONFIG GET` that reports something the
722 /// client never asked for, and neither end would notice.
723 #[test]
724 fn every_policy_name_survives_a_round_trip() {
725 // Written out rather than taken from `ALL`, because a test that reads
726 // its expectations out of the thing it is testing agrees with a typo.
727 // The order is Redis's own, which is what the CONFIG SET error message
728 // has to list them in.
729 let names = [
730 "volatile-lru",
731 "volatile-lfu",
732 "volatile-random",
733 "volatile-ttl",
734 "volatile-lrm",
735 "allkeys-lru",
736 "allkeys-lfu",
737 "allkeys-random",
738 "allkeys-lrm",
739 "noeviction",
740 ];
741 for name in names {
742 let p =
743 Policy::parse(name.as_bytes()).unwrap_or_else(|| panic!("{name} did not parse"));
744 assert_eq!(p.name(), name);
745 assert_eq!(Policy::parse(name.to_uppercase().as_bytes()), Some(p));
746 }
747 let listed: Vec<&str> = Policy::ALL.iter().map(|p| p.name()).collect();
748 assert_eq!(listed, names, "ALL is Redis's order and is all of them");
749 assert_eq!(Policy::parse(b"allkeys"), None);
750 assert_eq!(Policy::parse(b""), None);
751 assert_eq!(Policy::parse(b"allkeys-lru "), None, "no trimming here");
752 }
753
754 /// A key nothing has stamped reads as freshly used under both policies.
755 ///
756 /// This is the one that matters on the day somebody turns a policy on. Every
757 /// key already in the database is unstamped at that moment, and the wrong
758 /// answer here makes all of them the most attractive victims available, so
759 /// enabling `allkeys-lru` on a full database would throw the working set
760 /// away before it read any of it back.
761 #[test]
762 fn a_key_that_was_never_stamped_reads_as_freshly_used() {
763 let unset = Access::default();
764 assert!(unset.is_unset());
765 assert_eq!(unset.idle_secs(at(86_400 * 365)), 0, "not idle for a year");
766 assert_eq!(unset.freq(at(86_400 * 365), Lfu::DEFAULT), LFU_INIT);
767
768 // And it leaves the sentinel behind as soon as it is touched.
769 let mut rng = Rng::new(2);
770 let stamped = unset.touched(at(1_000), Lfu::DEFAULT, &mut rng);
771 assert!(!stamped.is_unset());
772 assert!(stamped.freq(at(1_000), Lfu::DEFAULT) >= LFU_INIT);
773 }
774
775 #[test]
776 fn the_default_is_to_refuse_the_write_rather_than_lose_data() {
777 assert_eq!(Policy::default(), Policy::NoEviction);
778 // It still keeps the clock, which is why OBJECT IDLETIME answers on a
779 // default server that will never evict anything.
780 assert!(Policy::default().stamps_on_read());
781 assert!(Policy::default().is_clock());
782 }
783
784 /// The axes each policy sits on, spelled out once so that a name added or a
785 /// `matches!` arm edited has to be edited here too.
786 #[test]
787 fn each_policy_is_on_the_axes_its_name_says() {
788 for (p, volatile, lru, lfu, lrm) in [
789 (Policy::VolatileLru, true, true, false, false),
790 (Policy::VolatileLfu, true, false, true, false),
791 (Policy::VolatileRandom, true, false, false, false),
792 (Policy::VolatileTtl, true, false, false, false),
793 (Policy::VolatileLrm, true, false, false, true),
794 (Policy::AllKeysLru, false, true, false, false),
795 (Policy::AllKeysLfu, false, false, true, false),
796 (Policy::AllKeysRandom, false, false, false, false),
797 (Policy::AllKeysLrm, false, false, false, true),
798 (Policy::NoEviction, false, false, false, false),
799 ] {
800 let n = p.name();
801 assert_eq!(p.volatile_only(), volatile, "{n} volatile");
802 assert_eq!(p.is_lru(), lru, "{n} lru");
803 assert_eq!(p.is_lfu(), lfu, "{n} lfu");
804 assert_eq!(p.is_lrm(), lrm, "{n} lrm");
805 // The field is a clock under everything except LFU, and a read
806 // moves it under everything except LRM. Those are two different
807 // questions with two different answers and it is worth pinning both.
808 assert_eq!(p.is_clock(), !lfu, "{n} clock");
809 assert_eq!(p.stamps_on_read(), !lrm, "{n} stamps on read");
810 assert!(p.stamps_on_write(), "{n} stamps on write");
811 }
812 }
813
814 /// Least recently modified is the pair that catches people out, so the rule
815 /// that separates it from least recently used gets its own test.
816 #[test]
817 fn only_the_lrm_pair_ignores_a_read() {
818 for p in Policy::ALL {
819 assert_eq!(p.stamps_on_read(), !p.is_lrm(), "{}", p.name());
820 }
821 // And they read the field the same way once it is set, because it is
822 // the same clock. Only the moment it is written apart.
823 assert!(Policy::AllKeysLrm.is_clock());
824 assert!(Policy::AllKeysLru.is_clock());
825 }
826}