aboutsummaryrefslogtreecommitdiffstats
path: root/src/state/refused.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/refused.rs')
-rw-r--r--src/state/refused.rs68
1 files changed, 52 insertions, 16 deletions
diff --git a/src/state/refused.rs b/src/state/refused.rs
index bb37af0..b01a84a 100644
--- a/src/state/refused.rs
+++ b/src/state/refused.rs
@@ -1,42 +1,62 @@
//! 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.
+//! 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.
+/// How long a refused write counts against its user, and the node.
const WINDOW: Duration = Duration::from_secs(60);
-/// Each user's writes refused within the last `WINDOW`, oldest first.
+/// 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(HashMap<String, VecDeque<Instant>>);
+pub(crate) struct RefusedWrites {
+ by_user: HashMap<String, VecDeque<Instant>>,
+ all: VecDeque<Instant>,
+}
+
+/// `refused` without those that left the minute before `now`.
+fn prune(refused: &mut VecDeque<Instant>, 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.
+ /// 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 {
- let Some(refused) = self.0.get_mut(user) else {
+ prune(&mut self.all, now);
+ let Some(refused) = self.by_user.get_mut(user) else {
return true;
};
- while refused
- .front()
- .is_some_and(|at| now.duration_since(*at) >= WINDOW)
- {
- refused.pop_front();
- }
+ prune(refused, now);
if refused.is_empty() {
- self.0.remove(user);
+ self.by_user.remove(user);
return true;
}
- refused.len() < limit
+ 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.0.entry(user.to_owned()).or_default().push_back(now);
+ self.by_user
+ .entry(user.to_owned())
+ .or_default()
+ .push_back(now);
+ self.all.push_back(now);
}
}
@@ -56,4 +76,20 @@ mod tests {
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));
+ }
}