//! Writes every node would refuse, counted by user (`AppState::mutate_doc_as`): each is //! rolled back by forking the document at the heads before it, which costs as much as //! the document's history, under the write lock. A user may have so many refused within //! a minute (`--refused-writes-per-minute`); past it, its writes are answered `429` //! untried until the oldest leave the minute. use std::collections::{HashMap, VecDeque}; use std::time::{Duration, Instant}; /// How long a refused write counts against its user. const WINDOW: Duration = Duration::from_secs(60); /// Each user's writes refused within the last `WINDOW`, oldest first. #[derive(Debug, Default)] pub(crate) struct RefusedWrites(HashMap>); impl RefusedWrites { /// Whether `user` may try another write at `now`: fewer than `limit` of its writes /// refused within the last minute. pub(crate) fn allows(&mut self, user: &str, limit: usize, now: Instant) -> bool { let Some(refused) = self.0.get_mut(user) else { return true; }; while refused .front() .is_some_and(|at| now.duration_since(*at) >= WINDOW) { refused.pop_front(); } if refused.is_empty() { self.0.remove(user); return true; } refused.len() < limit } /// One of `user`'s writes refused at `now`. pub(crate) fn note(&mut self, user: &str, now: Instant) { self.0.entry(user.to_owned()).or_default().push_back(now); } } #[cfg(test)] mod tests { use super::*; /// Past the limit within a minute, a user waits; others don't; a minute on, it may. #[test] fn refused_writes_per_user() { let mut refused = RefusedWrites::default(); let start = Instant::now(); assert!(refused.allows("alice", 2, start)); refused.note("alice", start); refused.note("alice", start); assert!(!refused.allows("alice", 2, start)); assert!(refused.allows("bob", 2, start)); assert!(refused.allows("alice", 2, start + WINDOW)); } }