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