Skip to main content

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}