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}