Skip to main content

pamoja_mesh/
seen.rs

1//! Duplicate suppression for flooded packets.
2
3/// A fixed-size memory of the most recently seen packets, so a node relays each one once.
4///
5/// In a flood every node rebroadcasts what it hears, so the same packet reaches a node
6/// from several neighbours. Without a memory of what it has already handled, a node would
7/// relay every copy and the flood would multiply without bound. This cache remembers the
8/// last `N` packet keys (a [`dedup_key`](crate::Frame::dedup_key), the source and sequence
9/// id) in a ring, evicting the oldest as new ones arrive, so the test for "have I seen
10/// this?" stays cheap and needs no allocation. `N` sets how far back the memory reaches;
11/// a small power of two such as 32 or 64 suits a local mesh.
12///
13/// # Examples
14///
15/// ```
16/// use pamoja_mesh::SeenCache;
17///
18/// let mut seen: SeenCache<8> = SeenCache::new();
19/// assert!(seen.record((0x42, 1)));  // first time: newly recorded
20/// assert!(!seen.record((0x42, 1))); // again: a duplicate
21/// assert!(seen.record((0x42, 2)));  // a different packet
22/// ```
23#[derive(Clone, Copy, Debug)]
24pub struct SeenCache<const N: usize> {
25    keys: [Option<(u32, u16)>; N],
26    next: usize,
27}
28
29impl<const N: usize> SeenCache<N> {
30    /// Creates an empty cache.
31    ///
32    /// # Returns
33    ///
34    /// A cache holding no keys.
35    pub const fn new() -> Self {
36        SeenCache {
37            keys: [None; N],
38            next: 0,
39        }
40    }
41
42    /// Reports whether a key is currently remembered.
43    ///
44    /// # Arguments
45    ///
46    /// * `key` - the packet key to look for, from [`dedup_key`](crate::Frame::dedup_key).
47    ///
48    /// # Returns
49    ///
50    /// `true` if the key is in the cache.
51    pub fn contains(&self, key: (u32, u16)) -> bool {
52        self.keys.contains(&Some(key))
53    }
54
55    /// Records a key, reporting whether it was new.
56    ///
57    /// This is the flood test: record the key of a received packet, and act on the packet
58    /// only when this returns `true`. The oldest remembered key is evicted once the cache
59    /// is full.
60    ///
61    /// # Arguments
62    ///
63    /// * `key` - the packet key to record, from [`dedup_key`](crate::Frame::dedup_key).
64    ///
65    /// # Returns
66    ///
67    /// `true` if the key was not already remembered (the packet is new), `false` if it was
68    /// (the packet is a duplicate).
69    pub fn record(&mut self, key: (u32, u16)) -> bool {
70        record_into(&mut self.keys, &mut self.next, key)
71    }
72}
73
74impl<const N: usize> Default for SeenCache<N> {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80// Records a key into a ring of slots, reporting whether it was new. Split out so the
81// fixed-size and runtime-sized caches share one implementation rather than two that can
82// drift.
83fn record_into(keys: &mut [Option<(u32, u16)>], next: &mut usize, key: (u32, u16)) -> bool {
84    if keys.contains(&Some(key)) {
85        return false;
86    }
87    // A zero-capacity cache remembers nothing, so every key reads as new and every packet
88    // is relayed, which is the behaviour of a node with no cache at all.
89    if keys.is_empty() {
90        return true;
91    }
92    keys[*next] = Some(key);
93    *next = (*next + 1) % keys.len();
94    true
95}
96
97/// A duplicate cache whose size is chosen when it is built, rather than at compile time.
98///
99/// [`SeenCache`] fixes its capacity in the type, which suits a microcontroller that knows
100/// its own limits. A gateway, or any caller reaching this through a language binding, does
101/// not know the size until it runs, and a const generic cannot cross a foreign function
102/// boundary at all. This is the same cache with its slots on the heap, so both share one
103/// implementation and answer identically.
104///
105/// Requires the `alloc` feature.
106///
107/// # Examples
108///
109/// ```
110/// use pamoja_mesh::DynamicSeenCache;
111///
112/// // A busy relay remembers more packets than a leaf node needs to.
113/// let mut seen = DynamicSeenCache::new(1024);
114/// assert!(seen.record((0x42, 1)));
115/// assert!(!seen.record((0x42, 1)));
116/// assert_eq!(seen.capacity(), 1024);
117/// ```
118#[cfg(any(feature = "alloc", test))]
119#[derive(Clone, Debug)]
120pub struct DynamicSeenCache {
121    keys: alloc::vec::Vec<Option<(u32, u16)>>,
122    next: usize,
123}
124
125#[cfg(any(feature = "alloc", test))]
126impl DynamicSeenCache {
127    /// Creates an empty cache remembering up to `capacity` packets.
128    ///
129    /// # Arguments
130    ///
131    /// * `capacity` - how many recently seen packets to remember. A capacity of zero is
132    ///   allowed and makes every packet read as new, which relays every copy.
133    ///
134    /// # Returns
135    ///
136    /// A cache holding no keys.
137    pub fn new(capacity: usize) -> Self {
138        DynamicSeenCache {
139            keys: alloc::vec![None; capacity],
140            next: 0,
141        }
142    }
143
144    /// Returns how many packets this cache remembers.
145    ///
146    /// # Returns
147    ///
148    /// The capacity it was created with.
149    pub fn capacity(&self) -> usize {
150        self.keys.len()
151    }
152
153    /// Reports whether a key is currently remembered.
154    ///
155    /// # Arguments
156    ///
157    /// * `key` - the packet key to look for, from [`dedup_key`](crate::Frame::dedup_key).
158    ///
159    /// # Returns
160    ///
161    /// `true` if the key is in the cache.
162    pub fn contains(&self, key: (u32, u16)) -> bool {
163        self.keys.contains(&Some(key))
164    }
165
166    /// Records a key, reporting whether it was new.
167    ///
168    /// # Arguments
169    ///
170    /// * `key` - the packet key to record, from [`dedup_key`](crate::Frame::dedup_key).
171    ///
172    /// # Returns
173    ///
174    /// `true` if the packet is new, `false` if it is a duplicate.
175    pub fn record(&mut self, key: (u32, u16)) -> bool {
176        record_into(&mut self.keys, &mut self.next, key)
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn a_key_is_new_once_then_a_duplicate() {
186        let mut seen: SeenCache<8> = SeenCache::new();
187        assert!(seen.record((1, 1)));
188        assert!(!seen.record((1, 1)));
189        assert!(seen.contains((1, 1)));
190    }
191
192    #[test]
193    fn different_sources_and_ids_are_distinct() {
194        let mut seen: SeenCache<8> = SeenCache::new();
195        assert!(seen.record((1, 1)));
196        assert!(seen.record((1, 2)));
197        assert!(seen.record((2, 1)));
198        assert!(!seen.record((1, 1)));
199    }
200
201    #[test]
202    fn the_oldest_key_is_evicted_when_full() {
203        let mut seen: SeenCache<2> = SeenCache::new();
204        assert!(seen.record((0, 1)));
205        assert!(seen.record((0, 2)));
206        // Recording a third key evicts the oldest, (0, 1).
207        assert!(seen.record((0, 3)));
208        assert!(!seen.contains((0, 1)));
209        assert!(seen.contains((0, 2)));
210        assert!(seen.contains((0, 3)));
211        // The evicted key is treated as new again.
212        assert!(seen.record((0, 1)));
213    }
214
215    #[test]
216    fn an_empty_cache_remembers_nothing() {
217        let seen: SeenCache<4> = SeenCache::default();
218        assert!(!seen.contains((1, 1)));
219    }
220
221    #[test]
222    fn a_zero_capacity_cache_reads_every_key_as_new_without_panicking() {
223        let mut seen: SeenCache<0> = SeenCache::new();
224        assert!(seen.record((1, 1)));
225        assert!(seen.record((1, 1))); // nothing was remembered, so it is new again
226        assert!(!seen.contains((1, 1)));
227    }
228
229    #[test]
230    fn a_runtime_sized_cache_answers_the_same_way() {
231        let mut fixed: SeenCache<4> = SeenCache::new();
232        let mut dynamic = DynamicSeenCache::new(4);
233        for key in [(1u32, 1u16), (1, 1), (1, 2), (2, 1), (1, 3), (1, 4), (1, 1)] {
234            assert_eq!(
235                fixed.record(key),
236                dynamic.record(key),
237                "the two caches evict identically"
238            );
239            assert_eq!(fixed.contains(key), dynamic.contains(key));
240        }
241    }
242
243    #[test]
244    fn a_runtime_sized_cache_remembers_what_it_was_sized_for() {
245        let mut seen = DynamicSeenCache::new(2);
246        assert_eq!(seen.capacity(), 2);
247        assert!(seen.record((1, 1)));
248        assert!(seen.record((1, 2)));
249        assert!(seen.record((1, 3)));
250        assert!(
251            !seen.contains((1, 1)),
252            "the oldest key is evicted once the cache is full"
253        );
254    }
255
256    #[test]
257    fn a_cache_with_no_room_relays_every_copy() {
258        let mut seen = DynamicSeenCache::new(0);
259        assert!(seen.record((1, 1)));
260        assert!(
261            seen.record((1, 1)),
262            "with nothing remembered every copy is new"
263        );
264    }
265}