mqtt5_protocol/
keepalive.rs

1use crate::numeric::u128_to_u64_saturating;
2use crate::time::Duration;
3
4pub const DEFAULT_LOCK_RETRY_ATTEMPTS: u32 = 100;
5pub const DEFAULT_LOCK_RETRY_DELAY_MS: u32 = 50;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct KeepaliveConfig {
9    pub ping_interval_percent: u8,
10    pub timeout_percent: u8,
11    pub lock_retry_attempts: u32,
12    pub lock_retry_delay_ms: u32,
13}
14
15impl Default for KeepaliveConfig {
16    fn default() -> Self {
17        Self {
18            ping_interval_percent: 75,
19            timeout_percent: 150,
20            lock_retry_attempts: DEFAULT_LOCK_RETRY_ATTEMPTS,
21            lock_retry_delay_ms: DEFAULT_LOCK_RETRY_DELAY_MS,
22        }
23    }
24}
25
26impl KeepaliveConfig {
27    #[must_use]
28    pub const fn new(ping_interval_percent: u8, timeout_percent: u8) -> Self {
29        Self {
30            ping_interval_percent,
31            timeout_percent,
32            lock_retry_attempts: DEFAULT_LOCK_RETRY_ATTEMPTS,
33            lock_retry_delay_ms: DEFAULT_LOCK_RETRY_DELAY_MS,
34        }
35    }
36
37    #[must_use]
38    pub const fn conservative() -> Self {
39        Self {
40            ping_interval_percent: 50,
41            timeout_percent: 150,
42            lock_retry_attempts: DEFAULT_LOCK_RETRY_ATTEMPTS,
43            lock_retry_delay_ms: DEFAULT_LOCK_RETRY_DELAY_MS,
44        }
45    }
46
47    #[must_use]
48    pub const fn with_lock_retry(mut self, attempts: u32, delay_ms: u32) -> Self {
49        self.lock_retry_attempts = attempts;
50        self.lock_retry_delay_ms = delay_ms;
51        self
52    }
53
54    #[must_use]
55    pub fn ping_interval(&self, keepalive: Duration) -> Duration {
56        let millis = u128_to_u64_saturating(keepalive.as_millis());
57        let ping_millis = millis * u64::from(self.ping_interval_percent) / 100;
58        Duration::from_millis(ping_millis)
59    }
60
61    #[must_use]
62    pub fn timeout_duration(&self, keepalive: Duration) -> Duration {
63        let millis = u128_to_u64_saturating(keepalive.as_millis());
64        let timeout_millis = millis * u64::from(self.timeout_percent) / 100;
65        Duration::from_millis(timeout_millis)
66    }
67}
68
69#[must_use]
70pub fn calculate_ping_interval(keepalive: Duration, percent: u8) -> Duration {
71    let millis = u128_to_u64_saturating(keepalive.as_millis());
72    let ping_millis = millis * u64::from(percent) / 100;
73    Duration::from_millis(ping_millis)
74}
75
76#[must_use]
77pub fn is_keepalive_timeout(
78    time_since_last_ping: Duration,
79    last_pong_received: bool,
80    keepalive: Duration,
81    timeout_percent: u8,
82) -> bool {
83    let config = KeepaliveConfig::new(0, timeout_percent);
84    let timeout = config.timeout_duration(keepalive);
85    !last_pong_received && time_since_last_ping > timeout
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn test_default_config() {
94        let config = KeepaliveConfig::default();
95        assert_eq!(config.ping_interval_percent, 75);
96        assert_eq!(config.timeout_percent, 150);
97    }
98
99    #[test]
100    fn test_conservative_config() {
101        let config = KeepaliveConfig::conservative();
102        assert_eq!(config.ping_interval_percent, 50);
103        assert_eq!(config.timeout_percent, 150);
104    }
105
106    #[test]
107    fn test_ping_interval_calculation() {
108        let config = KeepaliveConfig::default();
109        let keepalive = Duration::from_secs(60);
110        let ping_interval = config.ping_interval(keepalive);
111        assert_eq!(ping_interval, Duration::from_secs(45));
112    }
113
114    #[test]
115    fn test_ping_interval_50_percent() {
116        let config = KeepaliveConfig::conservative();
117        let keepalive = Duration::from_secs(60);
118        let ping_interval = config.ping_interval(keepalive);
119        assert_eq!(ping_interval, Duration::from_secs(30));
120    }
121
122    #[test]
123    fn test_timeout_duration() {
124        let config = KeepaliveConfig::default();
125        let keepalive = Duration::from_secs(60);
126        let timeout = config.timeout_duration(keepalive);
127        assert_eq!(timeout, Duration::from_secs(90));
128    }
129
130    #[test]
131    fn test_calculate_ping_interval_function() {
132        let keepalive = Duration::from_secs(60);
133        assert_eq!(
134            calculate_ping_interval(keepalive, 75),
135            Duration::from_secs(45)
136        );
137        assert_eq!(
138            calculate_ping_interval(keepalive, 50),
139            Duration::from_secs(30)
140        );
141    }
142
143    #[test]
144    fn test_is_keepalive_timeout_no_pong() {
145        let keepalive = Duration::from_secs(60);
146        let time_since_ping = Duration::from_secs(100);
147        assert!(is_keepalive_timeout(time_since_ping, false, keepalive, 150));
148    }
149
150    #[test]
151    fn test_is_keepalive_timeout_with_pong() {
152        let keepalive = Duration::from_secs(60);
153        let time_since_ping = Duration::from_secs(100);
154        assert!(!is_keepalive_timeout(time_since_ping, true, keepalive, 150));
155    }
156
157    #[test]
158    fn test_is_keepalive_timeout_not_expired() {
159        let keepalive = Duration::from_secs(60);
160        let time_since_ping = Duration::from_secs(80);
161        assert!(!is_keepalive_timeout(
162            time_since_ping,
163            false,
164            keepalive,
165            150
166        ));
167    }
168}