Skip to main content

sipx_transport/
timers.rs

1//! An earliest-deadline-first timer queue.
2//!
3//! One queue for a whole driver, not a task per timer. A busy proxy holds tens of thousands of live
4//! timers; spawning a task for each is how a stack acquires a scheduling problem nobody can profile.
5//!
6//! **The queue does not read the clock.** `now` is an argument to [`TimerQueue::set`] and to
7//! [`TimerQueue::take_due`], so a driver on virtual time — or a test asserting *when* a
8//! retransmission was scheduled rather than sleeping until it happens — uses the same queue the
9//! endpoint does. A queue that called `Instant::now()` internally would be unusable by either,
10//! which is what this one used to be.
11//!
12//! It is generic over its key for the same reason: the endpoint keys on
13//! `(TransactionKey, Timer)`, and nothing about earliest-deadline-first scheduling cares.
14//!
15//! And it is generic over its **instant**, which is what makes the paragraph above true rather
16//! than merely intended. [`tokio::time::Instant`] has only two constructors — `now()`, which reads
17//! the machine clock, and `from_std`, which needs a [`std::time::Instant`] that has no zero either
18//! — so a discrete-event simulator on virtual time had no instant to hand in and could not build
19//! one. The type parameter defaults to [`tokio::time::Instant`], so `TimerQueue<K>` still names
20//! exactly what it always named and every existing caller is untouched.
21
22use std::cmp::Reverse;
23use std::collections::{BinaryHeap, HashMap};
24use std::hash::Hash;
25use std::ops::Add;
26use std::time::Duration;
27
28use tokio::time::Instant;
29
30#[derive(Debug, PartialEq, Eq)]
31struct Entry<K, I> {
32    deadline: I,
33    generation: u64,
34    key: K,
35}
36
37// Ordering is by deadline alone, so the bound is on the instant rather than on the key: two
38// entries with the same deadline are interchangeable to an earliest-deadline-first queue.
39impl<K: Eq, I: Ord> Ord for Entry<K, I> {
40    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
41        self.deadline.cmp(&other.deadline)
42    }
43}
44
45impl<K: Eq, I: Ord> PartialOrd for Entry<K, I> {
46    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
47        Some(self.cmp(other))
48    }
49}
50
51/// Pending timers, earliest first.
52///
53/// Cancellation does not remove from the middle of the heap. Each key carries a generation counter;
54/// setting or clearing bumps it, and an entry whose generation is stale is discarded when it
55/// surfaces. Cancellation is common — every response cancels something — so making it O(1) and
56/// paying at pop time is the right trade.
57#[derive(Debug)]
58pub struct TimerQueue<K, I = Instant> {
59    heap: BinaryHeap<Reverse<Entry<K, I>>>,
60    generations: HashMap<K, u64>,
61    /// Queue-global identity for the next schedule, so forgetting and reusing a key cannot make a
62    /// stale heap entry live again.
63    next_generation: u64,
64}
65
66// The bounds match `Entry`'s `Ord` impl rather than `new`'s: `BinaryHeap::new` requires its element
67// to be `Ord` on our MSRV, and a queue that cannot order its entries has no empty value worth
68// naming either. Later toolchains relax the bound, which is why only the MSRV job caught this.
69impl<K: Eq, I: Ord> Default for TimerQueue<K, I> {
70    fn default() -> Self {
71        Self {
72            heap: BinaryHeap::new(),
73            generations: HashMap::new(),
74            next_generation: 0,
75        }
76    }
77}
78
79impl<K: Clone + Eq + Hash, I: Ord + Copy + Add<Duration, Output = I>> TimerQueue<K, I> {
80    /// An empty queue.
81    #[must_use]
82    pub fn new() -> Self {
83        Self::default()
84    }
85
86    /// How many entries are held, including stale ones not yet discarded.
87    #[must_use]
88    pub fn len(&self) -> usize {
89        self.heap.len()
90    }
91
92    /// Whether nothing is scheduled.
93    #[must_use]
94    pub fn is_empty(&self) -> bool {
95        self.heap.is_empty()
96    }
97
98    /// Schedule a timer for `after` from `now`, replacing any previous instance of the same key.
99    ///
100    /// `now` is the caller's, not this queue's. That is the whole difference between a queue a
101    /// tokio driver can use and one that any driver can: a caller on virtual time hands its own
102    /// clock in, and nothing here has an opinion about what an instant means.
103    pub fn set(&mut self, key: K, now: I, after: Duration) {
104        let generation = self.bump(&key);
105        self.heap.push(Reverse(Entry {
106            deadline: now + after,
107            generation,
108            key,
109        }));
110    }
111
112    /// Cancel a timer.
113    pub fn clear(&mut self, key: &K) {
114        self.bump(key);
115    }
116
117    /// Forget one timer key and its generation counter.
118    ///
119    /// The heap entry, if any, becomes stale and is discarded when it reaches the front. This is
120    /// the constant-time termination path for callers that can enumerate their small timer set;
121    /// [`Self::forget_matching`] remains the general full-map operation.
122    pub fn forget(&mut self, key: &K) {
123        self.generations.remove(key);
124    }
125
126    /// Cancel every timer whose key matches.
127    ///
128    /// The general form of "cancel everything belonging to this transaction", which is what
129    /// termination means — expressed as a predicate because the queue does not know what part of a
130    /// key identifies a transaction.
131    pub fn clear_matching(&mut self, matches: impl Fn(&K) -> bool) {
132        let keys: Vec<K> = self
133            .generations
134            .keys()
135            .filter(|key| matches(key))
136            .cloned()
137            .collect();
138        for key in keys {
139            self.bump(&key);
140        }
141    }
142
143    fn bump(&mut self, key: &K) -> u64 {
144        if self.next_generation == u64::MAX {
145            self.compact_generations();
146        }
147        self.next_generation += 1;
148        self.generations.insert(key.clone(), self.next_generation);
149        self.next_generation
150    }
151
152    /// Discard every stale entry and renumber the live set before the global identity wraps.
153    ///
154    /// This path requires `u64::MAX` schedules from one queue before it runs. Keeping it complete
155    /// avoids either a debug-build overflow panic or a wrapped identity reviving an old entry.
156    fn compact_generations(&mut self) {
157        let previous = std::mem::take(&mut self.generations);
158        let entries = std::mem::take(&mut self.heap);
159        self.next_generation = 0;
160        for Reverse(mut entry) in entries {
161            if previous.get(&entry.key) != Some(&entry.generation) {
162                continue;
163            }
164            self.next_generation += 1;
165            entry.generation = self.next_generation;
166            self.generations
167                .insert(entry.key.clone(), self.next_generation);
168            self.heap.push(Reverse(entry));
169        }
170    }
171
172    /// When the next live timer is due, if any.
173    ///
174    /// Discards stale entries as it looks, so a queue full of cancelled timers does not keep waking
175    /// the loop.
176    pub fn next_deadline(&mut self) -> Option<I> {
177        loop {
178            let Reverse(entry) = self.heap.peek()?;
179            if self.is_live(entry) {
180                return Some(entry.deadline);
181            }
182            self.heap.pop();
183        }
184    }
185
186    /// Take every timer due at or before `now`, earliest first.
187    pub fn take_due(&mut self, now: I) -> Vec<K> {
188        let mut fired = Vec::new();
189        while let Some(Reverse(entry)) = self.heap.peek() {
190            if entry.deadline > now {
191                break;
192            }
193            let Some(Reverse(entry)) = self.heap.pop() else {
194                break;
195            };
196            if !self.is_live(&entry) {
197                continue;
198            }
199            // Firing consumes the schedule: a later entry for the same key, set by whatever this
200            // fire produces, gets a fresh generation.
201            self.bump(&entry.key);
202            fired.push(entry.key);
203        }
204        fired
205    }
206
207    fn is_live(&self, entry: &Entry<K, I>) -> bool {
208        self.generations
209            .get(&entry.key)
210            .is_some_and(|&generation| generation == entry.generation)
211    }
212
213    /// Forget every key that matches, generation counters and all.
214    pub fn forget_matching(&mut self, matches: impl Fn(&K) -> bool) {
215        self.generations.retain(|key, _| !matches(key));
216    }
217}
218
219#[cfg(test)]
220#[allow(
221    clippy::unwrap_used,
222    clippy::expect_used,
223    clippy::panic,
224    clippy::indexing_slicing
225)]
226mod tests {
227    use super::*;
228    use sipx_sip::transaction::{Timer, TransactionKey};
229    use std::time::Duration;
230
231    fn key(branch: &str) -> TransactionKey {
232        TransactionKey::Rfc3261 {
233            branch: branch.as_bytes().to_vec(),
234            sent_by: b"h.example.com".to_vec(),
235            method: b"INVITE".to_vec(),
236        }
237    }
238
239    /// The endpoint's key type, which is what the generic parameter exists to accommodate.
240    type Transactions = TimerQueue<(TransactionKey, Timer)>;
241
242    #[tokio::test(start_paused = true)]
243    async fn timers_fire_in_deadline_order() {
244        let mut q = Transactions::new();
245        let now = Instant::now();
246        q.set((key("a"), Timer::A), now, Duration::from_millis(500));
247        q.set((key("b"), Timer::B), now, Duration::from_millis(100));
248        q.set((key("c"), Timer::E), now, Duration::from_millis(300));
249
250        let fired = q.take_due(now + Duration::from_millis(600));
251        let order: Vec<Timer> = fired.iter().map(|(_, timer)| *timer).collect();
252        assert_eq!(order, vec![Timer::B, Timer::E, Timer::A]);
253    }
254
255    /// The queue never reads the clock, so a caller can schedule and fire without any time
256    /// passing at all — which is what a virtual-time driver does and what a test that would
257    /// otherwise sleep wants.
258    #[tokio::test]
259    async fn scheduling_and_firing_need_no_real_time_to_pass() {
260        let mut q = Transactions::new();
261        let epoch = Instant::now();
262        q.set((key("a"), Timer::A), epoch, Duration::from_secs(3600));
263
264        assert!(q.take_due(epoch).is_empty(), "not due yet");
265        assert_eq!(
266            q.take_due(epoch + Duration::from_secs(3600)).len(),
267            1,
268            "an hour later, without an hour passing"
269        );
270    }
271
272    #[tokio::test(start_paused = true)]
273    async fn a_cleared_timer_does_not_fire() {
274        let mut q = Transactions::new();
275        let now = Instant::now();
276        q.set((key("a"), Timer::A), now, Duration::from_millis(100));
277        q.clear(&(key("a"), Timer::A));
278
279        assert!(q.take_due(now + Duration::from_millis(200)).is_empty());
280    }
281
282    #[tokio::test(start_paused = true)]
283    async fn forgetting_one_timer_discards_its_generation_without_scanning_others() {
284        let mut q = Transactions::new();
285        let now = Instant::now();
286        let forgotten = (key("a"), Timer::A);
287        let live = (key("z"), Timer::B);
288        q.set(forgotten.clone(), now, Duration::from_millis(100));
289        q.set(live.clone(), now, Duration::from_millis(200));
290
291        q.forget(&forgotten);
292
293        assert!(!q.generations.contains_key(&forgotten));
294        assert!(q.generations.contains_key(&live));
295        assert_eq!(q.take_due(now + Duration::from_millis(300)), vec![live]);
296    }
297
298    /// A peer may reuse a transaction key after its previous transaction has terminated. Its new
299    /// first timer must not share the old heap entry's generation and make that stale entry live.
300    #[tokio::test(start_paused = true)]
301    async fn reusing_a_forgotten_key_does_not_revive_its_stale_timer() {
302        let mut q = Transactions::new();
303        let now = Instant::now();
304        let reused = (key("a"), Timer::A);
305        q.set(reused.clone(), now, Duration::from_millis(100));
306        q.forget(&reused);
307
308        q.set(reused.clone(), now, Duration::from_millis(200));
309
310        assert!(q.take_due(now + Duration::from_millis(100)).is_empty());
311        assert_eq!(q.take_due(now + Duration::from_millis(200)), vec![reused]);
312    }
313
314    /// Re-setting a timer replaces it rather than adding a second one — the retransmission case,
315    /// which happens on every fire.
316    #[tokio::test(start_paused = true)]
317    async fn resetting_a_timer_replaces_it() {
318        let mut q = Transactions::new();
319        let now = Instant::now();
320        q.set((key("a"), Timer::A), now, Duration::from_millis(100));
321        q.set((key("a"), Timer::A), now, Duration::from_millis(500));
322
323        assert!(
324            q.take_due(now + Duration::from_millis(200)).is_empty(),
325            "the first schedule must not survive"
326        );
327        assert_eq!(q.take_due(now + Duration::from_millis(600)).len(), 1);
328    }
329
330    #[tokio::test(start_paused = true)]
331    async fn clearing_a_transaction_clears_all_of_its_timers() {
332        let mut q = Transactions::new();
333        let now = Instant::now();
334        q.set((key("a"), Timer::A), now, Duration::from_millis(100));
335        q.set((key("a"), Timer::B), now, Duration::from_millis(200));
336        q.set((key("z"), Timer::A), now, Duration::from_millis(100));
337        q.clear_matching(|(k, _)| k == &key("a"));
338
339        let fired = q.take_due(now + Duration::from_millis(300));
340        assert_eq!(fired.len(), 1);
341        assert_eq!(fired[0].0, key("z"));
342    }
343
344    #[tokio::test(start_paused = true)]
345    async fn cancelled_entries_do_not_keep_waking_the_loop() {
346        let mut q = Transactions::new();
347        let now = Instant::now();
348        for i in 0..100 {
349            q.set(
350                (key(&format!("k{i}")), Timer::A),
351                now,
352                Duration::from_millis(10),
353            );
354            q.clear(&(key(&format!("k{i}")), Timer::A));
355        }
356        q.set((key("live"), Timer::A), now, Duration::from_secs(60));
357
358        // The next deadline is the live one, not any of the hundred dead entries.
359        let deadline = q.next_deadline().expect("a deadline");
360        assert!(deadline >= now + Duration::from_secs(59));
361        assert_eq!(q.len(), 1, "stale entries are discarded while looking");
362    }
363
364    /// The story's failing-first test (`X-21`).
365    ///
366    /// A discrete-event simulator's clock: a counter with a zero, which `tokio::time::Instant` does
367    /// not have — its only constructors read the machine clock or take a `std::time::Instant` that
368    /// has no zero either. This is the caller the queue was generalised *for*, and until the
369    /// instant became a type parameter it was the one caller that could not use it.
370    ///
371    /// Note there is no `#[tokio::test]` here, and no runtime: the point is a queue that works with
372    /// no clock at all, not one that works with a paused clock.
373    #[test]
374    fn a_virtual_clock_drives_the_queue_with_no_runtime() {
375        /// Ticks since the simulation began. Nothing about it can read a clock.
376        #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
377        struct Virtual(u64);
378
379        impl std::ops::Add<Duration> for Virtual {
380            type Output = Self;
381            fn add(self, after: Duration) -> Self {
382                // A simulation that ran past u64 milliseconds is not a case worth a fallible
383                // conversion; saturating keeps the clock monotonic either way.
384                Self(
385                    self.0
386                        .saturating_add(u64::try_from(after.as_millis()).unwrap_or(u64::MAX)),
387                )
388            }
389        }
390
391        let mut q: TimerQueue<&'static str, Virtual> = TimerQueue::new();
392        let epoch = Virtual(0);
393
394        q.set("retransmit", epoch, Duration::from_millis(500));
395        q.set("give-up", epoch, Duration::from_secs(32));
396
397        assert!(q.take_due(epoch).is_empty(), "nothing is due at the epoch");
398        assert_eq!(
399            q.next_deadline(),
400            Some(Virtual(500)),
401            "the queue answers in the caller's own units"
402        );
403        assert_eq!(q.take_due(Virtual(500)), vec!["retransmit"]);
404        assert_eq!(q.take_due(Virtual(31_999)), Vec::<&str>::new());
405        assert_eq!(q.take_due(Virtual(32_000)), vec!["give-up"]);
406    }
407
408    /// The default type parameter is what keeps this additive: the endpoint's own alias names the
409    /// queue with one parameter and still means a `tokio::time::Instant` queue.
410    #[tokio::test(start_paused = true)]
411    async fn naming_the_queue_without_an_instant_still_means_the_tokio_one() {
412        let mut q: TimerQueue<(TransactionKey, Timer)> = TimerQueue::new();
413        let now: Instant = Instant::now();
414        q.set((key("a"), Timer::A), now, Duration::from_millis(100));
415        assert_eq!(q.next_deadline(), Some(now + Duration::from_millis(100)));
416    }
417
418    /// The key is opaque to the queue: anything hashable schedules.
419    #[tokio::test(start_paused = true)]
420    async fn the_queue_schedules_any_key_at_all() {
421        let mut q: TimerQueue<&'static str> = TimerQueue::new();
422        let now = Instant::now();
423        q.set("refresh", now, Duration::from_millis(50));
424        q.set("keepalive", now, Duration::from_millis(10));
425        assert_eq!(
426            q.take_due(now + Duration::from_millis(100)),
427            vec!["keepalive", "refresh"]
428        );
429    }
430}