mqtt5-protocol 0.12.0

MQTT v5.0 protocol implementation - packets, encoding, and validation
Documentation
use crate::numeric::u128_to_u64_saturating;
use crate::time::Duration;

pub const DEFAULT_LOCK_RETRY_ATTEMPTS: u32 = 100;
pub const DEFAULT_LOCK_RETRY_DELAY_MS: u32 = 50;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeepaliveConfig {
    pub ping_interval_percent: u8,
    pub timeout_percent: u8,
    pub lock_retry_attempts: u32,
    pub lock_retry_delay_ms: u32,
}

impl Default for KeepaliveConfig {
    fn default() -> Self {
        Self {
            ping_interval_percent: 75,
            timeout_percent: 150,
            lock_retry_attempts: DEFAULT_LOCK_RETRY_ATTEMPTS,
            lock_retry_delay_ms: DEFAULT_LOCK_RETRY_DELAY_MS,
        }
    }
}

impl KeepaliveConfig {
    #[must_use]
    pub const fn new(ping_interval_percent: u8, timeout_percent: u8) -> Self {
        Self {
            ping_interval_percent,
            timeout_percent,
            lock_retry_attempts: DEFAULT_LOCK_RETRY_ATTEMPTS,
            lock_retry_delay_ms: DEFAULT_LOCK_RETRY_DELAY_MS,
        }
    }

    #[must_use]
    pub const fn conservative() -> Self {
        Self {
            ping_interval_percent: 50,
            timeout_percent: 150,
            lock_retry_attempts: DEFAULT_LOCK_RETRY_ATTEMPTS,
            lock_retry_delay_ms: DEFAULT_LOCK_RETRY_DELAY_MS,
        }
    }

    #[must_use]
    pub const fn with_lock_retry(mut self, attempts: u32, delay_ms: u32) -> Self {
        self.lock_retry_attempts = attempts;
        self.lock_retry_delay_ms = delay_ms;
        self
    }

    #[must_use]
    pub fn ping_interval(&self, keepalive: Duration) -> Duration {
        let millis = u128_to_u64_saturating(keepalive.as_millis());
        let ping_millis = millis * u64::from(self.ping_interval_percent) / 100;
        Duration::from_millis(ping_millis)
    }

    #[must_use]
    pub fn timeout_duration(&self, keepalive: Duration) -> Duration {
        let millis = u128_to_u64_saturating(keepalive.as_millis());
        let timeout_millis = millis * u64::from(self.timeout_percent) / 100;
        Duration::from_millis(timeout_millis)
    }
}

#[must_use]
pub fn calculate_ping_interval(keepalive: Duration, percent: u8) -> Duration {
    let millis = u128_to_u64_saturating(keepalive.as_millis());
    let ping_millis = millis * u64::from(percent) / 100;
    Duration::from_millis(ping_millis)
}

#[must_use]
pub fn is_keepalive_timeout(
    time_since_last_ping: Duration,
    last_pong_received: bool,
    keepalive: Duration,
    timeout_percent: u8,
) -> bool {
    let config = KeepaliveConfig::new(0, timeout_percent);
    let timeout = config.timeout_duration(keepalive);
    !last_pong_received && time_since_last_ping > timeout
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_default_config() {
        let config = KeepaliveConfig::default();
        assert_eq!(config.ping_interval_percent, 75);
        assert_eq!(config.timeout_percent, 150);
    }

    #[test]
    fn test_conservative_config() {
        let config = KeepaliveConfig::conservative();
        assert_eq!(config.ping_interval_percent, 50);
        assert_eq!(config.timeout_percent, 150);
    }

    #[test]
    fn test_ping_interval_calculation() {
        let config = KeepaliveConfig::default();
        let keepalive = Duration::from_secs(60);
        let ping_interval = config.ping_interval(keepalive);
        assert_eq!(ping_interval, Duration::from_secs(45));
    }

    #[test]
    fn test_ping_interval_50_percent() {
        let config = KeepaliveConfig::conservative();
        let keepalive = Duration::from_secs(60);
        let ping_interval = config.ping_interval(keepalive);
        assert_eq!(ping_interval, Duration::from_secs(30));
    }

    #[test]
    fn test_timeout_duration() {
        let config = KeepaliveConfig::default();
        let keepalive = Duration::from_secs(60);
        let timeout = config.timeout_duration(keepalive);
        assert_eq!(timeout, Duration::from_secs(90));
    }

    #[test]
    fn test_calculate_ping_interval_function() {
        let keepalive = Duration::from_secs(60);
        assert_eq!(
            calculate_ping_interval(keepalive, 75),
            Duration::from_secs(45)
        );
        assert_eq!(
            calculate_ping_interval(keepalive, 50),
            Duration::from_secs(30)
        );
    }

    #[test]
    fn test_is_keepalive_timeout_no_pong() {
        let keepalive = Duration::from_secs(60);
        let time_since_ping = Duration::from_secs(100);
        assert!(is_keepalive_timeout(time_since_ping, false, keepalive, 150));
    }

    #[test]
    fn test_is_keepalive_timeout_with_pong() {
        let keepalive = Duration::from_secs(60);
        let time_since_ping = Duration::from_secs(100);
        assert!(!is_keepalive_timeout(time_since_ping, true, keepalive, 150));
    }

    #[test]
    fn test_is_keepalive_timeout_not_expired() {
        let keepalive = Duration::from_secs(60);
        let time_since_ping = Duration::from_secs(80);
        assert!(!is_keepalive_timeout(
            time_since_ping,
            false,
            keepalive,
            150
        ));
    }
}