use crate::keyspace::Keyspace;
use crate::value;
use yo_common::Addr;
const PER_ROUND: usize = 20;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Cycle {
pub examined: usize,
pub volatile: usize,
pub expired: usize,
}
impl Keyspace {
pub fn expire_cycle(&mut self, budget: usize) -> Cycle {
let mut c = Cycle::default();
if budget == 0 || self.expires() == 0 {
return c;
}
let now = self.clock.now_ms();
loop {
let round = self.sweep_round(now, budget - c.examined, &mut c);
if c.examined >= budget || round.expired * 4 <= round.volatile {
return c;
}
}
}
fn sweep_round(&mut self, now: u64, budget: usize, c: &mut Cycle) -> Cycle {
let mut found = [Addr::NONE; PER_ROUND];
let mut n = 0usize;
let mut round = Cycle::default();
let r = self.rng.next_u64();
self.map.sample_tagged(r, |_key, rec, addr| {
round.examined += 1;
debug_assert!(value::has_expiry(rec), "a marked key with no deadline");
if value::has_expiry(rec) {
round.volatile += 1;
if value::is_expired(rec, now) {
found[n] = addr;
n += 1;
}
}
round.examined < budget && round.volatile < PER_ROUND && n < PER_ROUND
});
c.examined += round.examined;
c.volatile += round.volatile;
for addr in &found[..n] {
let mut buf = core::mem::take(&mut self.scratch);
buf.clear();
buf.extend_from_slice(self.map.entry_at(*addr).0);
let gone = self.drop_key(&buf);
self.scratch = buf;
if gone {
c.expired += 1;
round.expired += 1;
self.expired += 1;
}
}
round
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clock::Clock;
fn db() -> Keyspace {
Keyspace::with_clock(Clock::fixed(1_000))
}
#[test]
fn a_database_with_no_deadlines_anywhere_is_not_swept() {
let mut d = db();
for i in 0..2_000u32 {
d.set_plain(format!("k{i}").as_bytes(), b"v").expect("room");
}
let c = d.expire_cycle(4096);
assert_eq!(c, Cycle::default(), "it should not have drawn anything");
assert_eq!(d.len(), 2_000);
}
#[test]
fn dead_keys_nobody_reads_are_reclaimed() {
let mut d = db();
for i in 0..2_000u32 {
d.psetex(format!("d{i}").as_bytes(), 100, b"v")
.expect("room");
}
for i in 0..2_000u32 {
d.set_plain(format!("k{i}").as_bytes(), b"v").expect("room");
}
assert_eq!(d.expires(), 2_000);
d.clock().advance(200);
assert_eq!(
d.len(),
4_000,
"and nothing has read them, so they are all still there"
);
let mut spent = 0;
for _ in 0..500 {
let c = d.expire_cycle(4096);
spent += c.examined;
if d.expires() == 0 {
break;
}
}
assert_eq!(d.expires(), 0, "spent {spent} looks and did not finish");
assert_eq!(d.len(), 2_000, "the keys with no deadline are untouched");
assert_eq!(d.expired_keys(), 2_000);
for i in 0..2_000u32 {
assert!(d.exists(format!("k{i}").as_bytes()));
}
}
#[test]
fn a_sweep_only_looks_at_keys_that_could_have_expired() {
let mut d = db();
for i in 0..10_000u32 {
d.set_plain(format!("k{i}").as_bytes(), b"v").expect("room");
}
for i in 0..100u32 {
d.psetex(format!("d{i}").as_bytes(), 100, b"v")
.expect("room");
}
assert_eq!(d.expires(), 100);
d.clock().advance(200);
let mut spent = 0;
for _ in 0..100 {
let c = d.expire_cycle(4096);
spent += c.examined;
assert_eq!(
c.examined, c.volatile,
"it looked at a key with no deadline"
);
if d.expires() == 0 {
break;
}
}
assert_eq!(d.expires(), 0);
assert_eq!(d.len(), 10_000, "and it took none of the others");
assert!(
spent <= 200,
"spent {spent} looks to reclaim a hundred keys"
);
}
#[test]
fn a_key_whose_deadline_has_not_passed_is_left_alone() {
let mut d = db();
let now = d.clock().now_ms();
for i in 0..500u32 {
d.set_plain(format!("k{i}").as_bytes(), b"v").expect("room");
d.set_expiry(format!("k{i}").as_bytes(), Some(now + 900_000));
}
for _ in 0..20 {
let c = d.expire_cycle(4096);
assert_eq!(c.expired, 0, "it took a key that was still live");
}
assert_eq!(d.len(), 500);
}
#[test]
fn the_budget_is_a_ceiling_on_what_a_sweep_looks_at() {
let mut d = db();
for i in 0..5_000u32 {
d.psetex(format!("d{i}").as_bytes(), 100, b"v")
.expect("room");
}
d.clock().advance(200);
let c = d.expire_cycle(1);
assert!(c.examined <= 8, "one round looked at {} keys", c.examined);
assert!(d.expires() > 4_900, "and it barely touched the database");
}
#[test]
fn a_mostly_permanent_database_still_gets_its_dead_keys_back() {
let mut d = db();
for i in 0..10_000u32 {
d.set_plain(format!("k{i}").as_bytes(), b"v").expect("room");
}
for i in 0..100u32 {
d.psetex(format!("d{i}").as_bytes(), 100, b"v")
.expect("room");
}
d.clock().advance(200);
let mut spent = 0;
for _ in 0..2_000 {
spent += d.expire_cycle(4096).examined;
if d.expires() == 0 {
break;
}
}
assert_eq!(d.expires(), 0, "one percent volatile, spent {spent} looks");
assert_eq!(d.len(), 10_000);
}
#[test]
fn the_cycle_leaves_collections_and_their_bodies_correct() {
let mut d = db();
let now = d.clock().now_ms();
for i in 0..200u32 {
let k = format!("s{i}");
d.sadd(k.as_bytes(), [b"a".as_slice(), b"b".as_slice()].into_iter())
.expect("room");
d.set_expiry(k.as_bytes(), Some(now + 100));
}
d.sadd(b"keep", [b"a".as_slice()].into_iter())
.expect("room");
d.clock().advance(200);
for _ in 0..500 {
d.expire_cycle(4096);
if d.expires() == 0 {
break;
}
}
assert_eq!(d.len(), 1);
assert_eq!(d.scard(b"keep"), Ok(1));
assert_eq!(d.bodies, 1);
}
}