ferro_lumberjack/sequence.rs
1// SPDX-License-Identifier: Apache-2.0
2//! Wrapping `u32` sequence-number arithmetic for Lumberjack ACKs.
3//!
4//! Lumberjack sequence numbers are `u32` and wrap modulo `2^32`. A naive
5//! signed comparison (`acked >= expected`) is **wrong** for long-running
6//! connections that emit more than `2^32` events between reconnects: the
7//! ACK seq wraps around and a correct ACK gets rejected as "stale".
8//!
9//! [RFC 1982] describes the standard solution — "serial number arithmetic"
10//! — and that is what this module implements.
11//!
12//! [RFC 1982]: https://www.rfc-editor.org/rfc/rfc1982
13
14/// A monotonic sequence number with wrapping `u32` arithmetic.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct Sequence(u32);
17
18impl Sequence {
19 /// Construct a sequence number from a raw `u32`.
20 #[must_use]
21 pub const fn new(value: u32) -> Self {
22 Self(value)
23 }
24
25 /// Underlying `u32` value.
26 #[must_use]
27 pub const fn value(self) -> u32 {
28 self.0
29 }
30
31 /// Advance the sequence number by `n`, wrapping at `u32::MAX`.
32 #[must_use]
33 pub const fn advance(self, n: u32) -> Self {
34 Self(self.0.wrapping_add(n))
35 }
36
37 /// Returns true if `acked` is exactly equal to this sequence under
38 /// wrapping arithmetic. Equivalent to `acked == self.value()`, but
39 /// the explicit form documents intent.
40 #[must_use]
41 pub const fn is_exactly_acked_by(self, acked: u32) -> bool {
42 acked.wrapping_sub(self.0) == 0
43 }
44
45 /// Returns true if `acked` is *at least* this sequence — i.e. the
46 /// receiver has acknowledged this sequence or any newer one — under
47 /// wrapping arithmetic per [RFC 1982].
48 ///
49 /// "At least" is interpreted on the half-circle: `acked` is at least
50 /// `self` iff `(acked - self) mod 2^32 < 2^31`. This means the
51 /// comparison is well-defined as long as the two values are within
52 /// `2^31` of each other on the wire — far larger than any plausible
53 /// in-flight window.
54 ///
55 /// [RFC 1982]: https://www.rfc-editor.org/rfc/rfc1982
56 #[must_use]
57 pub const fn is_at_least_acked_by(self, acked: u32) -> bool {
58 // If acked - self underflows past 2^31, acked is "behind" us.
59 acked.wrapping_sub(self.0) < 0x8000_0000
60 }
61}
62
63impl From<u32> for Sequence {
64 fn from(value: u32) -> Self {
65 Self::new(value)
66 }
67}
68
69impl From<Sequence> for u32 {
70 fn from(seq: Sequence) -> Self {
71 seq.value()
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn exact_match() {
81 let s = Sequence::new(100);
82 assert!(s.is_exactly_acked_by(100));
83 assert!(!s.is_exactly_acked_by(99));
84 assert!(!s.is_exactly_acked_by(101));
85 }
86
87 #[test]
88 fn exact_match_wraps_around() {
89 let s = Sequence::new(u32::MAX);
90 assert!(s.is_exactly_acked_by(u32::MAX));
91 // Not acked by 0 — that would be the *next* sequence.
92 assert!(!s.is_exactly_acked_by(0));
93 }
94
95 #[test]
96 fn advance_wraps() {
97 let s = Sequence::new(u32::MAX);
98 assert_eq!(s.advance(1).value(), 0);
99 assert_eq!(s.advance(2).value(), 1);
100 }
101
102 #[test]
103 fn at_least_basic() {
104 let s = Sequence::new(100);
105 assert!(s.is_at_least_acked_by(100));
106 assert!(s.is_at_least_acked_by(101));
107 assert!(s.is_at_least_acked_by(200));
108 assert!(!s.is_at_least_acked_by(99));
109 assert!(!s.is_at_least_acked_by(0));
110 }
111
112 #[test]
113 fn at_least_across_wrap() {
114 // Sender sent seq u32::MAX; receiver acks with seq 5 (after wrap).
115 // The ACK is "ahead" by 6 → at least.
116 let s = Sequence::new(u32::MAX);
117 assert!(s.is_at_least_acked_by(0));
118 assert!(s.is_at_least_acked_by(5));
119 // 2^31 - 1 ahead is still "ahead".
120 assert!(s.is_at_least_acked_by(0x7FFF_FFFE));
121 // 2^31 ahead is the equidistant midpoint — by the RFC 1982 rule,
122 // values exactly 2^31 away are unordered, and we treat them as
123 // "behind" (strict <).
124 assert!(!s.is_at_least_acked_by(0x7FFF_FFFF));
125 }
126
127 #[test]
128 fn round_trip_u32() {
129 let s = Sequence::from(42_u32);
130 let v: u32 = s.into();
131 assert_eq!(v, 42);
132 }
133}