mumble_server_runtime_shard/
ids.rs

1//! Identifiers, and the allocator that keeps them stable.
2//!
3//! Two rules are absolute on [`SessionId`] and [`ChannelId`]:
4//!
5//! - **Never reused.** A withdrawn identifier is dead forever, because the
6//!   Mumble client attaches local preferences (nickname overrides, per-user
7//!   volume, channel filter mode) to them.
8//! - **[`ChannelId::ROOT`] is the root**, and it belongs to the runtime rather
9//!   than to any shard.
10//!
11//! [`SessionId`] is deliberately not [`ConnectionId`]: not everyone visible is
12//! connected. The relation is `SessionId` ⊇ `ConnectionId` - an NPC, a player
13//! outside Mumble or a bot has a session but no socket, no key and no cursor.
14//! That is what [`Occupant::Synthetic`] is for.
15//!
16//! REF: docs/design/guide-implementation.md 4
17
18use std::collections::{BTreeMap, BTreeSet};
19use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
20
21use thiserror::Error;
22
23/// A shard: a unit of ownership, scheduling, rendering and id allocation.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
25pub struct ShardId(pub u64);
26
27/// A live connection: socket, crypto state, output queue.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
29pub struct ConnectionId(pub u64);
30
31/// A visible user with no voice connection: NPC, out-of-Mumble player, bot.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
33pub struct SyntheticId(pub u64);
34
35/// A visible user, as it goes on the wire.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
37pub struct SessionId(pub u32);
38
39/// A channel, as it goes on the wire.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
41pub struct ChannelId(pub u32);
42
43impl ChannelId {
44    /// The root channel. It exists client-side before any of our messages, and
45    /// shards render subtrees beneath it.
46    pub const ROOT: ChannelId = ChannelId(0);
47}
48
49/// A flavor-chosen stable identity for a channel.
50///
51/// # Why the builder asks for this
52///
53/// The guide's builder derives a channel's *scope* from its parent but never
54/// says where its *identity* comes from. Something has to: the diff needs to
55/// recognize the same channel across two renders, and an identity that drifts
56/// burns a wire id every turn.
57///
58/// Deriving it from the name would be the obvious guess and is exactly the trap
59/// the guide names (15, "never put changing content in an identity"): a channel
60/// whose name carries a clock would be destroyed and recreated ten times a
61/// second. So the flavor states the identity explicitly and keeps the name a
62/// field. Two channels rendered in one turn with the same key is a build error,
63/// not a merge.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
65pub struct ChannelKey(pub u64);
66
67impl ChannelKey {
68    /// The key [`crate::build::ShardBuilder::root`] gives the shard's root.
69    ///
70    /// Named because a flavor addressing its whole tree has to spell it, and a
71    /// bare zero in that position reads like a mistake.
72    pub const ROOT: ChannelKey = ChannelKey(0);
73}
74
75/// A flavor's name for a context action, stable across renders.
76///
77/// Same idea as [`ChannelKey`], for the same reason: the wire identifier is a
78/// free-form string the client stores and echoes back, and a flavor that used
79/// the label as the identifier would break its own buttons the day it renames
80/// one. The label stays a field; this is the identity.
81///
82/// REF: references/vendored/Mumble.proto : `ContextActionModify.action` and
83///   `ContextAction.action` are the same string.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
85pub struct ActionKey(pub u64);
86
87/// Who occupies a rendered user slot.
88///
89/// This *is* the user's identity, which is why - unlike channels - users need no
90/// separate key.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
92pub enum Occupant {
93    /// A player actually connected.
94    Connection(ConnectionId),
95    /// A visible user with no voice connection.
96    Synthetic(SyntheticId),
97}
98
99impl Occupant {
100    /// The connection behind this occupant, if any.
101    #[must_use]
102    pub fn connection(self) -> Option<ConnectionId> {
103        match self {
104            Occupant::Connection(connection) => Some(connection),
105            Occupant::Synthetic(_) => None,
106        }
107    }
108}
109
110/// The identifier space is exhausted.
111///
112/// Reached only after 2^32 distinct channels or users in one runtime's lifetime.
113/// It refuses rather than wrapping, because wrapping *is* reuse and the client
114/// would silently apply one user's local settings to another.
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
116pub enum Exhausted {
117    #[error("this runtime has allocated every channel id")]
118    Channels,
119    #[error("this runtime has allocated every session id")]
120    Sessions,
121}
122
123/// Maps stable identities onto wire identifiers until a channel is withdrawn.
124///
125/// Session entries live forever because an official client keeps its own user
126/// across migrations. Channel entries do not: once any client has accepted a
127/// `ChannelRemove`, that wire identifier is dead even if the same semantic
128/// channel later returns. Retiring a mapping never recycles its number because
129/// the allocation cursor only moves forward.
130///
131/// # Why one allocator serves the whole runtime
132///
133/// Allocating per shard looks natural - a shard owns its subtree - and it is
134/// wrong as soon as a connection can move between shards. The client keys its
135/// model on the wire id, so shard A withdrawing channel 5 and shard B later
136/// creating its own channel 5 is, from the client's seat, one identifier coming
137/// back as a different thing. Sessions are worse: the official client refuses to
138/// remove **itself** from its model, so a migrating connection that changed
139/// session would be a second user forever.
140///
141/// Keying channels on `(shard, key)` and sessions on [`Occupant`] fixes both at
142/// once. A migration keeps its session for free, because the occupant did not
143/// change.
144///
145/// REF: references/mumble/src/mumble/Messages.cpp : `MainWindow::msgUserRemove`
146///   ends with `if (pDst != pSelf) pmModel->removeUser(pDst);`.
147#[derive(Debug)]
148pub struct IdAllocator {
149    channels: BTreeMap<(ShardId, ChannelKey), ChannelId>,
150    sessions: BTreeMap<Occupant, SessionId>,
151    /// The next number to hand out. `None` once the space is spent, which is a
152    /// distinct state from "the last number", so the final id is actually used
153    /// rather than refused.
154    next_channel: Option<u32>,
155    next_session: Option<u32>,
156}
157
158impl Default for IdAllocator {
159    fn default() -> IdAllocator {
160        IdAllocator::new()
161    }
162}
163
164impl IdAllocator {
165    /// A fresh allocator. Channel ids start at 1: zero is the runtime's root.
166    #[must_use]
167    pub fn new() -> IdAllocator {
168        IdAllocator {
169            channels: BTreeMap::new(),
170            sessions: BTreeMap::new(),
171            next_channel: Some(1),
172            next_session: Some(1),
173        }
174    }
175
176    /// The id for `key` within `shard`, allocating one the first time it is seen.
177    ///
178    /// # Errors
179    ///
180    /// [`Exhausted::Channels`] once every id has been handed out.
181    pub fn channel(&mut self, shard: ShardId, key: ChannelKey) -> Result<ChannelId, Exhausted> {
182        if let Some(existing) = self.channels.get(&(shard, key)) {
183            return Ok(*existing);
184        }
185        let raw = self.next_channel.ok_or(Exhausted::Channels)?;
186        self.next_channel = raw.checked_add(1);
187        let id = ChannelId(raw);
188        self.channels.insert((shard, key), id);
189        Ok(id)
190    }
191
192    /// The session for `occupant`, allocating one the first time it is seen.
193    ///
194    /// # Errors
195    ///
196    /// [`Exhausted::Sessions`] once every session has been handed out.
197    pub fn session(&mut self, occupant: Occupant) -> Result<SessionId, Exhausted> {
198        if let Some(existing) = self.sessions.get(&occupant) {
199            return Ok(*existing);
200        }
201        let raw = self.next_session.ok_or(Exhausted::Sessions)?;
202        self.next_session = raw.checked_add(1);
203        let id = SessionId(raw);
204        self.sessions.insert(occupant, id);
205        Ok(id)
206    }
207
208    /// The session already allocated for `occupant`, without allocating.
209    #[must_use]
210    pub fn allocated_session(&self, occupant: Occupant) -> Option<SessionId> {
211        self.sessions.get(&occupant).copied()
212    }
213
214    /// The id already allocated for `key` within `shard`, without allocating.
215    ///
216    /// What a lookup outside a render needs: asking [`IdAllocator::channel`]
217    /// there would hand out an id for a key nothing rendered, and an id handed
218    /// out is an id spent for the life of the runtime.
219    #[must_use]
220    pub fn allocated_channel(&self, shard: ShardId, key: ChannelKey) -> Option<ChannelId> {
221        self.channels.get(&(shard, key)).copied()
222    }
223
224    /// Retire every channel identity from `shard` that the accepted render no
225    /// longer contains.
226    ///
227    /// Removing the mapping does not recycle its numeric id: `next_channel`
228    /// only moves forward. If the same semantic key returns later, it therefore
229    /// receives a fresh id, as required after the client has observed a
230    /// `ChannelRemove`.
231    pub fn retain_channels(&mut self, shard: ShardId, retained: &BTreeSet<ChannelKey>) {
232        self.channels
233            .retain(|(owner, key), _| *owner != shard || retained.contains(key));
234    }
235
236    /// Retire channel ids one client has accepted as removed.
237    pub fn retire_channel_ids(&mut self, shard: ShardId, retired: &BTreeSet<ChannelId>) {
238        self.channels
239            .retain(|(owner, _), id| *owner != shard || !retired.contains(id));
240    }
241}
242
243/// One allocator, shared by every shard of a runtime.
244///
245/// The lock is taken per allocation rather than for a whole render, so two
246/// shards rendering on two cores contend for a few nanoseconds at a time instead
247/// of serializing. It is never held across an `.await`: every method here
248/// returns before the caller can suspend.
249#[derive(Debug, Clone, Default)]
250pub struct SharedIds {
251    inner: Arc<Mutex<IdAllocator>>,
252}
253
254impl SharedIds {
255    #[must_use]
256    pub fn new() -> SharedIds {
257        SharedIds::default()
258    }
259
260    /// The id for `key` within `shard`.
261    ///
262    /// # Errors
263    ///
264    /// [`Exhausted::Channels`] once every id has been handed out.
265    pub fn channel(&self, shard: ShardId, key: ChannelKey) -> Result<ChannelId, Exhausted> {
266        self.guard().channel(shard, key)
267    }
268
269    /// The session for `occupant`, stable for as long as the runtime lives.
270    ///
271    /// # Errors
272    ///
273    /// [`Exhausted::Sessions`] once every session has been handed out.
274    pub fn session(&self, occupant: Occupant) -> Result<SessionId, Exhausted> {
275        self.guard().session(occupant)
276    }
277
278    /// The session already allocated for `occupant`, without allocating.
279    #[must_use]
280    pub fn allocated_session(&self, occupant: Occupant) -> Option<SessionId> {
281        self.guard().allocated_session(occupant)
282    }
283
284    /// The id already allocated for `key` within `shard`, without allocating.
285    #[must_use]
286    pub fn allocated_channel(&self, shard: ShardId, key: ChannelKey) -> Option<ChannelId> {
287        self.guard().allocated_channel(shard, key)
288    }
289
290    /// Retire the channel identities absent from one shard's accepted render.
291    pub fn retain_channels(&self, shard: ShardId, retained: &BTreeSet<ChannelKey>) {
292        self.guard().retain_channels(shard, retained);
293    }
294
295    /// Retire channel ids one client has accepted as removed.
296    pub fn retire_channel_ids(&self, shard: ShardId, retired: &BTreeSet<ChannelId>) {
297        self.guard().retire_channel_ids(shard, retired);
298    }
299
300    /// A poisoned allocator means a panic unwound while an id was being handed
301    /// out. The maps are still structurally sound - nothing here can leave one
302    /// half-updated - and refusing every later allocation would take the whole
303    /// runtime down over one thread, so the guard is recovered rather than
304    /// propagated.
305    fn guard(&self) -> MutexGuard<'_, IdAllocator> {
306        self.inner.lock().unwrap_or_else(PoisonError::into_inner)
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    #![allow(clippy::expect_used)]
313
314    use super::*;
315
316    #[test]
317    fn the_same_identity_always_gets_the_same_id() {
318        let mut ids = IdAllocator::new();
319        let first = ids.channel(ShardId(1), ChannelKey(7)).expect("allocatable");
320        let other = ids.channel(ShardId(1), ChannelKey(8)).expect("allocatable");
321        let again = ids.channel(ShardId(1), ChannelKey(7)).expect("allocatable");
322
323        assert_eq!(first, again);
324        assert_ne!(first, other);
325    }
326
327    #[test]
328    fn a_channel_that_returns_after_removal_gets_a_fresh_id() {
329        let mut ids = IdAllocator::new();
330        let shard = ShardId(1);
331        let key = ChannelKey(7);
332        let before = ids.channel(shard, key).expect("allocatable");
333
334        ids.retain_channels(shard, &BTreeSet::new());
335        let after = ids.channel(shard, key).expect("allocatable");
336
337        assert_ne!(before, after, "a retired wire id is dead forever");
338    }
339
340    #[test]
341    fn retaining_one_shard_does_not_retire_another_shards_channels() {
342        let mut ids = IdAllocator::new();
343        let key = ChannelKey(7);
344        let here = ids.channel(ShardId(1), key).expect("allocatable");
345        let there = ids.channel(ShardId(2), key).expect("allocatable");
346
347        ids.retain_channels(ShardId(1), &BTreeSet::new());
348
349        assert_ne!(ids.channel(ShardId(1), key).expect("allocatable"), here);
350        assert_eq!(ids.channel(ShardId(2), key), Ok(there));
351    }
352
353    #[test]
354    fn retiring_a_wire_id_rekeys_only_its_channel() {
355        let mut ids = IdAllocator::new();
356        let shard = ShardId(1);
357        let retired_key = ChannelKey(7);
358        let kept_key = ChannelKey(8);
359        let retired = ids.channel(shard, retired_key).expect("allocatable");
360        let kept = ids.channel(shard, kept_key).expect("allocatable");
361
362        ids.retire_channel_ids(shard, &BTreeSet::from([retired]));
363
364        assert_ne!(ids.channel(shard, retired_key), Ok(retired));
365        assert_eq!(ids.channel(shard, kept_key), Ok(kept));
366    }
367
368    #[test]
369    fn channel_ids_never_collide_with_the_runtime_root() {
370        let mut ids = IdAllocator::new();
371        for key in 0..16 {
372            let id = ids
373                .channel(ShardId(1), ChannelKey(key))
374                .expect("allocatable");
375            assert_ne!(id, ChannelId::ROOT, "the root belongs to the runtime");
376        }
377    }
378
379    #[test]
380    fn an_identity_that_vanishes_and_returns_keeps_its_id() {
381        // Nothing withdraws an entry, so this is a statement about the whole
382        // type: there is no removal path that could free a number for reuse.
383        let mut ids = IdAllocator::new();
384        let alice = Occupant::Connection(ConnectionId(1));
385        let before = ids.session(alice).expect("allocatable");
386
387        for other in 2..10 {
388            let _ = ids.session(Occupant::Connection(ConnectionId(other)));
389        }
390
391        assert_eq!(ids.session(alice).expect("allocatable"), before);
392    }
393
394    #[test]
395    fn connections_and_synthetics_share_the_session_space_without_colliding() {
396        let mut ids = IdAllocator::new();
397        let connected = ids
398            .session(Occupant::Connection(ConnectionId(3)))
399            .expect("allocatable");
400        let synthetic = ids
401            .session(Occupant::Synthetic(SyntheticId(3)))
402            .expect("allocatable");
403
404        assert_ne!(
405            connected, synthetic,
406            "the same raw number in two occupant kinds is two different users"
407        );
408    }
409
410    #[test]
411    fn exhaustion_refuses_rather_than_wrapping() {
412        let mut ids = IdAllocator::new();
413        ids.next_channel = Some(u32::MAX);
414
415        // The very last number must actually be handed out: refusing it would
416        // be an off-by-one that silently costs a channel.
417        let last = ids.channel(ShardId(1), ChannelKey(1)).expect("one left");
418        assert_eq!(last, ChannelId(u32::MAX));
419        assert_eq!(
420            ids.channel(ShardId(1), ChannelKey(2)),
421            Err(Exhausted::Channels)
422        );
423
424        // And an identity already allocated still resolves after exhaustion.
425        assert_eq!(ids.channel(ShardId(1), ChannelKey(1)), Ok(last));
426    }
427
428    #[test]
429    fn the_same_key_in_two_shards_is_two_channels() {
430        // Without this, a connection migrating from one shard to the other
431        // would be told to remove channel 5 and then to create channel 5 as a
432        // different thing, which is identifier reuse seen from the client.
433        let mut ids = IdAllocator::new();
434        let here = ids.channel(ShardId(1), ChannelKey(7)).expect("allocatable");
435        let there = ids.channel(ShardId(2), ChannelKey(7)).expect("allocatable");
436
437        assert_ne!(here, there);
438    }
439
440    #[test]
441    fn a_connection_keeps_its_session_across_shards() {
442        // The session is keyed on the occupant, which does not change when a
443        // connection moves. The official client never removes itself from its
444        // own model, so a session that changed mid-connection would leave a
445        // ghost behind forever.
446        let ids = SharedIds::new();
447        let who = Occupant::Connection(ConnectionId(4));
448
449        let in_lobby = ids.session(who).expect("allocatable");
450        let in_match = ids.session(who).expect("allocatable");
451
452        assert_eq!(in_lobby, in_match);
453    }
454
455    #[test]
456    fn looking_up_without_allocating_does_not_allocate() {
457        let mut ids = IdAllocator::new();
458        let occupant = Occupant::Connection(ConnectionId(5));
459        assert_eq!(ids.allocated_session(occupant), None);
460
461        let session = ids.session(occupant).expect("allocatable");
462        assert_eq!(ids.allocated_session(occupant), Some(session));
463    }
464}