Skip to main content

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}