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}