use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};
pub const TTL: Duration = Duration::from_secs(300);
pub const CAPACITY: usize = 100_000;
pub struct Redirects {
ttl: Duration,
capacity: usize,
targets: Mutex<HashMap<String, (Instant, String)>>,
}
impl Default for Redirects {
fn default() -> Self {
Self::new(TTL, CAPACITY)
}
}
impl Redirects {
pub fn new(ttl: Duration, capacity: usize) -> Self {
Self {
ttl,
capacity,
targets: Mutex::new(HashMap::new()),
}
}
pub fn get(&self, key: &str) -> Option<String> {
self.get_at(key, Instant::now())
}
pub fn insert(&self, key: String, target: String) {
self.insert_at(key, target, Instant::now());
}
fn get_at(&self, key: &str, now: Instant) -> Option<String> {
let targets = self.targets.lock().unwrap_or_else(|e| e.into_inner());
let (kept, target) = targets.get(key)?;
(now.saturating_duration_since(*kept) < self.ttl).then(|| target.clone())
}
fn insert_at(&self, key: String, target: String, now: Instant) {
let mut targets = self.targets.lock().unwrap_or_else(|e| e.into_inner());
if targets.len() >= self.capacity && !targets.contains_key(&key) {
targets.retain(|_, (kept, _)| now.saturating_duration_since(*kept) < self.ttl);
if targets.len() >= self.capacity {
targets.clear();
}
}
targets.insert(key, (now, target));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_target_is_kept_until_it_expires() {
let redirects = Redirects::new(Duration::from_secs(60), 10);
let t0 = Instant::now();
redirects.insert_at("10.5555/1".into(), "https://example.org/1".into(), t0);
assert_eq!(
redirects
.get_at("10.5555/1", t0 + Duration::from_secs(59))
.as_deref(),
Some("https://example.org/1")
);
assert_eq!(
redirects.get_at("10.5555/1", t0 + Duration::from_secs(60)),
None
);
assert_eq!(redirects.get_at("10.5555/2", t0), None);
}
#[test]
fn a_full_table_drops_expired_targets_first_then_everything() {
let redirects = Redirects::new(Duration::from_secs(60), 2);
let t0 = Instant::now();
redirects.insert_at("old".into(), "o".into(), t0);
redirects.insert_at("live".into(), "l".into(), t0 + Duration::from_secs(30));
let t1 = t0 + Duration::from_secs(70);
redirects.insert_at("new".into(), "n".into(), t1);
assert_eq!(redirects.get_at("live", t1).as_deref(), Some("l"));
assert_eq!(redirects.get_at("new", t1).as_deref(), Some("n"));
redirects.insert_at("newer".into(), "m".into(), t1);
assert_eq!(redirects.get_at("live", t1), None);
assert_eq!(redirects.get_at("newer", t1).as_deref(), Some("m"));
redirects.insert_at("newer".into(), "m2".into(), t1);
assert_eq!(redirects.get_at("newer", t1).as_deref(), Some("m2"));
}
}