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}