beck_rt/
presence.rs

1//! Who is connected now — the roster `presence()` reads.
2//!
3//! [`docs/10-decisions.md`](../../../../../docs/10-decisions.md) D6: "**Presence** (who is connected
4//! now) ships v1 as a first-class non-durable `Signal` — it is both the natural demo of per-session
5//! fanout and its permanent stress test."
6//!
7//! The compiler's half is a source in the signal graph
8//! ([`beck_core::signal::Op::Presence`]); this is the fact that source reads. It is deliberately
9//! small, and everything interesting about it is a consequence of one sentence: **this is the only
10//! input to a view that moves without an event**. A session is not in the log either, and it is
11//! fixed for the life of a subscription; the accumulator moves only when the log does.
12//!
13//! # What that sentence forbids
14//!
15//! Nothing here is appended, snapshotted or replayed. A process that restarts comes back with an
16//! empty roster and fills it as clients reconnect, which is correct rather than lossy: who is
17//! connected to a process that no longer exists is nobody. The checker keeps this from mattering
18//! anywhere it would — `presence` cannot reach the chokepoint (`B0515`), so no event's existence
19//! ever depended on it.
20//!
21//! # The bound, and why it is here rather than in a later hardening pass
22//!
23//! The obvious implementation is a map from actor to a count, and that map is **unbounded memory
24//! keyed by a string the client chooses** — which is
25//! [`docs/82`](../../../../../docs/82-the-edge-report.md) §82.5's finding
26//! exactly, one subsystem over. Under [`crate::identity::DevIdentity`] the actor is whatever the
27//! connection said it was, so a client opening sockets under fresh names would grow this table
28//! until the process died.
29//!
30//! [`crate::quota`] answers the same problem by sharding into a fixed table, and that answer is not
31//! available here: a quota needs a *number* per actor and may share buckets, while a roster needs
32//! the actor's **name** and would be nonsense if two names collided. So the bound is a capacity:
33//! past [`Config::capacity`] distinct actors, a new one is **not recorded** and
34//! [`Registry::refused`] counts it. Presence then under-reports rather than growing, which is the
35//! failure this direction should have — a page that says "127 here" when 200 are connected is
36//! wrong in a way that costs nothing, and the opposite is a process that dies.
37//!
38//! An actor already in the roster is never refused, whatever the capacity: the bound is on how many
39//! *names* are held, not on how many connections one of them may open.
40
41use std::collections::BTreeMap;
42use std::sync::atomic::{AtomicU64, Ordering};
43use std::sync::{Arc, Mutex};
44
45use beck_core::Value;
46use tokio::sync::watch;
47
48/// How large a roster this process will hold.
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub struct Config {
51    /// How many distinct actors may be in the roster at once.
52    pub capacity: usize,
53}
54
55impl Default for Config {
56    /// 4,096 actors.
57    ///
58    /// The number is chosen the way [`crate::quota::Quota::default`]'s is: large enough that no
59    /// legitimate deployment of a single process meets it — a Beck process serving more than four
60    /// thousand *distinct* identities at one instant has a fanout problem before it has a roster
61    /// problem — and small enough that the table is a few hundred kilobytes at worst.
62    fn default() -> Config {
63        Config { capacity: 4096 }
64    }
65}
66
67/// The connection set of one application.
68pub struct Registry {
69    /// The counts, and the published value derived from them.
70    ///
71    /// One mutex rather than a lock-free map: it is taken twice per *connection* — once on join and
72    /// once on leave — and never on a render or an event. The published value is rebuilt under it
73    /// so that what a subscriber reads is always a roster that existed.
74    inner: Mutex<BTreeMap<Arc<str>, u32>>,
75    value: watch::Sender<Value>,
76    config: Config,
77    refused: AtomicU64,
78}
79
80impl Registry {
81    pub fn new(config: Config) -> Arc<Registry> {
82        Arc::new(Registry {
83            inner: Mutex::new(BTreeMap::new()),
84            value: watch::channel(beck_core::edge::presence([])).0,
85            config,
86            refused: AtomicU64::new(0),
87        })
88    }
89
90    /// The roster as a Beck value: `Map[Str, Int]`, actor to connections.
91    pub fn value(&self) -> Value {
92        self.value.borrow().clone()
93    }
94
95    /// Wake on every change to it. A subscription watches this **only** when the program's page
96    /// reads `presence` — a program that never asks who is connected must not re-render when
97    /// somebody connects.
98    pub fn watch(&self) -> watch::Receiver<Value> {
99        self.value.subscribe()
100    }
101
102    /// How many actors are in the roster.
103    pub fn here(&self) -> usize {
104        self.inner.lock().expect("presence").len()
105    }
106
107    /// How many joins the capacity refused, for the life of this process.
108    pub fn refused(&self) -> u64 {
109        self.refused.load(Ordering::Relaxed)
110    }
111
112    /// Record a connection. The roster holds it until the returned guard is dropped.
113    ///
114    /// A guard rather than a pair of calls, for the reason every other guard in this crate is one:
115    /// a subscription ends by returning, by erroring or by its socket dying, and a roster that only
116    /// removed an actor on the happy path would fill up with people who left hours ago.
117    pub fn join(self: &Arc<Self>, actor: &str) -> Guard {
118        let held = {
119            let mut counts = self.inner.lock().expect("presence");
120            let room = counts.len() < self.config.capacity;
121            match counts.get_mut(actor) {
122                Some(n) => {
123                    *n += 1;
124                    true
125                }
126                None if room => {
127                    counts.insert(Arc::from(actor), 1);
128                    true
129                }
130                // The bound. Counted rather than logged per occurrence: whoever is doing this is
131                // doing it at a rate that would make a log line the denial of service.
132                None => {
133                    self.refused.fetch_add(1, Ordering::Relaxed);
134                    false
135                }
136            }
137        };
138        if held {
139            self.publish();
140        }
141        Guard {
142            registry: held.then(|| self.clone()),
143            actor: Arc::from(actor),
144        }
145    }
146
147    fn leave(&self, actor: &str) {
148        {
149            let mut counts = self.inner.lock().expect("presence");
150            match counts.get_mut(actor) {
151                Some(n) if *n > 1 => *n -= 1,
152                Some(_) => {
153                    counts.remove(actor);
154                }
155                None => return,
156            }
157        }
158        self.publish();
159    }
160
161    /// Rebuild the published value from the counts.
162    ///
163    /// `O(actors)` per connection change, which is the shape this trades for: a roster is read on
164    /// every render of every subscriber and written once per connection, so the cost belongs on the
165    /// write. What a reader gets is one `Arc` bump.
166    fn publish(&self) {
167        let counts = self.inner.lock().expect("presence");
168        let value = beck_core::edge::presence(
169            counts
170                .iter()
171                .map(|(actor, n)| (actor.as_ref(), i64::from(*n))),
172        );
173        // `send_replace` and not `send`: `send` fails when there is no receiver, and — the part
174        // that matters — leaves the value it was given *unpublished*. Nothing may subscribe to a
175        // roster until a program that reads one has a connection, so every join before the first
176        // subscription would have been lost, including the first client's own.
177        self.value.send_replace(value);
178    }
179}
180
181/// One connection's membership of the roster.
182pub struct Guard {
183    /// `None` when the capacity refused this join: the guard still exists, so the caller has one
184    /// code path, and dropping it removes nothing because nothing was added.
185    registry: Option<Arc<Registry>>,
186    actor: Arc<str>,
187}
188
189impl Guard {
190    /// Whether this connection is in the roster. False only when the capacity refused it.
191    pub fn recorded(&self) -> bool {
192        self.registry.is_some()
193    }
194}
195
196impl Drop for Guard {
197    fn drop(&mut self) {
198        if let Some(registry) = &self.registry {
199            registry.leave(&self.actor);
200        }
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    fn roster(r: &Registry) -> Vec<(String, i64)> {
209        let value = r.value();
210        let map = value.as_map().expect("a map");
211        map.iter()
212            .map(|(k, v)| {
213                (
214                    k.as_str().expect("actor").to_string(),
215                    v.as_int().expect("count"),
216                )
217            })
218            .collect()
219    }
220
221    #[test]
222    fn a_connection_joins_and_its_guard_removes_it() {
223        let r = Registry::new(Config::default());
224        assert_eq!(roster(&r), Vec::new());
225        {
226            let _ana = r.join("ana");
227            assert_eq!(roster(&r), vec![("ana".to_string(), 1)]);
228            let _bo = r.join("bo");
229            assert_eq!(
230                roster(&r),
231                vec![("ana".to_string(), 1), ("bo".to_string(), 1)]
232            );
233        }
234        assert_eq!(roster(&r), Vec::new());
235    }
236
237    #[test]
238    fn one_actor_with_two_tabs_is_one_row_counted_twice() {
239        let r = Registry::new(Config::default());
240        let first = r.join("ana");
241        let second = r.join("ana");
242        assert_eq!(roster(&r), vec![("ana".to_string(), 2)]);
243        drop(second);
244        assert_eq!(roster(&r), vec![("ana".to_string(), 1)]);
245        drop(first);
246        assert_eq!(roster(&r), Vec::new());
247    }
248
249    /// §82.5's finding, one subsystem over: the table is keyed by a string the client chooses, so
250    /// what stops it is a capacity rather than a hope.
251    #[test]
252    fn the_capacity_refuses_rather_than_growing() {
253        let r = Registry::new(Config { capacity: 2 });
254        let _a = r.join("a");
255        let _b = r.join("b");
256        let c = r.join("c");
257        assert!(!c.recorded(), "the third actor is not in the roster");
258        assert_eq!(r.here(), 2);
259        assert_eq!(r.refused(), 1);
260        // An actor already held is never refused, however full the table is.
261        let again = r.join("a");
262        assert!(again.recorded());
263        assert_eq!(roster(&r), vec![("a".to_string(), 2), ("b".to_string(), 1)]);
264        // And dropping the refused guard removes nothing.
265        drop(c);
266        assert_eq!(r.here(), 2);
267    }
268
269    #[test]
270    fn a_watcher_wakes_on_a_join_and_on_a_leave() {
271        let r = Registry::new(Config::default());
272        let mut w = r.watch();
273        assert!(!w.has_changed().expect("live"));
274        let ana = r.join("ana");
275        assert!(w.has_changed().expect("live"));
276        w.mark_unchanged();
277        drop(ana);
278        assert!(w.has_changed().expect("live"));
279    }
280}