Skip to main content

subetha_cxc/
reorder.rs

1//! Consumer-side exact-delivery reorder buffer for the best-effort
2//! `MergeByStamp` (merge_tsc) ordering mode.
3//!
4//! `MergeByStamp` merges producer ring heads by stamp cheaply, but on a
5//! host without an invariant TSC (stamps fall back to `SharedCounter`)
6//! it has no watermark gate, so under producer lag it can hand the
7//! consumer a smaller stamp late - a cross-core reservation-store race
8//! ([`crate::ordering`]). `MergeStrict` closes that by waiting on every
9//! producer's watermark: exact, but it couples release latency to the
10//! slowest producer and scans every producer line per pop.
11//!
12//! This buffer takes the other route: pop best-effort and reorder on the
13//! consumer side. It holds a bounded min-by-stamp window; once more than
14//! `window` items are buffered it releases the minimum. A stamp that
15//! arrives up to `window` positions late is still reordered ahead of the
16//! items buffered after it, so delivery is stamp-monotone as long as
17//! `window` covers the host's out-of-order displacement. It decouples
18//! from the slowest producer (the cost is a bounded buffering latency,
19//! not a watermark wait) and never touches a second CAS. Holes (a
20//! `Full`-failed push consumes a `SharedCounter` value that never lands
21//! in a ring) do not stall it: it releases the minimum of what it holds,
22//! so a missing stamp is simply skipped, delivery stays monotone.
23//!
24//! Measured on a 16-vCPU KVM guest (SharedCounter, 4 producers -> 1
25//! consumer): exact delivery at ~113 ns/item (window 8) vs ~138 ns for
26//! `MergeStrict` and ~105 ns for the raw best-effort merge.
27//!
28//! # Adaptive window
29//! The window starts at `floor` and grows (up to `cap`) whenever the
30//! buffer catches a stamp below the last one it released - i.e. whenever
31//! the current window was too small for the observed displacement.
32//! [`corrections`](ReorderBuffer::corrections) reports how many times
33//! that happened: it staying at zero means `floor` already covered the
34//! host; a value that rises then stops means the window found the right
35//! size; a value that keeps rising means displacement exceeds `cap`.
36//!
37//! # Guarantee (read this)
38//! Delivery is exactly stamp-monotone **while `window >= max
39//! displacement`**. This is NOT an unconditional, host-independent
40//! guarantee: if a displacement spike exceeds the current window, one
41//! item can be released out of order *before* the window grows to
42//! absorb the next spike. Start `floor` at or above the host's expected
43//! displacement for exact delivery from the first item. For an
44//! unconditional guarantee regardless of host, use `MergeStrict`.
45
46use std::cmp::Ordering;
47use std::cmp::Reverse;
48use std::collections::BinaryHeap;
49
50use crate::adaptive_ring::{AdaptiveRing, PinnedRing};
51use crate::ordering::{OrderingMode, StampKind, STAMPED_PAYLOAD_BYTES};
52
53/// Default starting window. The observed max out-of-order displacement
54/// on a 16-vCPU KVM guest was <= 4; 8 leaves margin for exact delivery
55/// from item zero on comparable hosts.
56pub const DEFAULT_FLOOR: usize = 8;
57/// Default window ceiling. Past this the buffering latency outweighs the
58/// reorder benefit; a stream needing more should use `MergeStrict`.
59pub const DEFAULT_CAP: usize = 1024;
60
61#[derive(Clone)]
62struct Entry {
63    stamp: u64,
64    seq: u64,
65    len: usize,
66    payload: [u8; STAMPED_PAYLOAD_BYTES],
67}
68
69impl PartialEq for Entry {
70    fn eq(&self, other: &Self) -> bool {
71        (self.stamp, self.seq) == (other.stamp, other.seq)
72    }
73}
74impl Eq for Entry {}
75impl PartialOrd for Entry {
76    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
77        Some(self.cmp(other))
78    }
79}
80impl Ord for Entry {
81    fn cmp(&self, other: &Self) -> Ordering {
82        // (stamp, seq): seq breaks ties so equal stamps keep arrival
83        // (FIFO) order and the heap is a total order.
84        (self.stamp, self.seq).cmp(&(other.stamp, other.seq))
85    }
86}
87
88/// Bounded min-by-stamp reorder buffer with an adaptive window. See the
89/// module docs for the guarantee and the `MergeStrict` trade-off.
90pub struct ReorderBuffer {
91    heap: BinaryHeap<Reverse<Entry>>,
92    window: usize,
93    cap: usize,
94    last_emitted: u64,
95    have_emitted: bool,
96    next_seq: u64,
97    corrections: u64,
98}
99
100impl Default for ReorderBuffer {
101    fn default() -> Self {
102        Self::with_window(DEFAULT_FLOOR, DEFAULT_CAP)
103    }
104}
105
106impl ReorderBuffer {
107    /// A reorder buffer with the default floor/cap window.
108    pub fn new() -> Self {
109        Self::default()
110    }
111
112    /// A reorder buffer whose window starts at `floor` and grows (on a
113    /// caught late stamp) up to `cap`. `cap` is clamped to at least
114    /// `floor`.
115    pub fn with_window(floor: usize, cap: usize) -> Self {
116        Self {
117            heap: BinaryHeap::new(),
118            window: floor,
119            cap: cap.max(floor),
120            last_emitted: 0,
121            have_emitted: false,
122            next_seq: 0,
123            corrections: 0,
124        }
125    }
126
127    /// Proactively raise the window (and, if needed, the cap) to at
128    /// least `min_window`. Called when the producer count GROWS at
129    /// runtime: displacement is bounded by the concurrent producer
130    /// count, so widening on growth keeps delivery provably exact
131    /// instead of waiting for a caught late stamp (which admits one
132    /// out-of-order release before the reactive growth kicks in).
133    pub fn widen_to(&mut self, min_window: usize) {
134        if min_window > self.window {
135            self.window = min_window;
136            self.cap = self.cap.max(min_window);
137        }
138    }
139
140    /// Buffer one popped item (its `stamp` and `payload`). `payload`
141    /// must fit in `STAMPED_PAYLOAD_BYTES`; longer input is truncated to
142    /// that bound (the ring never delivers more than a stamped slot
143    /// holds).
144    pub fn push(&mut self, stamp: u64, payload: &[u8]) {
145        let len = payload.len().min(STAMPED_PAYLOAD_BYTES);
146        let mut buf = [0u8; STAMPED_PAYLOAD_BYTES];
147        buf[..len].copy_from_slice(&payload[..len]);
148        let seq = self.next_seq;
149        self.next_seq += 1;
150        self.heap.push(Reverse(Entry { stamp, seq, len, payload: buf }));
151    }
152
153    /// Release the next in-order item into `out` if the buffer holds more
154    /// than `window` items, returning its stamp and payload length.
155    /// Returns `None` while the buffer is still filling the window (the
156    /// steady-state call: push a pop, then `try_take`).
157    pub fn try_take(&mut self, out: &mut [u8]) -> Option<(u64, usize)> {
158        if self.heap.len() > self.window {
159            self.release(out)
160        } else {
161            None
162        }
163    }
164
165    /// Drain-time release: pop the next in-order item regardless of the
166    /// window. Call in a loop after the source is exhausted to flush the
167    /// tail in stamp order.
168    pub fn flush_one(&mut self, out: &mut [u8]) -> Option<(u64, usize)> {
169        self.release(out)
170    }
171
172    fn release(&mut self, out: &mut [u8]) -> Option<(u64, usize)> {
173        let Reverse(entry) = self.heap.pop()?;
174        // A stamp below the last released one is a real out-of-order
175        // delivery: the window was smaller than the displacement. Record
176        // it and grow the window so the next spike is absorbed.
177        if self.have_emitted && entry.stamp < self.last_emitted {
178            self.corrections += 1;
179            self.window = self.window.saturating_mul(2).min(self.cap).max(1);
180        }
181        self.last_emitted = entry.stamp;
182        self.have_emitted = true;
183        let n = entry.len.min(out.len());
184        out[..n].copy_from_slice(&entry.payload[..n]);
185        Some((entry.stamp, n))
186    }
187
188    /// Items currently buffered.
189    pub fn len(&self) -> usize {
190        self.heap.len()
191    }
192
193    /// Whether the buffer holds no items.
194    pub fn is_empty(&self) -> bool {
195        self.heap.is_empty()
196    }
197
198    /// Current adaptive window size.
199    pub fn window(&self) -> usize {
200        self.window
201    }
202
203    /// How many times a late stamp forced the window to grow. Zero means
204    /// the starting floor covered every observed displacement; see the
205    /// module docs on reading this.
206    pub fn corrections(&self) -> u64 {
207        self.corrections
208    }
209}
210
211/// Ergonomic exact-delivery wrapper: pairs a stamped-ring consumer
212/// handle ([`PinnedRing`]) with a [`ReorderBuffer`] so a `GlobalFifo`
213/// (`MergeByStamp`) consumer receives items in exact stamp order without
214/// the `MergeStrict` watermark coupling or a Vyukov second CAS.
215///
216/// ```ignore
217/// let pin = ring.pin_current_shape();
218/// let mut rx = ReorderingReceiver::new(&pin, 0);
219/// let mut out = [0u8; 56];
220/// // steady state:
221/// while let Some((len, stamp)) = rx.try_recv(&mut out) {
222///     deliver(&out[..len], stamp);
223/// }
224/// // end of stream - drain the buffered tail in order:
225/// while let Some((len, stamp)) = rx.flush(&mut out) {
226///     deliver(&out[..len], stamp);
227/// }
228/// ```
229pub struct ReorderingReceiver<'a> {
230    pin: &'a PinnedRing<'a>,
231    consumer_id: usize,
232    buf: ReorderBuffer,
233    scratch: [u8; STAMPED_PAYLOAD_BYTES],
234}
235
236impl<'a> ReorderingReceiver<'a> {
237    /// Wrap `pin` (a stamped-ring consumer handle) with the default
238    /// adaptive window.
239    pub fn new(pin: &'a PinnedRing<'a>, consumer_id: usize) -> Self {
240        Self::with_window(pin, consumer_id, DEFAULT_FLOOR, DEFAULT_CAP)
241    }
242
243    /// Wrap `pin` with an explicit floor/cap window (see
244    /// [`ReorderBuffer::with_window`]).
245    pub fn with_window(
246        pin: &'a PinnedRing<'a>,
247        consumer_id: usize,
248        floor: usize,
249        cap: usize,
250    ) -> Self {
251        Self {
252            pin,
253            consumer_id,
254            buf: ReorderBuffer::with_window(floor, cap),
255            scratch: [0u8; STAMPED_PAYLOAD_BYTES],
256        }
257    }
258
259    /// Pop one item from the ring into the buffer (if available), then
260    /// release the next in-order item once the window is full. Returns
261    /// `(payload_len, stamp)`, or `None` while the window is still
262    /// filling or the ring is momentarily empty. On end of stream,
263    /// finish with [`flush`](Self::flush).
264    pub fn try_recv(&mut self, out: &mut [u8]) -> Option<(usize, u64)> {
265        if let Ok((n, stamp)) =
266            self.pin.ordered_try_pop_with_stamp(self.consumer_id, &mut self.scratch)
267        {
268            self.buf.push(stamp, &self.scratch[..n]);
269        }
270        self.buf.try_take(out).map(|(stamp, len)| (len, stamp))
271    }
272
273    /// Drain the buffered tail in stamp order. Call in a loop after the
274    /// producers have finished to release the last `window` items.
275    pub fn flush(&mut self, out: &mut [u8]) -> Option<(usize, u64)> {
276        self.buf.flush_one(out).map(|(stamp, len)| (len, stamp))
277    }
278
279    /// Times the adaptive window had to grow (see
280    /// [`ReorderBuffer::corrections`]).
281    pub fn corrections(&self) -> u64 {
282        self.buf.corrections()
283    }
284
285    /// Current adaptive window.
286    pub fn window(&self) -> usize {
287        self.buf.window()
288    }
289}
290
291/// Producer-count ceiling for the reorder-buffer strategy. The
292/// best-effort merge's out-of-order displacement is bounded by the
293/// concurrent producer count, so a reorder window `>= producers` is
294/// provably exact. Above this many producers the window (and its
295/// buffering latency) would be too large, so the adaptive receiver
296/// morphs the ring to `MergeStrict` instead.
297pub const REORDER_PRODUCER_CAP: usize = 256;
298
299enum ExactMode {
300    /// SharedCounter, producers <= cap: keep `MergeByStamp` and reorder
301    /// on the consumer with a window sized to the producer count.
302    Reorder(ReorderBuffer),
303    /// SharedCounter, producers > cap: ring morphed to `MergeStrict`;
304    /// the pop is already exact (watermark gate), no buffering.
305    Strict,
306    /// Time-based stamps (freshness-guarded merge) or unstamped: the pop
307    /// order needs no consumer-side correction.
308    Direct,
309}
310
311/// Automatic exact-delivery consumer for a stamped `GlobalFifo` ring.
312///
313/// Picks the cheapest strategy that is exact for the ring's
314/// configuration, mirroring the adaptive-ring philosophy (use the fast
315/// path while it is correct, morph when it is not):
316///
317/// - **SharedCounter stamps** (the config whose cheap `MergeByStamp`
318///   merge can deliver out of order under producer lag): if the
319///   producer count fits a bounded reorder window
320///   (`<= REORDER_PRODUCER_CAP`), keep `MergeByStamp` and correct on the
321///   consumer with a [`ReorderBuffer`] sized `>= producers` (provably
322///   exact, ~9% over the raw merge). Otherwise morph the ring to
323///   `MergeStrict` (exact at any scale via the watermark wait).
324/// - **Time-based stamps** (`Tsc`/`Monotonic`, freshness-guarded) or an
325///   unstamped ring: deliver directly, no correction needed.
326///
327/// ```ignore
328/// let mut rx = AdaptiveOrderedReceiver::new(&ring, 0);
329/// let mut out = [0u8; 56];
330/// while let Some((len, stamp)) = rx.try_recv(&mut out) { deliver(&out[..len], stamp); }
331/// while let Some((len, stamp)) = rx.flush(&mut out) { deliver(&out[..len], stamp); } // end of stream
332/// ```
333pub struct AdaptiveOrderedReceiver<'a> {
334    ring: &'a AdaptiveRing,
335    consumer_id: usize,
336    mode: ExactMode,
337}
338
339impl<'a> AdaptiveOrderedReceiver<'a> {
340    /// Set up exact delivery for `ring`, auto-selecting the strategy and
341    /// setting the ring's merge mode to match. Call once per consumer.
342    ///
343    /// The reorder window sizes to the PUBLISHED producer-slot count
344    /// (pre-allocated + grown), so a ring that grew past its
345    /// construction hint still gets the provably-exact window.
346    pub fn new(ring: &'a AdaptiveRing, consumer_id: usize) -> Self {
347        let producers = ring.published_producers().max(ring.max_producers());
348        let mode = match ring.stamp_kind() {
349            Some(StampKind::SharedCounter) => {
350                if producers <= REORDER_PRODUCER_CAP {
351                    ring.set_ordering_mode(OrderingMode::MergeByStamp).ok();
352                    let window = producers.max(DEFAULT_FLOOR);
353                    ExactMode::Reorder(ReorderBuffer::with_window(
354                        window,
355                        window.max(DEFAULT_CAP),
356                    ))
357                } else {
358                    ring.set_ordering_mode(OrderingMode::MergeStrict).ok();
359                    ExactMode::Strict
360                }
361            }
362            _ => ExactMode::Direct,
363        };
364        Self { ring, consumer_id, mode }
365    }
366
367    /// Deliver the next in-order item, or `None` if none is ready.
368    /// Under the reorder strategy this holds the window during
369    /// streaming; drain the tail with [`flush`](Self::flush) at end of
370    /// stream.
371    pub fn try_recv(&mut self, out: &mut [u8]) -> Option<(usize, u64)> {
372        let pin = self.ring.pin_current_shape();
373        let cid = self.consumer_id;
374        match &mut self.mode {
375            ExactMode::Reorder(rb) => {
376                // Producers can GROW mid-stream: widen the window with
377                // them (displacement is bounded by producer count).
378                // Past the reorder cap, flip the ring to the strict
379                // watermark wait; the buffer then sees monotone input
380                // and delivery stays exact.
381                let producers = self.ring.published_producers();
382                if producers > rb.window() {
383                    if producers > REORDER_PRODUCER_CAP {
384                        self.ring
385                            .set_ordering_mode(OrderingMode::MergeStrict)
386                            .ok();
387                    }
388                    rb.widen_to(producers.min(REORDER_PRODUCER_CAP));
389                }
390                let mut scratch = [0u8; STAMPED_PAYLOAD_BYTES];
391                if let Ok((n, stamp)) = pin.ordered_try_pop_with_stamp(cid, &mut scratch) {
392                    rb.push(stamp, &scratch[..n]);
393                }
394                rb.try_take(out).map(|(stamp, len)| (len, stamp))
395            }
396            ExactMode::Strict | ExactMode::Direct => {
397                match pin.ordered_try_pop_with_stamp(cid, out) {
398                    Ok((n, stamp)) => Some((n, stamp)),
399                    Err(_) => None,
400                }
401            }
402        }
403    }
404
405    /// Drain the buffered tail in order (reorder strategy only); `None`
406    /// once drained or under the strict/direct strategies.
407    pub fn flush(&mut self, out: &mut [u8]) -> Option<(usize, u64)> {
408        match &mut self.mode {
409            ExactMode::Reorder(rb) => rb.flush_one(out).map(|(stamp, len)| (len, stamp)),
410            _ => None,
411        }
412    }
413
414    /// Which exact-delivery strategy was auto-selected: `"reorder"`,
415    /// `"strict"`, or `"direct"`.
416    pub fn strategy(&self) -> &'static str {
417        match self.mode {
418            ExactMode::Reorder(_) => "reorder",
419            ExactMode::Strict => "strict",
420            ExactMode::Direct => "direct",
421        }
422    }
423
424    /// Times the reorder window had to grow (reorder strategy only); a
425    /// nonzero value means the observed displacement exceeded the
426    /// producer-count-derived window.
427    pub fn corrections(&self) -> u64 {
428        match &self.mode {
429            ExactMode::Reorder(rb) => rb.corrections(),
430            _ => 0,
431        }
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    /// A permutation of `1..=n` where each value is displaced from its
440    /// sorted index by at most `d` (blocks of `d+1` reversed).
441    fn displaced(n: u64, d: usize) -> Vec<u64> {
442        let mut v = Vec::with_capacity(n as usize);
443        let block = d as u64 + 1;
444        let mut base = 1u64;
445        while base <= n {
446            let end = (base + block - 1).min(n);
447            for s in (base..=end).rev() {
448                v.push(s);
449            }
450            base = end + 1;
451        }
452        v
453    }
454
455    /// Drive a stream through the buffer at a fixed window (no growth):
456    /// returns (emitted_stamps, corrections).
457    fn drive(stream: &[u64], floor: usize) -> (Vec<u64>, u64) {
458        let mut rb = ReorderBuffer::with_window(floor, floor); // cap==floor: no growth
459        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
460        let mut emitted = Vec::new();
461        for &s in stream {
462            rb.push(s, &s.to_le_bytes());
463            if let Some((stamp, _)) = rb.try_take(&mut out) {
464                emitted.push(stamp);
465            }
466        }
467        while let Some((stamp, _)) = rb.flush_one(&mut out) {
468            emitted.push(stamp);
469        }
470        (emitted, rb.corrections())
471    }
472
473    fn is_monotone(v: &[u64]) -> bool {
474        v.windows(2).all(|w| w[1] >= w[0])
475    }
476
477    #[test]
478    fn in_order_stream_is_untouched() {
479        let stream: Vec<u64> = (1..=1000).collect();
480        let (emitted, corr) = drive(&stream, 8);
481        assert_eq!(emitted, stream);
482        assert_eq!(corr, 0);
483    }
484
485    #[test]
486    fn window_at_or_above_displacement_is_exact() {
487        for d in [1usize, 3, 8] {
488            let stream = displaced(2000, d);
489            let (emitted, corr) = drive(&stream, d); // window == displacement
490            assert!(is_monotone(&emitted), "d={d}: not monotone");
491            assert_eq!(corr, 0, "d={d}: unexpected corrections at window==d");
492            assert_eq!(emitted.len(), stream.len());
493        }
494    }
495
496    #[test]
497    fn window_below_displacement_slips_and_is_flagged() {
498        // window 1 cannot cover displacement 4: some items slip and the
499        // corrections counter records it.
500        let stream = displaced(2000, 4);
501        let (emitted, corr) = drive(&stream, 1);
502        assert!(corr > 0, "expected corrections when window < displacement");
503        assert_eq!(emitted.len(), stream.len());
504    }
505
506    #[test]
507    fn adaptive_window_grows_toward_displacement() {
508        // Start below the displacement with headroom to grow: the window
509        // climbs and corrections are what drive the growth.
510        let stream = displaced(4000, 8);
511        let mut rb = ReorderBuffer::with_window(2, 64);
512        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
513        for &s in &stream {
514            rb.push(s, &s.to_le_bytes());
515            while rb.try_take(&mut out).is_some() {}
516        }
517        while rb.flush_one(&mut out).is_some() {}
518        assert!(rb.window() > 2, "window should have grown, got {}", rb.window());
519        assert!(rb.corrections() > 0, "growth is driven by caught slips");
520        assert!(rb.window() >= 8, "window should reach the displacement, got {}", rb.window());
521    }
522
523    #[test]
524    fn holes_do_not_stall_and_stay_monotone() {
525        // Stamps 1,2,4,5,7,8... (every third is a hole) arriving with a
526        // small local swap; delivery must stay monotone and skip holes.
527        let mut stream = Vec::new();
528        let mut s = 1u64;
529        while s < 300 {
530            // swap a pair to force reorder within the window
531            stream.push(s + 1);
532            stream.push(s);
533            s += 3; // skip s+2 -> a hole
534        }
535        let (emitted, _) = drive(&stream, 8);
536        assert!(is_monotone(&emitted), "delivery must be monotone across holes");
537        assert_eq!(emitted.len(), stream.len(), "no item dropped");
538    }
539
540    #[test]
541    fn payload_round_trips() {
542        let mut rb = ReorderBuffer::with_window(1, 1);
543        rb.push(10, b"hello");
544        rb.push(9, b"world");
545        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
546        // window 1, 2 buffered -> release min (stamp 9, "world")
547        let (stamp, n) = rb.try_take(&mut out).expect("release");
548        assert_eq!(stamp, 9);
549        assert_eq!(&out[..n], b"world");
550        let (stamp, n) = rb.flush_one(&mut out).expect("flush");
551        assert_eq!(stamp, 10);
552        assert_eq!(&out[..n], b"hello");
553    }
554
555    #[test]
556    fn adaptive_receiver_selects_strategy_by_config() {
557        use crate::adaptive_ring::{AdaptiveRing, RingShape};
558
559        // SharedCounter, few producers -> reorder (window covers them).
560        let ring = AdaptiveRing::create_anon(4, 1, 256)
561            .unwrap()
562            .with_ordering_stamps_kind(StampKind::SharedCounter)
563            .unwrap();
564        ring.morph_to(RingShape::Mpsc).unwrap();
565        let rx = AdaptiveOrderedReceiver::new(&ring, 0);
566        assert_eq!(rx.strategy(), "reorder");
567        assert_eq!(ring.ordering_mode(), Some(OrderingMode::MergeByStamp));
568
569        // SharedCounter, producers above the cap -> morph to strict.
570        let big = AdaptiveRing::create_anon(REORDER_PRODUCER_CAP + 1, 1, 64)
571            .unwrap()
572            .with_ordering_stamps_kind(StampKind::SharedCounter)
573            .unwrap();
574        big.morph_to(RingShape::Mpsc).unwrap();
575        let rx = AdaptiveOrderedReceiver::new(&big, 0);
576        assert_eq!(rx.strategy(), "strict");
577        assert_eq!(big.ordering_mode(), Some(OrderingMode::MergeStrict));
578
579        // Time-based stamp (freshness-guarded merge) -> direct.
580        let tsc = AdaptiveRing::create_anon(4, 1, 256)
581            .unwrap()
582            .with_ordering_stamps_kind(StampKind::Tsc)
583            .unwrap();
584        tsc.morph_to(RingShape::Mpsc).unwrap();
585        let rx = AdaptiveOrderedReceiver::new(&tsc, 0);
586        assert_eq!(rx.strategy(), "direct");
587    }
588}