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}