//! 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`), and the node `NODE_FACTOR` times as many in //! all; past either, the user's writes (past the node's, those of every user with one //! refused within the minute) are answered `429` untried until the oldest leave it. A //! user none of whose writes was refused within the minute writes on. use std::collections::{HashMap, VecDeque}; use std::time::{Duration, Instant}; /// How long a refused write counts against its user, and the node. const WINDOW: Duration = Duration::from_secs(60); /// How many refused writes the node takes within a minute, in all, as many times the /// limit of one user: ten users can't hold the write lock ten times as long. pub(crate) const NODE_FACTOR: usize = 6; /// The writes refused within the last `WINDOW`, oldest first: each user's, and all. #[derive(Debug, Default)] pub(crate) struct RefusedWrites { by_user: HashMap>, all: VecDeque, } /// `refused` without those that left the minute before `now`. fn prune(refused: &mut VecDeque, now: Instant) { while refused .front() .is_some_and(|at| now.duration_since(*at) >= WINDOW) { refused.pop_front(); } } impl RefusedWrites { /// Whether `user` may try another write at `now`: fewer than `limit` of its writes /// refused within the last minute; and, if any was, fewer than `NODE_FACTOR` times /// `limit` of all users'. pub(crate) fn allows(&mut self, user: &str, limit: usize, now: Instant) -> bool { prune(&mut self.all, now); let Some(refused) = self.by_user.get_mut(user) else { return true; }; prune(refused, now); if refused.is_empty() { self.by_user.remove(user); return true; } refused.len() < limit && self.all.len() < limit.saturating_mul(NODE_FACTOR) } /// One of `user`'s writes refused at `now`. pub(crate) fn note(&mut self, user: &str, now: Instant) { self.by_user .entry(user.to_owned()) .or_default() .push_back(now); self.all.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)); } /// Past the node's limit, every user with a write refused within the minute waits; /// one without writes on. #[test] fn refused_writes_per_node() { let mut refused = RefusedWrites::default(); let start = Instant::now(); // Two each, below a user's limit of three; the node's taken in all. for user in 0..3 * NODE_FACTOR / 2 { refused.note(&format!("u{user}"), start); refused.note(&format!("u{user}"), start); } assert!(!refused.allows("u0", 3, start)); assert!(refused.allows("carol", 3, start)); assert!(refused.allows("u0", 3, start + WINDOW)); } }