Skip to main content

subetha_cxc/
stream_mux.rs

1//! Slice 5: stream multiplexing over one connection.
2//!
3//! A [`StreamMuxSender`] / [`StreamMuxReceiver`] pair carries many independent
4//! byte streams over a single UDP socket and a single connection id. Each stream
5//! owns its own symbol space and is reassembled independently, so a loss on one
6//! stream never blocks delivery on another - the cross-stream head-of-line
7//! blocking that a single ordered transport suffers is gone by construction.
8//!
9//! Reliability is two-layered and **selective per stream**:
10//!
11//!  - A [`Protection::Protected`] stream rides the sliding-window RLC code (the
12//!    same [`crate::rlc_fec`] engine the single-stream transport uses), so most
13//!    losses are repaired forward with no round trip - the right choice for a
14//!    latency-critical stream.
15//!  - A [`Protection::Bulk`] stream carries no repairs; it relies on ARQ alone
16//!    (the receiver NAKs missing symbols), the throughput-efficient choice for
17//!    background data where a round trip of recovery latency is fine.
18//!
19//! [`recommend_protection`] picks between them from the measured loss, so the
20//! sensing plane drives the per-stream scheme just as it drives the single-stream
21//! coding parameters.
22//!
23//! Flow control is two-level: a per-stream window caps each stream's outstanding
24//! (sent-but-unacked) symbols so one stream cannot starve the others, and a
25//! connection window caps the total across all streams so the aggregate cannot
26//! overrun the receiver.
27//!
28//! Framing keeps every non-final symbol exactly one `symbol_len`, so the FEC
29//! always codes over equal-size symbols and a forward-repaired middle symbol is
30//! reassembled verbatim. A stream's final (possibly short) symbol and its byte
31//! length ride a reliable `STREAM_FIN` frame, decoupled from the data symbol's
32//! recovery, so a lost-then-repaired tail never loses its length or its fin.
33//!
34//! The connection id keeps every frame routable, so this layer composes with the
35//! connection-level concerns of the single-stream transport (the same id model
36//! the migration of Slices 3 / 4 routes on); the multiplexing here is orthogonal
37//! to that connection layer and leaves it untouched.
38
39use crate::rlc_fec::{RepairSymbol, RlcDecoder, RlcEncoder};
40use std::collections::{BTreeMap, HashMap, HashSet};
41use std::io;
42use std::net::{SocketAddr, ToSocketAddrs, UdpSocket};
43use std::time::{Duration, Instant};
44
45const STREAM: u8 = 20;
46const STREAM_REPAIR: u8 = 21;
47const STREAM_NAK: u8 = 22;
48const STREAM_ACK: u8 = 23;
49const STREAM_FIN: u8 = 24;
50
51/// STREAM header: type + connection id (u64) + stream id (u32) + source id (u32).
52/// A fixed `symbol_len` symbol follows.
53const STREAM_HDR: usize = 1 + 8 + 4 + 4;
54/// STREAM_REPAIR header: type + connection id + stream id + repair key + first
55/// source id + window size (u16) + density threshold (u8). One symbol follows.
56const REPAIR_HDR: usize = 1 + 8 + 4 + 4 + 4 + 2 + 1;
57/// STREAM_FIN: type + connection id + stream id + final source id + final length.
58const FIN_LEN: usize = 1 + 8 + 4 + 4 + 2;
59/// STREAM_ACK: type + connection id + stream id + cumulative delivered frontier.
60const ACK_LEN: usize = 1 + 8 + 4 + 4;
61
62/// Per-stream forward-error-correction policy.
63#[derive(Clone, Copy, PartialEq, Eq, Debug)]
64pub enum Protection {
65    /// Sliding-window RLC repairs ride alongside the stream - losses are repaired
66    /// forward with no round trip (latency-critical streams).
67    Protected,
68    /// No repairs; ARQ alone recovers losses (throughput streams).
69    Bulk,
70}
71
72/// Choose a per-stream protection from the measured loss rate: above ~1% the ARQ
73/// round trip a Bulk stream pays per loss is worth the forward repair overhead.
74pub fn recommend_protection(loss: f32) -> Protection {
75    if loss > 0.01 {
76        Protection::Protected
77    } else {
78        Protection::Bulk
79    }
80}
81
82/// Bytes delivered from one stream by a [`StreamMuxReceiver::poll`].
83#[derive(Clone, Debug)]
84pub struct StreamData {
85    /// The stream the bytes belong to.
86    pub stream_id: u32,
87    /// Contiguous bytes delivered in order (may be empty when only `fin` lands).
88    pub data: Vec<u8>,
89    /// Whether this delivery completes the stream.
90    pub fin: bool,
91}
92
93/// Derive a per-connection id from the wall clock and local port (the same shape
94/// the single-stream transport uses, so the two are interchangeable on the wire).
95fn derive_conn_id(local_port: u16) -> u64 {
96    let nanos = std::time::SystemTime::now()
97        .duration_since(std::time::UNIX_EPOCH)
98        .map(|d| d.as_nanos() as u64)
99        .unwrap_or(0);
100    let mut x = nanos ^ ((local_port as u64) << 48);
101    x = (x ^ (x >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
102    x = (x ^ (x >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
103    x ^ (x >> 31)
104}
105
106fn set_buffers(sock: &UdpSocket) {
107    let s = socket2::SockRef::from(sock);
108    s.set_recv_buffer_size(4 * 1024 * 1024).ok();
109    s.set_send_buffer_size(4 * 1024 * 1024).ok();
110}
111
112// ---------------------------------------------------------------------------
113// Sender
114// ---------------------------------------------------------------------------
115
116struct SendStream {
117    protection: Protection,
118    enc: Option<RlcEncoder>,
119    /// Unacked full symbols held for ARQ, keyed by source id.
120    sent: BTreeMap<u32, Vec<u8>>,
121    next_source_id: u32,
122    /// Highest contiguous source id the receiver has delivered (per-stream ack).
123    acked_through: u32,
124    /// Bytes buffered but not yet a full symbol (only the final symbol is short).
125    pending: Vec<u8>,
126    /// Once finished: the final source id and the final symbol's real length.
127    fin_info: Option<(u32, u16)>,
128}
129
130impl SendStream {
131    fn outstanding(&self) -> u32 {
132        self.sent.len() as u32
133    }
134    fn fully_acked(&self) -> bool {
135        match self.fin_info {
136            Some((final_sid, _)) => self.acked_through > final_sid,
137            None => false,
138        }
139    }
140}
141
142/// Sender side of the stream multiplexer.
143pub struct StreamMuxSender {
144    sock: UdpSocket,
145    peer: SocketAddr,
146    cid: u64,
147    symbol_len: usize,
148    streams: HashMap<u32, SendStream>,
149    /// Cap on total outstanding symbols across all streams.
150    conn_window: u32,
151    /// Cap on outstanding symbols per stream.
152    per_stream_window: u32,
153    /// RLC parameters applied to a Protected stream's encoder.
154    rlc_window: usize,
155    rlc_step: usize,
156    rlc_dt: u8,
157    /// Last time finished-but-unacked STREAM_FINs were resent (rate limit).
158    last_fin_resend: Instant,
159}
160
161impl StreamMuxSender {
162    /// Bind a local socket and connect to `peer`, coding over `symbol_len`-byte
163    /// symbols. `conn_window` caps total outstanding symbols; `per_stream_window`
164    /// caps each stream's share.
165    pub fn bind<A: ToSocketAddrs>(
166        local: A,
167        peer: SocketAddr,
168        symbol_len: usize,
169        conn_window: u32,
170        per_stream_window: u32,
171    ) -> io::Result<Self> {
172        let sock = UdpSocket::bind(local)?;
173        sock.set_nonblocking(true)?;
174        set_buffers(&sock);
175        let cid = derive_conn_id(sock.local_addr().map(|a| a.port()).unwrap_or(0));
176        Ok(Self {
177            sock,
178            peer,
179            cid,
180            symbol_len,
181            streams: HashMap::new(),
182            conn_window: conn_window.max(1),
183            per_stream_window: per_stream_window.max(1),
184            rlc_window: 32,
185            rlc_step: 4,
186            rlc_dt: 15,
187            last_fin_resend: Instant::now(),
188        })
189    }
190
191    /// The connection id stamped into every frame.
192    pub fn conn_id(&self) -> u64 {
193        self.cid
194    }
195
196    /// The FEC policy a stream was opened with, if it exists.
197    pub fn stream_protection(&self, stream_id: u32) -> Option<Protection> {
198        self.streams.get(&stream_id).map(|s| s.protection)
199    }
200
201    /// The sender's local address.
202    pub fn local_addr(&self) -> io::Result<SocketAddr> {
203        self.sock.local_addr()
204    }
205
206    /// Open a stream with the given FEC policy. Re-opening keeps the existing
207    /// stream (the policy is fixed at first open).
208    pub fn open_stream(&mut self, stream_id: u32, protection: Protection) {
209        let (w, s, d) = (self.rlc_window, self.rlc_step, self.rlc_dt);
210        let symbol_len = self.symbol_len;
211        self.streams.entry(stream_id).or_insert_with(|| SendStream {
212            protection,
213            enc: match protection {
214                Protection::Protected => Some(RlcEncoder::new(w, s, d, symbol_len)),
215                Protection::Bulk => None,
216            },
217            sent: BTreeMap::new(),
218            next_source_id: 0,
219            acked_through: 0,
220            pending: Vec::new(),
221            fin_info: None,
222        });
223    }
224
225    fn total_outstanding(&self) -> u32 {
226        self.streams.values().map(|s| s.outstanding()).sum()
227    }
228
229    /// Write `data` on `stream_id`. Full symbols are emitted immediately; a
230    /// partial tail is buffered until the next write or `fin`. With `fin` set the
231    /// stream is closed: the buffered tail goes out as the final (short) symbol
232    /// and a reliable STREAM_FIN announces its index and length.
233    pub fn write(&mut self, stream_id: u32, data: &[u8], fin: bool) -> io::Result<()> {
234        if !self.streams.contains_key(&stream_id) {
235            self.open_stream(stream_id, Protection::Bulk);
236        }
237        let chunk = self.symbol_len;
238        // Append to the per-stream pending buffer, then drain full symbols.
239        self.streams.get_mut(&stream_id).unwrap().pending.extend_from_slice(data);
240        loop {
241            let have = self.streams.get(&stream_id).unwrap().pending.len();
242            if have < chunk {
243                break;
244            }
245            let sym: Vec<u8> = self
246                .streams
247                .get_mut(&stream_id)
248                .unwrap()
249                .pending
250                .drain(..chunk)
251                .collect();
252            self.send_symbol(stream_id, &sym)?;
253        }
254        if fin {
255            // Flush the (possibly empty, possibly short) tail as the final symbol.
256            let tail: Vec<u8> = std::mem::take(&mut self.streams.get_mut(&stream_id).unwrap().pending);
257            let final_len = tail.len() as u16;
258            let final_sid = self.streams.get(&stream_id).unwrap().next_source_id;
259            self.send_symbol(stream_id, &tail)?;
260            self.streams.get_mut(&stream_id).unwrap().fin_info = Some((final_sid, final_len));
261            self.wire_fin(stream_id, final_sid, final_len)?;
262        }
263        Ok(())
264    }
265
266    /// Emit one symbol (padded to `symbol_len`), pacing against both flow windows.
267    fn send_symbol(&mut self, stream_id: u32, payload: &[u8]) -> io::Result<()> {
268        let start = Instant::now();
269        loop {
270            self.pump()?;
271            let per_stream_ok = self
272                .streams
273                .get(&stream_id)
274                .map(|s| s.outstanding() < self.per_stream_window)
275                .unwrap_or(true);
276            let conn_ok = self.total_outstanding() < self.conn_window;
277            if per_stream_ok && conn_ok {
278                break;
279            }
280            if start.elapsed() > Duration::from_secs(60) {
281                break;
282            }
283            std::thread::sleep(Duration::from_micros(50));
284        }
285
286        let mut sym = vec![0u8; self.symbol_len];
287        sym[..payload.len()].copy_from_slice(payload);
288
289        let stream = self.streams.get_mut(&stream_id).unwrap();
290        let sid = stream.next_source_id;
291        stream.next_source_id += 1;
292        stream.sent.insert(sid, sym.clone());
293        let repair = stream.enc.as_mut().and_then(|e| e.push_source(&sym).1);
294
295        self.wire_stream(stream_id, sid, &sym)?;
296        if let Some(r) = repair {
297            self.wire_repair(stream_id, &r)?;
298        }
299        Ok(())
300    }
301
302    fn wire_stream(&self, stream_id: u32, sid: u32, sym: &[u8]) -> io::Result<()> {
303        let mut pkt = Vec::with_capacity(STREAM_HDR + sym.len());
304        pkt.push(STREAM);
305        pkt.extend_from_slice(&self.cid.to_le_bytes());
306        pkt.extend_from_slice(&stream_id.to_le_bytes());
307        pkt.extend_from_slice(&sid.to_le_bytes());
308        pkt.extend_from_slice(sym);
309        self.sock.send_to(&pkt, self.peer)?;
310        Ok(())
311    }
312
313    fn wire_fin(&self, stream_id: u32, final_sid: u32, final_len: u16) -> io::Result<()> {
314        let mut pkt = Vec::with_capacity(FIN_LEN);
315        pkt.push(STREAM_FIN);
316        pkt.extend_from_slice(&self.cid.to_le_bytes());
317        pkt.extend_from_slice(&stream_id.to_le_bytes());
318        pkt.extend_from_slice(&final_sid.to_le_bytes());
319        pkt.extend_from_slice(&final_len.to_le_bytes());
320        self.sock.send_to(&pkt, self.peer)?;
321        Ok(())
322    }
323
324    fn wire_repair(&self, stream_id: u32, r: &RepairSymbol) -> io::Result<()> {
325        let mut pkt = Vec::with_capacity(REPAIR_HDR + r.payload.len());
326        pkt.push(STREAM_REPAIR);
327        pkt.extend_from_slice(&self.cid.to_le_bytes());
328        pkt.extend_from_slice(&stream_id.to_le_bytes());
329        pkt.extend_from_slice(&r.repair_key.to_le_bytes());
330        pkt.extend_from_slice(&r.first_source_id.to_le_bytes());
331        pkt.extend_from_slice(&r.window_size.to_le_bytes());
332        pkt.push(r.dt);
333        pkt.extend_from_slice(&r.payload);
334        self.sock.send_to(&pkt, self.peer)?;
335        Ok(())
336    }
337
338    /// Drain incoming NAK / ACK frames: retransmit NAK'd symbols and advance each
339    /// stream's acked frontier (which frees the flow windows). Also resends the
340    /// STREAM_FIN of any finished-but-unacked stream on a slow cadence, so a
341    /// terminator lost while other streams are still being written still
342    /// converges (not only once the final `flush` begins).
343    pub fn pump(&mut self) -> io::Result<()> {
344        let mut buf = vec![0u8; self.symbol_len + STREAM_HDR + 64];
345        loop {
346            match self.sock.recv_from(&mut buf) {
347                Ok((n, _)) if n >= 1 => self.handle_feedback(&buf[..n])?,
348                Ok(_) => {}
349                Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => break,
350                Err(ref e) if e.kind() == io::ErrorKind::TimedOut => break,
351                Err(ref e) if e.kind() == io::ErrorKind::ConnectionReset => break,
352                Err(e) => return Err(e),
353            }
354        }
355        if self.last_fin_resend.elapsed() >= Duration::from_millis(5) {
356            let pending: Vec<(u32, u32, u16)> = self
357                .streams
358                .iter()
359                .filter(|(_, s)| s.fin_info.is_some() && !s.fully_acked())
360                .map(|(&id, s)| {
361                    let (fs, fl) = s.fin_info.unwrap();
362                    (id, fs, fl)
363                })
364                .collect();
365            for (id, fs, fl) in pending {
366                self.wire_fin(id, fs, fl)?;
367            }
368            self.last_fin_resend = Instant::now();
369        }
370        Ok(())
371    }
372
373    fn handle_feedback(&mut self, m: &[u8]) -> io::Result<()> {
374        if m.len() < 13 {
375            return Ok(());
376        }
377        let cid = u64::from_le_bytes(m[1..9].try_into().unwrap());
378        if cid != self.cid {
379            return Ok(());
380        }
381        let stream_id = u32::from_le_bytes(m[9..13].try_into().unwrap());
382        match m[0] {
383            STREAM_NAK => {
384                let mut off = 13;
385                let mut want = Vec::new();
386                while off + 4 <= m.len() {
387                    want.push(u32::from_le_bytes(m[off..off + 4].try_into().unwrap()));
388                    off += 4;
389                }
390                let mut resend_fin = false;
391                for sid in want {
392                    let frame = self
393                        .streams
394                        .get(&stream_id)
395                        .and_then(|s| s.sent.get(&sid))
396                        .cloned();
397                    if let Some(sym) = frame {
398                        self.wire_stream(stream_id, sid, &sym)?;
399                    }
400                    // A NAK at or past the final symbol may mean the FIN was lost.
401                    if let Some((final_sid, _)) =
402                        self.streams.get(&stream_id).and_then(|s| s.fin_info)
403                        && sid >= final_sid
404                    {
405                        resend_fin = true;
406                    }
407                }
408                if resend_fin
409                    && let Some((final_sid, final_len)) =
410                        self.streams.get(&stream_id).and_then(|s| s.fin_info)
411                {
412                    self.wire_fin(stream_id, final_sid, final_len)?;
413                }
414            }
415            STREAM_ACK if m.len() >= ACK_LEN => {
416                let through = u32::from_le_bytes(m[13..17].try_into().unwrap());
417                if let Some(s) = self.streams.get_mut(&stream_id)
418                    && through > s.acked_through
419                {
420                    s.acked_through = through;
421                    s.sent.retain(|&sid, _| sid >= through);
422                    if let Some(e) = s.enc.as_mut() {
423                        e.forget_below(through);
424                    }
425                }
426            }
427            _ => {}
428        }
429        Ok(())
430    }
431
432    /// Block until every stream is fully acked (all flow windows empty and each
433    /// finished stream's frontier past its final symbol) or the timeout elapses.
434    /// Resends each finished stream's STREAM_FIN so a lost terminator converges.
435    pub fn flush(&mut self, timeout: Duration) -> io::Result<bool> {
436        let start = Instant::now();
437        loop {
438            self.pump()?;
439            let pending: Vec<(u32, u32, u16)> = self
440                .streams
441                .iter()
442                .filter(|(_, s)| !s.fully_acked() && s.fin_info.is_some())
443                .map(|(&id, s)| {
444                    let (fs, fl) = s.fin_info.unwrap();
445                    (id, fs, fl)
446                })
447                .collect();
448            let done = self.total_outstanding() == 0
449                && self.streams.values().all(|s| s.fin_info.is_none() || s.fully_acked());
450            if done || start.elapsed() >= timeout {
451                return Ok(done);
452            }
453            for (id, fs, fl) in pending {
454                self.wire_fin(id, fs, fl)?;
455            }
456            std::thread::sleep(Duration::from_micros(200));
457        }
458    }
459}
460
461// ---------------------------------------------------------------------------
462// Receiver
463// ---------------------------------------------------------------------------
464
465struct RecvStream {
466    protection: Protection,
467    dec: Option<RlcDecoder>,
468    /// Reassembly buffer: source id -> full symbol bytes.
469    chunks: BTreeMap<u32, Vec<u8>>,
470    /// Next source id to deliver (everything below is delivered).
471    delivered_through: u32,
472    /// First-seen time of each gap, for NAK timing.
473    gap_since: BTreeMap<u32, Instant>,
474    highest_seen: u32,
475    /// Once the FIN is known: the final source id and the final symbol's length.
476    final_info: Option<(u32, u16)>,
477    fin_delivered: bool,
478}
479
480/// Receiver side of the stream multiplexer.
481pub struct StreamMuxReceiver {
482    sock: UdpSocket,
483    peer: Option<SocketAddr>,
484    cid: Option<u64>,
485    symbol_len: usize,
486    streams: HashMap<u32, RecvStream>,
487    last_nak: Instant,
488    /// Diagnostic per-stream loss injection (seeded by source id).
489    debug_loss: HashMap<u32, (u32, u64)>,
490    /// `(stream, source id)` pairs already dropped once, so injected loss erases
491    /// only the first transmission of a symbol (a recoverable loss); the
492    /// retransmit / a later copy passes, as a real one-off loss would.
493    dropped_once: HashSet<(u32, u32)>,
494    /// Total symbols recovered by FEC across all streams (telemetry).
495    fec_recovered: u64,
496    naks_sent: u64,
497}
498
499impl StreamMuxReceiver {
500    /// Bind a receiver over `symbol_len`-byte symbols.
501    pub fn bind<A: ToSocketAddrs>(local: A, symbol_len: usize) -> io::Result<Self> {
502        let sock = UdpSocket::bind(local)?;
503        sock.set_nonblocking(true)?;
504        set_buffers(&sock);
505        Ok(Self {
506            sock,
507            peer: None,
508            cid: None,
509            symbol_len,
510            streams: HashMap::new(),
511            last_nak: Instant::now(),
512            debug_loss: HashMap::new(),
513            dropped_once: HashSet::new(),
514            fec_recovered: 0,
515            naks_sent: 0,
516        })
517    }
518
519    /// The bound local address.
520    pub fn local_addr(&self) -> io::Result<SocketAddr> {
521        self.sock.local_addr()
522    }
523
524    /// Total symbols recovered by FEC (no retransmit) across all streams.
525    pub fn fec_recovered(&self) -> u64 {
526        self.fec_recovered
527    }
528
529    /// NAK frames sent across all streams.
530    pub fn naks_sent(&self) -> u64 {
531        self.naks_sent
532    }
533
534    /// The FEC policy a stream is being received with, if it exists.
535    pub fn stream_protection(&self, stream_id: u32) -> Option<Protection> {
536        self.streams.get(&stream_id).map(|s| s.protection)
537    }
538
539    /// Declare a stream's FEC policy on the receive side (so a Protected stream
540    /// runs an RLC decoder). A stream the sender opens before this is seen
541    /// defaults to Bulk.
542    pub fn expect_stream(&mut self, stream_id: u32, protection: Protection) {
543        self.stream_entry(stream_id, protection);
544    }
545
546    /// Inject seeded diagnostic loss on a stream (percent of arriving symbols),
547    /// to exercise recovery on loopback.
548    pub fn with_stream_loss(mut self, stream_id: u32, pct: u32, seed: u64) -> Self {
549        self.debug_loss.insert(stream_id, (pct.min(100), seed | 1));
550        self
551    }
552
553    fn stream_entry(&mut self, stream_id: u32, protection: Protection) -> &mut RecvStream {
554        let symbol_len = self.symbol_len;
555        self.streams.entry(stream_id).or_insert_with(|| RecvStream {
556            protection,
557            dec: match protection {
558                Protection::Protected => Some(RlcDecoder::new(symbol_len).with_horizon(128)),
559                Protection::Bulk => None,
560            },
561            chunks: BTreeMap::new(),
562            delivered_through: 0,
563            gap_since: BTreeMap::new(),
564            highest_seen: 0,
565            final_info: None,
566            fin_delivered: false,
567        })
568    }
569
570    fn stream_entry_for(&mut self, stream_id: u32) -> &mut RecvStream {
571        if !self.streams.contains_key(&stream_id) {
572            self.stream_entry(stream_id, Protection::Bulk);
573        }
574        self.streams.get_mut(&stream_id).unwrap()
575    }
576
577    /// Receive any pending datagrams, run FEC recovery, and return the bytes that
578    /// became deliverable on each stream (each stream independently in order).
579    pub fn poll(&mut self) -> io::Result<Vec<StreamData>> {
580        let mut buf = vec![0u8; self.symbol_len + REPAIR_HDR + 64];
581        loop {
582            match self.sock.recv_from(&mut buf) {
583                Ok((n, from)) if n >= 1 => {
584                    self.ingest(&buf[..n], from);
585                }
586                Ok(_) => {}
587                Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => break,
588                Err(ref e) if e.kind() == io::ErrorKind::TimedOut => break,
589                Err(ref e) if e.kind() == io::ErrorKind::ConnectionReset => break,
590                Err(e) => return Err(e),
591            }
592        }
593        let mut out = Vec::new();
594        let stream_ids: Vec<u32> = self.streams.keys().copied().collect();
595        for sid in stream_ids {
596            self.recover_stream(sid);
597            if let Some(d) = self.deliver_stream(sid) {
598                out.push(d);
599            }
600        }
601        self.maybe_nak()?;
602        self.send_acks()?;
603        if out.is_empty() {
604            std::thread::sleep(Duration::from_micros(100));
605        }
606        Ok(out)
607    }
608
609    /// Parse one datagram into its stream.
610    fn ingest(&mut self, pkt: &[u8], from: SocketAddr) {
611        if pkt.len() < 13 {
612            return;
613        }
614        let cid = u64::from_le_bytes(pkt[1..9].try_into().unwrap());
615        match self.cid {
616            None => {
617                self.cid = Some(cid);
618                self.peer = Some(from);
619            }
620            Some(c) if c == cid => self.peer = Some(from),
621            Some(_) => return,
622        }
623        let stream_id = u32::from_le_bytes(pkt[9..13].try_into().unwrap());
624        match pkt[0] {
625            STREAM if pkt.len() >= STREAM_HDR + self.symbol_len => {
626                let sid = u32::from_le_bytes(pkt[13..17].try_into().unwrap());
627                if self.drop_symbol(stream_id, sid) {
628                    return;
629                }
630                let sym = pkt[STREAM_HDR..STREAM_HDR + self.symbol_len].to_vec();
631                let st = self.stream_entry_for(stream_id);
632                st.highest_seen = st.highest_seen.max(sid);
633                st.gap_since.remove(&sid);
634                if sid >= st.delivered_through {
635                    st.chunks.entry(sid).or_insert(sym.clone());
636                }
637                if let Some(d) = st.dec.as_mut() {
638                    d.on_source(sid, &sym);
639                }
640            }
641            STREAM_REPAIR if pkt.len() >= REPAIR_HDR + self.symbol_len => {
642                let repair_key = u32::from_le_bytes(pkt[13..17].try_into().unwrap());
643                let first_source_id = u32::from_le_bytes(pkt[17..21].try_into().unwrap());
644                let window_size = u16::from_le_bytes(pkt[21..23].try_into().unwrap());
645                let dt = pkt[23];
646                let payload = pkt[REPAIR_HDR..REPAIR_HDR + self.symbol_len].to_vec();
647                let st = self.stream_entry_for(stream_id);
648                if let Some(d) = st.dec.as_mut() {
649                    d.add_repair(RepairSymbol {
650                        repair_key,
651                        first_source_id,
652                        window_size,
653                        dt,
654                        payload,
655                    });
656                }
657            }
658            STREAM_FIN if pkt.len() >= FIN_LEN => {
659                let final_sid = u32::from_le_bytes(pkt[13..17].try_into().unwrap());
660                let final_len = u16::from_le_bytes(pkt[17..19].try_into().unwrap());
661                let st = self.stream_entry_for(stream_id);
662                st.final_info = Some((final_sid, final_len));
663                st.highest_seen = st.highest_seen.max(final_sid);
664            }
665            _ => {}
666        }
667    }
668
669    fn recover_stream(&mut self, stream_id: u32) {
670        let Some(st) = self.streams.get_mut(&stream_id) else {
671            return;
672        };
673        let Some(dec) = st.dec.as_mut() else {
674            return;
675        };
676        let recovered = dec.recover();
677        for sid in &recovered {
678            if *sid >= st.delivered_through
679                && let Some(sym) = dec.get(*sid)
680            {
681                st.chunks.entry(*sid).or_insert_with(|| sym.to_vec());
682                st.gap_since.remove(sid);
683            }
684        }
685        self.fec_recovered += recovered.len() as u64;
686    }
687
688    /// Deliver the contiguous prefix of a stream that has become available. The
689    /// final symbol is trimmed to the length the STREAM_FIN announced. The
690    /// highest buffered symbol is HELD while it is unknown whether it is the
691    /// final one (a symbol is known non-final only once a higher one is seen or
692    /// the reliable STREAM_FIN places the end past it) - otherwise the final
693    /// symbol could be delivered full-length and without its fin.
694    fn deliver_stream(&mut self, stream_id: u32) -> Option<StreamData> {
695        let st = self.streams.get_mut(&stream_id)?;
696        let mut data = Vec::new();
697        let mut fin = false;
698        loop {
699            let s = st.delivered_through;
700            let is_final = st.final_info.map(|(fs, _)| s == fs).unwrap_or(false);
701            let known_non_final = match st.final_info {
702                Some((fs, _)) => s < fs,
703                None => s < st.highest_seen,
704            };
705            if !(is_final || known_non_final) {
706                break;
707            }
708            let Some(sym) = st.chunks.remove(&s) else {
709                break;
710            };
711            if is_final {
712                let len = st.final_info.unwrap().1 as usize;
713                data.extend_from_slice(&sym[..len.min(sym.len())]);
714                st.delivered_through += 1;
715                fin = true;
716                st.fin_delivered = true;
717                break;
718            }
719            data.extend_from_slice(&sym);
720            st.delivered_through += 1;
721        }
722        if let Some(d) = st.dec.as_mut() {
723            d.forget_below(st.delivered_through);
724        }
725        if data.is_empty() && !fin {
726            return None;
727        }
728        Some(StreamData {
729            stream_id,
730            data,
731            fin,
732        })
733    }
734
735    /// NAK the lowest missing source ids of each stalled stream (rate-limited),
736    /// so a gap the FEC cannot repair forward is retransmitted via ARQ.
737    fn maybe_nak(&mut self) -> io::Result<()> {
738        if self.last_nak.elapsed() < Duration::from_millis(2) {
739            return Ok(());
740        }
741        let Some(peer) = self.peer else {
742            return Ok(());
743        };
744        let cid = self.cid.unwrap_or(0);
745        let now = Instant::now();
746        let mut sent_any = false;
747        let stream_ids: Vec<u32> = self.streams.keys().copied().collect();
748        for stream_id in stream_ids {
749            let st = self.streams.get_mut(&stream_id).unwrap();
750            let mut missing = Vec::new();
751            let mut sid = st.delivered_through;
752            while sid <= st.highest_seen && missing.len() < 16 {
753                if !st.chunks.contains_key(&sid) {
754                    let first = *st.gap_since.entry(sid).or_insert(now);
755                    // 2ms grace: let the FEC repair a Protected gap before ARQ.
756                    if now.duration_since(first) >= Duration::from_millis(2) {
757                        missing.push(sid);
758                    }
759                }
760                sid += 1;
761            }
762            if missing.is_empty() {
763                continue;
764            }
765            let mut pkt = Vec::with_capacity(13 + 4 * missing.len());
766            pkt.push(STREAM_NAK);
767            pkt.extend_from_slice(&cid.to_le_bytes());
768            pkt.extend_from_slice(&stream_id.to_le_bytes());
769            for m in missing {
770                pkt.extend_from_slice(&m.to_le_bytes());
771            }
772            self.sock.send_to(&pkt, peer)?;
773            self.naks_sent += 1;
774            sent_any = true;
775        }
776        if sent_any {
777            self.last_nak = now;
778        }
779        Ok(())
780    }
781
782    /// Send each stream's cumulative delivered frontier so the sender frees its
783    /// per-stream and connection flow windows.
784    fn send_acks(&mut self) -> io::Result<()> {
785        let Some(peer) = self.peer else {
786            return Ok(());
787        };
788        let cid = self.cid.unwrap_or(0);
789        let frontiers: Vec<(u32, u32)> = self
790            .streams
791            .iter()
792            .map(|(&id, s)| (id, s.delivered_through))
793            .collect();
794        for (stream_id, through) in frontiers {
795            let mut pkt = Vec::with_capacity(ACK_LEN);
796            pkt.push(STREAM_ACK);
797            pkt.extend_from_slice(&cid.to_le_bytes());
798            pkt.extend_from_slice(&stream_id.to_le_bytes());
799            pkt.extend_from_slice(&through.to_le_bytes());
800            self.sock.send_to(&pkt, peer)?;
801        }
802        Ok(())
803    }
804
805    /// Seeded diagnostic drop decision for `with_stream_loss`. A retransmit (a
806    /// source id already buffered or delivered) always passes, so ARQ converges.
807    fn drop_symbol(&mut self, stream_id: u32, sid: u32) -> bool {
808        let Some((pct, seed)) = self.debug_loss.get(&stream_id).copied() else {
809            return false;
810        };
811        if pct == 0 {
812            return false;
813        }
814        if let Some(st) = self.streams.get(&stream_id)
815            && (st.chunks.contains_key(&sid) || sid < st.delivered_through)
816        {
817            return false;
818        }
819        // Erase only the first transmission of a symbol: a deterministic per-sid
820        // draw decides if it is unlucky, but once dropped it is recorded so the
821        // retransmit passes - a recoverable one-off loss, not a black hole.
822        if self.dropped_once.contains(&(stream_id, sid)) {
823            return false;
824        }
825        let mut x = seed ^ (sid as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15);
826        x = (x ^ (x >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
827        x ^= x >> 27;
828        if ((x % 100) as u32) < pct {
829            self.dropped_once.insert((stream_id, sid));
830            true
831        } else {
832            false
833        }
834    }
835}
836
837#[cfg(test)]
838mod tests {
839    use super::*;
840    use std::sync::mpsc;
841
842    /// Two streams - one Protected (RLC, with injected loss), one Bulk (ARQ) -
843    /// run concurrently over one connection and each is delivered exactly and in
844    /// order. The protected stream recovers losses forward (fec_recovered > 0).
845    #[test]
846    fn two_streams_deliver_independently_with_selective_fec() {
847        let symbol_len = 256usize;
848        let per_stream = 20_000usize;
849        let (addr_tx, addr_rx) = mpsc::channel();
850
851        let expected1: Vec<u8> = (0..per_stream).map(|i| (i % 251) as u8).collect();
852        let expected2: Vec<u8> = (0..per_stream).map(|i| ((i * 7 + 3) % 251) as u8).collect();
853        let e1 = expected1.clone();
854        let e2 = expected2.clone();
855
856        let rx = std::thread::spawn(move || {
857            let mut recv = StreamMuxReceiver::bind("127.0.0.1:0", symbol_len)
858                .unwrap()
859                .with_stream_loss(1, 12, 0xC0DE);
860            recv.expect_stream(1, Protection::Protected);
861            recv.expect_stream(2, Protection::Bulk);
862            addr_tx.send(recv.local_addr().unwrap()).unwrap();
863            let (mut got1, mut got2) = (Vec::new(), Vec::new());
864            let (mut fin1, mut fin2) = (false, false);
865            let start = Instant::now();
866            while !(fin1 && fin2) {
867                if start.elapsed() > Duration::from_secs(30) {
868                    break;
869                }
870                for d in recv.poll().unwrap() {
871                    match d.stream_id {
872                        1 => {
873                            got1.extend_from_slice(&d.data);
874                            fin1 |= d.fin;
875                        }
876                        2 => {
877                            got2.extend_from_slice(&d.data);
878                            fin2 |= d.fin;
879                        }
880                        _ => {}
881                    }
882                }
883            }
884            for _ in 0..50 {
885                recv.poll().ok();
886                std::thread::sleep(Duration::from_millis(2));
887            }
888            (got1, got2, fin1, fin2, recv.fec_recovered())
889        });
890
891        let recv_addr = addr_rx.recv().unwrap();
892        let tx = std::thread::spawn(move || {
893            let mut send =
894                StreamMuxSender::bind("127.0.0.1:0", recv_addr, symbol_len, 256, 64).unwrap();
895            send.open_stream(1, Protection::Protected);
896            send.open_stream(2, Protection::Bulk);
897            let chunk = 300usize;
898            let (mut o1, mut o2) = (0usize, 0usize);
899            while o1 < e1.len() || o2 < e2.len() {
900                if o1 < e1.len() {
901                    let end = (o1 + chunk).min(e1.len());
902                    send.write(1, &e1[o1..end], end == e1.len()).unwrap();
903                    o1 = end;
904                }
905                if o2 < e2.len() {
906                    let end = (o2 + chunk).min(e2.len());
907                    send.write(2, &e2[o2..end], end == e2.len()).unwrap();
908                    o2 = end;
909                }
910            }
911            send.flush(Duration::from_secs(25)).unwrap();
912        });
913
914        let (got1, got2, fin1, fin2, fec) = rx.join().unwrap();
915        tx.join().unwrap();
916        assert!(fin1 && fin2, "both streams must finish");
917        assert_eq!(got1, expected1, "protected stream 1 delivered exactly");
918        assert_eq!(got2, expected2, "bulk stream 2 delivered exactly");
919        assert!(fec > 0, "the protected stream must have recovered losses via FEC");
920    }
921
922    /// No cross-stream head-of-line blocking: stream 1 takes heavy loss and lags,
923    /// but stream 2 (clean) delivers completely and independently - its
924    /// delivery frontier runs ahead while stream 1 is still recovering.
925    #[test]
926    fn a_stalled_stream_does_not_block_another() {
927        let symbol_len = 128usize;
928        let n = 6_000usize;
929        let (addr_tx, addr_rx) = mpsc::channel();
930        let expected2: Vec<u8> = (0..n).map(|i| (i % 251) as u8).collect();
931        let e2 = expected2.clone();
932
933        let rx = std::thread::spawn(move || {
934            // Stream 1 is Bulk with 30% loss (ARQ-only, so it lags badly); stream
935            // 2 is clean. Stream 2 must complete without waiting for stream 1.
936            let mut recv = StreamMuxReceiver::bind("127.0.0.1:0", symbol_len)
937                .unwrap()
938                .with_stream_loss(1, 30, 0xBEEF);
939            recv.expect_stream(1, Protection::Bulk);
940            recv.expect_stream(2, Protection::Bulk);
941            addr_tx.send(recv.local_addr().unwrap()).unwrap();
942            let mut got2 = Vec::new();
943            let mut fin2 = false;
944            let start = Instant::now();
945            while !fin2 {
946                if start.elapsed() > Duration::from_secs(30) {
947                    break;
948                }
949                for d in recv.poll().unwrap() {
950                    if d.stream_id == 2 {
951                        got2.extend_from_slice(&d.data);
952                        fin2 |= d.fin;
953                    }
954                }
955            }
956            for _ in 0..50 {
957                recv.poll().ok();
958                std::thread::sleep(Duration::from_millis(2));
959            }
960            (got2, fin2)
961        });
962
963        let recv_addr = addr_rx.recv().unwrap();
964        let tx = std::thread::spawn(move || {
965            let mut send =
966                StreamMuxSender::bind("127.0.0.1:0", recv_addr, symbol_len, 512, 256).unwrap();
967            send.open_stream(1, Protection::Bulk);
968            send.open_stream(2, Protection::Bulk);
969            // Heavy data on stream 1 (lossy), interleaved with stream 2 (clean).
970            let chunk = 100usize;
971            let (mut o1, mut o2) = (0usize, 0usize);
972            let one: Vec<u8> = (0..n).map(|i| (i % 13) as u8).collect();
973            while o1 < one.len() || o2 < e2.len() {
974                if o1 < one.len() {
975                    let end = (o1 + chunk).min(one.len());
976                    send.write(1, &one[o1..end], end == one.len()).unwrap();
977                    o1 = end;
978                }
979                if o2 < e2.len() {
980                    let end = (o2 + chunk).min(e2.len());
981                    send.write(2, &e2[o2..end], end == e2.len()).unwrap();
982                    o2 = end;
983                }
984            }
985            send.flush(Duration::from_secs(25)).unwrap();
986        });
987
988        let (got2, fin2) = rx.join().unwrap();
989        tx.join().unwrap();
990        assert!(fin2, "the clean stream finished despite the other stalling");
991        assert_eq!(got2, expected2, "the clean stream delivered exactly");
992    }
993}