use yo_common::rng::Rng;
const BITS: u32 = 24;
const MAX: u32 = (1 << BITS) - 1;
const LRU_RESOLUTION_MS: u64 = 1000;
pub const LFU_INIT: u8 = 5;
pub const LFU_LOG_FACTOR: u32 = 10;
pub const LFU_DECAY_MINUTES: u32 = 1;
const LFU_TIME_MAX: u32 = 0xffff;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Lfu {
pub log_factor: u32,
pub decay_minutes: u32,
}
impl Lfu {
pub const DEFAULT: Lfu = Lfu {
log_factor: LFU_LOG_FACTOR,
decay_minutes: LFU_DECAY_MINUTES,
};
}
impl Default for Lfu {
fn default() -> Lfu {
Lfu::DEFAULT
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Policy {
#[default]
NoEviction,
AllKeysLru,
AllKeysLfu,
AllKeysRandom,
AllKeysLrm,
VolatileLru,
VolatileLfu,
VolatileRandom,
VolatileTtl,
VolatileLrm,
}
impl Policy {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Policy::NoEviction => "noeviction",
Policy::AllKeysLru => "allkeys-lru",
Policy::AllKeysLfu => "allkeys-lfu",
Policy::AllKeysRandom => "allkeys-random",
Policy::AllKeysLrm => "allkeys-lrm",
Policy::VolatileLru => "volatile-lru",
Policy::VolatileLfu => "volatile-lfu",
Policy::VolatileRandom => "volatile-random",
Policy::VolatileTtl => "volatile-ttl",
Policy::VolatileLrm => "volatile-lrm",
}
}
pub const ALL: [Policy; 10] = [
Policy::VolatileLru,
Policy::VolatileLfu,
Policy::VolatileRandom,
Policy::VolatileTtl,
Policy::VolatileLrm,
Policy::AllKeysLru,
Policy::AllKeysLfu,
Policy::AllKeysRandom,
Policy::AllKeysLrm,
Policy::NoEviction,
];
#[must_use]
pub fn parse(s: &[u8]) -> Option<Policy> {
Policy::ALL
.into_iter()
.find(|p| s.eq_ignore_ascii_case(p.name().as_bytes()))
}
#[must_use]
pub const fn volatile_only(self) -> bool {
matches!(
self,
Policy::VolatileLru
| Policy::VolatileLfu
| Policy::VolatileRandom
| Policy::VolatileTtl
| Policy::VolatileLrm
)
}
#[must_use]
pub const fn is_lfu(self) -> bool {
matches!(self, Policy::AllKeysLfu | Policy::VolatileLfu)
}
#[must_use]
pub const fn is_lru(self) -> bool {
matches!(self, Policy::AllKeysLru | Policy::VolatileLru)
}
#[must_use]
pub const fn is_lrm(self) -> bool {
matches!(self, Policy::AllKeysLrm | Policy::VolatileLrm)
}
#[must_use]
pub const fn is_clock(self) -> bool {
!self.is_lfu()
}
#[must_use]
pub const fn stamps_on_read(self) -> bool {
!self.is_lrm()
}
#[must_use]
pub const fn stamps_on_write(self) -> bool {
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Access(u32);
impl Access {
#[inline]
#[must_use]
pub const fn bits(self) -> u32 {
self.0 & MAX
}
#[inline]
#[must_use]
pub const fn from_bits(bits: u32) -> Access {
Access(bits & MAX)
}
#[inline]
#[must_use]
pub const fn is_unset(self) -> bool {
self.bits() == 0
}
#[inline]
#[must_use]
pub const fn lru(now_ms: u64) -> Access {
Access(clock_at(now_ms))
}
#[inline]
#[must_use]
pub const fn lfu(now_ms: u64) -> Access {
Access::pack(minutes_at(now_ms), LFU_INIT)
}
#[inline]
#[must_use]
pub const fn idle_secs(self, now_ms: u64) -> u64 {
if self.is_unset() {
return 0;
}
let now = clock_at(now_ms);
let then = self.bits();
let ticks = if now >= then {
now - then
} else {
now + (MAX - then)
};
ticks as u64
}
#[inline]
#[must_use]
pub const fn freq(self, now_ms: u64, lfu: Lfu) -> u8 {
if self.is_unset() {
return LFU_INIT;
}
let counter = self.counter();
if lfu.decay_minutes == 0 {
return counter;
}
let periods = self.elapsed_minutes(now_ms) / lfu.decay_minutes;
if periods >= counter as u32 {
0
} else {
counter - periods as u8
}
}
#[must_use]
pub fn touched(self, now_ms: u64, lfu: Lfu, rng: &mut Rng) -> Access {
let counter = self.freq(now_ms, lfu);
Access::pack(minutes_at(now_ms), incr(counter, lfu.log_factor, rng))
}
#[inline]
const fn counter(self) -> u8 {
(self.0 & 0xff) as u8
}
#[inline]
const fn elapsed_minutes(self, now_ms: u64) -> u32 {
let now = minutes_at(now_ms);
let then = (self.0 >> 8) & LFU_TIME_MAX;
if now >= then {
now - then
} else {
LFU_TIME_MAX - then + now
}
}
#[inline]
const fn pack(minutes: u32, counter: u8) -> Access {
Access(((minutes & LFU_TIME_MAX) << 8) | counter as u32)
}
}
#[inline]
const fn clock_at(now_ms: u64) -> u32 {
((now_ms / LRU_RESOLUTION_MS) & MAX as u64) as u32
}
#[inline]
const fn minutes_at(now_ms: u64) -> u32 {
((now_ms / 60_000) & LFU_TIME_MAX as u64) as u32
}
#[inline]
fn incr(counter: u8, log_factor: u32, rng: &mut Rng) -> u8 {
if counter == u8::MAX {
return u8::MAX;
}
let base = counter.saturating_sub(LFU_INIT) as u32;
if rng.chance(1, base * log_factor + 1) {
counter + 1
} else {
counter
}
}
#[cfg(test)]
mod tests {
use super::*;
const fn at(secs: u64) -> u64 {
secs * 1000
}
const NO_DECAY: Lfu = Lfu {
decay_minutes: 0,
..Lfu::DEFAULT
};
const EVEN: Lfu = Lfu {
log_factor: 0,
..Lfu::DEFAULT
};
#[test]
fn idle_time_is_seconds_since_the_key_was_touched() {
let a = Access::lru(at(1_000));
assert_eq!(a.idle_secs(at(1_000)), 0);
assert_eq!(a.idle_secs(at(1_030)), 30);
assert_eq!(a.idle_secs(at(1_000 + 86_400)), 86_400);
}
#[test]
fn idle_time_ignores_the_part_of_a_second_that_has_not_finished() {
let a = Access::lru(1_500);
assert_eq!(a.idle_secs(1_900), 0);
assert_eq!(a.idle_secs(2_100), 1);
}
#[test]
fn idle_time_survives_the_clock_going_round() {
let wrap = at(MAX as u64);
let a = Access::lru(wrap - at(5));
assert_eq!(a.idle_secs(wrap - at(5)), 0);
assert_eq!(a.idle_secs(wrap + at(3)), 7);
}
#[test]
fn a_new_key_starts_at_the_initial_frequency() {
let a = Access::lfu(at(0));
assert_eq!(a.freq(at(0), Lfu::DEFAULT), LFU_INIT);
}
#[test]
fn the_counter_decays_one_step_per_decay_period() {
let a = Access::lfu(at(0));
assert_eq!(a.freq(at(60), Lfu::DEFAULT), LFU_INIT - 1);
assert_eq!(a.freq(at(180), Lfu::DEFAULT), LFU_INIT - 3);
assert_eq!(a.freq(at(3_600), Lfu::DEFAULT), 0, "and stops at zero");
}
#[test]
fn a_decay_time_of_zero_turns_decay_off() {
let a = Access::lfu(at(0));
assert_eq!(a.freq(at(86_400 * 30), NO_DECAY), LFU_INIT);
}
#[test]
fn the_counter_decays_across_its_own_wrap() {
let a = Access::lfu(at(60 * (LFU_TIME_MAX as u64 - 2)));
assert_eq!(
a.freq(at(60 * (LFU_TIME_MAX as u64 - 1)), Lfu::DEFAULT),
LFU_INIT - 1
);
assert_eq!(
a.freq(at(60 * (LFU_TIME_MAX as u64 + 1)), Lfu::DEFAULT),
LFU_INIT - 2
);
}
#[test]
fn a_hot_key_climbs_and_a_cold_one_does_not() {
let mut rng = Rng::new(1);
let mut hot = Access::lfu(at(0));
for i in 0..10_000u64 {
hot = hot.touched(at(i / 100), Lfu::DEFAULT, &mut rng);
}
let cold = Access::lfu(at(0));
assert!(
hot.freq(at(100), Lfu::DEFAULT) > cold.freq(at(100), Lfu::DEFAULT),
"hot {} cold {}",
hot.freq(at(100), Lfu::DEFAULT),
cold.freq(at(100), Lfu::DEFAULT)
);
}
#[test]
fn the_counter_flattens_out_rather_than_running_away() {
let mut rng = Rng::new(7);
let mut a = Access::lfu(at(0));
for _ in 0..10_000 {
a = a.touched(at(0), Lfu::DEFAULT, &mut rng);
}
let f = a.freq(at(0), Lfu::DEFAULT);
assert!((30..=90).contains(&f), "ten thousand accesses reached {f}");
}
#[test]
fn the_counter_saturates_instead_of_wrapping() {
let mut rng = Rng::new(3);
let mut a = Access::lfu(at(0));
for _ in 0..100_000 {
a = a.touched(at(0), EVEN, &mut rng);
}
assert_eq!(a.freq(at(0), Lfu::DEFAULT), u8::MAX);
}
#[test]
fn a_decayed_key_climbs_again_at_even_odds() {
let mut rng = Rng::new(11);
let mut a = Access::lfu(at(0));
assert_eq!(a.freq(at(3_600), Lfu::DEFAULT), 0);
a = a.touched(at(3_600), Lfu::DEFAULT, &mut rng);
assert_eq!(a.freq(at(3_600), Lfu::DEFAULT), 1);
}
#[test]
fn the_field_is_twenty_four_bits_and_survives_a_round_trip() {
let mut rng = Rng::new(5);
let mut a = Access::lfu(at(0));
for i in 0..1_000u64 {
a = a.touched(at(i * 37), Lfu::DEFAULT, &mut rng);
assert_eq!(a.bits() >> BITS, 0, "wrote above bit 23");
assert_eq!(Access::from_bits(a.bits()), a);
}
for i in 0..1_000u64 {
let l = Access::lru(at(i * 100_003));
assert_eq!(l.bits() >> BITS, 0);
assert_eq!(Access::from_bits(l.bits()), l);
}
}
#[test]
fn bits_above_the_field_are_dropped_rather_than_trusted() {
assert_eq!(Access::from_bits(0xffff_ffff).bits(), MAX);
assert_eq!(Access::from_bits(0xff00_0000).bits(), 0);
}
#[test]
fn every_policy_name_survives_a_round_trip() {
let names = [
"volatile-lru",
"volatile-lfu",
"volatile-random",
"volatile-ttl",
"volatile-lrm",
"allkeys-lru",
"allkeys-lfu",
"allkeys-random",
"allkeys-lrm",
"noeviction",
];
for name in names {
let p =
Policy::parse(name.as_bytes()).unwrap_or_else(|| panic!("{name} did not parse"));
assert_eq!(p.name(), name);
assert_eq!(Policy::parse(name.to_uppercase().as_bytes()), Some(p));
}
let listed: Vec<&str> = Policy::ALL.iter().map(|p| p.name()).collect();
assert_eq!(listed, names, "ALL is Redis's order and is all of them");
assert_eq!(Policy::parse(b"allkeys"), None);
assert_eq!(Policy::parse(b""), None);
assert_eq!(Policy::parse(b"allkeys-lru "), None, "no trimming here");
}
#[test]
fn a_key_that_was_never_stamped_reads_as_freshly_used() {
let unset = Access::default();
assert!(unset.is_unset());
assert_eq!(unset.idle_secs(at(86_400 * 365)), 0, "not idle for a year");
assert_eq!(unset.freq(at(86_400 * 365), Lfu::DEFAULT), LFU_INIT);
let mut rng = Rng::new(2);
let stamped = unset.touched(at(1_000), Lfu::DEFAULT, &mut rng);
assert!(!stamped.is_unset());
assert!(stamped.freq(at(1_000), Lfu::DEFAULT) >= LFU_INIT);
}
#[test]
fn the_default_is_to_refuse_the_write_rather_than_lose_data() {
assert_eq!(Policy::default(), Policy::NoEviction);
assert!(Policy::default().stamps_on_read());
assert!(Policy::default().is_clock());
}
#[test]
fn each_policy_is_on_the_axes_its_name_says() {
for (p, volatile, lru, lfu, lrm) in [
(Policy::VolatileLru, true, true, false, false),
(Policy::VolatileLfu, true, false, true, false),
(Policy::VolatileRandom, true, false, false, false),
(Policy::VolatileTtl, true, false, false, false),
(Policy::VolatileLrm, true, false, false, true),
(Policy::AllKeysLru, false, true, false, false),
(Policy::AllKeysLfu, false, false, true, false),
(Policy::AllKeysRandom, false, false, false, false),
(Policy::AllKeysLrm, false, false, false, true),
(Policy::NoEviction, false, false, false, false),
] {
let n = p.name();
assert_eq!(p.volatile_only(), volatile, "{n} volatile");
assert_eq!(p.is_lru(), lru, "{n} lru");
assert_eq!(p.is_lfu(), lfu, "{n} lfu");
assert_eq!(p.is_lrm(), lrm, "{n} lrm");
assert_eq!(p.is_clock(), !lfu, "{n} clock");
assert_eq!(p.stamps_on_read(), !lrm, "{n} stamps on read");
assert!(p.stamps_on_write(), "{n} stamps on write");
}
}
#[test]
fn only_the_lrm_pair_ignores_a_read() {
for p in Policy::ALL {
assert_eq!(p.stamps_on_read(), !p.is_lrm(), "{}", p.name());
}
assert!(Policy::AllKeysLrm.is_clock());
assert!(Policy::AllKeysLru.is_clock());
}
}