use crate::hash::Hash;
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
}
pub fn field_expire_cycle(&mut self, budget: usize) -> usize {
if budget == 0 || self.field_deadlines.is_empty() {
return 0;
}
let now = self.clock.now_ms();
let mut left = budget.min(self.field_deadlines.len());
let mut looked = 0;
while left > 0 && !self.field_deadlines.is_empty() {
left -= 1;
if self.field_at >= self.field_deadlines.len() {
self.field_at = 0;
}
looked += 1;
if self.field_look(self.field_at, now) {
self.field_at += 1;
} else {
self.field_deadlines.swap_remove(self.field_at);
}
}
looked
}
fn field_look(&mut self, at: usize, now: u64) -> bool {
let key = &self.field_deadlines[at];
let Some(rec) = self.map.get(key) else {
return false;
};
let meta = value::Meta::from_byte(rec[0]);
if meta.kind() != value::Kind::Hash {
return false;
}
if meta.is_cold() {
return true;
}
let slot = value::slot(rec);
match self.hashes.get(slot).map(Hash::soonest_deadline) {
Some(Some(soonest)) if soonest <= now => {}
_ => return true,
}
let mut buf = core::mem::take(&mut self.scratch);
buf.clear();
buf.extend_from_slice(&self.field_deadlines[at]);
let gone = self.reap_fields(&buf, slot, now, true);
self.scratch = buf;
!gone
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clock::Clock;
use crate::many;
use crate::ttl::Cond;
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);
}
#[test]
fn a_field_nobody_reads_goes_on_its_own() {
let mut d = db();
let now = d.clock().now_ms();
d.hset(b"h", [(b"a".as_slice(), b"1".as_slice())].into_iter())
.expect("room");
d.hset(b"h", [(b"b".as_slice(), b"2".as_slice())].into_iter())
.expect("room");
d.hexpire(
b"h",
now + 100,
Cond::Always,
[b"a".as_slice()].into_iter(),
|_| {},
)
.expect("room");
d.clock().advance(200);
assert_eq!(d.field_expire_cycle(16), 1, "one hash to look at");
assert_eq!(d.hlen(b"h"), Ok(1), "and the other field is still there");
assert_eq!(d.expired_fields(), 1);
assert_eq!(d.expired_fields_active(), 1);
assert_eq!(d.expired_keys(), 0, "the key itself had no deadline");
}
#[test]
fn the_last_field_takes_the_key_with_it() {
let mut d = db();
let now = d.clock().now_ms();
d.hset(b"h", [(b"a".as_slice(), b"1".as_slice())].into_iter())
.expect("room");
d.hexpire(
b"h",
now + 100,
Cond::Always,
[b"a".as_slice()].into_iter(),
|_| {},
)
.expect("room");
d.clock().advance(200);
d.field_expire_cycle(16);
assert!(!d.exists(b"h"));
assert_eq!(d.len(), 0);
assert_eq!(d.bodies, 0, "and the body went back to its slab");
assert_eq!(d.field_expire_cycle(16), 0);
}
#[test]
fn hashes_with_no_field_deadlines_are_not_swept() {
let mut d = db();
for i in 0..100u32 {
d.hset(
format!("h{i}").as_bytes(),
[(b"a".as_slice(), b"1".as_slice())].into_iter(),
)
.expect("room");
}
assert_eq!(d.field_expire_cycle(4096), 0);
}
#[test]
fn a_name_that_is_no_longer_a_hash_comes_off_the_list() {
let mut d = db();
let now = d.clock().now_ms();
for i in 0..3u32 {
let k = format!("h{i}");
d.hset(
k.as_bytes(),
[(b"a".as_slice(), b"1".as_slice())].into_iter(),
)
.expect("room");
d.hexpire(
k.as_bytes(),
now + 100_000,
Cond::Always,
[b"a".as_slice()].into_iter(),
|_| {},
)
.expect("room");
}
d.del(b"h0");
d.set_plain(b"h1", b"v").expect("room");
d.field_expire_cycle(3);
assert_eq!(d.field_deadlines.len(), 1);
assert_eq!(d.field_deadlines[0].as_ref(), b"h2");
}
#[test]
fn a_hash_is_only_listed_once_however_often_it_is_touched() {
let mut d = db();
let now = d.clock().now_ms();
d.hset(b"h", [(b"a".as_slice(), b"1".as_slice())].into_iter())
.expect("room");
for i in 0..50u64 {
d.hexpire(
b"h",
now + 100_000 + i,
Cond::Always,
[b"a".as_slice()].into_iter(),
|_| {},
)
.expect("room");
}
assert_eq!(d.field_deadlines.len(), 1);
}
}