use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};
const TTL: Duration = Duration::from_secs(300);
#[derive(Debug)]
pub struct IdempotencyGuard {
ttl: Duration,
seen: Mutex<HashMap<String, Instant>>,
}
impl Default for IdempotencyGuard {
fn default() -> Self {
Self::new(TTL)
}
}
impl IdempotencyGuard {
pub fn new(ttl: Duration) -> Self {
Self {
ttl,
seen: Mutex::new(HashMap::new()),
}
}
pub fn check_and_register(&self, task_id: &str) -> bool {
if task_id.is_empty() {
return true;
}
let now = Instant::now();
let mut seen = self.seen.lock().expect("idempotency lock");
seen.retain(|_, &mut exp| exp > now);
if seen.contains_key(task_id) {
return false;
}
seen.insert(task_id.to_string(), now + self.ttl);
true
}
pub fn size(&self) -> usize {
self.seen.lock().expect("idempotency lock").len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_new_duplicate_rejected() {
let g = IdempotencyGuard::default();
assert!(g.check_and_register("t1"));
assert!(!g.check_and_register("t1"));
}
#[test]
fn distinct_ids_both_new() {
let g = IdempotencyGuard::default();
assert!(g.check_and_register("a"));
assert!(g.check_and_register("b"));
}
#[test]
fn empty_always_new() {
let g = IdempotencyGuard::default();
assert!(g.check_and_register(""));
assert!(g.check_and_register(""));
}
#[test]
fn ttl_expiry_allows_reuse() {
let g = IdempotencyGuard::new(Duration::from_millis(30));
assert!(g.check_and_register("t"));
assert!(!g.check_and_register("t"));
std::thread::sleep(Duration::from_millis(50));
assert!(g.check_and_register("t"));
}
}