Skip to main content

didbot_serve/
subscribers.rs

1//! How many streams this server holds open at once, and how many of them one
2//! caller may hold.
3//!
4//! `com.atproto.sync.subscribeRepos` is the route whose answer does not end. Every other route's cost is bounded by
5//! [`crate::MAX_CONCURRENT_REQUESTS`], which releases its permit once the
6//! response head is sent — which for a stream is immediately, and for the
7//! next hour of that stream's life is nothing at all. What each one keeps
8//! after that is a task and a frame in flight per subscriber, and the
9//! routes are public, so without a bound of their own a caller can open them
10//! until the process runs out of memory.
11//!
12//! Two bounds, for the reason `crate::rate_limit`'s own doc gives for its
13//! two: a total, which is the resource actually shared, and a per-address
14//! one, so a single caller cannot spend the whole total on its own. A slot is
15//! held for the life of the stream rather than the life of the request, which
16//! is what makes it a bound on concurrency rather than on arrival rate.
17
18use std::collections::HashMap;
19use std::sync::{Arc, Mutex};
20
21/// How many streams this server holds open across every caller.
22///
23/// Well past what an honest deployment reaches — a relay, an operator's
24/// dashboard, and one daemon per agent host — and far enough under
25/// [`crate::MAX_CONCURRENT_REQUESTS`] that a flood of subscriptions cannot
26/// crowd out ordinary requests.
27pub const DEFAULT_MAX_SUBSCRIBERS: usize = 128;
28
29/// How many of those one caller address may hold.
30///
31/// An agent host runs one daemon, and a relay crawls with one connection. A
32/// browser holding a dashboard open is the case that legitimately wants more
33/// than one, which is what this leaves room for.
34pub const DEFAULT_MAX_SUBSCRIBERS_PER_CALLER: usize = 8;
35
36/// The two bounds, and what is currently held against them.
37#[derive(Debug)]
38pub struct SubscriberLimit {
39    total: usize,
40    per_caller: usize,
41    held: Mutex<Counts>,
42}
43
44#[derive(Debug, Default)]
45struct Counts {
46    /// How many streams are open in total.
47    total: usize,
48    /// How many each caller holds. A caller with none is not in the map, so
49    /// this is bounded by `total` rather than by how many addresses have ever
50    /// subscribed.
51    per_caller: HashMap<String, usize>,
52}
53
54impl Default for SubscriberLimit {
55    fn default() -> Self {
56        Self::new(DEFAULT_MAX_SUBSCRIBERS, DEFAULT_MAX_SUBSCRIBERS_PER_CALLER)
57    }
58}
59
60impl SubscriberLimit {
61    /// A limiter with bounds of a deployment's own choosing.
62    pub fn new(total: usize, per_caller: usize) -> Self {
63        Self {
64            total,
65            per_caller,
66            held: Mutex::new(Counts::default()),
67        }
68    }
69
70    /// Takes a slot for `caller`, or `None` if either bound is full.
71    ///
72    /// The slot is released when what it is handed back to drops, so a caller
73    /// that disconnects gives its slot back without anything having to notice
74    /// the disconnection.
75    pub fn take(self: &Arc<Self>, caller: &str) -> Option<SubscriberSlot> {
76        let mut held = self.held.lock().unwrap_or_else(|p| p.into_inner());
77        if held.total >= self.total {
78            return None;
79        }
80        let mine = held.per_caller.entry(caller.to_owned()).or_insert(0);
81        if *mine >= self.per_caller {
82            // Left at zero rather than removed: the next `take` for this
83            // caller re-inserts it, and an entry at zero is the same size as
84            // the absence of one for as long as this lock is held.
85            if *mine == 0 {
86                held.per_caller.remove(caller);
87            }
88            return None;
89        }
90        *mine += 1;
91        held.total += 1;
92        Some(SubscriberSlot {
93            limit: Arc::clone(self),
94            caller: caller.to_owned(),
95        })
96    }
97
98    /// How many streams are open right now. For a test, and for anything
99    /// that later wants to log it.
100    pub fn held(&self) -> usize {
101        self.held.lock().unwrap_or_else(|p| p.into_inner()).total
102    }
103
104    fn release(&self, caller: &str) {
105        let mut held = self.held.lock().unwrap_or_else(|p| p.into_inner());
106        held.total = held.total.saturating_sub(1);
107        if let Some(mine) = held.per_caller.get_mut(caller) {
108            *mine -= 1;
109            if *mine == 0 {
110                held.per_caller.remove(caller);
111            }
112        }
113    }
114}
115
116/// One open stream's claim on [`SubscriberLimit`], released when it drops.
117///
118/// Carried by the stream itself rather than by the handler, because the
119/// handler returns as soon as the response head is written and the stream
120/// outlives it by as long as the subscriber stays connected.
121#[derive(Debug)]
122pub struct SubscriberSlot {
123    limit: Arc<SubscriberLimit>,
124    caller: String,
125}
126
127impl Drop for SubscriberSlot {
128    fn drop(&mut self) {
129        self.limit.release(&self.caller);
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn a_caller_may_not_hold_more_than_its_share() {
139        let limit = Arc::new(SubscriberLimit::new(10, 2));
140        let first = limit.take("1.2.3.4").expect("the first fits");
141        let _second = limit.take("1.2.3.4").expect("the second fits");
142        assert!(limit.take("1.2.3.4").is_none(), "a third is past the share");
143
144        // Another caller is unaffected, and the first gets its slot back the
145        // moment one is dropped.
146        assert!(limit.take("5.6.7.8").is_some());
147        drop(first);
148        assert!(limit.take("1.2.3.4").is_some());
149    }
150
151    #[test]
152    fn the_total_refuses_whoever_asks_past_it() {
153        let limit = Arc::new(SubscriberLimit::new(2, 10));
154        let held: Vec<_> = ["a", "b"]
155            .iter()
156            .map(|caller| limit.take(caller).expect("both fit"))
157            .collect();
158        assert!(limit.take("c").is_none());
159        assert_eq!(limit.held(), 2);
160        drop(held);
161        assert_eq!(limit.held(), 0);
162        assert!(limit.take("c").is_some());
163    }
164
165    /// A caller that has given every slot back leaves nothing behind, so the
166    /// map is a function of what is open rather than of how many addresses
167    /// have ever subscribed — which, on a public route, is unbounded.
168    #[test]
169    fn a_caller_holding_nothing_is_not_remembered() {
170        let limit = Arc::new(SubscriberLimit::new(100, 1));
171        for n in 0..1000 {
172            let slot = limit.take(&format!("10.0.0.{n}")).expect("each fits");
173            drop(slot);
174        }
175        assert_eq!(limit.held(), 0);
176        assert!(limit.held.lock().unwrap().per_caller.is_empty());
177    }
178}