use std::{
collections::HashMap,
sync::{
Mutex, PoisonError,
atomic::{AtomicU64, Ordering},
},
};
pub trait AgreementStore: Send + Sync {
fn record(&self, name: &str, agreed: bool);
fn stats(&self, name: &str) -> (u64, u64);
}
#[derive(Debug, Default)]
pub struct AgreementTable {
stats: Mutex<HashMap<String, (u64, u64)>>,
max_components: Option<usize>,
dropped: AtomicU64,
}
impl AgreementTable {
pub fn new() -> Self {
Self::default()
}
pub fn with_capacity(max_components: Option<usize>) -> Self {
Self {
stats: Mutex::new(HashMap::new()),
max_components,
dropped: AtomicU64::new(0),
}
}
pub fn record(&self, name: &str, agreed: bool) {
let mut stats = self.lock();
if let Some(entry) = stats.get_mut(name) {
entry.1 += 1;
if agreed {
entry.0 += 1;
}
return;
}
if let Some(cap) = self.max_components
&& stats.len() >= cap
{
drop(stats);
self.dropped.fetch_add(1, Ordering::Relaxed);
return;
}
stats.insert(name.to_owned(), (u64::from(agreed), 1));
}
pub fn dropped(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
pub fn stats(&self, name: &str) -> (u64, u64) {
self.lock().get(name).copied().unwrap_or((0, 0))
}
fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, (u64, u64)>> {
self.stats.lock().unwrap_or_else(PoisonError::into_inner)
}
}
impl AgreementStore for AgreementTable {
fn record(&self, name: &str, agreed: bool) {
AgreementTable::record(self, name, agreed);
}
fn stats(&self, name: &str) -> (u64, u64) {
AgreementTable::stats(self, name)
}
}
#[cfg(test)]
mod tests {
use super::AgreementTable;
#[test]
fn record_tracks_agree_and_total() {
let table = AgreementTable::new();
table.record("webgl", true);
table.record("webgl", true);
table.record("webgl", false);
assert_eq!(table.stats("webgl"), (2, 3));
}
#[test]
fn unseen_name_returns_zero() {
let table = AgreementTable::new();
assert_eq!(table.stats("nope"), (0, 0));
}
#[test]
fn distinct_key_cap_drops_overflow_but_keeps_counting_tracked() {
let table = AgreementTable::with_capacity(Some(2));
table.record("a", true);
table.record("b", false);
table.record("c", true); table.record("c", true); table.record("a", false);
assert_eq!(table.stats("a"), (1, 2));
assert_eq!(table.stats("b"), (0, 1));
assert_eq!(table.stats("c"), (0, 0)); assert_eq!(table.dropped(), 2);
}
}