Skip to main content

storage/
queue.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! One queue implementation serving three lanes — signals, approvals,
7//! repairs.
8//!
9//! Two orderings carry the whole design. Admission is decided before
10//! anything is stored, so a shed item never occupies a slot it was
11//! refused; and deduplication happens before consumption, so a key that
12//! has already been handed out cannot reach the consumer twice. An
13//! effect that runs twice is worse than one that never runs, and this
14//! is the module that makes the difference structural rather than
15//! careful.
16//!
17//! Nothing here is persistent. The queue is a projection of enqueued
18//! minus consumed — the Ledger is already the history, and a second
19//! durable copy would be a second answer.
20//!
21//! Specified by `crates/storage/spec/Queue.lean` §8-10.
22
23use std::collections::BTreeMap;
24
25use kernel::backpressure::admit;
26use kernel::{Admission, IdemKey, ItemMeta, Payload, QueueStats, TimeMs};
27
28use crate::error::StorageError;
29
30/// How long an admitted key stays recognisable as one, in milliseconds:
31/// six hours.
32///
33/// A duplicate arrives because a sender retried, and a sender retries
34/// within its own attempt, not a day later. Remembering every key ever
35/// admitted made a city that runs for weeks carry one entry per event
36/// it ever queued, for a guarantee nothing was still asking for; this
37/// window is the span over which a repeat is a retry rather than a new
38/// request.
39const IDEM_WINDOW_MS: u64 = 6 * 60 * 60 * 1000;
40
41/// Which lane a queue serves. The lanes differ in their accounting
42/// names and nothing else; the day they differ in behaviour is the day
43/// they become separate modules, and that exit condition is stated
44/// here rather than discovered later.
45#[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    /// Each admitted key against the moment it was admitted.
75    ///
76    /// Membership, not occupancy: an item that has been consumed must
77    /// still be recognised as a duplicate. Bounded by time rather than
78    /// by count, because what makes a key uninteresting is that the
79    /// retry window it belongs to has closed, and dropping the oldest
80    /// key of a still-running wave would let its retry through.
81    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    /// Admission first, then storage. A duplicate key is admitted-and-
101    /// ignored rather than refused: the sender did its job, and telling
102    /// it otherwise would invite a retry that changes nothing.
103    ///
104    /// # Errors
105    /// Returns nothing today; the fallible signature is the port's, and
106    /// storage that can fail is what it is there for.
107    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                // Shedding refuses the new item and touches nothing that
122                // is already queued.
123                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    /// Drops the keys admitted longer ago than the idempotency window.
145    ///
146    /// A clock that steps backwards keeps every key rather than
147    /// dropping the wrong ones: the queue holds no opinion about the
148    /// clock, and an over-long memory only refuses to run an effect
149    /// twice.
150    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    /// Takes the oldest item. The key stays in `seen`, so a late
158    /// duplicate arriving after consumption is still recognised.
159    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    /// Counters for the accounting payloads: how many were refused, how
184    /// many arrived twice.
185    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        // A duplicate arriving after consumption is still a duplicate:
228        // the effect already ran once.
229        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        // The shed key was never admitted, so a retry is not a duplicate.
252        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    /// The dedup set is what a long-running city would otherwise carry
281    /// one entry of per event it ever queued. A key outside the window
282    /// is gone, and the same key after it is a new request.
283    #[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        // Inside the window: still the same request.
291        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        // Past it: the memory is gone and so is the entry holding it.
297        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}