didbot_serve/write_meter.rs
1//! Per-account write metering: one token bucket on write operations and one
2//! on request bytes, both keyed by the authenticated account.
3//!
4//! Every account shares one write-ahead log, and `[capacity]
5//! log_budget_bytes` bounds it for all of them together. The server trims the
6//! log at `[capacity] checkpoint_log_bytes`, so what the budget has to hold is
7//! what all the accounts write between two checkpoints. The meter bounds how
8//! fast one account can grow it, so an account in a loop is one share of that
9//! rather than all of it. It bounds a rate, not a total: every account has its
10//! own buckets.
11//!
12//! # What is charged
13//!
14//! `createRecord`, `putRecord` and `deleteRecord` cost one operation;
15//! `applyWrites` costs one per write in its batch. Each also costs its
16//! request body's length in bytes. `uploadBlob` costs one operation and no
17//! bytes, because `[blobs] account_quota_bytes` already bounds what an
18//! account's blobs hold. A request is charged once write policy has allowed
19//! it, just before it commits, so neither a token that may not write nor a
20//! client that policy refuses can spend the account's share. A write the
21//! store refuses after that, such as a failed swap, has still spent its
22//! tokens. A write refused by the byte bucket has already spent its
23//! operations.
24//!
25//! The buckets are in memory and start full when the server starts.
26//!
27//! # The defaults, worked through
28//!
29//! A write's cost to the log does not depend on the record's size: the body
30//! goes to the record heap, and the log gets the record's slot and the
31//! commit naming it. Measured, that is about 1 KiB for a single
32//! `createRecord`, `putRecord` or `deleteRecord`, and about 240 bytes per
33//! write inside a 200-write `applyWrites`. So the operation bucket is what
34//! bounds the log:
35//!
36//! - At most [`DEFAULT_OPS_BURST`] + [`DEFAULT_OPS_PER_HOUR`] = 4,600
37//! operations in any hour.
38//! - 4,600 × 1 KiB ≈ 4.5 MiB of log an hour, about 0.2% of the 2 GiB budget
39//! a default 20 GiB volume gets.
40//! - One account writing flat out reaches the default 1 GiB watermark, where
41//! the server checkpoints and trims, in about 227 hours. A repository deep
42//! enough to double the cost of a write halves that.
43//!
44//! The byte bucket bounds the record heap, which shares the volume:
45//!
46//! - At most [`DEFAULT_BYTES_BURST`] + [`DEFAULT_BYTES_PER_HOUR`] = 72 MiB of
47//! request bodies in any hour, about 0.35% of a 20 GiB volume.
48//! - A loop of full 2 MiB requests gets four back to back, then one every
49//! 112 seconds.
50//!
51//! An agent writing a 1 KiB record every five seconds spends 720 operations
52//! and under 1 MiB an hour: 20% of the operation rate and about 1% of the
53//! byte rate.
54
55use std::num::NonZeroU32;
56use std::sync::Mutex;
57use std::time::{Duration, Instant};
58
59use governor::clock::{Clock, DefaultClock};
60use governor::middleware::NoOpMiddleware;
61use governor::state::keyed::HashMapStateStore;
62use governor::{Quota, RateLimiter};
63
64/// [`WriteLimits::ops_per_hour`]'s default: one operation a second.
65pub const DEFAULT_OPS_PER_HOUR: u32 = 3_600;
66
67/// [`WriteLimits::ops_burst`]'s default.
68pub const DEFAULT_OPS_BURST: u32 = 1_000;
69
70/// [`WriteLimits::bytes_per_hour`]'s default: 64 MiB.
71pub const DEFAULT_BYTES_PER_HOUR: u64 = 64 * 1024 * 1024;
72
73/// [`WriteLimits::bytes_burst`]'s default: 8 MiB, four full request bodies.
74pub const DEFAULT_BYTES_BURST: u64 = 8 * 1024 * 1024;
75
76/// How fast one account may write, and how much it may write at once.
77///
78/// `[writes]` in the configuration file; each field's range is documented
79/// there, on `didbot_config::sections::WritesSection`.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct WriteLimits {
82 /// Operations an account's bucket regains an hour.
83 pub ops_per_hour: u32,
84 /// Operations an account's bucket holds when full.
85 pub ops_burst: u32,
86 /// Request bytes an account's bucket regains an hour.
87 pub bytes_per_hour: u64,
88 /// Request bytes an account's bucket holds when full.
89 pub bytes_burst: u64,
90}
91
92impl Default for WriteLimits {
93 fn default() -> Self {
94 Self {
95 ops_per_hour: DEFAULT_OPS_PER_HOUR,
96 ops_burst: DEFAULT_OPS_BURST,
97 bytes_per_hour: DEFAULT_BYTES_PER_HOUR,
98 bytes_burst: DEFAULT_BYTES_BURST,
99 }
100 }
101}
102
103/// A `[writes]` key written outside the range it is documented with.
104#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
105#[error("{field} must be {low} to {high}, not {value}")]
106pub struct WriteLimitError {
107 /// The key, spelled as `[writes]` spells it.
108 pub field: &'static str,
109 /// The value that was written.
110 pub value: u64,
111 /// The lowest value the key accepts.
112 pub low: u64,
113 /// The highest value the key accepts.
114 pub high: u64,
115}
116
117impl WriteLimits {
118 /// Checks each key against the range `WritesSection` documents.
119 ///
120 /// The two burst floors are hard: a smaller `ops_burst` refuses every
121 /// full `applyWrites`, and a smaller `bytes_burst` refuses every request
122 /// body near the size limit, however long the caller waits.
123 pub fn check(&self) -> Result<(), WriteLimitError> {
124 for (field, value, low, high) in [
125 ("ops_per_hour", u64::from(self.ops_per_hour), 600, 360_000),
126 (
127 "ops_burst",
128 u64::from(self.ops_burst),
129 crate::wire::MAX_APPLY_WRITES as u64,
130 100_000,
131 ),
132 ("bytes_per_hour", self.bytes_per_hour, 8 << 20, 4 << 30),
133 (
134 "bytes_burst",
135 self.bytes_burst,
136 crate::routes::MAX_REQUEST_BODY as u64,
137 1 << 30,
138 ),
139 ] {
140 if value < low || value > high {
141 return Err(WriteLimitError {
142 field,
143 value,
144 low,
145 high,
146 });
147 }
148 }
149 Ok(())
150 }
151}
152
153/// Which bucket refused a write.
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum Bucket {
156 /// [`WriteLimits::ops_per_hour`] and [`WriteLimits::ops_burst`].
157 Ops,
158 /// [`WriteLimits::bytes_per_hour`] and [`WriteLimits::bytes_burst`].
159 Bytes,
160}
161
162/// A write the meter refused.
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct Refusal {
165 /// The bucket that was short.
166 pub bucket: Bucket,
167 /// What the write asked that bucket for.
168 pub wanted: u64,
169 /// How long until the bucket holds `wanted`. `None` when it never can,
170 /// because `wanted` is more than the bucket holds when full.
171 pub retry_after: Option<Duration>,
172}
173
174/// How often [`WriteMeter::charge`] drops buckets that have refilled.
175const SWEEP_EVERY: Duration = Duration::from_secs(60);
176
177type Keyed<C> =
178 RateLimiter<String, HashMapStateStore<String>, C, NoOpMiddleware<<C as Clock>::Instant>>;
179
180/// The two buckets for every account that has written lately. A bucket
181/// that has refilled is dropped, since a missing one starts full anyway.
182pub struct WriteMeter<C: Clock = DefaultClock> {
183 limits: WriteLimits,
184 ops: Keyed<C>,
185 bytes: Keyed<C>,
186 clock: C,
187 last_swept: Mutex<Instant>,
188}
189
190impl WriteMeter {
191 /// A meter on the system's monotonic clock.
192 pub fn new(limits: WriteLimits) -> Self {
193 Self::with_clock(limits, DefaultClock::default())
194 }
195}
196
197impl Default for WriteMeter {
198 fn default() -> Self {
199 Self::new(WriteLimits::default())
200 }
201}
202
203impl<C: Clock + Clone> WriteMeter<C> {
204 /// A meter on `clock`, which a test can advance by hand.
205 ///
206 /// # Panics
207 ///
208 /// If a rate or burst in `limits` is zero, a burst is past `u32::MAX`, or
209 /// a rate refills a token in under a nanosecond. Overflows if a slow rate
210 /// meets a large burst. [`WriteLimits::check`] refuses each of these.
211 pub fn with_clock(limits: WriteLimits, clock: C) -> Self {
212 let hour = Duration::from_secs(3600);
213 let ops = quota(hour / limits.ops_per_hour, u64::from(limits.ops_burst));
214 let bytes = quota(
215 Duration::from_nanos(
216 u64::try_from(hour.as_nanos() / u128::from(limits.bytes_per_hour))
217 .unwrap_or(u64::MAX),
218 ),
219 limits.bytes_burst,
220 );
221 Self {
222 limits,
223 ops: RateLimiter::hashmap_with_clock(ops, clock.clone()),
224 bytes: RateLimiter::hashmap_with_clock(bytes, clock.clone()),
225 clock,
226 last_swept: Mutex::new(Instant::now()),
227 }
228 }
229
230 /// The limits this meter enforces.
231 pub fn limits(&self) -> WriteLimits {
232 self.limits
233 }
234
235 /// Spends `ops` operations and `bytes` bytes of `did`'s buckets, or
236 /// refuses and spends nothing more.
237 ///
238 /// Operations are spent first. A write the byte bucket refuses keeps
239 /// them spent.
240 pub fn charge(&self, did: &str, ops: u32, bytes: u64) -> Result<(), Refusal> {
241 self.sweep();
242 let key = did.to_owned();
243 self.spend(&self.ops, &key, Bucket::Ops, u64::from(ops))?;
244 self.spend(&self.bytes, &key, Bucket::Bytes, bytes)
245 }
246
247 /// Drops refilled buckets, at most once per [`SWEEP_EVERY`], so the maps
248 /// hold the accounts writing now rather than every account that has.
249 fn sweep(&self) {
250 let mut last = self
251 .last_swept
252 .lock()
253 .unwrap_or_else(|poisoned| poisoned.into_inner());
254 if last.elapsed() < SWEEP_EVERY {
255 return;
256 }
257 *last = Instant::now();
258 for limiter in [&self.ops, &self.bytes] {
259 limiter.retain_recent();
260 limiter.shrink_to_fit();
261 }
262 }
263
264 fn spend(
265 &self,
266 limiter: &Keyed<C>,
267 key: &String,
268 bucket: Bucket,
269 wanted: u64,
270 ) -> Result<(), Refusal> {
271 let Some(n) = u32::try_from(wanted).ok().and_then(NonZeroU32::new) else {
272 if wanted == 0 {
273 return Ok(());
274 }
275 return Err(Refusal {
276 bucket,
277 wanted,
278 retry_after: None,
279 });
280 };
281 match limiter.check_key_n(key, n) {
282 Ok(Ok(())) => Ok(()),
283 Ok(Err(not_until)) => Err(Refusal {
284 bucket,
285 wanted,
286 retry_after: Some(not_until.wait_time_from(self.clock.now())),
287 }),
288 Err(_insufficient) => Err(Refusal {
289 bucket,
290 wanted,
291 retry_after: None,
292 }),
293 }
294 }
295}
296
297fn quota(per_token: Duration, burst: u64) -> Quota {
298 Quota::with_period(per_token)
299 .expect("a write rate refills each token in at least a nanosecond")
300 .allow_burst(
301 u32::try_from(burst)
302 .ok()
303 .and_then(NonZeroU32::new)
304 .expect("a write burst is 1 to u32::MAX"),
305 )
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use governor::clock::FakeRelativeClock;
312
313 const ALICE: &str = "did:web:alice.agents.localhost";
314
315 #[test]
316 fn the_defaults_are_inside_their_documented_ranges() {
317 assert_eq!(WriteLimits::default().check(), Ok(()));
318 }
319
320 #[test]
321 fn a_burst_below_one_full_request_is_refused() {
322 let limits = WriteLimits {
323 bytes_burst: 1024 * 1024,
324 ..WriteLimits::default()
325 };
326 assert_eq!(limits.check().unwrap_err().field, "bytes_burst");
327 let limits = WriteLimits {
328 ops_burst: 100,
329 ..WriteLimits::default()
330 };
331 assert_eq!(limits.check().unwrap_err().field, "ops_burst");
332 }
333
334 /// A day of an agent at work: a 1 KiB record every five seconds, a
335 /// 50-write `applyWrites` of 200 KiB every ten minutes, and a blob every
336 /// hour. None of it is refused at the defaults.
337 #[test]
338 fn a_busy_agent_stays_under_the_defaults_for_a_day() {
339 let clock = FakeRelativeClock::default();
340 let meter = WriteMeter::with_clock(WriteLimits::default(), clock.clone());
341 for tick in 0..(24 * 3600 / 5) {
342 meter
343 .charge(ALICE, 1, 1024)
344 .unwrap_or_else(|refusal| panic!("tick {tick}: {refusal:?}"));
345 if tick % 120 == 0 {
346 meter
347 .charge(ALICE, 50, 200 * 1024)
348 .unwrap_or_else(|refusal| panic!("batch at tick {tick}: {refusal:?}"));
349 }
350 if tick % 720 == 0 {
351 meter
352 .charge(ALICE, 1, 0)
353 .unwrap_or_else(|refusal| panic!("blob at tick {tick}: {refusal:?}"));
354 }
355 clock.advance(Duration::from_secs(5));
356 }
357 }
358
359 /// A loop of full-size requests that waits out each refusal gets its
360 /// burst, then the byte rate, and no more than the two in an hour.
361 #[test]
362 fn a_loop_of_full_size_requests_is_held_to_burst_plus_rate() {
363 let clock = FakeRelativeClock::default();
364 let limits = WriteLimits::default();
365 let meter = WriteMeter::with_clock(limits, clock.clone());
366 let body = crate::routes::MAX_REQUEST_BODY as u64;
367 let hour = Duration::from_secs(3600);
368 let (mut elapsed, mut written) = (Duration::ZERO, 0u64);
369 while elapsed < hour {
370 match meter.charge(ALICE, 1, body) {
371 Ok(()) => written += body,
372 Err(refusal) => {
373 assert_eq!(refusal.bucket, Bucket::Bytes);
374 let wait = refusal.retry_after.expect("a full body fits the bucket");
375 clock.advance(wait);
376 elapsed += wait;
377 }
378 }
379 }
380 let ceiling = limits.bytes_burst + limits.bytes_per_hour;
381 assert!(written <= ceiling, "{written} bytes past {ceiling}");
382 assert!(written >= ceiling - body, "only {written} bytes admitted");
383 }
384
385 #[test]
386 fn more_than_a_full_bucket_can_never_be_admitted() {
387 let meter = WriteMeter::new(WriteLimits::default());
388 let refusal = meter.charge(ALICE, DEFAULT_OPS_BURST + 1, 0).unwrap_err();
389 assert_eq!(refusal.bucket, Bucket::Ops);
390 assert_eq!(refusal.retry_after, None);
391 }
392}