mumble_server_runtime_shard/
queue.rs

1//! One connection's bounded output queue.
2//!
3//! The queue is the **commit point**: accepted into the queue *is* committed,
4//! because acceptance is synchronous and the stream underneath delivers in order
5//! or not at all. That only works if a whole transition is admitted all at once,
6//! which is what [`OutboundQueue::try_send_all`] guarantees by reserving every
7//! slot before writing any of them. A half-delivered transition would leave the
8//! client in a state nobody can describe.
9//!
10//! Admission never blocks. Connections push view updates onto each other's
11//! queues, so a blocking push would let one slow client stall every task trying
12//! to notify it, and two connections pushing to each other with both queues full
13//! would deadlock outright. Non-blocking makes both impossible by construction.
14
15use std::sync::atomic::{AtomicBool, Ordering};
16
17use mumble_server_runtime_protocol::ControlMessage;
18use thiserror::Error;
19use tokio::sync::mpsc;
20
21/// How many messages one connection's queue holds.
22pub const CAPACITY: usize = 1024;
23
24/// The depth past which tunnelled voice is refused.
25///
26/// Voice is bounded in **latency**, not in volume: at a speaker's usual 10 ms of
27/// framing, 64 queued messages is roughly 640 ms of backlog, and a packet that
28/// late is worth nothing to anyone. Control messages have no such bound because
29/// a skipped `UserState` leaves the client on a view that silently diverges.
30///
31/// One queue rather than two, because the client discards audio whose sender
32/// session it does not know: the `UserState` introducing a speaker has to reach
33/// the client before that speaker's tunnelled audio, and two independent queues
34/// cannot promise that.
35///
36/// REF: references/mumble/src/mumble/ServerHandler.cpp : `handleVoicePacket`
37///   looks the sender up with `ClientUser::get(senderSession)` and drops the
38///   packet when it is absent.
39pub const MAX_DEPTH_FOR_VOICE: usize = 64;
40
41/// What became of a tunnelled voice packet.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum VoiceAdmission {
44    Accepted,
45    /// The queue was too deep for the packet to still be worth hearing. Counted
46    /// rather than silent (R6), and never a reason to end the connection.
47    Dropped,
48}
49
50/// Why a transition could not be admitted.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
52pub enum Refused {
53    /// Not enough free slots right now. Nothing was queued, the committed state
54    /// stays where it is, and the next attempt is planned against a fresher
55    /// desired state. Ordinary backpressure, not a fault.
56    #[error("output queue has {free} free slots, {needed} were needed")]
57    Congested { needed: usize, free: usize },
58
59    /// Larger than the queue can *ever* hold. Retrying would never succeed, so
60    /// this is a livelock rather than backpressure and the connection is torn
61    /// down: reconnecting rebuilds the view from scratch.
62    #[error("transition of {needed} messages exceeds the queue capacity of {capacity}")]
63    TooLarge { needed: usize, capacity: usize },
64
65    /// The connection's writer has ended.
66    #[error("the connection's writer has ended")]
67    Closed,
68}
69
70/// The sending half of one connection's output queue.
71#[derive(Debug)]
72pub struct OutboundQueue {
73    sender: mpsc::Sender<ControlMessage>,
74    /// Set once a transition could never fit, or the writer is gone. The owning
75    /// task polls it and ends the connection.
76    must_close: AtomicBool,
77}
78
79impl OutboundQueue {
80    /// Create a queue, returning the receiving half for the connection's writer.
81    #[must_use]
82    pub fn new() -> (OutboundQueue, mpsc::Receiver<ControlMessage>) {
83        Self::with_capacity(CAPACITY)
84    }
85
86    /// Create a queue of a given depth. Exists so tests can produce congestion
87    /// without queueing a thousand messages first.
88    #[must_use]
89    pub fn with_capacity(capacity: usize) -> (OutboundQueue, mpsc::Receiver<ControlMessage>) {
90        let (sender, receiver) = mpsc::channel(capacity.max(1));
91        (
92            OutboundQueue {
93                sender,
94                must_close: AtomicBool::new(false),
95            },
96            receiver,
97        )
98    }
99
100    /// Admit a whole transition, all of it or none of it.
101    ///
102    /// Atomicity comes from reserving every slot before sending any of them: a
103    /// permit holds capacity, and dropping the collected permits releases them
104    /// without having written anything. Failing halfway is therefore impossible
105    /// rather than merely unlikely.
106    ///
107    /// # Errors
108    ///
109    /// [`Refused`], with nothing queued in every case.
110    pub fn try_send_all(&self, messages: Vec<ControlMessage>) -> Result<(), Refused> {
111        let needed = messages.len();
112        if needed == 0 {
113            return Ok(());
114        }
115
116        let capacity = self.sender.max_capacity();
117        if needed > capacity {
118            self.must_close.store(true, Ordering::Relaxed);
119            return Err(Refused::TooLarge { needed, capacity });
120        }
121
122        let mut permits = Vec::with_capacity(needed);
123        for _ in 0..needed {
124            match self.sender.try_reserve() {
125                Ok(permit) => permits.push(permit),
126                Err(mpsc::error::TrySendError::Full(())) => {
127                    // Every permit taken so far is released by this return, so
128                    // the queue is left exactly as it was found.
129                    return Err(Refused::Congested {
130                        needed,
131                        free: permits.len(),
132                    });
133                }
134                Err(mpsc::error::TrySendError::Closed(())) => {
135                    self.must_close.store(true, Ordering::Relaxed);
136                    return Err(Refused::Closed);
137                }
138            }
139        }
140
141        for (permit, message) in permits.into_iter().zip(messages) {
142            permit.send(message);
143        }
144        Ok(())
145    }
146
147    /// Offer one tunnelled voice packet, dropping it if the queue is already
148    /// too deep for it to arrive in time.
149    ///
150    /// Deliberately not `try_send_all`: a refused transition is retried, while a
151    /// refused voice packet is gone, and conflating the two would either close
152    /// connections over lost audio or replay stale speech.
153    pub fn push_voice(&self, message: ControlMessage) -> VoiceAdmission {
154        if self.depth() >= MAX_DEPTH_FOR_VOICE {
155            return VoiceAdmission::Dropped;
156        }
157        match self.sender.try_send(message) {
158            Ok(()) => VoiceAdmission::Accepted,
159            Err(mpsc::error::TrySendError::Full(_)) => VoiceAdmission::Dropped,
160            Err(mpsc::error::TrySendError::Closed(_)) => {
161                self.must_close.store(true, Ordering::Relaxed);
162                VoiceAdmission::Dropped
163            }
164        }
165    }
166
167    /// Whether the owning task must end this connection.
168    #[must_use]
169    pub fn must_close(&self) -> bool {
170        self.must_close.load(Ordering::Relaxed)
171    }
172
173    /// Mark this connection for teardown.
174    pub fn mark_fatal(&self) {
175        self.must_close.store(true, Ordering::Relaxed);
176    }
177
178    /// Messages currently queued.
179    #[must_use]
180    pub fn depth(&self) -> usize {
181        self.sender
182            .max_capacity()
183            .saturating_sub(self.sender.capacity())
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    #![allow(clippy::expect_used)]
190
191    use super::*;
192    use mumble_server_runtime_protocol::messages::tcp;
193
194    fn message(session: u32) -> ControlMessage {
195        ControlMessage::UserRemove(tcp::UserRemove {
196            session,
197            ..Default::default()
198        })
199    }
200
201    #[tokio::test]
202    async fn an_admitted_transition_arrives_whole_and_in_order() {
203        let (queue, mut receiver) = OutboundQueue::new();
204        queue
205            .try_send_all(vec![message(1), message(2), message(3)])
206            .expect("an empty queue has room");
207
208        let mut received = Vec::new();
209        for _ in 0..3 {
210            received.push(receiver.recv().await.expect("queued message"));
211        }
212        assert_eq!(
213            received,
214            vec![message(1), message(2), message(3)],
215            "plan order is what carries the ordering rules"
216        );
217    }
218
219    #[tokio::test]
220    async fn a_transition_that_does_not_fit_queues_nothing_at_all() {
221        let (queue, mut receiver) = OutboundQueue::with_capacity(4);
222        queue
223            .try_send_all(vec![message(1), message(2)])
224            .expect("room for two");
225        let depth_before = queue.depth();
226
227        let refused = queue
228            .try_send_all(vec![message(3); 3])
229            .expect_err("three messages cannot fit in two slots");
230        assert_eq!(refused, Refused::Congested { needed: 3, free: 2 });
231        assert_eq!(
232            queue.depth(),
233            depth_before,
234            "a refused transition must leave the queue exactly as it was"
235        );
236        assert!(
237            !queue.must_close(),
238            "ordinary backpressure is not a reason to drop the connection"
239        );
240
241        receiver.recv().await.expect("queued message");
242        queue
243            .try_send_all(vec![message(3); 3])
244            .expect("one drained slot is all that was missing");
245    }
246
247    #[test]
248    fn a_transition_larger_than_the_queue_ends_the_connection() {
249        let (queue, _receiver) = OutboundQueue::with_capacity(4);
250        let refused = queue
251            .try_send_all(vec![message(1); 5])
252            .expect_err("it cannot fit, now or ever");
253
254        assert_eq!(
255            refused,
256            Refused::TooLarge {
257                needed: 5,
258                capacity: 4
259            }
260        );
261        assert!(
262            queue.must_close(),
263            "retrying something that can never fit is a livelock, not backpressure"
264        );
265    }
266
267    #[test]
268    fn a_closed_writer_ends_the_connection() {
269        let (queue, receiver) = OutboundQueue::with_capacity(4);
270        drop(receiver);
271
272        assert_eq!(queue.try_send_all(vec![message(1)]), Err(Refused::Closed));
273        assert!(queue.must_close());
274    }
275
276    #[test]
277    fn voice_is_refused_long_before_the_queue_is_full() {
278        let (queue, _receiver) = OutboundQueue::new();
279        for _ in 0..MAX_DEPTH_FOR_VOICE {
280            assert_eq!(queue.push_voice(message(1)), VoiceAdmission::Accepted);
281        }
282
283        assert_eq!(queue.push_voice(message(1)), VoiceAdmission::Dropped);
284        assert!(
285            !queue.must_close(),
286            "dropping late audio is the correct outcome, not a fault"
287        );
288        assert!(
289            queue.depth() < CAPACITY,
290            "the point of the voice bound is that control still has room"
291        );
292    }
293
294    #[test]
295    fn control_still_fits_when_voice_has_been_refused() {
296        let (queue, _receiver) = OutboundQueue::with_capacity(MAX_DEPTH_FOR_VOICE + 8);
297        for _ in 0..MAX_DEPTH_FOR_VOICE {
298            assert_eq!(queue.push_voice(message(1)), VoiceAdmission::Accepted);
299        }
300        assert_eq!(queue.push_voice(message(1)), VoiceAdmission::Dropped);
301
302        assert_eq!(queue.try_send_all(vec![message(2); 8]), Ok(()));
303    }
304
305    #[test]
306    fn an_empty_transition_is_free() {
307        let (queue, _receiver) = OutboundQueue::with_capacity(1);
308        assert_eq!(queue.try_send_all(Vec::new()), Ok(()));
309        assert_eq!(queue.depth(), 0);
310    }
311}