1use std::collections::BTreeMap;
24
25use kernel::backpressure::admit;
26use kernel::{Admission, IdemKey, ItemMeta, Payload, QueueStats, TimeMs};
27
28use crate::error::StorageError;
29
30const IDEM_WINDOW_MS: u64 = 6 * 60 * 60 * 1000;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum QueueLane {
47 Signal,
48 Approval,
49 Repair,
50}
51
52impl QueueLane {
53 pub fn as_str(self) -> &'static str {
54 match self {
55 QueueLane::Signal => "signal",
56 QueueLane::Approval => "approval",
57 QueueLane::Repair => "repair",
58 }
59 }
60}
61
62#[derive(Debug, Clone)]
63pub struct QueueItem {
64 pub id: u64,
65 pub key: IdemKey,
66 pub payload: Payload,
67 pub enqueued_t: TimeMs,
68}
69
70pub struct EventQueue {
71 lane: QueueLane,
72 items: BTreeMap<u64, QueueItem>,
73 next_id: u64,
74 seen: BTreeMap<IdemKey, TimeMs>,
82 capacity: u64,
83 shed: u64,
84 duplicates: u64,
85}
86
87impl EventQueue {
88 pub fn new(lane: QueueLane, capacity: u64) -> EventQueue {
89 EventQueue {
90 lane,
91 items: BTreeMap::new(),
92 next_id: 0,
93 seen: BTreeMap::new(),
94 capacity,
95 shed: 0,
96 duplicates: 0,
97 }
98 }
99
100 pub fn enqueue(
108 &mut self,
109 key: IdemKey,
110 payload: Payload,
111 now: TimeMs,
112 ) -> Result<Admission, StorageError> {
113 self.forget_stale(now);
114 if self.seen.contains_key(&key) {
115 self.duplicates = self.duplicates.saturating_add(1);
116 return Ok(Admission::Admit);
117 }
118 let verdict = admit(&self.stats(), &ItemMeta { cost: 1 });
119 match verdict {
120 Admission::Shed { .. } => {
121 self.shed = self.shed.saturating_add(1);
124 Ok(verdict)
125 }
126 Admission::Admit => {
127 let id = self.next_id;
128 self.next_id = self.next_id.saturating_add(1);
129 self.seen.insert(key, now);
130 self.items.insert(
131 id,
132 QueueItem {
133 id,
134 key,
135 payload,
136 enqueued_t: now,
137 },
138 );
139 Ok(Admission::Admit)
140 }
141 }
142 }
143
144 fn forget_stale(&mut self, now: TimeMs) {
151 let Some(cutoff) = now.value().checked_sub(IDEM_WINDOW_MS) else {
152 return;
153 };
154 self.seen.retain(|_, admitted| admitted.value() > cutoff);
155 }
156
157 pub fn consume(&mut self) -> Option<QueueItem> {
160 let id = self.items.keys().next().copied()?;
161 self.items.remove(&id)
162 }
163
164 pub fn stats(&self) -> QueueStats {
165 QueueStats {
166 depth: self.len(),
167 capacity: self.capacity,
168 }
169 }
170
171 pub fn len(&self) -> u64 {
172 u64::try_from(self.items.len()).unwrap_or(u64::MAX)
173 }
174
175 pub fn is_empty(&self) -> bool {
176 self.items.is_empty()
177 }
178
179 pub fn lane(&self) -> QueueLane {
180 self.lane
181 }
182
183 pub fn shed_count(&self) -> u64 {
186 self.shed
187 }
188
189 pub fn duplicate_count(&self) -> u64 {
190 self.duplicates
191 }
192}
193
194#[cfg(test)]
195#[allow(
196 clippy::unwrap_used,
197 clippy::expect_used,
198 clippy::panic,
199 clippy::indexing_slicing,
200 reason = "test code"
201)]
202mod tests {
203 use super::*;
204 use kernel::{RunId, Seq};
205
206 fn key(n: u8) -> IdemKey {
207 IdemKey::derive(&RunId::from_bytes([n; 16]), Seq::new(u64::from(n)), b"act")
208 }
209
210 fn payload(tag: &str) -> Payload {
211 let mut map = serde_json::Map::new();
212 map.insert("tag".to_owned(), serde_json::Value::String(tag.to_owned()));
213 Payload::new(map).unwrap()
214 }
215
216 #[test]
217 fn dedup_happens_before_the_consumer_ever_sees_it() {
218 let mut queue = EventQueue::new(QueueLane::Signal, 8);
219 let k = key(1);
220 queue.enqueue(k, payload("first"), TimeMs::new(0)).unwrap();
221 queue.enqueue(k, payload("again"), TimeMs::new(1)).unwrap();
222 assert_eq!(queue.len(), 1, "the second never became an item");
223 assert_eq!(queue.duplicate_count(), 1);
224 let item = queue.consume().unwrap();
225 assert_eq!(item.key, k);
226 assert!(queue.consume().is_none());
227 queue.enqueue(k, payload("late"), TimeMs::new(2)).unwrap();
230 assert!(queue.is_empty(), "a consumed key cannot come back");
231 assert_eq!(queue.duplicate_count(), 2);
232 }
233
234 #[test]
235 fn shedding_refuses_the_newcomer_and_keeps_what_is_queued() {
236 let mut queue = EventQueue::new(QueueLane::Approval, 2);
237 for n in 1..=2u8 {
238 assert_eq!(
239 queue
240 .enqueue(key(n), payload("in"), TimeMs::new(0))
241 .unwrap(),
242 Admission::Admit
243 );
244 }
245 let verdict = queue
246 .enqueue(key(3), payload("over"), TimeMs::new(0))
247 .unwrap();
248 assert!(matches!(verdict, Admission::Shed { .. }));
249 assert_eq!(queue.len(), 2, "nothing already queued was dropped");
250 assert_eq!(queue.shed_count(), 1);
251 queue.consume();
253 assert_eq!(
254 queue
255 .enqueue(key(3), payload("retry"), TimeMs::new(1))
256 .unwrap(),
257 Admission::Admit
258 );
259 assert_eq!(queue.duplicate_count(), 0);
260 }
261
262 #[test]
263 fn consumption_is_arrival_ordered() {
264 let mut queue = EventQueue::new(QueueLane::Repair, 8);
265 for n in 1..=4u8 {
266 queue
267 .enqueue(
268 key(n),
269 payload(&format!("item{n}")),
270 TimeMs::new(u64::from(n)),
271 )
272 .unwrap();
273 }
274 let ids: Vec<u64> = std::iter::from_fn(|| queue.consume())
275 .map(|item| item.id)
276 .collect();
277 assert_eq!(ids, vec![0, 1, 2, 3]);
278 }
279
280 #[test]
284 fn a_key_older_than_the_window_stops_being_a_duplicate() {
285 let mut queue = EventQueue::new(QueueLane::Signal, 8);
286 let k = key(1);
287 queue.enqueue(k, payload("first"), TimeMs::new(0)).unwrap();
288 queue.consume();
289
290 let inside = TimeMs::new(IDEM_WINDOW_MS.saturating_sub(1));
292 queue.enqueue(k, payload("retry"), inside).unwrap();
293 assert_eq!(queue.duplicate_count(), 1);
294 assert!(queue.is_empty(), "a retry inside the window runs nothing");
295
296 let outside = TimeMs::new(IDEM_WINDOW_MS.saturating_add(1));
298 queue.enqueue(k, payload("later"), outside).unwrap();
299 assert_eq!(queue.duplicate_count(), 1, "not counted twice");
300 assert_eq!(queue.len(), 1, "it is a new request");
301 assert_eq!(queue.seen.len(), 1, "and the set holds only that one");
302 }
303
304 #[test]
305 fn three_lanes_one_implementation() {
306 for lane in [QueueLane::Signal, QueueLane::Approval, QueueLane::Repair] {
307 let mut queue = EventQueue::new(lane, 4);
308 queue.enqueue(key(9), payload("x"), TimeMs::new(0)).unwrap();
309 assert_eq!(queue.len(), 1);
310 assert_eq!(queue.lane(), lane);
311 assert!(!lane.as_str().is_empty());
312 }
313 }
314}