use std::collections::HashMap;
use std::time::{Duration, Instant};
use super::lock_key::LockKey;
const HOT_KEY_WINDOW: Duration = Duration::from_secs(10);
const HOT_KEY_ABORT_THRESHOLD: u32 = 3;
struct AbortCounter {
count: u32,
window_start: Instant,
}
pub struct HotKeyTable {
counts: HashMap<LockKey, AbortCounter>,
window: Duration,
threshold: u32,
}
impl HotKeyTable {
pub fn new() -> Self {
Self {
counts: HashMap::new(),
window: HOT_KEY_WINDOW,
threshold: HOT_KEY_ABORT_THRESHOLD,
}
}
pub fn record_abort(&mut self, key: &LockKey, now: Instant) {
let e = self.counts.entry(key.clone()).or_insert(AbortCounter {
count: 0,
window_start: now,
});
if now.duration_since(e.window_start) > self.window {
e.count = 0;
e.window_start = now;
}
e.count = e.count.saturating_add(1);
}
pub fn is_hot(&self, key: &LockKey, now: Instant) -> bool {
match self.counts.get(key) {
Some(e) if now.duration_since(e.window_start) <= self.window => {
e.count >= self.threshold
}
_ => false,
}
}
}
impl Default for HotKeyTable {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
fn key() -> LockKey {
LockKey::Surrogate {
collection: Arc::from("docs"),
surrogate: 42,
}
}
#[test]
fn below_threshold_is_not_hot() {
let mut table = HotKeyTable::new();
let now = Instant::now();
let k = key();
table.record_abort(&k, now);
table.record_abort(&k, now + Duration::from_secs(1));
assert!(!table.is_hot(&k, now + Duration::from_secs(1)));
}
#[test]
fn at_threshold_is_hot() {
let mut table = HotKeyTable::new();
let now = Instant::now();
let k = key();
table.record_abort(&k, now);
table.record_abort(&k, now + Duration::from_secs(1));
table.record_abort(&k, now + Duration::from_secs(2));
assert!(table.is_hot(&k, now + Duration::from_secs(2)));
}
#[test]
fn window_expiry_resets_to_not_hot() {
let mut table = HotKeyTable::new();
let now = Instant::now();
let k = key();
table.record_abort(&k, now);
table.record_abort(&k, now + Duration::from_secs(1));
table.record_abort(&k, now + Duration::from_secs(2));
assert!(table.is_hot(&k, now + Duration::from_secs(2)));
let later = now + HOT_KEY_WINDOW + Duration::from_secs(1);
table.record_abort(&k, later);
assert!(!table.is_hot(&k, later));
}
#[test]
fn fresh_key_is_not_hot() {
let table = HotKeyTable::new();
let now = Instant::now();
assert!(!table.is_hot(&key(), now));
}
}