nntp-proxy 0.5.0

High-performance NNTP proxy server with connection pooling and authentication
Documentation
//! Connection pool metric newtypes

use nutype::nutype;
use std::fmt;

#[allow(clippy::cast_precision_loss)] // Pool percentages are display values derived from exact usize counters.
const fn pool_count_as_f64(value: usize) -> f64 {
    // Pool utilization is a percentage for display/thresholding. The exact
    // connection counts remain available as usize newtypes.
    value as f64
}

/// Macro to reduce boilerplate for pool connection count types.
/// All pool types have the same `zero()` and `get()` implementations.
macro_rules! pool_count_type {
    ($(#[$meta:meta])* $name:ident) => {
        $(#[$meta])*
        #[nutype(derive(
            Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Display, From
        ))]
        pub struct $name(usize);

        impl $name {
            #[inline]
            pub fn zero() -> Self {
                Self::new(0)
            }

            #[inline]
            #[must_use]
            pub fn get(&self) -> usize {
                self.into_inner()
            }
        }
    };
}

pool_count_type!(
    /// Number of available connections in the pool
    AvailableConnections
);

pool_count_type!(
    /// Maximum size of the connection pool
    MaxPoolSize
);

pool_count_type!(
    /// Total number of connections created in the pool's lifetime
    CreatedConnections
);

pool_count_type!(
    /// Number of connections currently in use
    InUseConnections
);

impl InUseConnections {
    /// Calculate from pool capacity and availability
    #[inline]
    #[must_use]
    pub fn from_pool_stats(max: MaxPoolSize, available: AvailableConnections) -> Self {
        Self::new(max.get().saturating_sub(available.get()))
    }
}

/// Pool utilization as a percentage (0-100)
///
/// Calculated as: (`in_use` / `max_size`) * 100
/// Useful for monitoring pool health and load.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PoolUtilization(f64);

impl PoolUtilization {
    /// Create a new pool utilization percentage
    ///
    /// # Panics
    /// Panics if percentage is not in range [0.0, 100.0]
    #[inline]
    #[must_use]
    pub fn new(percentage: f64) -> Self {
        assert!(
            (0.0..=100.0).contains(&percentage),
            "Utilization must be 0-100%, got {percentage}"
        );
        Self(percentage)
    }

    /// Calculate utilization from pool stats
    #[inline]
    #[must_use]
    pub fn from_pool_stats(max: MaxPoolSize, available: AvailableConnections) -> Self {
        let max_size = max.get();
        if max_size == 0 {
            return Self(0.0);
        }

        let in_use = max_size.saturating_sub(available.get());
        let utilization = (pool_count_as_f64(in_use) / pool_count_as_f64(max_size)) * 100.0;
        Self(utilization)
    }

    #[inline]
    #[must_use]
    pub const fn as_percentage(self) -> f64 {
        self.0
    }

    #[inline]
    #[must_use]
    pub fn is_full(self) -> bool {
        self.0 >= 100.0
    }

    #[inline]
    #[must_use]
    pub fn is_empty(self) -> bool {
        self.0 == 0.0
    }

    #[inline]
    #[must_use]
    pub fn is_high_load(self) -> bool {
        self.0 >= 80.0
    }
}

impl fmt::Display for PoolUtilization {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.1}%", self.0)
    }
}

#[cfg(test)]
#[allow(clippy::float_cmp)] // These tests compare exact percentage values from bounded pool math.
mod tests {
    use super::*;

    #[test]
    fn test_available_connections() {
        let available = AvailableConnections::new(5);
        assert_eq!(available.get(), 5);
        assert_eq!(format!("{available}"), "5");

        let zero = AvailableConnections::zero();
        assert_eq!(zero.get(), 0);
    }

    #[test]
    fn test_max_pool_size() {
        let max = MaxPoolSize::new(10);
        assert_eq!(max.get(), 10);
        assert_eq!(format!("{max}"), "10");
    }

    #[test]
    fn test_created_connections() {
        let created = CreatedConnections::new(25);
        assert_eq!(created.get(), 25);
        assert_eq!(format!("{created}"), "25");

        let zero = CreatedConnections::zero();
        assert_eq!(zero.get(), 0);
    }

    #[test]
    fn test_in_use_connections() {
        let max = MaxPoolSize::new(10);
        let available = AvailableConnections::new(3);
        let in_use = InUseConnections::from_pool_stats(max, available);
        assert_eq!(in_use.get(), 7);
    }

    #[test]
    fn test_in_use_saturating() {
        let max = MaxPoolSize::new(5);
        let available = AvailableConnections::new(10);
        let in_use = InUseConnections::from_pool_stats(max, available);
        assert_eq!(in_use.get(), 0);
    }

    #[test]
    fn test_pool_utilization() {
        let max = MaxPoolSize::new(10);
        let available = AvailableConnections::new(3);
        let utilization = PoolUtilization::from_pool_stats(max, available);
        assert_eq!(utilization.as_percentage(), 70.0);
        assert_eq!(format!("{utilization}"), "70.0%");
    }

    #[test]
    fn test_pool_utilization_full() {
        let max = MaxPoolSize::new(10);
        let available = AvailableConnections::new(0);
        let utilization = PoolUtilization::from_pool_stats(max, available);
        assert!(utilization.is_full());
        assert!(utilization.is_high_load());
        assert!(!utilization.is_empty());
    }

    #[test]
    fn test_pool_utilization_empty() {
        let max = MaxPoolSize::new(10);
        let available = AvailableConnections::new(10);
        let utilization = PoolUtilization::from_pool_stats(max, available);
        assert!(utilization.is_empty());
        assert!(!utilization.is_full());
        assert!(!utilization.is_high_load());
    }

    #[test]
    fn test_pool_utilization_high_load() {
        let max = MaxPoolSize::new(10);
        let available = AvailableConnections::new(1);
        let utilization = PoolUtilization::from_pool_stats(max, available);
        assert!(utilization.is_high_load());
        assert!(!utilization.is_full());
    }

    #[test]
    fn test_pool_utilization_zero_max() {
        let max = MaxPoolSize::new(0);
        let available = AvailableConnections::new(0);
        let utilization = PoolUtilization::from_pool_stats(max, available);
        assert_eq!(utilization.as_percentage(), 0.0);
    }

    #[test]
    #[should_panic(expected = "Utilization must be 0-100%")]
    fn test_pool_utilization_invalid() {
        let _ = PoolUtilization::new(150.0);
    }

    #[test]
    fn test_ordering() {
        let small = AvailableConnections::new(1);
        let large = AvailableConnections::new(10);
        assert!(small < large);
        assert_eq!(small, small);
    }

    #[test]
    fn test_from_conversions() {
        let available: AvailableConnections = 5usize.into();
        assert_eq!(available.get(), 5);

        let max: MaxPoolSize = 10usize.into();
        assert_eq!(max.get(), 10);

        let created: CreatedConnections = 25usize.into();
        assert_eq!(created.get(), 25);
    }
}