const NONE: u64 = u64::MAX;
pub const MAX_AT: u64 = 0x0000_3FFF_FFFF_FFFF;
#[must_use]
pub const fn valid_at(ms: u64) -> bool {
ms <= MAX_AT
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Cond {
#[default]
Always,
NotSet,
AlreadySet,
Greater,
Less,
LessAndSet,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Applied {
Missing = -2,
NotMet = 0,
Ok = 1,
Deleted = 2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Ask {
Missing,
NoDeadline,
At(u64),
}
impl Ask {
#[must_use]
pub const fn remaining_ms(self, now: u64) -> i64 {
match self {
Ask::Missing => -2,
Ask::NoDeadline => -1,
Ask::At(at) if at <= now => -2,
Ask::At(at) => (at - now) as i64,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Deadlines {
at: Vec<u64>,
rows: usize,
live: usize,
soonest: u64,
}
impl Deadlines {
#[must_use]
pub const fn new() -> Deadlines {
Deadlines {
at: Vec::new(),
rows: 0,
live: 0,
soonest: NONE,
}
}
#[inline]
#[must_use]
pub const fn is_empty(&self) -> bool {
self.live == 0
}
#[inline]
#[must_use]
pub const fn len(&self) -> usize {
self.live
}
#[inline]
#[must_use]
pub const fn armed(&self) -> bool {
!self.at.is_empty()
}
#[inline]
#[must_use]
pub const fn soonest(&self) -> Option<u64> {
if self.soonest == NONE {
None
} else {
Some(self.soonest)
}
}
pub fn refresh_soonest(&mut self) {
self.soonest = self.at.iter().copied().min().unwrap_or(NONE);
}
#[inline]
pub fn inserted(&mut self) {
self.rows += 1;
if !self.at.is_empty() {
self.at.push(NONE);
}
self.check();
}
#[inline]
fn check(&self) {
debug_assert!(
self.at.is_empty() || self.at.len() == self.rows,
"the deadlines have drifted from the table: {} against {} rows",
self.at.len(),
self.rows
);
}
pub fn removed(&mut self, row: usize) {
debug_assert!(row < self.rows, "removing a row that is not there");
self.rows -= 1;
if !self.at.is_empty() {
if self.at.swap_remove(row) != NONE {
self.live -= 1;
}
if self.live == 0 {
self.at = Vec::new();
self.soonest = NONE;
}
}
self.check();
}
pub fn cleared(&mut self) {
self.at = Vec::new();
self.rows = 0;
self.live = 0;
self.soonest = NONE;
}
#[inline]
#[must_use]
pub fn get(&self, row: usize) -> Option<u64> {
match self.at.get(row) {
Some(&NONE) | None => None,
Some(&at) => Some(at),
}
}
#[inline]
#[must_use]
pub fn is_expired(&self, row: usize, now: u64) -> bool {
if self.live == 0 {
return false;
}
match self.at.get(row) {
Some(&at) => at != NONE && at <= now,
None => false,
}
}
#[inline]
#[must_use]
pub fn ask(&self, row: usize) -> Ask {
match self.get(row) {
Some(at) => Ask::At(at),
None => Ask::NoDeadline,
}
}
pub fn set(&mut self, row: usize, at: u64, cond: Cond, now: u64) -> Applied {
debug_assert!(
row < self.rows,
"setting a deadline on a row that is not there"
);
self.check();
let prev = self.get(row);
match decide(prev, at, cond, now) {
Applied::Ok => {}
other => return other,
}
if self.at.is_empty() {
self.at = vec![NONE; self.rows];
}
if prev.is_none() {
self.live += 1;
}
self.at[row] = at;
self.soonest = self.soonest.min(at);
Applied::Ok
}
pub fn clear(&mut self, row: usize) -> Ask {
let Some(was) = self.get(row) else {
return Ask::NoDeadline;
};
self.at[row] = NONE;
self.live -= 1;
if self.live == 0 {
self.at = Vec::new();
self.soonest = NONE;
}
Ask::At(was)
}
#[must_use]
pub fn memory_bytes(&self) -> usize {
self.at.capacity() * size_of::<u64>()
}
}
#[must_use]
pub const fn decide(prev: Option<u64>, at: u64, cond: Cond, now: u64) -> Applied {
if !allowed(prev, at, cond) {
return Applied::NotMet;
}
if at <= now {
return Applied::Deleted;
}
Applied::Ok
}
const fn allowed(prev: Option<u64>, at: u64, cond: Cond) -> bool {
match (prev, cond) {
(_, Cond::Always) => true,
(None, Cond::AlreadySet | Cond::Greater | Cond::LessAndSet) => false,
(None, Cond::NotSet | Cond::Less) => true,
(Some(_), Cond::NotSet) => false,
(Some(_), Cond::AlreadySet) => true,
(Some(p), Cond::Greater) => p < at,
(Some(p), Cond::Less | Cond::LessAndSet) => p > at,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Elements;
fn with_rows(n: usize) -> Deadlines {
let mut d = Deadlines::new();
for _ in 0..n {
d.inserted();
}
d
}
#[test]
fn a_collection_with_no_field_ttl_costs_nothing() {
let mut d = with_rows(1000);
assert!(d.is_empty());
assert_eq!(d.memory_bytes(), 0);
assert_eq!(d.soonest(), None);
assert!(!d.is_expired(0, u64::MAX));
assert_eq!(d.ask(7), Ask::NoDeadline);
assert_eq!(d.set(7, 5000, Cond::Always, 0), Applied::Ok);
assert_eq!(d.memory_bytes(), 8000);
assert_eq!(d.len(), 1);
}
#[test]
fn a_deadline_goes_on_and_comes_off() {
let mut d = with_rows(4);
assert_eq!(d.set(2, 900, Cond::Always, 100), Applied::Ok);
assert_eq!(d.get(2), Some(900));
assert_eq!(d.ask(2), Ask::At(900));
assert_eq!(d.get(1), None);
assert_eq!(d.clear(2), Ask::At(900));
assert_eq!(d.get(2), None);
assert_eq!(
d.clear(2),
Ask::NoDeadline,
"twice is not an error, it is -1"
);
assert!(d.is_empty());
assert_eq!(d.memory_bytes(), 0, "the last one off gives the array back");
}
#[test]
fn a_field_is_expired_only_once_its_moment_has_passed() {
let mut d = with_rows(2);
d.set(0, 1000, Cond::Always, 0);
assert!(!d.is_expired(0, 999));
assert!(d.is_expired(0, 1000), "the deadline itself has passed");
assert!(d.is_expired(0, 1001));
assert!(!d.is_expired(1, u64::MAX), "no deadline is not expired");
}
#[test]
fn a_deadline_in_the_past_deletes_instead_of_being_stored() {
let mut d = with_rows(2);
assert_eq!(
d.set(0, 500, Cond::Always, 500),
Applied::Deleted,
"now counts"
);
assert_eq!(d.set(0, 499, Cond::Always, 500), Applied::Deleted);
assert_eq!(d.get(0), None, "nothing was stored");
assert_eq!(d.memory_bytes(), 0, "and nothing was allocated for it");
}
#[test]
fn the_conditions_are_the_ones_redis_applies() {
assert!(!allowed(None, 100, Cond::AlreadySet));
assert!(!allowed(None, 100, Cond::Greater));
assert!(allowed(None, 100, Cond::NotSet));
assert!(allowed(None, 100, Cond::Less));
assert!(allowed(None, 100, Cond::Always));
assert!(!allowed(Some(50), 100, Cond::NotSet));
assert!(allowed(Some(50), 100, Cond::AlreadySet));
assert!(allowed(Some(50), 100, Cond::Greater));
assert!(!allowed(Some(50), 20, Cond::Greater));
assert!(!allowed(Some(50), 50, Cond::Greater));
assert!(allowed(Some(50), 20, Cond::Less));
assert!(!allowed(Some(50), 100, Cond::Less));
assert!(!allowed(Some(50), 50, Cond::Less));
}
#[test]
fn a_condition_that_fails_changes_nothing() {
let mut d = with_rows(2);
d.set(0, 1000, Cond::Always, 0);
assert_eq!(d.set(0, 500, Cond::Greater, 0), Applied::NotMet);
assert_eq!(d.get(0), Some(1000));
assert_eq!(d.set(1, 500, Cond::AlreadySet, 0), Applied::NotMet);
assert_eq!(d.get(1), None);
assert_eq!(d.len(), 1);
}
#[test]
fn a_failed_condition_beats_a_past_deadline() {
let mut d = with_rows(1);
assert_eq!(d.set(0, 0, Cond::AlreadySet, 100), Applied::NotMet);
}
#[test]
fn deadlines_follow_the_table_through_a_swap_remove() {
let mut table: Elements<u32> = Elements::new();
let mut d = Deadlines::new();
for i in 0..5u32 {
table.insert(format!("f{i}").as_bytes(), i).expect("room");
d.inserted();
}
for i in 0..5 {
d.set(i, 1000 + i as u64, Cond::Always, 0);
}
let at = table.iter().position(|(n, _)| n == b"f1").expect("there");
table.remove_at(at).expect("there");
d.removed(at);
assert_eq!(table.len(), 4);
for (row, (name, _)) in table.iter().enumerate() {
let i: u64 = String::from_utf8_lossy(&name[1..]).parse().expect("f<n>");
assert_eq!(
d.get(row),
Some(1000 + i),
"field {} kept someone else's deadline",
String::from_utf8_lossy(name)
);
}
}
#[test]
fn removing_the_last_field_with_a_deadline_gives_the_array_back() {
let mut d = with_rows(3);
d.set(1, 1000, Cond::Always, 0);
assert_eq!(d.memory_bytes(), 24);
d.removed(1);
assert!(d.is_empty());
assert_eq!(d.memory_bytes(), 0);
assert_eq!(d.soonest(), None);
}
#[test]
fn the_soonest_deadline_is_a_bound_and_leans_early() {
let mut d = with_rows(3);
d.set(0, 5000, Cond::Always, 0);
d.set(1, 3000, Cond::Always, 0);
d.set(2, 9000, Cond::Always, 0);
assert_eq!(d.soonest(), Some(3000));
d.clear(1);
assert_eq!(
d.soonest(),
Some(3000),
"still early, which is the safe way"
);
d.refresh_soonest();
assert_eq!(
d.soonest(),
Some(5000),
"and exact once someone pays for it"
);
}
#[test]
fn what_is_left_is_what_redis_replies() {
assert_eq!(Ask::Missing.remaining_ms(0), -2);
assert_eq!(Ask::NoDeadline.remaining_ms(0), -1);
assert_eq!(Ask::At(5000).remaining_ms(1000), 4000);
assert_eq!(Ask::At(5000).remaining_ms(5000), -2, "due now is gone");
assert_eq!(Ask::At(5000).remaining_ms(9000), -2);
}
#[test]
fn the_ceiling_is_the_one_redis_enforces() {
assert_eq!(MAX_AT, 0x0000_FFFF_FFFF_FFFF >> 2);
assert!(valid_at(MAX_AT));
assert!(!valid_at(MAX_AT + 1));
assert!(valid_at(0));
}
#[test]
fn clearing_the_collection_forgets_everything() {
let mut d = with_rows(3);
d.set(0, 1000, Cond::Always, 0);
d.cleared();
assert!(d.is_empty());
assert_eq!(d.memory_bytes(), 0);
assert_eq!(d.soonest(), None);
d.inserted();
assert_eq!(d.set(0, 1000, Cond::Always, 0), Applied::Ok);
}
}