Skip to main content

sipx_media/ice/
timing.rs

1//! ICE's timers (RFC 8445 §14, RFC 5389 §7.2.1; [spec] §9).
2//!
3//! Everything the agent waits for is a value in [`Timers`], and nothing in the state machine is a
4//! literal — the same rule the transaction timers follow
5//! ([`sipx_sip::transaction::Timers`](https://docs.rs/sipx-sip)), for the same reason: a duration
6//! written into a `match` arm is a duration nobody can configure and no test can shorten.
7//!
8//! The one that is not a constant at all is [`Timers::rto`]. §14.3 computes the retransmission
9//! interval from how many checks are outstanding *right now* — "the RTO will be different for
10//! each transaction as the number of checks in the Waiting and In-Progress states change" — so it
11//! is a function of the checklist set and is evaluated when a check goes out, not once at
12//! construction.
13//!
14//! [spec]: https://github.com/codewandler/sipx/blob/main/docs/specs/ice.md
15
16use std::time::Duration;
17
18/// The timers of RFC 8445 §14 and RFC 5389 §7.2.1, plus the one stopping value that is sipx's own.
19///
20/// Every field is a value the deployment may change; [`Timers::default`] is what the RFCs
21/// recommend. Two of them have normative floors that [`Timers::pacing`] and [`Timers::rto`]
22/// enforce rather than trust: Ta may not pace faster than 5 ms across every agent in the process
23/// (§14.2), and an RTO may never be below 500 ms (§14.3).
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct Timers {
26    /// Ta — the pacing interval: one check leaves per tick, across the whole checklist set
27    /// (§14.2). Default 50 ms.
28    pub ta: Duration,
29    /// The floor under Ta, "as though there were one global Ta value for pacing all agents"
30    /// (§14.2). Default 5 ms.
31    pub pacing_floor: Duration,
32    /// The floor under the RTO. §14.3: agents "MUST NOT use an RTO value smaller than 500 ms".
33    pub rto_floor: Duration,
34    /// Rc — how many times a request is transmitted before the transaction fails
35    /// (RFC 5389 §7.2.1). Default 7.
36    pub rc: u32,
37    /// Rm — the multiplier on the wait after the last transmission (RFC 5389 §7.2.1). Default 16.
38    pub rm: u32,
39    /// Tr — how long a selected pair may carry no data before a keepalive is sent (§11).
40    /// Default 15 s, which §11 also makes the minimum: "MUST NOT use a value smaller".
41    pub tr: Duration,
42    /// Tn — sipx's own stopping value: how long the controlling agent keeps checking after the
43    /// first valid pair appears before it nominates ([spec] §8). Default 1 s.
44    ///
45    /// §8.1.1 leaves the stopping criterion to local optimisation and requires only that exactly
46    /// one pair is eventually nominated. A number here rather than an emergent behaviour is what
47    /// makes that choice testable.
48    ///
49    /// [spec]: https://github.com/codewandler/sipx/blob/main/docs/specs/ice.md
50    pub tn: Duration,
51}
52
53impl Default for Timers {
54    fn default() -> Self {
55        Self {
56            ta: Duration::from_millis(50),
57            pacing_floor: Duration::from_millis(5),
58            rto_floor: Duration::from_millis(500),
59            rc: 7,
60            rm: 16,
61            tr: Duration::from_secs(15),
62            tn: Duration::from_secs(1),
63        }
64    }
65}
66
67impl Timers {
68    /// The interval between checks: Ta, but never below the process-wide floor (§14.2).
69    #[must_use]
70    pub fn pacing(&self) -> Duration {
71        self.ta.max(self.pacing_floor)
72    }
73
74    /// The retransmission interval for a check being sent now (§14.3):
75    ///
76    /// ```text
77    /// RTO = MAX(500ms, Ta * N * (Num-Waiting + Num-In-Progress))
78    /// ```
79    ///
80    /// `checks` is `N`, the total number of connectivity checks to be performed — the size of the
81    /// checklist set, not of one checklist. `outstanding` is `Num-Waiting + Num-In-Progress`
82    /// across that same set, which is why this cannot be computed once: it falls as the checks
83    /// drain, and §14.3 says so outright.
84    #[must_use]
85    pub fn rto(&self, checks: usize, outstanding: usize) -> Duration {
86        let scale = u32::try_from(checks.saturating_mul(outstanding)).unwrap_or(u32::MAX);
87        self.pacing().saturating_mul(scale).max(self.rto_floor)
88    }
89
90    /// The wait after the last transmission of a check, after which the transaction has timed out
91    /// (RFC 5389 §7.2.1: "a duration equal to Rm times the RTO").
92    #[must_use]
93    pub fn final_wait(&self, rto: Duration) -> Duration {
94        rto.saturating_mul(self.rm)
95    }
96
97    /// The next retransmission interval: RFC 5389 §7.2.1 doubles it after every transmission.
98    #[must_use]
99    pub fn double(&self, rto: Duration) -> Duration {
100        rto.saturating_mul(2)
101    }
102}
103
104#[cfg(test)]
105#[allow(
106    clippy::unwrap_used,
107    clippy::expect_used,
108    clippy::panic,
109    clippy::indexing_slicing
110)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn the_defaults_are_the_values_the_spec_tabulates() {
116        let timers = Timers::default();
117        assert_eq!(timers.ta, Duration::from_millis(50));
118        assert_eq!(timers.pacing_floor, Duration::from_millis(5));
119        assert_eq!(timers.rto_floor, Duration::from_millis(500));
120        assert_eq!(timers.rc, 7);
121        assert_eq!(timers.rm, 16);
122        assert_eq!(timers.tr, Duration::from_secs(15));
123        assert_eq!(timers.tn, Duration::from_secs(1));
124    }
125
126    /// §14.2's floor is across every agent in the process, so it applies to a configured Ta as
127    /// well as to the default one.
128    #[test]
129    fn pacing_never_goes_below_the_five_millisecond_floor() {
130        let timers = Timers {
131            ta: Duration::from_millis(1),
132            ..Timers::default()
133        };
134        assert_eq!(timers.pacing(), Duration::from_millis(5));
135    }
136
137    /// The whole point of §14.3: the same agent computes a different RTO for the check it sends
138    /// now and the one it sends when the checklist has drained.
139    #[test]
140    fn the_rto_falls_as_the_outstanding_checks_drain() {
141        let timers = Timers::default();
142        // 50 ms * 10 checks * 10 outstanding = 5 s.
143        assert_eq!(timers.rto(10, 10), Duration::from_secs(5));
144        // Same checklist, one check left outstanding: 50 ms * 10 * 1 = 500 ms.
145        assert_eq!(timers.rto(10, 1), Duration::from_millis(500));
146        // And below the floor it is the floor, never the product.
147        assert_eq!(timers.rto(1, 1), Duration::from_millis(500));
148        assert_eq!(timers.rto(0, 0), Duration::from_millis(500));
149    }
150
151    #[test]
152    fn retransmissions_double_and_the_last_wait_is_rm_times_the_rto() {
153        let timers = Timers::default();
154        let rto = timers.rto(4, 4);
155        assert_eq!(rto, Duration::from_millis(800));
156        assert_eq!(timers.double(rto), Duration::from_millis(1600));
157        assert_eq!(timers.final_wait(rto), Duration::from_millis(800 * 16));
158    }
159}