nwep-rs 0.1.8

Rust bindings for the NWEP (WEB/1) protocol library
Documentation
use crate::error::{Error, check};
use crate::ffi;
use crate::types::Tstamp;
use std::ffi::CString;

/// `POOL_MAX_SERVERS` is the maximum number of servers a [`LogServerPool`] can hold.
pub const POOL_MAX_SERVERS: usize = 32;

/// `POOL_HEALTH_CHECK_FAILURES` is the number of consecutive failures before a server is marked `Unhealthy`.
pub const POOL_HEALTH_CHECK_FAILURES: i32 = 3;

/// `PoolStrategy` controls how [`LogServerPool::select`] picks the next server.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PoolStrategy {
    /// `RoundRobin` cycles through healthy servers in order, distributing load evenly.
    RoundRobin,
    /// `Random` picks a healthy server at random on each call to [`select`](LogServerPool::select).
    Random,
}

impl From<ffi::nwep_pool_strategy> for PoolStrategy {
    fn from(s: ffi::nwep_pool_strategy) -> Self {
        match s {
            ffi::nwep_pool_strategy_NWEP_POOL_RANDOM => PoolStrategy::Random,
            _ => PoolStrategy::RoundRobin,
        }
    }
}

fn pool_strategy_to_ffi(s: PoolStrategy) -> ffi::nwep_pool_strategy {
    match s {
        PoolStrategy::RoundRobin => ffi::nwep_pool_strategy_NWEP_POOL_ROUND_ROBIN,
        PoolStrategy::Random => ffi::nwep_pool_strategy_NWEP_POOL_RANDOM,
    }
}

/// `ServerHealth` indicates whether a server in the pool is currently considered usable.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ServerHealth {
    /// `Healthy` servers are included in selection by [`LogServerPool::select`].
    Healthy,
    /// `Unhealthy` servers are skipped until [`reset_health`](LogServerPool::reset_health) is called.
    Unhealthy,
}

impl From<ffi::nwep_server_health> for ServerHealth {
    fn from(h: ffi::nwep_server_health) -> Self {
        match h {
            ffi::nwep_server_health_NWEP_SERVER_UNHEALTHY => ServerHealth::Unhealthy,
            _ => ServerHealth::Healthy,
        }
    }
}

/// `PoolServer` is a snapshot of a single server's state in the [`LogServerPool`].
#[derive(Clone, Debug)]
pub struct PoolServer {
    /// The `web://` URL of the log server.
    pub url: String,
    /// Current health status.
    pub health: ServerHealth,
    /// Number of consecutive request failures since the last success.
    pub consecutive_failures: i32,
    /// Timestamp of the most recent successful request (nanoseconds since epoch).
    pub last_success: Tstamp,
    /// Timestamp of the most recent failed request (nanoseconds since epoch).
    pub last_failure: Tstamp,
}

impl PoolServer {
    fn from_ffi(p: &ffi::nwep_pool_server) -> Self {
        let url = unsafe {
            std::ffi::CStr::from_ptr(p.url.as_ptr())
                .to_string_lossy()
                .into_owned()
        };
        PoolServer {
            url,
            health: ServerHealth::from(p.health),
            consecutive_failures: p.consecutive_failures,
            last_success: p.last_success,
            last_failure: p.last_failure,
        }
    }
}

/// `PoolSettings` configures a [`LogServerPool`] at creation time.
#[derive(Clone, Debug)]
pub struct PoolSettings {
    /// Server selection strategy.
    pub strategy: PoolStrategy,
    /// Number of consecutive failures before a server is marked `Unhealthy`. Defaults to [`POOL_HEALTH_CHECK_FAILURES`].
    pub max_failures: i32,
}

impl Default for PoolSettings {
    fn default() -> Self {
        let mut s = unsafe { std::mem::zeroed::<ffi::nwep_log_server_pool_settings>() };
        unsafe { ffi::nwep_log_server_pool_settings_default(&mut s) };
        PoolSettings {
            strategy: PoolStrategy::from(s.strategy),
            max_failures: s.max_failures,
        }
    }
}

/// `LogServerPool` manages a set of log server URLs with health tracking and load balancing.
///
/// `LogServerPool` wraps the C library's pool implementation. After each request attempt,
/// call [`mark_success`](LogServerPool::mark_success) or [`mark_failure`](LogServerPool::mark_failure)
/// to update the health state so that [`select`](LogServerPool::select) can skip unhealthy servers.
pub struct LogServerPool {
    ptr: *mut ffi::nwep_log_server_pool,
}

unsafe impl Send for LogServerPool {}

impl LogServerPool {
    /// `new` creates a `LogServerPool` with the given settings.
    ///
    /// Pass `None` to use the default settings (round-robin, 3 max failures).
    ///
    /// # Errors
    ///
    /// Returns `Err` if the underlying C allocation fails.
    pub fn new(settings: Option<&PoolSettings>) -> Result<Self, Error> {
        let mut ptr: *mut ffi::nwep_log_server_pool = std::ptr::null_mut();
        let rc = match settings {
            Some(s) => {
                let mut ffi_s = unsafe { std::mem::zeroed::<ffi::nwep_log_server_pool_settings>() };
                unsafe { ffi::nwep_log_server_pool_settings_default(&mut ffi_s) };
                ffi_s.strategy = pool_strategy_to_ffi(s.strategy);
                ffi_s.max_failures = s.max_failures;
                unsafe { ffi::nwep_log_server_pool_new(&mut ptr, &ffi_s) }
            }
            None => unsafe { ffi::nwep_log_server_pool_new(&mut ptr, std::ptr::null()) },
        };
        check(rc)?;
        Ok(LogServerPool { ptr })
    }

    /// `add` registers a log server URL with the pool.
    ///
    /// # Errors
    ///
    /// Returns `Err` if `url` contains a null byte, is already registered, or the pool is full.
    pub fn add(&mut self, url: &str) -> Result<(), Error> {
        let curl = CString::new(url)
            .map_err(|_| crate::error::Error::from_code(crate::error::ERR_INTERNAL_INVALID_ARG))?;
        check(unsafe { ffi::nwep_log_server_pool_add(self.ptr, curl.as_ptr()) })
    }

    /// `remove` unregisters a log server URL from the pool.
    ///
    /// # Errors
    ///
    /// Returns `Err` if `url` is not registered or contains a null byte.
    pub fn remove(&mut self, url: &str) -> Result<(), Error> {
        let curl = CString::new(url)
            .map_err(|_| crate::error::Error::from_code(crate::error::ERR_INTERNAL_INVALID_ARG))?;
        check(unsafe { ffi::nwep_log_server_pool_remove(self.ptr, curl.as_ptr()) })
    }

    /// `select` picks the next healthy server according to the pool strategy.
    ///
    /// # Errors
    ///
    /// Returns `Err` if all servers are unhealthy or the pool is empty.
    pub fn select(&mut self) -> Result<PoolServer, Error> {
        let mut out = unsafe { std::mem::zeroed::<ffi::nwep_pool_server>() };
        check(unsafe { ffi::nwep_log_server_pool_select(self.ptr, &mut out) })?;
        Ok(PoolServer::from_ffi(&out))
    }

    /// `mark_success` records a successful request to `url` and resets its failure counter.
    pub fn mark_success(&mut self, url: &str, now: Tstamp) {
        if let Ok(curl) = CString::new(url) {
            unsafe { ffi::nwep_log_server_pool_mark_success(self.ptr, curl.as_ptr(), now) }
        }
    }

    /// `mark_failure` records a failed request to `url`, incrementing its consecutive failure count.
    ///
    /// Once `consecutive_failures >= max_failures` the server is marked `Unhealthy` and skipped by [`select`](LogServerPool::select).
    pub fn mark_failure(&mut self, url: &str, now: Tstamp) {
        if let Ok(curl) = CString::new(url) {
            unsafe { ffi::nwep_log_server_pool_mark_failure(self.ptr, curl.as_ptr(), now) }
        }
    }

    /// `size` returns the total number of servers registered in the pool (healthy + unhealthy).
    pub fn size(&self) -> usize {
        unsafe { ffi::nwep_log_server_pool_size(self.ptr) }
    }

    /// `healthy_count` returns the number of servers currently marked `Healthy`.
    pub fn healthy_count(&self) -> usize {
        unsafe { ffi::nwep_log_server_pool_healthy_count(self.ptr) }
    }

    /// `get` retrieves the server at position `index` in the pool.
    ///
    /// # Errors
    ///
    /// Returns `Err` if `index >= size()`.
    pub fn get(&self, index: usize) -> Result<PoolServer, Error> {
        let mut out = unsafe { std::mem::zeroed::<ffi::nwep_pool_server>() };
        check(unsafe { ffi::nwep_log_server_pool_get(self.ptr, index, &mut out) })?;
        Ok(PoolServer::from_ffi(&out))
    }

    /// `reset_health` marks all servers in the pool as `Healthy` and clears their failure counters.
    ///
    /// Use this after a network outage to allow all servers to be tried again.
    pub fn reset_health(&mut self) {
        unsafe { ffi::nwep_log_server_pool_reset_health(self.ptr) }
    }
}

impl Drop for LogServerPool {
    fn drop(&mut self) {
        if !self.ptr.is_null() {
            unsafe { ffi::nwep_log_server_pool_free(self.ptr) }
        }
    }
}