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.reaped(&buf);
self.scratch = buf;
if gone {
c.expired += 1;
round.expired += 1;
}
}
round
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clock::Clock;
use crate::many;
fn db() -> Keyspace {
Keyspace::with_clock(Clock::fixed(1_000))
}
#[test]
fn a_database_with_no_deadlines_anywhere_is_not_swept() {
let n = many(2_000u32);
let mut d = db();
for i in 0..n {
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(), n as usize);
}
#[test]
fn dead_keys_nobody_reads_are_reclaimed() {
let n = many(2_000u32);
let mut d = db();
for i in 0..n {
d.psetex(format!("d{i}").as_bytes(), 100, b"v")
.expect("room");
}
for i in 0..n {
d.set_plain(format!("k{i}").as_bytes(), b"v").expect("room");
}
assert_eq!(d.expires(), n as usize);
d.clock().advance(200);
assert_eq!(
d.len(),
n as usize * 2,
"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(),
n as usize,
"the keys with no deadline are untouched"
);
assert_eq!(d.expired_keys(), u64::from(n));
for i in 0..n {
assert!(d.exists(format!("k{i}").as_bytes()));
}
}
#[test]
fn a_sweep_only_looks_at_keys_that_could_have_expired() {
let keys = many(10_000u32);
let mut d = db();
for i in 0..keys {
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(), keys as usize, "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 n = many(5_000u32);
let mut d = db();
for i in 0..n {
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);
let left = n as usize - n as usize / 50;
assert!(d.expires() > left, "and it barely touched the database");
}
#[test]
fn a_mostly_permanent_database_still_gets_its_dead_keys_back() {
let (keys, dying) = (many(10_000u32), many(100u32));
let mut d = db();
for i in 0..keys {
d.set_plain(format!("k{i}").as_bytes(), b"v").expect("room");
}
for i in 0..dying {
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(), keys as usize);
}
#[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);
}
}