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}