subetha_cxc/reliable_udp.rs
1//! Sens-O-Matic protocol: a reliable-UDP transport, FEC-primary,
2//! ARQ-fallback.
3//!
4//! The coding and wire format for Sens-O-Matic, the sighted,
5//! forward-correcting reliable-UDP transport. The socket layer that
6//! drives it lives in [`crate::udp_bridge`].
7//!
8//! This is the encryption-free reliable datagram layer that gives a
9//! trusted-network bridge ordered, lossless delivery over `UdpSocket`
10//! without TLS. Reliability comes from two mechanisms, in priority
11//! order:
12//!
13//! 1. **FEC (primary).** Source items are grouped into blocks of `k`
14//! shards and shipped with `r` Cauchy Reed-Solomon parity shards
15//! ([`crate::fec`]). Up to `r` losses per block are reconstructed by
16//! the receiver with **no retransmit round-trip**.
17//! 2. **ARQ (fallback).** When a block loses MORE than `r` shards - the
18//! rare burst FEC cannot cover - the receiver NAKs the missing shard
19//! indices and the sender retransmits exactly those.
20//!
21//! The parity rate `r` is **automatic**: the receiver reports its
22//! measured loss fraction on every feedback packet and the sender raises
23//! or lowers `r` for subsequent blocks so FEC carries the common case
24//! (small `r` on a clean LAN, larger `r` on lossy Wi-Fi) and ARQ stays a
25//! fallback.
26//!
27//! The protocol is transport-agnostic: [`Encoder`] turns items into
28//! datagrams and [`Decoder`] turns datagrams back into ordered items,
29//! both over byte slices. A real socket or a deterministic lossy channel
30//! plugs in identically, which is what lets the FEC/ARQ behavior be
31//! proven without a network.
32
33use std::collections::BTreeMap;
34use std::sync::atomic::{AtomicU32, Ordering};
35
36use crate::fec::RsCode;
37use crate::loss_class_sensor::LossClassSensor;
38use crate::temporal_sensor::TemporalSensor;
39use crate::tower::SegmentCode;
40
41/// Packet type tag (first wire byte). Data datagrams use this tag; the
42/// control plane (ACK / NAK / loss / timing / ring / path / link / ...) rides
43/// the framed `PKT_CONTROL` container in [`crate::control_frame`].
44const PKT_DATA: u8 = 1;
45
46/// Fixed data-packet header length: `type(1) block_id(4) shard_index(1)
47/// k(1) r(1) flags(1)`.
48pub const DATA_HEADER: usize = 9;
49
50/// `flags` bit: this shard is a parity shard (index `>= k`).
51const FLAG_PARITY: u8 = 0b0000_0001;
52
53/// `flags` bit: this block is a tower outer-parity block - fire-and-forget
54/// cross-block redundancy used opportunistically by the receiver, never
55/// ARQ-tracked (ARQ on the data blocks is the correctness floor).
56const FLAG_OUTER: u8 = 0b0000_0010;
57
58/// `flags` bit: this datagram is an ARQ retransmit. A data shard arriving
59/// with this flag for the first time means its original was dropped, so the
60/// receiver counts it as a wire loss even though ARQ recovered it - the
61/// signal that lets the loss estimator see drops Passthrough hides behind ARQ.
62const FLAG_RETRANSMIT: u8 = 0b0000_0100;
63
64/// High bit set on an outer-parity block id, separating it from the
65/// sequential data-block id space. The low bits encode
66/// `(segment << 8) | outer_index`.
67const OUTER_ID_BIT: u32 = 0x8000_0000;
68
69/// Maximum shards per block (`k + r`); keeps the received-bitmap in one
70/// `u32`.
71pub const MAX_SHARDS: usize = 32;
72
73/// Per-data-shard payload prefix: the real item length in bytes.
74const ITEM_LEN_PREFIX: usize = 2;
75
76/// Sentinel `nak_block` meaning "no retransmit requested".
77pub const NAK_NONE: u32 = u32::MAX;
78
79/// Whether the reordering guard subtracts spurious-retransmit false recoveries
80/// (the D-SACK signal) from the loss estimate. Default on; `SUBETHA_REORDER_GUARD=0`
81/// disables the subtraction for the A/B baseline that shows reordering inflating
82/// the loss estimate without it. Read once and cached.
83fn reorder_guard_enabled() -> bool {
84 static EN: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
85 *EN.get_or_init(|| {
86 std::env::var("SUBETHA_REORDER_GUARD")
87 .map(|v| v != "0")
88 .unwrap_or(true)
89 })
90}
91
92/// The receiver-side control state - ack frontier, selective NAK, and the
93/// fused channel readings - that the bridge carries as `Ack` / `Nak` / `Loss`
94/// frames in a [`crate::control_frame`] CONTROL packet. Kept as a struct
95/// because it is the form the sender's controller already consumes.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct Feedback {
98 /// Next block the receiver still needs (everything below is
99 /// delivered); the sender frees retransmit state below this.
100 pub ack_through: u32,
101 /// Block whose missing shards should be retransmitted, or
102 /// [`NAK_NONE`].
103 pub nak_block: u32,
104 /// Bitmap of MISSING shard indices in `nak_block`.
105 pub nak_mask: u32,
106 /// Estimated loss fraction scaled to `0..=255`.
107 pub loss_x255: u8,
108 /// Estimated burstiness scaled to `0..=255` (clustering of loss).
109 pub burstiness_x255: u8,
110 /// One-way-delay trend class: 0 = falling, 1 = flat, 2 = rising.
111 pub owd_trend_class: u8,
112 /// Loss-class code (0 = no loss, 1 = wireless, 2 = congestion, 3 = mixed)
113 /// from the receiver's [`crate::loss_class_sensor`].
114 pub loss_class: u8,
115}
116
117/// Returns `true` if `buf` is a tower outer-parity datagram.
118pub fn is_outer_datagram(buf: &[u8]) -> bool {
119 buf.len() > DATA_HEADER && buf[0] == PKT_DATA && (buf[8] & FLAG_OUTER) != 0
120}
121
122/// Returns `true` if `buf` is a data datagram (vs feedback).
123pub fn is_data(buf: &[u8]) -> bool {
124 !buf.is_empty() && buf[0] == PKT_DATA
125}
126
127/// A built block held by the sender for possible ARQ retransmission.
128struct PendingBlock {
129 k: u8,
130 r: u8,
131 shard_len: usize,
132 /// `k + r` shard payloads (data first, then parity).
133 shards: Vec<Vec<u8>>,
134}
135
136impl PendingBlock {
137 fn datagram(&self, block_id: u32, idx: usize) -> Vec<u8> {
138 self.datagram_flagged(block_id, idx, 0)
139 }
140
141 fn datagram_flagged(&self, block_id: u32, idx: usize, extra_flags: u8) -> Vec<u8> {
142 let mut pkt = Vec::with_capacity(DATA_HEADER + self.shard_len);
143 pkt.push(PKT_DATA);
144 pkt.extend_from_slice(&block_id.to_le_bytes());
145 pkt.push(idx as u8);
146 pkt.push(self.k);
147 pkt.push(self.r);
148 let parity = if idx >= self.k as usize { FLAG_PARITY } else { 0 };
149 pkt.push(parity | extra_flags);
150 pkt.extend_from_slice(&self.shards[idx]);
151 pkt
152 }
153}
154
155/// Sender side: groups items into FEC-protected blocks and answers
156/// ARQ retransmit requests.
157pub struct Encoder {
158 k: usize,
159 /// Current parity count; adapts to reported loss between
160 /// [`r_min`](Self::r_min) and [`r_max`](Self::r_max).
161 r: usize,
162 r_min: usize,
163 r_max: usize,
164 /// Usable payload bytes per shard (item + length prefix).
165 shard_len: usize,
166 next_block: u32,
167 /// Highest `ack_through` reported by the receiver; below this every
168 /// block is delivered.
169 acked_through: u32,
170 /// Max blocks in flight (sent but not yet acked) before the producer
171 /// should apply backpressure; matches the receiver's window.
172 flow_window: u32,
173 /// Items accumulated for the block under construction.
174 staged: Vec<Vec<u8>>,
175 /// Built-but-unacked blocks, keyed by block id, for ARQ.
176 pending: BTreeMap<u32, PendingBlock>,
177 /// Tower outer code dimensions: `(d, r_outer)`; `(0, 0)` = disabled.
178 tower_d: usize,
179 tower_r_outer: usize,
180 /// Data-block infos (the `k` data shards concatenated) accumulated for
181 /// the current segment.
182 seg_infos: Vec<Vec<u8>>,
183 /// Current segment id.
184 seg_id: u32,
185 /// Blocks sealed at zero parity (Passthrough); telemetry that proves the
186 /// controller actually dropped FEC off the wire on a clean link.
187 passthrough_blocks: u64,
188 /// Blocks sealed with parity (r >= 1); telemetry counterpart.
189 fec_blocks: u64,
190}
191
192impl Encoder {
193 /// Create an encoder. `k` data shards per block, initial `r` parity
194 /// shards (clamped to `r_min..=r_max`), `max_item` largest item
195 /// byte length.
196 pub fn new(k: usize, r: usize, max_item: usize) -> Self {
197 // r_min = 0 lets the fusion controller drop to zero parity
198 // (CodingLevel::Passthrough) on a provably-clean link: the block
199 // ships its k data shards with no FEC encode and no parity datagrams,
200 // and ARQ remains the reliability floor. The controller only selects
201 // r=0 after a sustained-clean confidence window and re-arms to r>=1
202 // the instant loss, burstiness, or link stress appears.
203 let r_min = 0;
204 // Cap parity so k + r never exceeds MAX_SHARDS (the per-block received/NAK
205 // bitmap is a u32, and `1 << idx` for idx >= 32 overflows). saturating_sub
206 // with a 0 floor means k == MAX_SHARDS yields r_max = 0 (Passthrough,
207 // ARQ-only) rather than a 1 that would overflow the bitmap. The full
208 // k + r = MAX_SHARDS is decode-sound (Cauchy over GF(256); see
209 // fec::tests::recovery_k16_r16_high_parity), so the only ceiling is the
210 // bitmap - a high-loss block can provision parity up to it.
211 let r_max = MAX_SHARDS.saturating_sub(k);
212 Self {
213 k,
214 r: r.clamp(r_min, r_max),
215 r_min,
216 r_max,
217 shard_len: max_item + ITEM_LEN_PREFIX,
218 next_block: 0,
219 acked_through: 0,
220 flow_window: 256,
221 staged: Vec::with_capacity(k),
222 pending: BTreeMap::new(),
223 tower_d: 0,
224 tower_r_outer: 0,
225 seg_infos: Vec::new(),
226 seg_id: 0,
227 passthrough_blocks: 0,
228 fec_blocks: 0,
229 }
230 }
231
232 /// Blocks sealed at zero parity (Passthrough) so far, and blocks sealed
233 /// with parity. A nonzero first value proves FEC actually switched off on
234 /// the wire; the ratio shows how much of the stream rode unprotected.
235 pub fn coding_counts(&self) -> (u64, u64) {
236 (self.passthrough_blocks, self.fec_blocks)
237 }
238
239 /// Enable the tower outer code: every `d` data blocks ship with
240 /// `r_outer` fire-and-forget outer-parity blocks that recover whole
241 /// lost data blocks without a retransmit. `(0, _)` or `(_, 0)`
242 /// disables it.
243 pub fn enable_tower(&mut self, d: usize, r_outer: usize) {
244 if d == 0 || r_outer == 0 || d + r_outer > MAX_SHARDS {
245 self.tower_d = 0;
246 self.tower_r_outer = 0;
247 } else {
248 self.tower_d = d;
249 self.tower_r_outer = r_outer;
250 }
251 self.seg_infos.clear();
252 }
253
254 /// Set the in-flight flow window (blocks sent but not yet acked).
255 /// Match this to the receiver's [`Decoder::with_window`].
256 pub fn with_flow_window(mut self, blocks: u32) -> Self {
257 self.flow_window = blocks.max(1);
258 self
259 }
260
261 /// Adjust the in-flight flow window at runtime - the bufferbloat pacer
262 /// shrinks it toward the BDP to drain a self-induced queue, and restores it
263 /// when the queue clears. The receiver's window is the hard ceiling, so the
264 /// pacer only ever clamps DOWN from the configured maximum.
265 pub fn set_flow_window(&mut self, blocks: u32) {
266 self.flow_window = blocks.max(1);
267 }
268
269 /// Current in-flight flow window (blocks).
270 pub fn flow_window(&self) -> u32 {
271 self.flow_window
272 }
273
274 /// Blocks sent but not yet acked by the receiver.
275 pub fn in_flight(&self) -> u32 {
276 self.next_block.wrapping_sub(self.acked_through)
277 }
278
279 /// `true` when the producer should pause sending new blocks until an
280 /// ack frees window space (keeps the receiver's bounded window from
281 /// dropping far-ahead blocks).
282 pub fn flow_blocked(&self) -> bool {
283 self.in_flight() >= self.flow_window
284 }
285
286 /// Largest item this encoder accepts.
287 pub fn max_item(&self) -> usize {
288 self.shard_len - ITEM_LEN_PREFIX
289 }
290
291 /// Current parity count.
292 pub fn parity(&self) -> usize {
293 self.r
294 }
295
296 /// The id the NEXT sealed block will take; the block just sealed by a
297 /// non-empty [`push`](Self::push) / [`flush`](Self::flush) is this minus
298 /// one. Lets the sender record a per-block send time for RTT sampling.
299 pub fn next_block_id(&self) -> u32 {
300 self.next_block
301 }
302
303 /// Stage `item` for transmission. Returns the datagrams to send when
304 /// the staged set reaches `k` items (a full block); otherwise an
305 /// empty vec. Call [`flush`](Self::flush) to force a short final
306 /// block.
307 pub fn push(&mut self, item: &[u8]) -> Vec<Vec<u8>> {
308 debug_assert!(item.len() <= self.max_item());
309 // Stage the unpadded shard (length prefix + item). seal_block pads
310 // every shard in the block to the block's largest item - so a block
311 // of small (e.g. schema-compressed) items ships small datagrams.
312 let mut shard = Vec::with_capacity(ITEM_LEN_PREFIX + item.len());
313 shard.extend_from_slice(&(item.len() as u16).to_le_bytes());
314 shard.extend_from_slice(item);
315 self.staged.push(shard);
316 if self.staged.len() == self.k {
317 self.seal_block()
318 } else {
319 Vec::new()
320 }
321 }
322
323 /// Force the staged items (fewer than `k`) into a final padded
324 /// block. Returns its datagrams, or empty if nothing is staged.
325 pub fn flush(&mut self) -> Vec<Vec<u8>> {
326 let mut out = if self.staged.is_empty() {
327 Vec::new()
328 } else {
329 self.seal_block()
330 };
331 // Seal a partial final segment so its blocks get tower protection
332 // too (otherwise a whole-block loss in the tail segment has no
333 // outer parity to recover from).
334 if self.tower_d > 0 && !self.seg_infos.is_empty() {
335 out.extend(self.seal_segment());
336 }
337 out
338 }
339
340 fn seal_block(&mut self) -> Vec<Vec<u8>> {
341 // Per-block shard length: the largest staged shard in this block,
342 // so a block of small items ships small datagrams. The tower's
343 // cross-block outer code needs uniform blocks across a segment, so
344 // when it is enabled the fixed maximum is used instead. The decoder
345 // reads each block's shard length from the datagram size, so no
346 // header field is required.
347 let block_shard_len = if self.tower_d > 0 {
348 self.shard_len
349 } else {
350 self.staged
351 .iter()
352 .map(|s| s.len())
353 .max()
354 .unwrap_or(ITEM_LEN_PREFIX)
355 .max(ITEM_LEN_PREFIX)
356 };
357 for s in &mut self.staged {
358 s.resize(block_shard_len, 0);
359 }
360 // Pad with zero-length items up to k data shards.
361 while self.staged.len() < self.k {
362 let mut pad = vec![0u8; block_shard_len];
363 pad[0..2].copy_from_slice(&0u16.to_le_bytes());
364 self.staged.push(pad);
365 }
366 let r = self.r;
367 let mut shards: Vec<Vec<u8>> = std::mem::take(&mut self.staged);
368 // Capture this block's info (the k data shards) for the tower,
369 // before parity is appended.
370 let tower_info = if self.tower_d > 0 {
371 Some(shards.concat())
372 } else {
373 None
374 };
375 // Passthrough (r=0): ship the k data shards with no parity encode.
376 // ARQ recovers any dropped data shard; the controller only reaches
377 // r=0 on a sustained-clean link.
378 if r == 0 {
379 self.passthrough_blocks += 1;
380 } else {
381 self.fec_blocks += 1;
382 }
383 if r > 0 {
384 let mut parity: Vec<Vec<u8>> = vec![vec![0u8; block_shard_len]; r];
385 {
386 let code = RsCode::new(self.k, r).expect("valid k,r");
387 let data_refs: Vec<&[u8]> = shards.iter().map(|s| s.as_slice()).collect();
388 let mut par_refs: Vec<&mut [u8]> =
389 parity.iter_mut().map(|s| s.as_mut_slice()).collect();
390 code.encode(&data_refs, &mut par_refs).expect("encode");
391 }
392 shards.extend(parity);
393 }
394 let block_id = self.next_block;
395 self.next_block += 1;
396 let pb = PendingBlock {
397 k: self.k as u8,
398 r: r as u8,
399 shard_len: block_shard_len,
400 shards,
401 };
402 let mut datagrams: Vec<Vec<u8>> =
403 (0..self.k + r).map(|i| pb.datagram(block_id, i)).collect();
404 self.pending.insert(block_id, pb);
405 self.staged = Vec::with_capacity(self.k);
406 // Tower: accumulate this block's info; emit outer-parity blocks
407 // when the segment fills.
408 if let Some(info) = tower_info {
409 self.seg_infos.push(info);
410 if self.seg_infos.len() == self.tower_d {
411 datagrams.extend(self.seal_segment());
412 }
413 }
414 datagrams
415 }
416
417 /// Compute and emit the segment's outer-parity blocks (fire-and-forget:
418 /// not added to `pending`, so they are never retransmitted - ARQ on the
419 /// data blocks is the floor).
420 fn seal_segment(&mut self) -> Vec<Vec<u8>> {
421 let r_outer = self.tower_r_outer;
422 let infos = std::mem::take(&mut self.seg_infos);
423 // Use the ACTUAL block count: a full segment has `tower_d`, the
424 // final partial segment (flushed) has fewer. The count is encoded
425 // in the outer id so the receiver protects partial segments too.
426 let d = infos.len();
427 if d == 0 || r_outer == 0 {
428 return Vec::new();
429 }
430 let info_len = infos[0].len();
431 let seg = SegmentCode::new(d, r_outer).expect("valid d,r_outer");
432 let mut outer: Vec<Vec<u8>> = vec![vec![0u8; info_len]; r_outer];
433 {
434 let dref: Vec<&[u8]> = infos.iter().map(|v| v.as_slice()).collect();
435 let mut pref: Vec<&mut [u8]> = outer.iter_mut().map(|v| v.as_mut_slice()).collect();
436 seg.encode(&dref, &mut pref).expect("outer encode");
437 }
438 let seg_id = self.seg_id;
439 self.seg_id += 1;
440 let r = self.r;
441 let mut out = Vec::new();
442 for (oidx, oinfo) in outer.into_iter().enumerate() {
443 // The outer info is k data shards; inner-encode it like any
444 // block so it survives shard loss on the wire too.
445 let mut oshards: Vec<Vec<u8>> =
446 oinfo.chunks(self.shard_len).map(|c| c.to_vec()).collect();
447 let mut oparity: Vec<Vec<u8>> = vec![vec![0u8; self.shard_len]; r];
448 {
449 let code = RsCode::new(self.k, r).expect("valid k,r");
450 let dref: Vec<&[u8]> = oshards.iter().map(|s| s.as_slice()).collect();
451 let mut pref: Vec<&mut [u8]> =
452 oparity.iter_mut().map(|s| s.as_mut_slice()).collect();
453 code.encode(&dref, &mut pref).expect("inner encode outer");
454 }
455 oshards.extend(oparity);
456 let opb = PendingBlock {
457 k: self.k as u8,
458 r: r as u8,
459 shard_len: self.shard_len,
460 shards: oshards,
461 };
462 // Self-describing id: bit31 = OUTER, bits27-30 = d (1..15),
463 // bits24-26 = r_outer (1..7), bits8-23 = segment, bits0-7 =
464 // outer index. The receiver learns the segment structure from
465 // the wire, no out-of-band config.
466 let oid = OUTER_ID_BIT
467 | ((d as u32) << 27)
468 | ((r_outer as u32) << 24)
469 | (seg_id << 8)
470 | oidx as u32;
471 for i in 0..self.k + r {
472 out.push(opb.datagram_flagged(oid, i, FLAG_OUTER));
473 }
474 }
475 out
476 }
477
478 /// Set the parity shards per new block, clamped to the encoder's
479 /// `[r_min, r_max]`. The fusion controller drives this from the
480 /// control table; the encoder no longer self-adapts parity.
481 pub fn set_parity(&mut self, r: usize) {
482 self.r = r.clamp(self.r_min, self.r_max);
483 }
484
485 /// Set parity to at least `floor` (the fusion controller's burst / feed-forward
486 /// signal) AND enough to FEC-recover a `loss` fraction of THIS block: to
487 /// recover a fraction p of the k + r shards, r / (k + r) >= p, i.e.
488 /// r >= p * k / (1 - p). A 20% margin covers a spike above the mean. Capped at
489 /// `r_max` (the bitmap ceiling). Without this, parity tracked only the
490 /// controller's modest floor and a high-loss block fell to ARQ round trips
491 /// instead of recovering in-FEC; this lets block-RS provision to the loss the
492 /// way the sliding-window RLC rate law already does.
493 pub fn set_parity_covering(&mut self, floor: usize, loss: f32) {
494 let p = (loss * 1.2).clamp(0.0, 0.95);
495 let cover = (p * self.k as f32 / (1.0 - p)).ceil() as usize;
496 self.r = floor.max(cover).clamp(self.r_min, self.r_max);
497 }
498
499 /// Apply receiver feedback: free acked blocks and return any ARQ
500 /// retransmit datagrams. Parity adaptation is the controller's job
501 /// (see [`set_parity`](Self::set_parity)), not this method's.
502 pub fn on_feedback(&mut self, fb: &Feedback) -> Vec<Vec<u8>> {
503 if fb.ack_through > self.acked_through {
504 self.acked_through = fb.ack_through;
505 }
506 // Free everything the receiver has fully delivered.
507 let acked: Vec<u32> = self
508 .pending
509 .range(..fb.ack_through)
510 .map(|(&id, _)| id)
511 .collect();
512 for id in acked {
513 self.pending.remove(&id);
514 }
515 // ARQ: retransmit the requested missing shards.
516 let mut out = Vec::new();
517 if fb.nak_block != NAK_NONE
518 && let Some(pb) = self.pending.get(&fb.nak_block)
519 {
520 let n = pb.shards.len();
521 for idx in 0..n {
522 if fb.nak_mask & (1 << idx) != 0 {
523 out.push(pb.datagram_flagged(fb.nak_block, idx, FLAG_RETRANSMIT));
524 }
525 }
526 }
527 out
528 }
529
530 /// Number of unacked blocks held for ARQ.
531 pub fn pending_len(&self) -> usize {
532 self.pending.len()
533 }
534
535 /// The oldest unacked block id - the one the receiver's in-order frontier
536 /// is waiting on - or `None` if everything is acked.
537 pub fn oldest_pending(&self) -> Option<u32> {
538 self.pending.keys().next().copied()
539 }
540
541 /// Retransmit datagrams (flagged `RETRANSMIT`) for the `k` DATA shards of
542 /// one pending block - a liveness probe that also pre-positions the block
543 /// the receiver's frontier is stalled on. Empty if the block is already
544 /// acked.
545 pub fn probe_block(&self, block_id: u32) -> Vec<Vec<u8>> {
546 match self.pending.get(&block_id) {
547 Some(pb) => (0..pb.k as usize)
548 .map(|idx| pb.datagram_flagged(block_id, idx, FLAG_RETRANSMIT))
549 .collect(),
550 None => Vec::new(),
551 }
552 }
553
554 /// Retransmit datagrams (flagged `RETRANSMIT`) for the `k` DATA shards of
555 /// EVERY pending block, oldest-first - the proactive burst on link recovery
556 /// that resends the whole unacked window WITHOUT waiting for the receiver's
557 /// NAKs (the sender already holds the exact unacked set, so no estimation
558 /// is needed). The receiver dedups any datagram it already has via its
559 /// D-SACK / false-recovery path, so over-resending is safe. `k` data shards
560 /// per block suffice to decode a fully-lost block; any shard still missing
561 /// after the burst is recovered by the normal reactive NAK.
562 pub fn retransmit_all_data(&self) -> Vec<Vec<u8>> {
563 let mut out = Vec::new();
564 // BTreeMap iterates in key order, i.e. oldest block first.
565 for (&id, pb) in &self.pending {
566 for idx in 0..pb.k as usize {
567 out.push(pb.datagram_flagged(id, idx, FLAG_RETRANSMIT));
568 }
569 }
570 out
571 }
572}
573
574/// One block being reassembled on the receiver.
575struct RxBlock {
576 k: usize,
577 r: usize,
578 shard_len: usize,
579 /// Received bitmap: bit `i` set means shard `i` is present.
580 mask: AtomicU32,
581 /// Bitmap of shards whose first arrival was an ARQ retransmit (their
582 /// original was dropped) - the wire-loss evidence for the estimator.
583 retransmitted: u32,
584 /// Bitmap of positions where the original (non-retransmit) shard arrived
585 /// AFTER an ARQ retransmit had already filled the slot. A duplicate of an
586 /// already-recovered shard is the D-SACK signal (RFC 2883): "significant
587 /// reordering followed by a false (unnecessary) retransmission", so the
588 /// shard was reordered (late), not lost, and the retransmit-counted loss
589 /// was a false positive the estimator subtracts (reordering vs loss per
590 /// RACK-TLP, RFC 8985).
591 false_recovery: u32,
592 shards: Vec<Option<Vec<u8>>>,
593 decoded: bool,
594}
595
596impl RxBlock {
597 fn new(k: usize, r: usize, shard_len: usize) -> Self {
598 Self {
599 k,
600 r,
601 shard_len,
602 mask: AtomicU32::new(0),
603 retransmitted: 0,
604 false_recovery: 0,
605 shards: vec![None; k + r],
606 decoded: false,
607 }
608 }
609
610 #[inline]
611 fn count(&self) -> u32 {
612 self.mask.load(Ordering::Relaxed).count_ones()
613 }
614}
615
616/// Receiver side: reassembles blocks, FEC-recovers losses, emits items
617/// in order, and produces ARQ feedback.
618pub struct Decoder {
619 window: BTreeMap<u32, RxBlock>,
620 /// Next block id to deliver; everything below is delivered.
621 next_deliver: AtomicU32,
622 /// Highest block id seen, for stall detection.
623 highest_seen: u32,
624 /// Highest DATA block fully decoded. Genuine gaps (blocks needing a
625 /// retransmit) sit only below this: a later block fully arrived, so
626 /// the missing one's shards are lost, not in flight. On a clean link
627 /// this tracks the delivery frontier, so the selective-NAK gap scan is
628 /// empty - that cost is paid only under real loss, not every poll.
629 highest_decoded: u32,
630 /// Rolling loss accounting.
631 total_expected: u64,
632 total_missing: u64,
633 /// Highest loss estimate (0..=255) reached over the receiver's lifetime.
634 /// Diagnostics for the reordering guard.
635 peak_loss: u8,
636 /// Lifetime count of D-SACK false recoveries detected: spurious
637 /// retransmissions whose reordered original later arrived, which the guard
638 /// excludes from the loss estimate. A nonzero value on a reorder-carrying
639 /// link is the guard firing on real reordered traffic. Diagnostics.
640 false_recoveries: u64,
641 /// Max blocks retained before forcing progress / NAK.
642 window_cap: usize,
643 /// Timing estimator fed by sender heartbeats (OWD trend, jitter).
644 temporal: TemporalSensor,
645 /// Loss differentiator (congestion vs wireless): fed shard inter-arrivals
646 /// and heartbeat ROTT, consulted when a block delivers with loss.
647 loss_class: LossClassSensor,
648 /// Gilbert-Elliott burst-loss fit: fed each delivered block's per-shard
649 /// original-loss trace, it yields a REAL mean burst length. When
650 /// `use_ge_burst` is set the reported burstiness is derived from it
651 /// (interleave at least the mean burst), instead of the jitter-ratio
652 /// heuristic - the A/B knob.
653 burst_model: crate::burst_model_sensor::BurstModel,
654 use_ge_burst: bool,
655 /// Receiver-clock microseconds of the previous data-shard arrival, for the
656 /// inter-arrival the loss differentiator's Biaz test needs (`None` until a
657 /// timestamped shard arrives via [`Decoder::on_packet_at`]).
658 last_data_recv_us: Option<u64>,
659 /// Most recent data-shard inter-arrival (microseconds), classified against
660 /// the loss gap when a block delivers.
661 last_interarrival_us: f64,
662 /// Tower segment structure, learned from outer block ids (`0` until
663 /// the first outer block arrives).
664 tower_d: usize,
665 tower_r_outer: usize,
666 /// Inner block geometry, learned from received data blocks.
667 inner_k: usize,
668 inner_shard_len: usize,
669 /// Decoded data-block infos (k data shards concatenated), kept for
670 /// tower recovery until delivered.
671 data_infos: BTreeMap<u32, Vec<u8>>,
672 /// Reassembly buffers for in-flight outer-parity blocks.
673 outer_rx: BTreeMap<u32, RxBlock>,
674 /// Recovered outer infos per segment: `seg_id -> (outer_idx -> info)`.
675 seg_outer: BTreeMap<u32, BTreeMap<u32, Vec<u8>>>,
676 /// Actual data-block count per segment (a partial final segment has
677 /// fewer than `tower_d`).
678 seg_d: BTreeMap<u32, usize>,
679}
680
681impl Default for Decoder {
682 fn default() -> Self {
683 Self::new()
684 }
685}
686
687impl Decoder {
688 /// Create a receiver with a default 256-block reassembly window -
689 /// deep enough to keep the wire full across the ack round-trip while a
690 /// gap recovers in the background (the sender pipelines new blocks and
691 /// the receiver buffers them out of order, draining in order once the
692 /// gap is recovered).
693 pub fn new() -> Self {
694 Self::with_window(256)
695 }
696
697 /// Create a receiver bounding the reassembly window to `window_cap`
698 /// blocks. A sender should use a matching
699 /// [`Encoder::with_flow_window`] so it never transmits beyond what
700 /// the receiver will buffer.
701 pub fn with_window(window_cap: usize) -> Self {
702 Self {
703 window: BTreeMap::new(),
704 next_deliver: AtomicU32::new(0),
705 highest_seen: 0,
706 highest_decoded: 0,
707 total_expected: 0,
708 total_missing: 0,
709 peak_loss: 0,
710 false_recoveries: 0,
711 window_cap: window_cap.max(1),
712 temporal: TemporalSensor::default(),
713 loss_class: LossClassSensor::new(),
714 burst_model: crate::burst_model_sensor::BurstModel::new(),
715 use_ge_burst: false,
716 last_data_recv_us: None,
717 last_interarrival_us: 0.0,
718 tower_d: 0,
719 tower_r_outer: 0,
720 inner_k: 0,
721 inner_shard_len: 0,
722 data_infos: BTreeMap::new(),
723 outer_rx: BTreeMap::new(),
724 seg_outer: BTreeMap::new(),
725 seg_d: BTreeMap::new(),
726 }
727 }
728
729 /// The configured reassembly-window bound, in blocks.
730 pub fn window_cap(&self) -> usize {
731 self.window_cap
732 }
733
734 /// Feed a sender heartbeat's `(send_ts, recv_ts)` pair (microseconds)
735 /// to the timing estimator, so the next feedback reports the OWD
736 /// trend and jitter-derived burstiness.
737 pub fn on_heartbeat(&mut self, send_ts: u64, recv_ts: u64) {
738 self.temporal.observe(send_ts, recv_ts);
739 // The relative one-way trip time (clock offset cancels in the Spike
740 // min/max range) feeds the loss differentiator's Spike (ROTT) input.
741 self.loss_class.observe_owd(recv_ts as f64 - send_ts as f64);
742 }
743
744 /// Current OWD trend slope from the timing estimator (raw, skew-inclusive).
745 pub fn owd_trend(&self) -> f64 {
746 self.temporal.owd_trend()
747 }
748
749 /// Estimated clock skew (the Moon-Skelly-Towsley lower-hull slope) and the
750 /// skew-corrected OWD trend the controller actually consumes (telemetry).
751 pub fn owd_skew(&self) -> f64 {
752 self.temporal.skew()
753 }
754
755 pub fn owd_trend_debiased(&self) -> f64 {
756 self.temporal.owd_trend_debiased()
757 }
758
759 /// Highest loss estimate (0..=255) the receiver has reached (telemetry).
760 pub fn peak_loss_x255(&self) -> u8 {
761 self.peak_loss
762 }
763
764 /// Drive the reported burstiness from the Gilbert-Elliott burst model (a
765 /// real mean burst length) instead of the jitter-ratio heuristic - the A/B
766 /// knob for confirming the model beats the heuristic at sizing interleave.
767 pub fn set_ge_burst(&mut self, on: bool) {
768 self.use_ge_burst = on;
769 }
770
771 /// Fitted mean burst length (consecutive lost shards) from the
772 /// Gilbert-Elliott model, or -1 before the fit converges (telemetry / A/B).
773 pub fn mean_burst_len(&self) -> f32 {
774 self.burst_model.mean_burst_len().map(|m| m as f32).unwrap_or(-1.0)
775 }
776
777 /// Lifetime count of D-SACK false recoveries the reordering guard detected:
778 /// spurious retransmissions whose reordered original later arrived. Zero on
779 /// a clean link; a nonzero value on a reorder-carrying link is the guard
780 /// firing on real reordered traffic (RFC 2883 / RFC 8985).
781 pub fn false_recovery_count(&self) -> u64 {
782 self.false_recoveries
783 }
784
785 /// Block id the receiver next needs (everything below is delivered).
786 pub fn next_needed(&self) -> u32 {
787 self.next_deliver.load(Ordering::Relaxed)
788 }
789
790 /// Ingest one data datagram. Returns any items that became
791 /// deliverable, in stream order. Non-data datagrams yield nothing.
792 pub fn on_packet(&mut self, buf: &[u8]) -> Vec<Vec<u8>> {
793 self.ingest(buf, None)
794 }
795
796 /// Like [`on_packet`](Self::on_packet) but with the datagram's receiver-
797 /// clock arrival time (microseconds), which feeds the loss differentiator's
798 /// inter-arrival (Biaz) input. The socket layer supplies it; callers that do
799 /// not time arrivals use [`on_packet`](Self::on_packet) and the
800 /// differentiator falls back to its Spike (ROTT) signal alone.
801 pub fn on_packet_at(&mut self, buf: &[u8], recv_us: u64) -> Vec<Vec<u8>> {
802 self.ingest(buf, Some(recv_us))
803 }
804
805 fn ingest(&mut self, buf: &[u8], recv_us: Option<u64>) -> Vec<Vec<u8>> {
806 if !is_data(buf) || buf.len() < DATA_HEADER {
807 return Vec::new();
808 }
809 let block_id = u32::from_le_bytes([buf[1], buf[2], buf[3], buf[4]]);
810 let shard_index = buf[5] as usize;
811 let k = buf[6] as usize;
812 let r = buf[7] as usize;
813 let is_retransmit = buf[8] & FLAG_RETRANSMIT != 0;
814 let payload = &buf[DATA_HEADER..];
815 // r == 0 is the Passthrough block: k data shards, no parity. It is a
816 // valid shape (the block completes when all k data shards arrive, via
817 // ARQ if any drop), so it is NOT rejected here.
818 if k == 0 || k + r > MAX_SHARDS || shard_index >= k + r {
819 return Vec::new();
820 }
821 // Tower outer-parity blocks live in a separate id space; they are
822 // handled opportunistically to recover whole-lost data blocks.
823 if block_id & OUTER_ID_BIT != 0 {
824 self.handle_outer(block_id, shard_index, k, r, payload);
825 return self.drain_in_order();
826 }
827 // A timestamped DATA-shard arrival feeds the loss differentiator's
828 // inter-arrival input (Biaz `T_min` / `T_i`). Outer-parity shards are
829 // excluded above, so this is the data-stream spacing the LDA expects.
830 if let Some(now) = recv_us {
831 if let Some(prev) = self.last_data_recv_us {
832 let ia = now.wrapping_sub(prev) as f64;
833 self.last_interarrival_us = ia;
834 self.loss_class.observe_interarrival(ia);
835 }
836 self.last_data_recv_us = Some(now);
837 }
838 if self.inner_k == 0 {
839 self.inner_k = k;
840 self.inner_shard_len = payload.len();
841 }
842 // Ignore packets for already-delivered blocks (duplicates /
843 // late ARQ).
844 if block_id < self.next_deliver.load(Ordering::Relaxed) {
845 return Vec::new();
846 }
847 // Bound the reassembly window: refuse blocks too far ahead of
848 // the delivery frontier. The sender's flow window
849 // ([`Encoder::in_flight`]) keeps it from outrunning this, so in
850 // correct operation this guard only fires under a bug or a
851 // hostile peer - it caps memory either way.
852 let next = self.next_deliver.load(Ordering::Relaxed);
853 if block_id >= next.saturating_add(self.window_cap as u32) {
854 return Vec::new();
855 }
856 if block_id > self.highest_seen {
857 self.highest_seen = block_id;
858 }
859 let shard_len = payload.len();
860 let blk = self
861 .window
862 .entry(block_id)
863 .or_insert_with(|| RxBlock::new(k, r, shard_len));
864 // FEC operates symbol-wise across equal-length shards; reject a
865 // packet whose shape disagrees with the block it joins.
866 if blk.shard_len != shard_len || blk.k != k || blk.r != r {
867 return self.drain_in_order();
868 }
869 let bit = 1u32 << shard_index;
870 if blk.mask.load(Ordering::Relaxed) & bit == 0 {
871 blk.mask.fetch_or(bit, Ordering::Relaxed);
872 blk.shards[shard_index] = Some(payload.to_vec());
873 // First arrival via ARQ retransmit: its original was dropped, so
874 // record it as wire loss for the estimator (otherwise a drop that
875 // ARQ recovered at Passthrough would be invisible).
876 if is_retransmit {
877 blk.retransmitted |= bit;
878 }
879 } else if !is_retransmit && (blk.retransmitted & bit) != 0 {
880 // The original arrives AFTER its ARQ retransmit already filled this
881 // slot - a duplicate of an already-recovered shard. That is the
882 // D-SACK signal (RFC 2883): reordering followed by a spurious
883 // retransmission, NOT a loss. Mark it so the estimator discounts
884 // the retransmit it counted. The slot keeps the retransmit's bytes
885 // (identical to the original), so delivery is unchanged.
886 blk.false_recovery |= bit;
887 }
888 // FEC-decode as soon as k of k+r shards are present.
889 let mut decoded_info: Option<Vec<u8>> = None;
890 if !blk.decoded && blk.count() as usize >= blk.k {
891 // r == 0 is Passthrough: no parity to recover from, so the block
892 // is complete exactly when all k data shards have arrived (ARQ
893 // fills any gap before count reaches k). r > 0 uses RS erasure
894 // decoding to rebuild missing shards from parity.
895 let recovered = if blk.r == 0 {
896 (0..blk.k).all(|i| blk.shards[i].is_some())
897 } else {
898 RsCode::new(blk.k, blk.r)
899 .expect("valid k,r")
900 .decode(&mut blk.shards)
901 .is_ok()
902 };
903 if recovered {
904 blk.decoded = true;
905 // Concatenate the k data shards into the block info with one
906 // allocation and k memcpys (extend_from_slice), not a clone
907 // of each shard plus a byte-by-byte flatten - this is the
908 // receiver's hottest per-block path.
909 let mut info = Vec::with_capacity(blk.k * blk.shard_len);
910 for i in 0..blk.k {
911 if let Some(s) = &blk.shards[i] {
912 info.extend_from_slice(s);
913 }
914 }
915 decoded_info = Some(info);
916 }
917 }
918 // Keep every decoded block's info available for tower recovery of
919 // a neighbor in the same segment (bounded to the window by the
920 // prune in `drain_in_order`).
921 if let Some(info) = decoded_info {
922 self.data_infos.insert(block_id, info);
923 self.highest_decoded = self.highest_decoded.max(block_id);
924 }
925 self.drain_in_order()
926 }
927
928 /// Reassemble an outer-parity block; on inner-decode, record its info
929 /// for the segment so a whole-lost data block can be reconstructed.
930 fn handle_outer(&mut self, oid: u32, shard_index: usize, k: usize, r: usize, payload: &[u8]) {
931 let d = ((oid >> 27) & 0xF) as usize;
932 let r_outer = ((oid >> 24) & 0x7) as usize;
933 let seg_id = (oid >> 8) & 0xFFFF;
934 let oidx = oid & 0xFF;
935 if d == 0 || r_outer == 0 {
936 return;
937 }
938 // `tower_d` tracks the FULL segment size (for segment-id math);
939 // `seg_d` records this segment's actual data-block count, which is
940 // smaller for the final partial segment.
941 self.tower_d = d.max(self.tower_d);
942 self.tower_r_outer = r_outer;
943 self.seg_d.insert(seg_id, d);
944 let shard_len = payload.len();
945 let blk = self
946 .outer_rx
947 .entry(oid)
948 .or_insert_with(|| RxBlock::new(k, r, shard_len));
949 if blk.shard_len != shard_len || blk.k != k || blk.r != r {
950 return;
951 }
952 let bit = 1u32 << shard_index;
953 if blk.mask.load(Ordering::Relaxed) & bit == 0 {
954 blk.mask.fetch_or(bit, Ordering::Relaxed);
955 blk.shards[shard_index] = Some(payload.to_vec());
956 }
957 if !blk.decoded && blk.count() as usize >= blk.k {
958 let code = RsCode::new(blk.k, blk.r).expect("valid k,r");
959 if code.decode(&mut blk.shards).is_ok() {
960 blk.decoded = true;
961 let mut info = Vec::with_capacity(blk.k * blk.shard_len);
962 for i in 0..blk.k {
963 if let Some(s) = &blk.shards[i] {
964 info.extend_from_slice(s);
965 }
966 }
967 self.outer_rx.remove(&oid);
968 self.seg_outer.entry(seg_id).or_default().insert(oidx, info);
969 }
970 }
971 }
972
973 /// Attempt to reconstruct a whole-lost data block from its segment's
974 /// surviving blocks plus outer parity. On success, inserts a decoded
975 /// block into the window so [`drain_in_order`] delivers it. Returns
976 /// `true` if the block was recovered.
977 fn try_tower_recover(&mut self, block_id: u32) -> bool {
978 let big_d = self.tower_d;
979 let r_outer = self.tower_r_outer;
980 if big_d == 0 || r_outer == 0 || self.inner_k == 0 {
981 return false;
982 }
983 // Segment id / base use the full segment size; the segment's
984 // actual data-block count may be smaller (partial final segment).
985 let seg_id = block_id / big_d as u32;
986 let base = seg_id * big_d as u32;
987 let d = match self.seg_d.get(&seg_id) {
988 Some(&d) => d,
989 None => return false,
990 };
991 let idx_in_seg = (block_id - base) as usize;
992 if idx_in_seg >= d {
993 return false;
994 }
995 let outers = match self.seg_outer.get(&seg_id) {
996 Some(m) => m,
997 None => return false,
998 };
999 // Gather the d data infos and the r_outer outer infos.
1000 let mut blocks: Vec<Option<Vec<u8>>> = Vec::with_capacity(d + r_outer);
1001 for i in 0..d {
1002 blocks.push(self.data_infos.get(&(base + i as u32)).cloned());
1003 }
1004 for j in 0..r_outer {
1005 blocks.push(outers.get(&(j as u32)).cloned());
1006 }
1007 if blocks.iter().filter(|b| b.is_some()).count() < d {
1008 return false;
1009 }
1010 let code = match SegmentCode::new(d, r_outer) {
1011 Ok(c) => c,
1012 Err(_) => return false,
1013 };
1014 if code.decode(&mut blocks).is_err() {
1015 return false;
1016 }
1017 let info = match blocks[idx_in_seg].take() {
1018 Some(v) => v,
1019 None => return false,
1020 };
1021 // Split the recovered info back into k data shards and inject a
1022 // ready-to-deliver block.
1023 let k = self.inner_k;
1024 let shard_len = self.inner_shard_len.max(1);
1025 if info.len() != k * shard_len {
1026 return false;
1027 }
1028 let mut rb = RxBlock::new(k, 0, shard_len);
1029 for i in 0..k {
1030 rb.shards[i] = Some(info[i * shard_len..(i + 1) * shard_len].to_vec());
1031 rb.mask.fetch_or(1u32 << i, Ordering::Relaxed);
1032 }
1033 rb.decoded = true;
1034 self.data_infos.insert(block_id, info);
1035 self.highest_decoded = self.highest_decoded.max(block_id);
1036 self.window.insert(block_id, rb);
1037 true
1038 }
1039
1040 /// Deliver every contiguous decoded block starting at
1041 /// `next_deliver`.
1042 fn drain_in_order(&mut self) -> Vec<Vec<u8>> {
1043 let mut out = Vec::new();
1044 loop {
1045 let id = self.next_deliver.load(Ordering::Relaxed);
1046 let ready = matches!(self.window.get(&id), Some(b) if b.decoded);
1047 if !ready {
1048 // Head block missing or undecoded: try tower recovery
1049 // (reconstruct it from its segment's outer parity) before
1050 // stalling. ARQ remains the fallback if this fails.
1051 if !self.window.contains_key(&id) && self.try_tower_recover(id) {
1052 continue;
1053 }
1054 break;
1055 }
1056 let blk = self.window.remove(&id).unwrap();
1057 // Loss = data shards that did NOT arrive directly and had to be
1058 // recovered: FEC-reconstructed (a data position never received, so
1059 // absent from the mask) plus ARQ-retransmitted (received, but only
1060 // after its original dropped). Parity shards are redundancy, not
1061 // loss, so they are excluded - counting them made a clean link read
1062 // as r/(k+r) loss and pinned FEC on. The counters decay per block
1063 // (~32-block window) so the estimate follows the CURRENT link and
1064 // falls back to zero - and the controller back to Passthrough -
1065 // once loss clears.
1066 let data_mask: u32 = if blk.k >= 32 { u32::MAX } else { (1u32 << blk.k) - 1 };
1067 let data_present = (blk.mask.load(Ordering::Relaxed) & data_mask).count_ones() as u64;
1068 let fec_recovered = (blk.k as u64).saturating_sub(data_present);
1069 // A retransmit whose original later arrived (false_recovery) was a
1070 // spurious retransmission from reordering, not a drop; exclude it
1071 // so reordering does not inflate the estimate and needlessly arm
1072 // FEC (RACK-TLP reordering-vs-loss, RFC 8985). A retransmit with no
1073 // late original is a genuine loss and still counts. The guard's
1074 // subtraction is the A/B knob; the baseline counts every retransmit.
1075 let arq_recovered = if reorder_guard_enabled() {
1076 (blk.retransmitted & !blk.false_recovery & data_mask).count_ones() as u64
1077 } else {
1078 (blk.retransmitted & data_mask).count_ones() as u64
1079 };
1080 // Count the D-SACK false recoveries this block carried (the guard
1081 // firing on real reordered traffic), whether or not the subtraction
1082 // knob is on, so the count reflects detection on the wire.
1083 self.false_recoveries += (blk.false_recovery & data_mask).count_ones() as u64;
1084 self.total_expected = (self.total_expected * 31 / 32) + blk.k as u64;
1085 self.total_missing = (self.total_missing * 31 / 32) + fec_recovered + arq_recovered;
1086 // Differentiate this block's loss congestion-vs-wireless (Biaz +
1087 // Spike hybrid) so the sender treats the two regimes differently.
1088 // The gap is the real lost-shard count (false recoveries already
1089 // excluded from arq_recovered above).
1090 let gap = (fec_recovered + arq_recovered) as u32;
1091 if gap > 0 {
1092 let ia = self.last_interarrival_us;
1093 self.loss_class.classify(gap, ia);
1094 }
1095 // Feed the Gilbert-Elliott burst model the block's per-shard
1096 // original-loss trace in shard order: a shard received on its first
1097 // transmission is `mask & !retransmitted`; everything else (FEC-
1098 // reconstructed or ARQ-retried) was originally lost. At interleave
1099 // depth 1 this is the wire loss order, so the fit sees the native
1100 // burst structure.
1101 let first_tx = blk.mask.load(Ordering::Relaxed) & !blk.retransmitted;
1102 for i in 0..(blk.k + blk.r) {
1103 self.burst_model.observe(first_tx & (1u32 << i) == 0);
1104 }
1105 // Track the peak loss estimate (telemetry).
1106 let cur_loss = self
1107 .total_missing
1108 .saturating_mul(255)
1109 .checked_div(self.total_expected)
1110 .unwrap_or(0)
1111 .min(255) as u8;
1112 self.peak_loss = self.peak_loss.max(cur_loss);
1113 for i in 0..blk.k {
1114 let shard = blk.shards[i].as_ref().expect("decoded data shard");
1115 let item_len =
1116 u16::from_le_bytes([shard[0], shard[1]]) as usize;
1117 if item_len > 0 {
1118 let end = (ITEM_LEN_PREFIX + item_len).min(shard.len());
1119 out.push(shard[ITEM_LEN_PREFIX..end].to_vec());
1120 }
1121 }
1122 self.next_deliver.store(id + 1, Ordering::Relaxed);
1123 }
1124 // Bound bookkeeping to the reassembly window.
1125 let nd = self.next_deliver.load(Ordering::Relaxed);
1126 let keep_from = nd.saturating_sub(self.window_cap as u32);
1127 self.data_infos.retain(|&id, _| id >= keep_from);
1128 if self.tower_d > 0 {
1129 let keep_seg = (keep_from / self.tower_d as u32).saturating_sub(1);
1130 self.seg_outer.retain(|&s, _| s >= keep_seg);
1131 self.seg_d.retain(|&s, _| s >= keep_seg);
1132 self.outer_rx
1133 .retain(|&oid, _| ((oid >> 8) & 0xFFFF) >= keep_seg);
1134 }
1135 out
1136 }
1137
1138 /// Produce a feedback packet: always an ACK of the delivery
1139 /// frontier, plus a NAK for the oldest stalled block.
1140 ///
1141 /// `drive_arq` requests an unconditional NAK of the head block when
1142 /// it is present but undecoded. A receiver sets it on a recv timeout
1143 /// (no fresh data) so the LAST block - which has no newer block to
1144 /// trigger a NAK - still recovers from tail loss. With `drive_arq`
1145 /// false the NAK only fires once a newer block has arrived, which
1146 /// avoids NAKing a block whose shards may still be in flight.
1147 pub fn feedback(&self, drive_arq: bool) -> Feedback {
1148 let ack_through = self.next_deliver.load(Ordering::Relaxed);
1149 let (mut nak_block, mut nak_mask) = (NAK_NONE, 0u32);
1150 // The block we are waiting on is `ack_through`. We chase it once
1151 // it is overdue: a newer block arrived, or the caller is draining
1152 // a stalled tail.
1153 let overdue = drive_arq || self.highest_seen > ack_through;
1154 if overdue {
1155 match self.window.get(&ack_through) {
1156 // Partially received: NAK only the missing shards.
1157 Some(blk) if !blk.decoded => {
1158 let present = blk.mask.load(Ordering::Relaxed);
1159 let full = if blk.k + blk.r >= 32 {
1160 u32::MAX
1161 } else {
1162 (1u32 << (blk.k + blk.r)) - 1
1163 };
1164 nak_block = ack_through;
1165 nak_mask = full & !present;
1166 }
1167 // Entirely missing (zero shards) while later blocks have
1168 // arrived OR the caller is draining the tail: request ALL
1169 // of its shards. The sender clamps the mask to the
1170 // block's real shard count (and ignores a block it does
1171 // not hold). Without this, a head or tail block that
1172 // loses every shard can never be re-requested and
1173 // delivery deadlocks.
1174 None => {
1175 nak_block = ack_through;
1176 nak_mask = u32::MAX;
1177 }
1178 _ => {}
1179 }
1180 }
1181 let loss = self
1182 .total_missing
1183 .saturating_mul(255)
1184 .checked_div(self.total_expected)
1185 .unwrap_or(0)
1186 .min(255) as u8;
1187 // Burstiness proxy: jitter relative to the mean inter-arrival.
1188 // Steady spacing -> ~0; clustered arrivals (bursts) -> toward 1.
1189 let mean_ia = self.temporal.interarrival_micros().max(1.0);
1190 let heuristic = (self.temporal.jitter_micros() / mean_ia).clamp(0.0, 1.0);
1191 // With the Gilbert-Elliott model enabled, derive burstiness from the
1192 // REAL mean burst length (`mean_burst / 16` maps through the sender's
1193 // interleave mapping `depth = burstiness * 16` to `depth = mean_burst`),
1194 // falling back to the jitter heuristic until the fit converges.
1195 let burstiness = if self.use_ge_burst {
1196 self.burst_model
1197 .mean_burst_len()
1198 .map(|mb| (mb / 16.0).clamp(0.0, 1.0))
1199 .unwrap_or(heuristic)
1200 } else {
1201 heuristic
1202 };
1203 // Clock-skew-corrected: a relative clock drift makes the raw OWD slope
1204 // read a false rising / falling trend; the skew estimate removes it, so
1205 // only genuine queueing reaches the controller.
1206 let trend = self.temporal.owd_trend_debiased();
1207 let owd_trend_class = if trend > 0.02 {
1208 2
1209 } else if trend < -0.02 {
1210 0
1211 } else {
1212 1
1213 };
1214 Feedback {
1215 ack_through,
1216 nak_block,
1217 nak_mask,
1218 loss_x255: loss,
1219 burstiness_x255: (burstiness * 255.0) as u8,
1220 owd_trend_class,
1221 loss_class: self.loss_class.class_code(),
1222 }
1223 }
1224
1225 /// Enumerate EVERY gap the reassembly window is holding, as
1226 /// `(block_id, missing_shard_mask)`, so a caller can NAK them all in
1227 /// one feedback cycle instead of one-gap-per-round-trip serial
1228 /// recovery. A block received in part returns its still-missing shards;
1229 /// a block not seen at all returns `u32::MAX` (the sender clamps the
1230 /// mask to the block's real shard count). Gaps strictly below
1231 /// `highest_seen` are always overdue - a later block has arrived, so
1232 /// this one's shards are lost, not merely in flight. The block AT
1233 /// `highest_seen` (the tail) is included only when `drive_tail` is set,
1234 /// matching [`feedback`](Self::feedback)'s single-NAK overdue rule: the
1235 /// tail has no newer block to prove its shards should have arrived, so
1236 /// it is NAK'd only on a recv-timeout drain. The drain ALSO re-requests
1237 /// the head block when `next_deliver` has advanced AT OR ABOVE
1238 /// `highest_seen` - the case where every shard of the next expected
1239 /// (tail) block was lost, so it was never "seen" and sits above the
1240 /// `[next_deliver, highest_seen)` sweep. Without that, delivery
1241 /// deadlocks on a tail block whose whole datagrams were dropped. At
1242 /// most `max` gaps are returned (nearest the delivery frontier first),
1243 /// bounding the feedback burst; the rest are picked up on the next
1244 /// cycle.
1245 pub fn missing_blocks(&self, max: usize, drive_tail: bool) -> Vec<(u32, u32)> {
1246 let nd = self.next_deliver.load(Ordering::Relaxed);
1247 let hi = self.highest_seen;
1248 let mut gaps = Vec::new();
1249 let mut id = nd;
1250 // Genuine gaps sit only below the highest DECODED block: a later
1251 // block fully arrived, proving this one's shards are lost rather
1252 // than still in flight. On a clean link `highest_decoded` tracks
1253 // the delivery frontier, so this loop does nothing - the O(window)
1254 // scan that dominated the receiver is now paid only under real
1255 // loss, not on every poll.
1256 while id <= self.highest_decoded && gaps.len() < max {
1257 self.push_gap(id, &mut gaps);
1258 id = id.saturating_add(1);
1259 }
1260 // Under a drain, chase the block we are BLOCKED on: the tail at
1261 // `highest_seen` (nd <= hi), or the never-seen head above it
1262 // (nd > hi, every shard of the tail block lost). `nd.max(hi)`
1263 // selects whichever it is; a fully-lost tail block returns
1264 // `u32::MAX` (request all shards) so it cannot deadlock delivery.
1265 if drive_tail && gaps.len() < max {
1266 self.push_gap(nd.max(hi), &mut gaps);
1267 }
1268 gaps
1269 }
1270
1271 /// Append `(block_id, missing_mask)` to `gaps` if `block_id` is a gap
1272 /// (received-but-undecoded, or entirely unseen). A decoded block is
1273 /// not a gap and is skipped.
1274 fn push_gap(&self, id: u32, gaps: &mut Vec<(u32, u32)>) {
1275 match self.window.get(&id) {
1276 Some(blk) if !blk.decoded => {
1277 let present = blk.mask.load(Ordering::Relaxed);
1278 let full = if blk.k + blk.r >= 32 {
1279 u32::MAX
1280 } else {
1281 (1u32 << (blk.k + blk.r)) - 1
1282 };
1283 gaps.push((id, full & !present));
1284 }
1285 None => gaps.push((id, u32::MAX)),
1286 _ => {}
1287 }
1288 }
1289
1290 /// Blocks currently held in the reassembly window.
1291 pub fn window_len(&self) -> usize {
1292 self.window.len()
1293 }
1294
1295 /// Give up on the current head block (a gap held past its recovery
1296 /// deadline) and advance delivery past it, returning any items that
1297 /// become deliverable. This is the partial-reliability escape hatch:
1298 /// it skips an unrecoverable gap so the stream is not blocked forever,
1299 /// at the cost of those items. The caller decides the deadline; the
1300 /// transport holds the gap and recovers it via FEC/ARQ until then.
1301 pub fn skip_head(&mut self) -> Vec<Vec<u8>> {
1302 let id = self.next_deliver.load(Ordering::Relaxed);
1303 self.window.remove(&id);
1304 self.data_infos.remove(&id);
1305 self.next_deliver.store(id + 1, Ordering::Relaxed);
1306 self.drain_in_order()
1307 }
1308
1309 /// Diagnostic snapshot of the block currently blocking in-order
1310 /// delivery: `(block_id, received_shards, k, decoded)`, or `None`
1311 /// when that block has not been seen at all (no shard received yet).
1312 pub fn head_status(&self) -> Option<(u32, u32, usize, bool)> {
1313 let id = self.next_deliver.load(Ordering::Relaxed);
1314 self.window
1315 .get(&id)
1316 .map(|b| (id, b.count(), b.k, b.decoded))
1317 }
1318}
1319
1320#[cfg(test)]
1321mod tests {
1322 use super::*;
1323
1324 /// Per-block adaptive shard length: a block of small items ships
1325 /// datagrams sized to the item, not to `max_item`, so schema
1326 /// compression actually reaches the wire. A block sizes to its largest
1327 /// member, and all shards of a block are equal length (the FEC matrix
1328 /// requires it). The decoder reads each block's length from the
1329 /// datagram size, so no header field is added.
1330 #[test]
1331 fn per_block_shard_len_sizes_datagrams_to_items() {
1332 // Generous max_item; small items must NOT be padded up to it.
1333 let mut enc = Encoder::new(8, 2, 256);
1334 let mut dgrams = Vec::new();
1335 for _ in 0..8 {
1336 dgrams.extend(enc.push(&[7u8; 38]));
1337 }
1338 assert_eq!(dgrams.len(), 10, "k+r datagrams per block");
1339 let dlen = dgrams[0].len();
1340 assert_eq!(
1341 dlen,
1342 DATA_HEADER + ITEM_LEN_PREFIX + 38,
1343 "datagram sized to the 38B item, not max_item(256)"
1344 );
1345 assert!(
1346 dgrams.iter().all(|d| d.len() == dlen),
1347 "all shards of a block are equal length"
1348 );
1349
1350 // A block of larger items ships proportionally larger datagrams.
1351 let mut enc2 = Encoder::new(8, 2, 256);
1352 let mut big = Vec::new();
1353 for _ in 0..8 {
1354 big.extend(enc2.push(&[9u8; 200]));
1355 }
1356 assert_eq!(big[0].len(), DATA_HEADER + ITEM_LEN_PREFIX + 200);
1357 assert!(big[0].len() > dlen, "bigger items ship bigger datagrams");
1358
1359 // A mixed-size block sizes to its largest member.
1360 let mut enc3 = Encoder::new(8, 2, 256);
1361 let mut mixed = Vec::new();
1362 for n in [10usize, 50, 20, 40, 30, 12, 8, 25] {
1363 mixed.extend(enc3.push(&vec![1u8; n]));
1364 }
1365 assert_eq!(
1366 mixed[0].len(),
1367 DATA_HEADER + ITEM_LEN_PREFIX + 50,
1368 "block sizes to its 50B max member"
1369 );
1370 }
1371
1372 /// Deterministic LCG so loss / reorder patterns are reproducible.
1373 struct Lcg(u64);
1374 impl Lcg {
1375 fn next_u32(&mut self) -> u32 {
1376 self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
1377 (self.0 >> 33) as u32
1378 }
1379 /// `true` with probability `pct/100`.
1380 fn drop(&mut self, pct: u32) -> bool {
1381 self.next_u32() % 100 < pct
1382 }
1383 }
1384
1385 /// Drive `n` items end-to-end through a channel that drops `loss_pct`
1386 /// of DATA datagrams, with ARQ feedback flowing back. Asserts every
1387 /// item is delivered exactly once, in order.
1388 fn round_trip(n: usize, k: usize, r: usize, loss_pct: u32, seed: u64) {
1389 let mut enc = Encoder::new(k, r, 8);
1390 let mut dec = Decoder::new();
1391 let mut rng = Lcg(seed);
1392 let mut delivered: Vec<u64> = Vec::new();
1393
1394 // Outstanding datagrams from sender to receiver.
1395 let mut wire: Vec<Vec<u8>> = Vec::new();
1396 let send = |wire: &mut Vec<Vec<u8>>, pkts: Vec<Vec<u8>>| wire.extend(pkts);
1397
1398 for i in 0..n as u64 {
1399 send(&mut wire, enc.push(&i.to_le_bytes()));
1400 }
1401 send(&mut wire, enc.flush());
1402
1403 // Pump: deliver (lossily) sender->receiver, feed feedback back,
1404 // until the receiver has everything or we give up.
1405 let mut rounds = 0;
1406 while delivered.len() < n {
1407 rounds += 1;
1408 assert!(rounds < 10_000, "no convergence: {} / {n}", delivered.len());
1409 let batch = std::mem::take(&mut wire);
1410 for pkt in batch {
1411 if rng.drop(loss_pct) {
1412 continue; // packet lost on the wire
1413 }
1414 for item in dec.on_packet(&pkt) {
1415 delivered.push(u64::from_le_bytes(item.try_into().unwrap()));
1416 }
1417 }
1418 // Receiver feedback -> sender (feedback never lost here, so
1419 // ARQ can always make progress; FEC handles the data loss).
1420 // Each pump drives ARQ so a stalled tail is re-requested.
1421 let fb = dec.feedback(true);
1422 send(&mut wire, enc.on_feedback(&fb));
1423 if wire.is_empty() && delivered.len() < n {
1424 // Nothing in flight but still missing: re-request.
1425 let fb = dec.feedback(true);
1426 send(&mut wire, enc.on_feedback(&fb));
1427 if wire.is_empty() {
1428 panic!("stalled with {} / {n} delivered", delivered.len());
1429 }
1430 }
1431 }
1432 let expected: Vec<u64> = (0..n as u64).collect();
1433 assert_eq!(delivered, expected, "ordered exactly-once delivery");
1434 }
1435
1436 #[test]
1437 fn clean_channel_delivers_all() {
1438 round_trip(100, 8, 2, 0, 1);
1439 }
1440
1441 // k + r must be <= MAX_SHARDS (32): the per-block received bitmap is a u32,
1442 // and `1 << idx` for idx >= 32 overflows (a panic in debug, a wrapped mask in
1443 // release -> blocks never complete). k=16 leaves room for r up to 16 (50%
1444 // redundancy), enough for the extreme-loss regime the crossover targets.
1445 #[test]
1446 fn rs_k16_r8_clean() {
1447 round_trip(100, 16, 8, 0, 1);
1448 }
1449
1450 #[test]
1451 fn rs_k16_r16_clean() {
1452 round_trip(100, 16, 16, 0, 1);
1453 }
1454
1455 #[test]
1456 fn rs_k16_r8_loss30() {
1457 round_trip(2000, 16, 8, 30, 7);
1458 }
1459
1460 // k == MAX_SHARDS leaves no room for parity: the encoder must clamp r to 0
1461 // (Passthrough, ARQ-only) rather than emit k + r = 33 shards, which would
1462 // overflow the u32 bitmap (`1 << 32`). Before the r_max fix this panicked /
1463 // stalled; now the clean link delivers via the ARQ floor.
1464 #[test]
1465 fn rs_k32_clamps_to_passthrough() {
1466 round_trip(100, 32, 5, 0, 1);
1467 }
1468
1469 #[test]
1470 fn fec_recovers_light_loss_without_arq() {
1471 // ~10% loss with r=3 over k=8 is within FEC budget most blocks;
1472 // delivery must still be exact.
1473 round_trip(200, 8, 3, 10, 7);
1474 }
1475
1476 #[test]
1477 fn arq_recovers_heavy_loss() {
1478 // 35% loss exceeds any sane parity budget on many blocks; ARQ
1479 // must carry the rest.
1480 round_trip(150, 8, 2, 35, 42);
1481 }
1482
1483 #[test]
1484 fn tiny_blocks_and_flush() {
1485 // n not a multiple of k exercises the padded final block.
1486 round_trip(5, 4, 2, 0, 3);
1487 round_trip(13, 8, 2, 15, 99);
1488 }
1489
1490 #[test]
1491 fn heartbeat_feeds_owd_trend() {
1492 // A genuinely building queue must push the reported trend class to
1493 // "rising" (2). It climbs but dips to a flat baseline periodically -
1494 // a CLEAN linear rise would be indistinguishable from clock skew and
1495 // is removed by the skew correction, so the queue must touch baseline.
1496 let mut dec = Decoder::new();
1497 for i in 0..40u64 {
1498 let send = i * 1000;
1499 let queue = if i % 4 == 0 { 0 } else { i * 60 };
1500 let recv = send + 5000 + queue;
1501 dec.on_heartbeat(send, recv);
1502 }
1503 assert!(dec.owd_trend() > 0.0);
1504 assert_eq!(dec.feedback(true).owd_trend_class, 2, "rising trend reported");
1505 }
1506
1507 #[test]
1508 fn tail_loss_recovered_by_timeout_arq() {
1509 // Drop ALL parity (and one data shard) of the FINAL block - more
1510 // than r losses, and no newer block exists to trigger a NAK.
1511 // Only timeout-driven ARQ (`drive_arq`) can recover it.
1512 let k = 4;
1513 let r = 2;
1514 let mut enc = Encoder::new(k, r, 8);
1515 let mut dec = Decoder::new();
1516 let n = 4; // exactly one block
1517 let mut datagrams = Vec::new();
1518 for i in 0..n as u64 {
1519 datagrams.extend(enc.push(&i.to_le_bytes()));
1520 }
1521 datagrams.extend(enc.flush());
1522 // First pass: deliver only data shards 0,1,2 (drop shard 3 and
1523 // both parity) - block has 3 of 4, cannot FEC-decode.
1524 let mut delivered: Vec<u64> = Vec::new();
1525 for pkt in &datagrams {
1526 let idx = pkt[5];
1527 if idx <= 2 {
1528 for it in dec.on_packet(pkt) {
1529 delivered.push(u64::from_le_bytes(it.try_into().unwrap()));
1530 }
1531 }
1532 }
1533 assert!(delivered.is_empty(), "block not yet recoverable");
1534 // No newer block: a non-driving feedback must NOT NAK.
1535 assert_eq!(dec.feedback(false).nak_block, u32::MAX);
1536 // Timeout-driven feedback NAKs the stalled head.
1537 let fb = dec.feedback(true);
1538 assert_eq!(fb.nak_block, 0);
1539 let arq = enc.on_feedback(&fb);
1540 assert!(!arq.is_empty(), "sender retransmits the missing shards");
1541 for pkt in &arq {
1542 for it in dec.on_packet(pkt) {
1543 delivered.push(u64::from_le_bytes(it.try_into().unwrap()));
1544 }
1545 }
1546 assert_eq!(delivered, vec![0, 1, 2, 3], "tail recovered via ARQ");
1547 }
1548
1549 #[test]
1550 fn missing_head_block_recovered_by_whole_block_nak() {
1551 // A middle block that loses ALL its shards must still be
1552 // re-requested once a later block arrives, or delivery deadlocks
1553 // (the cross-host Direction-2 failure).
1554 let (k, r) = (4usize, 2usize);
1555 let mut enc = Encoder::new(k, r, 8);
1556 let mut dec = Decoder::new();
1557 let mut blocks: Vec<Vec<Vec<u8>>> = Vec::new();
1558 for i in 0..12u64 {
1559 let b = enc.push(&i.to_le_bytes());
1560 if !b.is_empty() {
1561 blocks.push(b);
1562 }
1563 }
1564 assert_eq!(blocks.len(), 3, "12 items / k=4 = 3 blocks");
1565
1566 let mut delivered: Vec<u64> = Vec::new();
1567 let feed = |dec: &mut Decoder, pkts: &[Vec<u8>], out: &mut Vec<u64>| {
1568 for p in pkts {
1569 for it in dec.on_packet(p) {
1570 out.push(u64::from_le_bytes(it.try_into().unwrap()));
1571 }
1572 }
1573 };
1574 // Deliver block 0, DROP all of block 1, deliver block 2.
1575 feed(&mut dec, &blocks[0], &mut delivered);
1576 feed(&mut dec, &blocks[2], &mut delivered);
1577 assert_eq!(delivered, vec![0, 1, 2, 3], "only block 0 deliverable");
1578 assert_eq!(dec.head_status(), None, "block 1 missing entirely");
1579
1580 // Non-drive feedback must now request the whole missing block 1.
1581 let fb = dec.feedback(false);
1582 assert_eq!(fb.nak_block, 1);
1583 assert_eq!(fb.nak_mask, u32::MAX, "request all shards of the lost block");
1584 let rtx = enc.on_feedback(&fb);
1585 assert!(!rtx.is_empty(), "sender retransmits the whole block");
1586 feed(&mut dec, &rtx, &mut delivered);
1587 assert_eq!(delivered, (0..12).collect::<Vec<_>>(), "blocks 1 and 2 delivered");
1588 }
1589
1590 #[test]
1591 fn fully_lost_tail_block_recovered_by_drain_nak() {
1592 // Whole-datagram loss at the TAIL via the selective-NAK path the
1593 // bridge uses (`missing_blocks`). Deliver block 0, then lose EVERY
1594 // shard of the final block 1: `next_deliver` advances to 1 while
1595 // `highest_seen` stays 0, so block 1 sits ABOVE the
1596 // [next_deliver, highest_seen) sweep. The drain must still
1597 // re-request it or delivery deadlocks on the tail - the cross-host
1598 // 30%-loss TIMEOUT this guards against.
1599 let (k, r) = (4usize, 2usize);
1600 let mut enc = Encoder::new(k, r, 8);
1601 let mut dec = Decoder::new();
1602 let mut blocks: Vec<Vec<Vec<u8>>> = Vec::new();
1603 for i in 0..8u64 {
1604 let b = enc.push(&i.to_le_bytes());
1605 if !b.is_empty() {
1606 blocks.push(b);
1607 }
1608 }
1609 assert_eq!(blocks.len(), 2, "8 items / k=4 = 2 blocks");
1610
1611 let mut delivered: Vec<u64> = Vec::new();
1612 let feed = |dec: &mut Decoder, pkts: &[Vec<u8>], out: &mut Vec<u64>| {
1613 for p in pkts {
1614 for it in dec.on_packet(p) {
1615 out.push(u64::from_le_bytes(it.try_into().unwrap()));
1616 }
1617 }
1618 };
1619 // Deliver block 0 fully; DROP every shard of the tail block 1.
1620 feed(&mut dec, &blocks[0], &mut delivered);
1621 assert_eq!(delivered, vec![0, 1, 2, 3], "block 0 delivered, tail unseen");
1622
1623 // Without a drain the unseen tail is not chased (shards could still
1624 // be in flight); under a drain it MUST be re-requested in full.
1625 assert!(
1626 dec.missing_blocks(64, false).is_empty(),
1627 "no drain: unseen tail not yet re-requested"
1628 );
1629 assert_eq!(
1630 dec.missing_blocks(64, true),
1631 vec![(1, u32::MAX)],
1632 "drain re-requests the whole lost tail block"
1633 );
1634
1635 // Sender retransmits block 1; delivery completes to the tail.
1636 let fb = Feedback {
1637 ack_through: 1,
1638 nak_block: 1,
1639 nak_mask: u32::MAX,
1640 loss_x255: 0,
1641 burstiness_x255: 0,
1642 owd_trend_class: 1,
1643 loss_class: 0,
1644 };
1645 let rtx = enc.on_feedback(&fb);
1646 assert!(!rtx.is_empty(), "sender retransmits the lost tail block");
1647 feed(&mut dec, &rtx, &mut delivered);
1648 assert_eq!(
1649 delivered,
1650 (0..8).collect::<Vec<_>>(),
1651 "tail recovered, all delivered"
1652 );
1653 }
1654
1655 #[test]
1656 fn tower_recovers_whole_lost_block_without_arq() {
1657 // A whole data block is erased (every shard). With the tower on,
1658 // the receiver reconstructs it from the segment's surviving blocks
1659 // plus outer parity - no NAK, no ARQ, delivered straight from
1660 // on_packet.
1661 let (k, r) = (4usize, 2usize);
1662 let (d, r_outer) = (4usize, 2usize);
1663 let mut enc = Encoder::new(k, r, 8);
1664 enc.enable_tower(d, r_outer);
1665 let mut dec = Decoder::new();
1666 let n = (d * k) as u64; // one full segment
1667 let mut wire: Vec<Vec<u8>> = Vec::new();
1668 for i in 0..n {
1669 wire.extend(enc.push(&i.to_le_bytes()));
1670 }
1671 let mut delivered: Vec<u64> = Vec::new();
1672 for pkt in &wire {
1673 let bid = u32::from_le_bytes([pkt[1], pkt[2], pkt[3], pkt[4]]);
1674 let is_outer = bid & 0x8000_0000 != 0;
1675 // Erase the ENTIRE second data block (id 1).
1676 if !is_outer && bid == 1 {
1677 continue;
1678 }
1679 for it in dec.on_packet(pkt) {
1680 delivered.push(u64::from_le_bytes(it.try_into().unwrap()));
1681 }
1682 }
1683 assert_eq!(
1684 delivered,
1685 (0..n).collect::<Vec<_>>(),
1686 "tower reconstructed the whole-lost block with no ARQ"
1687 );
1688 }
1689
1690 #[test]
1691 fn window_cap_bounds_far_ahead_blocks() {
1692 // A receiver with a 4-block window must refuse a block 10 ahead
1693 // of the delivery frontier (memory bound / backpressure).
1694 let mut dec = Decoder::with_window(4);
1695 let mut enc = Encoder::new(4, 1, 8);
1696 // Build block id 10 by sealing 10 blocks; keep only its packets.
1697 let mut far = Vec::new();
1698 for b in 0..=10u64 {
1699 let pkts = {
1700 let mut last = Vec::new();
1701 for i in 0..4u64 {
1702 last = enc.push(&(b * 4 + i).to_le_bytes());
1703 }
1704 last
1705 };
1706 if b == 10 {
1707 far = pkts;
1708 }
1709 }
1710 assert!(!far.is_empty(), "sealed block 10");
1711 for pkt in &far {
1712 assert!(dec.on_packet(pkt).is_empty());
1713 }
1714 assert_eq!(dec.window_len(), 0, "block 10 refused by the 4-block window");
1715 assert_eq!(dec.window_cap(), 4);
1716 }
1717
1718 #[test]
1719 fn flow_window_tracks_in_flight() {
1720 let mut enc = Encoder::new(8, 2, 8).with_flow_window(3);
1721 assert_eq!(enc.in_flight(), 0);
1722 for blk in 1..=4u64 {
1723 for i in 0..8u64 {
1724 enc.push(&(blk * 100 + i).to_le_bytes());
1725 }
1726 assert_eq!(enc.in_flight(), blk as u32);
1727 }
1728 assert!(enc.flow_blocked(), "4 in flight exceeds the 3-block window");
1729 // Receiver acks through block 3 (delivered 0,1,2): two remain.
1730 enc.on_feedback(&Feedback {
1731 ack_through: 3,
1732 nak_block: NAK_NONE,
1733 nak_mask: 0,
1734 loss_x255: 0,
1735 burstiness_x255: 0,
1736 owd_trend_class: 1,
1737 loss_class: 0,
1738 });
1739 assert_eq!(enc.in_flight(), 1);
1740 assert!(!enc.flow_blocked());
1741 }
1742
1743 #[test]
1744 fn proactive_retransmit_resends_unacked_oldest_first() {
1745 let k = 8usize;
1746 let mut enc = Encoder::new(k, 2, 8);
1747 // Seal three blocks (0, 1, 2); none acked yet.
1748 for blk in 0..3u64 {
1749 for i in 0..k as u64 {
1750 enc.push(&(blk * 100 + i).to_le_bytes());
1751 }
1752 }
1753 assert_eq!(enc.pending_len(), 3);
1754 assert_eq!(
1755 enc.oldest_pending(),
1756 Some(0),
1757 "block 0 is the frontier the receiver needs first"
1758 );
1759 // A probe of one block is its k data shards, retransmit-flagged.
1760 let probe = enc.probe_block(0);
1761 assert_eq!(probe.len(), k, "probe is the k data shards of the block");
1762 assert!(
1763 probe[0][8] & FLAG_RETRANSMIT != 0,
1764 "probe datagrams are retransmit-flagged for the D-SACK path"
1765 );
1766 assert!(enc.probe_block(99).is_empty(), "no probe for an unknown / acked block");
1767 // The recovery burst is the k data shards of every pending block,
1768 // oldest-first.
1769 let burst = enc.retransmit_all_data();
1770 assert_eq!(burst.len(), 3 * k, "k data shards per pending block");
1771 let lead = u32::from_le_bytes([burst[0][1], burst[0][2], burst[0][3], burst[0][4]]);
1772 assert_eq!(lead, 0, "burst leads with the oldest unacked block");
1773 // After the receiver acks through block 1 (delivered block 0), the
1774 // burst shrinks to the still-unacked blocks.
1775 enc.on_feedback(&Feedback {
1776 ack_through: 1,
1777 nak_block: NAK_NONE,
1778 nak_mask: 0,
1779 loss_x255: 0,
1780 burstiness_x255: 0,
1781 owd_trend_class: 1,
1782 loss_class: 0,
1783 });
1784 assert_eq!(enc.oldest_pending(), Some(1));
1785 assert_eq!(
1786 enc.retransmit_all_data().len(),
1787 2 * k,
1788 "the acked block is dropped from the burst"
1789 );
1790 }
1791
1792 #[test]
1793 fn parity_is_controller_driven_not_self_adapting() {
1794 let mut enc = Encoder::new(8, 1, 8);
1795 assert_eq!(enc.parity(), 1);
1796 // on_feedback must NOT change parity any more - that is the
1797 // fusion controller's job via set_parity.
1798 enc.on_feedback(&Feedback {
1799 ack_through: 0,
1800 nak_block: NAK_NONE,
1801 nak_mask: 0,
1802 loss_x255: (0.25 * 255.0) as u8,
1803 burstiness_x255: 0,
1804 owd_trend_class: 1,
1805 loss_class: 0,
1806 });
1807 assert_eq!(enc.parity(), 1, "feedback no longer self-adapts parity");
1808 enc.set_parity(3);
1809 assert_eq!(enc.parity(), 3, "controller sets parity");
1810 enc.set_parity(99);
1811 // r_max is now the bitmap ceiling MAX_SHARDS - k (k=8 -> 24), not the old
1812 // fixed 8, so a high-loss block can provision parity up to k + r = 32.
1813 assert_eq!(enc.parity(), MAX_SHARDS - 8, "clamped to r_max = MAX_SHARDS - k");
1814 }
1815
1816 #[test]
1817 fn reordered_original_after_retransmit_excluded_from_loss() {
1818 // A shard reordered on the wire: its premature ARQ retransmit arrives
1819 // and fills the slot first, then the late original arrives. Receiving
1820 // the same shard twice is the D-SACK signal (RFC 2883) - reordering,
1821 // not loss - so the estimator must not count it.
1822 let mut enc = Encoder::new(4, 0, 8); // r=0 Passthrough: ARQ-only recovery
1823 let mut dec = Decoder::new();
1824 let mut dgrams = Vec::new();
1825 for i in 0..4u64 {
1826 dgrams.extend(enc.push(&i.to_le_bytes()));
1827 }
1828 assert_eq!(dgrams.len(), 4, "k=4 r=0 -> 4 data datagrams");
1829
1830 // Shard 0 original.
1831 dec.on_packet(&dgrams[0]);
1832 // Shard 1 arrives FIRST as an ARQ retransmit (premature NAK), filling
1833 // the slot and counting as a wire loss.
1834 let mut rtx1 = dgrams[1].clone();
1835 rtx1[8] |= FLAG_RETRANSMIT;
1836 dec.on_packet(&rtx1);
1837 // The late ORIGINAL of shard 1 now arrives: the D-SACK duplicate.
1838 let out = dec.on_packet(&dgrams[1]);
1839 assert!(out.is_empty(), "block still incomplete (2 of 4)");
1840 // Complete the block with the remaining originals; it decodes/delivers.
1841 dec.on_packet(&dgrams[2]);
1842 let delivered = dec.on_packet(&dgrams[3]);
1843 let got: Vec<u64> = delivered
1844 .iter()
1845 .map(|it| u64::from_le_bytes(it.as_slice().try_into().unwrap()))
1846 .collect();
1847 assert_eq!(got, vec![0, 1, 2, 3], "in-order byte-exact delivery preserved");
1848
1849 // The reordered shard's retransmit was a spurious retransmission, so
1850 // the loss estimate - and its running peak - stay at zero.
1851 assert_eq!(
1852 dec.feedback(false).loss_x255,
1853 0,
1854 "reordering not counted as loss"
1855 );
1856 assert_eq!(dec.peak_loss_x255(), 0, "peak loss stays zero under reordering");
1857 assert_eq!(
1858 dec.false_recovery_count(),
1859 1,
1860 "the guard detected exactly one D-SACK false recovery"
1861 );
1862 }
1863
1864 #[test]
1865 fn genuine_retransmit_without_original_counts_as_loss() {
1866 // A shard whose original is truly lost: only its ARQ retransmit
1867 // arrives, with no late original to follow. That is a real drop
1868 // (RACK-TLP keeps it a loss, RFC 8985), so the estimator still counts
1869 // it - the reordering guard must not suppress genuine loss.
1870 let mut enc = Encoder::new(4, 0, 8);
1871 let mut dec = Decoder::new();
1872 let mut dgrams = Vec::new();
1873 for i in 0..4u64 {
1874 dgrams.extend(enc.push(&i.to_le_bytes()));
1875 }
1876 dec.on_packet(&dgrams[0]);
1877 dec.on_packet(&dgrams[1]);
1878 dec.on_packet(&dgrams[2]);
1879 // Shard 3's original was dropped; only its retransmit arrives.
1880 let mut rtx3 = dgrams[3].clone();
1881 rtx3[8] |= FLAG_RETRANSMIT;
1882 let delivered = dec.on_packet(&rtx3);
1883 assert_eq!(delivered.len(), 4, "block completes via the retransmit");
1884 assert!(
1885 dec.feedback(false).loss_x255 > 0,
1886 "a real drop recovered by ARQ is still counted as loss"
1887 );
1888 assert_eq!(
1889 dec.false_recovery_count(),
1890 0,
1891 "a genuine drop is not a D-SACK false recovery"
1892 );
1893 }
1894}