Skip to main content

matc/
mrp.rs

1//! Message Reliability Protocol (MRP) timing parameters and backoff math
2//! per Matter specification section 4.12.
3//!
4//! Peers advertise their session intervals in mDNS TXT records (keys SII,
5//! SAI, SAT - decimal milliseconds). Senders derive retransmission deadlines
6//! from these values using exponential backoff with jitter:
7//!
8//! `t = i * MARGIN * BASE^max(0, n - THRESHOLD) * (1 + rand * JITTER)`
9//!
10//! where `i` is the peer's active interval (SAI) if the peer was heard from
11//! within the active threshold (SAT), otherwise its idle interval (SII), and
12//! `n` is the 0-based retransmission index. A message is given up on after
13//! [`MRP_MAX_TRANSMISSIONS`] total transmissions.
14//!
15//! Used by [`crate::retransmit`] (handshake exchanges) and
16//! [`crate::active_connection`] (operational traffic). Parameters are stored
17//! on the transport connection ([`crate::transport::ConnectionTrait::mrp_params`]).
18
19use std::time::Duration;
20
21/// Maximum number of transmissions of a single message (initial + retransmits).
22pub const MRP_MAX_TRANSMISSIONS: u32 = 5;
23pub const MRP_BACKOFF_MARGIN: f64 = 1.1;
24pub const MRP_BACKOFF_BASE: f64 = 1.6;
25pub const MRP_BACKOFF_THRESHOLD: u32 = 1;
26pub const MRP_BACKOFF_JITTER: f64 = 0.25;
27/// Spec cap for advertised SII/SAI values (milliseconds).
28pub const MRP_MAX_INTERVAL_MS: u32 = 3_600_000;
29
30const DEFAULT_IDLE_INTERVAL_MS: u32 = 500;
31const DEFAULT_ACTIVE_INTERVAL_MS: u32 = 300;
32const DEFAULT_ACTIVE_THRESHOLD_MS: u32 = 4000;
33
34/// Peer MRP intervals, typically taken from its mDNS TXT records.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct MrpParameters {
37    /// SII - retransmission interval when the peer is idle (sleepy).
38    pub session_idle_interval: Duration,
39    /// SAI - retransmission interval when the peer is active.
40    pub session_active_interval: Duration,
41    /// SAT - how long after the last received message the peer counts as active.
42    pub session_active_threshold: Duration,
43}
44
45impl Default for MrpParameters {
46    fn default() -> Self {
47        Self {
48            session_idle_interval: Duration::from_millis(DEFAULT_IDLE_INTERVAL_MS as u64),
49            session_active_interval: Duration::from_millis(DEFAULT_ACTIVE_INTERVAL_MS as u64),
50            session_active_threshold: Duration::from_millis(DEFAULT_ACTIVE_THRESHOLD_MS as u64),
51        }
52    }
53}
54
55impl MrpParameters {
56    /// Build from optional TXT-record millisecond values (keys SII/SAI/SAT).
57    /// Missing values fall back to spec defaults; SII/SAI are clamped to
58    /// [`MRP_MAX_INTERVAL_MS`].
59    pub fn from_txt_ms(sii: Option<u32>, sai: Option<u32>, sat: Option<u32>) -> Self {
60        let clamp = |v: u32| v.min(MRP_MAX_INTERVAL_MS) as u64;
61        Self {
62            session_idle_interval: Duration::from_millis(clamp(
63                sii.unwrap_or(DEFAULT_IDLE_INTERVAL_MS),
64            )),
65            session_active_interval: Duration::from_millis(clamp(
66                sai.unwrap_or(DEFAULT_ACTIVE_INTERVAL_MS),
67            )),
68            session_active_threshold: Duration::from_millis(
69                sat.unwrap_or(DEFAULT_ACTIVE_THRESHOLD_MS) as u64,
70            ),
71        }
72    }
73}
74
75/// Base retransmission interval: the peer's active interval if it was heard
76/// from within the active threshold, otherwise its idle interval.
77pub fn base_interval(params: &MrpParameters, last_rx_elapsed: Option<Duration>) -> Duration {
78    match last_rx_elapsed {
79        Some(elapsed) if elapsed < params.session_active_threshold => {
80            params.session_active_interval
81        }
82        _ => params.session_idle_interval,
83    }
84}
85
86/// Wait time before the next retransmission per spec 4.12 backoff formula.
87/// `retransmission_index` is 0 for the wait after the initial transmission.
88pub fn backoff_interval(base: Duration, retransmission_index: u32) -> Duration {
89    let exponent = retransmission_index.saturating_sub(MRP_BACKOFF_THRESHOLD);
90    let t = base.as_secs_f64()
91        * MRP_BACKOFF_MARGIN
92        * MRP_BACKOFF_BASE.powi(exponent as i32)
93        * (1.0 + rand::random::<f64>() * MRP_BACKOFF_JITTER);
94    Duration::from_secs_f64(t)
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_defaults() {
103        let p = MrpParameters::default();
104        assert_eq!(p.session_idle_interval, Duration::from_millis(500));
105        assert_eq!(p.session_active_interval, Duration::from_millis(300));
106        assert_eq!(p.session_active_threshold, Duration::from_millis(4000));
107        assert_eq!(p, MrpParameters::from_txt_ms(None, None, None));
108    }
109
110    #[test]
111    fn test_from_txt_ms_clamps() {
112        let p = MrpParameters::from_txt_ms(Some(4_000_000), Some(300), Some(4000));
113        assert_eq!(
114            p.session_idle_interval,
115            Duration::from_millis(MRP_MAX_INTERVAL_MS as u64)
116        );
117        let p = MrpParameters::from_txt_ms(Some(5000), None, None);
118        assert_eq!(p.session_idle_interval, Duration::from_millis(5000));
119        assert_eq!(p.session_active_interval, Duration::from_millis(300));
120    }
121
122    #[test]
123    fn test_base_interval_selection() {
124        let p = MrpParameters::default();
125        assert_eq!(base_interval(&p, None), p.session_idle_interval);
126        assert_eq!(
127            base_interval(&p, Some(Duration::from_millis(1000))),
128            p.session_active_interval
129        );
130        assert_eq!(
131            base_interval(&p, Some(Duration::from_millis(4000))),
132            p.session_idle_interval
133        );
134    }
135
136    #[test]
137    fn test_backoff_interval_bounds() {
138        let base = Duration::from_millis(500);
139        let mut prev_lower = 0.0f64;
140        for n in 0..MRP_MAX_TRANSMISSIONS {
141            let exponent = n.saturating_sub(MRP_BACKOFF_THRESHOLD);
142            let lower = 0.5 * MRP_BACKOFF_MARGIN * MRP_BACKOFF_BASE.powi(exponent as i32);
143            let upper = lower * (1.0 + MRP_BACKOFF_JITTER);
144            for _ in 0..50 {
145                let t = backoff_interval(base, n).as_secs_f64();
146                assert!(t >= lower - 1e-9, "n={} t={} lower={}", n, t, lower);
147                assert!(t <= upper + 1e-9, "n={} t={} upper={}", n, t, upper);
148            }
149            assert!(lower >= prev_lower);
150            prev_lower = lower;
151        }
152    }
153}