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}