diff options
Diffstat (limited to 'src/state/refused.rs')
| -rw-r--r-- | src/state/refused.rs | 59 |
1 files changed, 59 insertions, 0 deletions
diff --git a/src/state/refused.rs b/src/state/refused.rs new file mode 100644 index 0000000..bb37af0 --- /dev/null +++ b/src/state/refused.rs @@ -0,0 +1,59 @@ +//! 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<String, VecDeque<Instant>>); + +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)); + } +} |
