use std::collections::{HashMap, hash_map};
use std::sync::Mutex;
use std::time::{Duration, SystemTime};
const HOUSEKEEPING_INTERVAL: Duration = Duration::from_secs(1);
pub struct TransactionTracker {
retention_period: Duration,
transactions: Mutex<Transactions>,
}
struct Transactions {
states: HashMap<(u32, String), TransactionState>,
last_housekeeping: SystemTime,
}
impl TransactionTracker {
pub fn new(retention_period: Duration) -> Self {
Self {
retention_period,
transactions: Mutex::new(Transactions { states: HashMap::new(), last_housekeeping: SystemTime::now() }),
}
}
pub fn is_retransmission(&self, xid: u32, client_addr: &str) -> bool {
let key = (xid, client_addr.to_string());
let mut transactions = self.transactions.lock().expect("unable to unlock transactions mutex");
let now = SystemTime::now();
if now.duration_since(transactions.last_housekeeping).map_or(true, |since| since >= HOUSEKEEPING_INTERVAL) {
housekeeping(&mut transactions.states, now, self.retention_period);
transactions.last_housekeeping = now;
}
if let hash_map::Entry::Vacant(e) = transactions.states.entry(key) {
e.insert(TransactionState::InProgress);
false
} else {
true
}
}
pub fn mark_processed(&self, xid: u32, client_addr: &str) {
let key = (xid, client_addr.to_string());
let completion_time = SystemTime::now();
let mut transactions = self.transactions.lock().expect("unable to unlock transactions mutex");
if let Some(tx) = transactions.states.get_mut(&key) {
*tx = TransactionState::Completed(completion_time);
}
}
}
fn housekeeping(transactions: &mut HashMap<(u32, String), TransactionState>, now: SystemTime, max_age: Duration) {
let cutoff = now - max_age;
transactions.retain(|_, v| match v {
TransactionState::InProgress => true,
TransactionState::Completed(completion_time) => *completion_time >= cutoff,
});
}
pub enum TransactionState {
InProgress,
Completed(SystemTime),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retransmissions_are_caught_and_old_ones_dropped() {
let tracker = TransactionTracker::new(Duration::from_millis(50));
assert!(!tracker.is_retransmission(1, "client"));
assert!(tracker.is_retransmission(1, "client"), "still in progress");
tracker.mark_processed(1, "client");
assert!(tracker.is_retransmission(1, "client"), "answered within the retention period");
assert!(!tracker.is_retransmission(1, "other"), "another client's");
std::thread::sleep(HOUSEKEEPING_INTERVAL + Duration::from_millis(100));
assert!(!tracker.is_retransmission(1, "client"));
}
}