qmux 0.4.0

QMux protocol (draft-ietf-quic-qmux-02) over reliable transports
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
//! Per-stream priority scheduling for outbound STREAM data.
//!
//! Replaces the bounded `mpsc` data channel the session used to interleave
//! STREAM frames purely by arrival order. Frames are bucketed by stream
//! priority (a `u8`, higher = sent first, matching the W3C `sendOrder`
//! convention) so a high-priority stream can jump ahead of a low-priority
//! backlog. Re-prioritization is retroactive and cheap: only the stream's
//! scheduling pointer moves between bands, never its queued frames, so the
//! per-stream FIFO order the receiver relies on (it appends STREAM data by
//! arrival, ignoring the wire offset) is always preserved.
//!
//! Control frames are *not* routed here — the session keeps its separate
//! unbounded `outbound_priority` channel and a `biased` select so control
//! always precedes any data this queue yields.

use std::{
    collections::{BTreeMap, HashMap, VecDeque},
    sync::{Arc, Mutex},
};

use tokio::sync::Notify;

use crate::{Error, Frame, StreamId};

/// Per-stream FIFO of pending frames plus the stream's current priority band.
struct StreamSlot {
    priority: u8,
    frames: VecDeque<Frame>,
}

struct Inner {
    /// Ready streams bucketed by priority. The MAX key is served first.
    /// A `StreamId` appears in at most one band at a time, and only while its
    /// slot has at least one queued frame.
    bands: BTreeMap<u8, VecDeque<StreamId>>,
    /// Per-stream queued frames + current priority.
    streams: HashMap<StreamId, StreamSlot>,
    /// Total queued frames across all streams (the capacity bound).
    len: usize,
    /// Set on session teardown; unblocks producers and the consumer.
    closed: bool,
}

impl Inner {
    /// Schedule `id` in `band` if it isn't already scheduled somewhere.
    ///
    /// Caller guarantees the slot exists and has at least one frame.
    fn arm(&mut self, id: StreamId, band: u8) {
        // A StreamId lives in at most one band. The slot's `priority` is the
        // single source of truth for which band it would be in, so checking the
        // band's queue for membership is unnecessary as long as callers only arm
        // a stream that was previously unscheduled (empty slot just created, or
        // a slot drained then refilled).
        self.bands.entry(band).or_default().push_back(id);
    }
}

/// A bounded, priority-aware queue of outbound STREAM frames shared between the
/// session's writer loop (consumer) and its `SendStream`s (producers).
///
/// Cloning shares the same underlying queue.
#[derive(Clone)]
pub struct PriorityQueue {
    inner: Arc<Mutex<Inner>>,
    /// Notified when a frame becomes available (or the queue is closed).
    non_empty: Arc<Notify>,
    /// Notified when capacity frees up (or the queue is closed).
    has_space: Arc<Notify>,
    capacity: usize,
}

impl PriorityQueue {
    /// Create a queue holding at most `capacity` frames total before `push`
    /// blocks.
    pub fn new(capacity: usize) -> Self {
        Self {
            inner: Arc::new(Mutex::new(Inner {
                bands: BTreeMap::new(),
                streams: HashMap::new(),
                len: 0,
                closed: false,
            })),
            non_empty: Arc::new(Notify::new()),
            has_space: Arc::new(Notify::new()),
            capacity,
        }
    }

    /// Append a frame to `id`'s FIFO, blocking while the queue is full.
    ///
    /// Cancel-safe: the mutation happens synchronously right before returning,
    /// so if the caller's `select!` drops this future before it resolves, no
    /// frame is enqueued (matching the old `outbound.send` race against
    /// `inbound_stopped`).
    pub async fn push(&self, priority: u8, id: StreamId, frame: Frame) -> Result<(), Error> {
        loop {
            // Register interest *before* checking, so a `notify_one` that fires
            // between our check and `.await` isn't lost.
            let notified = self.has_space.notified();
            {
                let mut inner = self.inner.lock().unwrap();
                if inner.closed {
                    return Err(Error::Closed);
                }
                if inner.len < self.capacity {
                    self.push_locked(&mut inner, priority, id, frame);
                    return Ok(());
                }
            }
            notified.await;
        }
    }

    /// Enqueue a frame synchronously, bypassing the capacity bound — for small,
    /// must-not-be-dropped control markers like a stream FIN, which would
    /// otherwise have to either block (impossible from a sync caller) or be
    /// detached to a task (racing reset/teardown). The frame still lands in the
    /// stream's band, after its data. Fails only if the queue is closed.
    pub fn push_now(&self, priority: u8, id: StreamId, frame: Frame) -> Result<(), Error> {
        let mut inner = self.inner.lock().unwrap();
        if inner.closed {
            return Err(Error::Closed);
        }
        self.push_locked(&mut inner, priority, id, frame);
        Ok(())
    }

    fn push_locked(&self, inner: &mut Inner, priority: u8, id: StreamId, frame: Frame) {
        match inner.streams.get_mut(&id) {
            Some(slot) => {
                // Already scheduled (its band points at `id`); just append.
                slot.frames.push_back(frame);
            }
            None => {
                let mut frames = VecDeque::new();
                frames.push_back(frame);
                inner.streams.insert(id, StreamSlot { priority, frames });
                inner.arm(id, priority);
            }
        }
        inner.len += 1;
        self.non_empty.notify_one();
    }

    /// Pop the next frame to send, honoring priority then round-robin fairness
    /// among equal-priority streams. Blocks until a frame is available or the
    /// queue is closed (returns `None` once closed and drained).
    pub async fn pop(&self) -> Option<Frame> {
        loop {
            let notified = self.non_empty.notified();
            {
                let mut inner = self.inner.lock().unwrap();
                if let Some(frame) = self.pop_locked(&mut inner) {
                    return Some(frame);
                }
                if inner.closed {
                    return None;
                }
            }
            notified.await;
        }
    }

    fn pop_locked(&self, inner: &mut Inner) -> Option<Frame> {
        // Highest band first.
        let (&band, queue) = inner.bands.iter_mut().next_back()?;
        let id = queue.pop_front().expect("scheduled band must be non-empty");
        if queue.is_empty() {
            inner.bands.remove(&band);
        }

        let slot = inner
            .streams
            .get_mut(&id)
            .expect("scheduled stream must have a slot");
        let frame = slot
            .frames
            .pop_front()
            .expect("scheduled slot must be non-empty");

        if slot.frames.is_empty() {
            // Drained: drop the slot so a future push re-arms it cleanly.
            inner.streams.remove(&id);
        } else {
            // Round-robin: re-arm at the back of its *current* band (which may
            // differ from `band` if set_priority moved it; but since it was at
            // the head of the max band, its priority equals `band`).
            let priority = slot.priority;
            inner.arm(id, priority);
        }

        inner.len -= 1;
        self.has_space.notify_one();
        Some(frame)
    }

    /// Retroactively re-prioritize a stream. Frames don't move — only the
    /// scheduling pointer relocates from the old band to `new`. No-op if the
    /// stream has no queued frames.
    pub fn set_priority(&self, id: StreamId, new: u8) {
        let mut inner = self.inner.lock().unwrap();
        let old = match inner.streams.get(&id) {
            Some(slot) => slot.priority,
            None => return,
        };
        if old == new {
            return;
        }

        // Relocate the scheduling pointer if the stream is currently scheduled.
        // It is scheduled iff it has frames (invariant), which it does here.
        if let Some(queue) = inner.bands.get_mut(&old) {
            if let Some(pos) = queue.iter().position(|&s| s == id) {
                queue.remove(pos);
                if queue.is_empty() {
                    inner.bands.remove(&old);
                }
                inner.bands.entry(new).or_default().push_back(id);
            }
        }

        if let Some(slot) = inner.streams.get_mut(&id) {
            slot.priority = new;
        }
    }

    /// Drop every queued frame for `id`, unscheduling it and freeing the
    /// capacity they held. Returns the number of queued STREAM payload bytes
    /// removed so the caller can return flow-control credit that never reached
    /// the wire. Used when a stream is reset: buffered STREAM data must not trail
    /// RESET_STREAM. Returns zero if the stream has nothing queued.
    pub fn remove(&self, id: StreamId) -> u64 {
        let mut inner = self.inner.lock().unwrap();
        let Some(slot) = inner.streams.remove(&id) else {
            return 0;
        };
        let removed = slot.frames.len();
        let removed_bytes = slot
            .frames
            .iter()
            .map(|frame| match frame {
                Frame::Stream(stream) => stream.data.len() as u64,
                _ => 0,
            })
            .sum();

        // Unschedule it from its band (it's scheduled iff it had frames, which it
        // did). `slot.priority` is the single source of truth for which band.
        if let Some(queue) = inner.bands.get_mut(&slot.priority) {
            if let Some(pos) = queue.iter().position(|&s| s == id) {
                queue.remove(pos);
            }
            if queue.is_empty() {
                inner.bands.remove(&slot.priority);
            }
        }

        inner.len -= removed;
        drop(inner);
        // Freed `removed` slots; wake that many blocked producers (mirrors the
        // per-slot notify in `pop_locked`).
        for _ in 0..removed {
            self.has_space.notify_one();
        }
        removed_bytes
    }

    /// Close the queue, unblocking any blocked producers and the consumer.
    pub fn close(&self) {
        {
            let mut inner = self.inner.lock().unwrap();
            inner.closed = true;
        }
        self.non_empty.notify_waiters();
        self.has_space.notify_waiters();
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use bytes::Bytes;

    use crate::proto::Stream;
    use crate::{StreamDir, StreamId};

    fn sid(index: u64) -> StreamId {
        StreamId::new(index, StreamDir::Uni, false)
    }

    fn frame(id: StreamId, tag: u8) -> Frame {
        Frame::Stream(Stream {
            id,
            offset: 0,
            data: Bytes::copy_from_slice(&[tag]),
            fin: false,
        })
    }

    fn tag_of(frame: &Frame) -> u8 {
        match frame {
            Frame::Stream(s) => s.data[0],
            _ => panic!("expected stream frame"),
        }
    }

    fn id_of(frame: &Frame) -> StreamId {
        match frame {
            Frame::Stream(s) => s.id,
            _ => panic!("expected stream frame"),
        }
    }

    #[tokio::test]
    async fn higher_priority_first() {
        let q = PriorityQueue::new(8);
        let lo = sid(0);
        let hi = sid(1);

        q.push(10, lo, frame(lo, b'l')).await.unwrap();
        q.push(200, hi, frame(hi, b'h')).await.unwrap();

        // High priority drains first.
        assert_eq!(tag_of(&q.pop().await.unwrap()), b'h');
        assert_eq!(tag_of(&q.pop().await.unwrap()), b'l');
    }

    #[tokio::test]
    async fn equal_priority_round_robin() {
        let q = PriorityQueue::new(8);
        let a = sid(0);
        let b = sid(1);

        q.push(5, a, frame(a, 1)).await.unwrap();
        q.push(5, a, frame(a, 2)).await.unwrap();
        q.push(5, b, frame(b, 1)).await.unwrap();
        q.push(5, b, frame(b, 2)).await.unwrap();

        // Round-robin between equal-priority streams: a, b, a, b.
        assert_eq!(id_of(&q.pop().await.unwrap()), a);
        assert_eq!(id_of(&q.pop().await.unwrap()), b);
        assert_eq!(id_of(&q.pop().await.unwrap()), a);
        assert_eq!(id_of(&q.pop().await.unwrap()), b);
    }

    #[tokio::test]
    async fn per_stream_fifo_preserved() {
        let q = PriorityQueue::new(8);
        let a = sid(0);

        for i in 0..4u8 {
            q.push(5, a, frame(a, i)).await.unwrap();
        }
        for i in 0..4u8 {
            assert_eq!(tag_of(&q.pop().await.unwrap()), i);
        }
    }

    #[tokio::test]
    async fn set_priority_moves_pointer_not_frames() {
        let q = PriorityQueue::new(8);
        let lo = sid(0);
        let hi = sid(1);

        // Two frames each, lo enqueued first.
        q.push(10, lo, frame(lo, 1)).await.unwrap();
        q.push(10, lo, frame(lo, 2)).await.unwrap();
        q.push(20, hi, frame(hi, 1)).await.unwrap();

        // Promote lo above hi; its frames keep their order.
        q.set_priority(lo, 100);

        assert_eq!(id_of(&q.pop().await.unwrap()), lo);
        assert_eq!(tag_of(&q.pop().await.unwrap()), 2); // lo's second frame, in order
        assert_eq!(id_of(&q.pop().await.unwrap()), hi);
    }

    #[tokio::test]
    async fn set_priority_unknown_stream_is_noop() {
        let q = PriorityQueue::new(8);
        q.set_priority(sid(99), 50); // must not panic
    }

    #[tokio::test]
    async fn close_unblocks_pop() {
        let q = PriorityQueue::new(8);
        let q2 = q.clone();
        let handle = tokio::spawn(async move { q2.pop().await });
        tokio::task::yield_now().await;
        q.close();
        assert!(handle.await.unwrap().is_none());
    }

    #[tokio::test]
    async fn close_unblocks_push() {
        let q = PriorityQueue::new(1);
        let a = sid(0);
        q.push(5, a, frame(a, 1)).await.unwrap();

        let q2 = q.clone();
        let handle = tokio::spawn(async move { q2.push(5, sid(1), frame(sid(1), 2)).await });
        tokio::task::yield_now().await;
        q.close();
        assert!(matches!(handle.await.unwrap(), Err(Error::Closed)));
    }

    #[tokio::test]
    async fn backpressure_blocks_at_capacity() {
        let q = PriorityQueue::new(2);
        let a = sid(0);
        q.push(5, a, frame(a, 1)).await.unwrap();
        q.push(5, a, frame(a, 2)).await.unwrap();

        let q2 = q.clone();
        let pushing = tokio::spawn(async move { q2.push(5, a, frame(a, 3)).await });
        tokio::task::yield_now().await;
        assert!(!pushing.is_finished(), "push should block while full");

        // Draining one frame makes room.
        q.pop().await.unwrap();
        pushing.await.unwrap().unwrap();
    }

    #[tokio::test]
    async fn remove_drops_a_streams_queued_frames() {
        let q = PriorityQueue::new(8);
        let a = sid(0);
        let b = sid(1);

        q.push(5, a, frame(a, 1)).await.unwrap();
        q.push(5, a, frame(a, 2)).await.unwrap();
        q.push(5, b, frame(b, 9)).await.unwrap();

        // Reset `a`: its queued frames vanish, `b`'s survive.
        assert_eq!(q.remove(a), 2);

        assert_eq!(id_of(&q.pop().await.unwrap()), b);
        // Nothing left: the next pop blocks, so a closed-drain returns None.
        q.close();
        assert!(q.pop().await.is_none());
    }

    #[tokio::test]
    async fn remove_frees_capacity_for_blocked_producers() {
        let q = PriorityQueue::new(2);
        let a = sid(0);
        let b = sid(1);
        q.push(5, a, frame(a, 1)).await.unwrap();
        q.push(5, a, frame(a, 2)).await.unwrap();

        // Queue is full; a push for `b` blocks.
        let q2 = q.clone();
        let pushing = tokio::spawn(async move { q2.push(5, b, frame(b, 1)).await });
        tokio::task::yield_now().await;
        assert!(!pushing.is_finished(), "push should block while full");

        // Removing `a` frees both slots and wakes the blocked producer.
        assert_eq!(q.remove(a), 2);
        pushing.await.unwrap().unwrap();
        assert_eq!(id_of(&q.pop().await.unwrap()), b);
    }

    #[tokio::test]
    async fn remove_unknown_stream_is_noop() {
        let q = PriorityQueue::new(8);
        assert_eq!(q.remove(sid(99)), 0); // must not panic or underflow
    }
}