#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Outcome {
Unchanged,
Changed,
Error,
Gone,
}
impl Outcome {
#[must_use]
pub fn as_tag(self) -> u8 {
match self {
Self::Unchanged => 0,
Self::Changed => 1,
Self::Error => 2,
Self::Gone => 3,
}
}
#[must_use]
pub fn from_tag(tag: u8) -> Self {
match tag {
0 => Self::Unchanged,
1 => Self::Changed,
3 => Self::Gone,
_ => Self::Error,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TtlConfig {
pub base_s: u32,
pub min_s: u32,
pub max_s: u32,
pub grow_num: u32,
pub grow_den: u32,
pub cut_num: u32,
pub cut_den: u32,
pub importance_bias_permille: u32,
}
impl Default for TtlConfig {
fn default() -> Self {
Self {
base_s: 21_600, min_s: 900, max_s: 2_592_000, grow_num: 3,
grow_den: 2,
cut_num: 1,
cut_den: 4,
importance_bias_permille: 500,
}
}
}
impl TtlConfig {
#[must_use]
pub fn next_interval(&self, interval_s: u32, outcome: Outcome) -> u32 {
let clamp =
|v: u64| -> u32 { v.clamp(u64::from(self.min_s), u64::from(self.max_s)) as u32 };
match outcome {
Outcome::Unchanged => clamp(
u64::from(interval_s) * u64::from(self.grow_num) / u64::from(self.grow_den.max(1)),
),
Outcome::Changed => clamp(
u64::from(interval_s) * u64::from(self.cut_num) / u64::from(self.cut_den.max(1)),
),
Outcome::Error => self.min_s,
Outcome::Gone => interval_s,
}
}
#[must_use]
pub fn next_check_at_ms(&self, fetched_at_ms: u64, interval_s: u32, rank_permille: u16) -> u64 {
let bias = u64::from(self.importance_bias_permille.min(1000));
let mult = 1000 - bias * u64::from(rank_permille.min(1000)) / 1000;
fetched_at_ms.saturating_add(u64::from(interval_s) * 1000 * mult / 1000)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grows_on_unchanged_until_ceiling() {
let cfg = TtlConfig::default();
let mut iv = cfg.base_s;
let mut prev = 0;
for _ in 0..64 {
iv = cfg.next_interval(iv, Outcome::Unchanged);
assert!(iv >= prev, "monotone growth");
assert!(iv <= cfg.max_s);
prev = iv;
}
assert_eq!(iv, cfg.max_s, "parks at the ceiling");
}
#[test]
fn changed_cuts_and_error_floors() {
let cfg = TtlConfig::default();
let grown = cfg.next_interval(cfg.max_s, Outcome::Unchanged);
let cut = cfg.next_interval(grown, Outcome::Changed);
assert!(cut < grown);
assert!(cut >= cfg.min_s);
assert_eq!(cfg.next_interval(grown, Outcome::Error), cfg.min_s);
assert_eq!(cfg.next_interval(grown, Outcome::Gone), grown);
}
#[test]
fn importance_shortens_the_effective_interval() {
let cfg = TtlConfig::default();
let nobody = cfg.next_check_at_ms(0, 1000, 0);
let star = cfg.next_check_at_ms(0, 1000, 1000);
assert_eq!(nobody, 1_000_000);
assert_eq!(star, 500_000, "top rank halves the interval at bias 500");
assert!(cfg.next_check_at_ms(0, 1000, 500) < nobody);
}
#[test]
fn outcome_tags_round_trip() {
for o in [Outcome::Unchanged, Outcome::Changed, Outcome::Error, Outcome::Gone] {
assert_eq!(Outcome::from_tag(o.as_tag()), o);
}
assert_eq!(Outcome::from_tag(200), Outcome::Error);
}
}