Skip to main content

nwep/
pool.rs

1use crate::error::{Error, check};
2use crate::ffi;
3use crate::types::Tstamp;
4use std::ffi::CString;
5
6/// `POOL_MAX_SERVERS` is the maximum number of servers a [`LogServerPool`] can hold.
7pub const POOL_MAX_SERVERS: usize = 32;
8
9/// `POOL_HEALTH_CHECK_FAILURES` is the number of consecutive failures before a server is marked `Unhealthy`.
10pub const POOL_HEALTH_CHECK_FAILURES: i32 = 3;
11
12/// `PoolStrategy` controls how [`LogServerPool::select`] picks the next server.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum PoolStrategy {
15    /// `RoundRobin` cycles through healthy servers in order, distributing load evenly.
16    RoundRobin,
17    /// `Random` picks a healthy server at random on each call to [`select`](LogServerPool::select).
18    Random,
19}
20
21impl From<ffi::nwep_pool_strategy> for PoolStrategy {
22    fn from(s: ffi::nwep_pool_strategy) -> Self {
23        match s {
24            ffi::nwep_pool_strategy_NWEP_POOL_RANDOM => PoolStrategy::Random,
25            _ => PoolStrategy::RoundRobin,
26        }
27    }
28}
29
30fn pool_strategy_to_ffi(s: PoolStrategy) -> ffi::nwep_pool_strategy {
31    match s {
32        PoolStrategy::RoundRobin => ffi::nwep_pool_strategy_NWEP_POOL_ROUND_ROBIN,
33        PoolStrategy::Random => ffi::nwep_pool_strategy_NWEP_POOL_RANDOM,
34    }
35}
36
37/// `ServerHealth` indicates whether a server in the pool is currently considered usable.
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum ServerHealth {
40    /// `Healthy` servers are included in selection by [`LogServerPool::select`].
41    Healthy,
42    /// `Unhealthy` servers are skipped until [`reset_health`](LogServerPool::reset_health) is called.
43    Unhealthy,
44}
45
46impl From<ffi::nwep_server_health> for ServerHealth {
47    fn from(h: ffi::nwep_server_health) -> Self {
48        match h {
49            ffi::nwep_server_health_NWEP_SERVER_UNHEALTHY => ServerHealth::Unhealthy,
50            _ => ServerHealth::Healthy,
51        }
52    }
53}
54
55/// `PoolServer` is a snapshot of a single server's state in the [`LogServerPool`].
56#[derive(Clone, Debug)]
57pub struct PoolServer {
58    /// The `web://` URL of the log server.
59    pub url: String,
60    /// Current health status.
61    pub health: ServerHealth,
62    /// Number of consecutive request failures since the last success.
63    pub consecutive_failures: i32,
64    /// Timestamp of the most recent successful request (nanoseconds since epoch).
65    pub last_success: Tstamp,
66    /// Timestamp of the most recent failed request (nanoseconds since epoch).
67    pub last_failure: Tstamp,
68}
69
70impl PoolServer {
71    fn from_ffi(p: &ffi::nwep_pool_server) -> Self {
72        let url = unsafe {
73            std::ffi::CStr::from_ptr(p.url.as_ptr())
74                .to_string_lossy()
75                .into_owned()
76        };
77        PoolServer {
78            url,
79            health: ServerHealth::from(p.health),
80            consecutive_failures: p.consecutive_failures,
81            last_success: p.last_success,
82            last_failure: p.last_failure,
83        }
84    }
85}
86
87/// `PoolSettings` configures a [`LogServerPool`] at creation time.
88#[derive(Clone, Debug)]
89pub struct PoolSettings {
90    /// Server selection strategy.
91    pub strategy: PoolStrategy,
92    /// Number of consecutive failures before a server is marked `Unhealthy`. Defaults to [`POOL_HEALTH_CHECK_FAILURES`].
93    pub max_failures: i32,
94}
95
96impl Default for PoolSettings {
97    fn default() -> Self {
98        let mut s = unsafe { std::mem::zeroed::<ffi::nwep_log_server_pool_settings>() };
99        unsafe { ffi::nwep_log_server_pool_settings_default(&mut s) };
100        PoolSettings {
101            strategy: PoolStrategy::from(s.strategy),
102            max_failures: s.max_failures,
103        }
104    }
105}
106
107/// `LogServerPool` manages a set of log server URLs with health tracking and load balancing.
108///
109/// `LogServerPool` wraps the C library's pool implementation. After each request attempt,
110/// call [`mark_success`](LogServerPool::mark_success) or [`mark_failure`](LogServerPool::mark_failure)
111/// to update the health state so that [`select`](LogServerPool::select) can skip unhealthy servers.
112pub struct LogServerPool {
113    ptr: *mut ffi::nwep_log_server_pool,
114}
115
116unsafe impl Send for LogServerPool {}
117
118impl LogServerPool {
119    /// `new` creates a `LogServerPool` with the given settings.
120    ///
121    /// Pass `None` to use the default settings (round-robin, 3 max failures).
122    ///
123    /// # Errors
124    ///
125    /// Returns `Err` if the underlying C allocation fails.
126    pub fn new(settings: Option<&PoolSettings>) -> Result<Self, Error> {
127        let mut ptr: *mut ffi::nwep_log_server_pool = std::ptr::null_mut();
128        let rc = match settings {
129            Some(s) => {
130                let mut ffi_s = unsafe { std::mem::zeroed::<ffi::nwep_log_server_pool_settings>() };
131                unsafe { ffi::nwep_log_server_pool_settings_default(&mut ffi_s) };
132                ffi_s.strategy = pool_strategy_to_ffi(s.strategy);
133                ffi_s.max_failures = s.max_failures;
134                unsafe { ffi::nwep_log_server_pool_new(&mut ptr, &ffi_s) }
135            }
136            None => unsafe { ffi::nwep_log_server_pool_new(&mut ptr, std::ptr::null()) },
137        };
138        check(rc)?;
139        Ok(LogServerPool { ptr })
140    }
141
142    /// `add` registers a log server URL with the pool.
143    ///
144    /// # Errors
145    ///
146    /// Returns `Err` if `url` contains a null byte, is already registered, or the pool is full.
147    pub fn add(&mut self, url: &str) -> Result<(), Error> {
148        let curl = CString::new(url)
149            .map_err(|_| crate::error::Error::from_code(crate::error::ERR_INTERNAL_INVALID_ARG))?;
150        check(unsafe { ffi::nwep_log_server_pool_add(self.ptr, curl.as_ptr()) })
151    }
152
153    /// `remove` unregisters a log server URL from the pool.
154    ///
155    /// # Errors
156    ///
157    /// Returns `Err` if `url` is not registered or contains a null byte.
158    pub fn remove(&mut self, url: &str) -> Result<(), Error> {
159        let curl = CString::new(url)
160            .map_err(|_| crate::error::Error::from_code(crate::error::ERR_INTERNAL_INVALID_ARG))?;
161        check(unsafe { ffi::nwep_log_server_pool_remove(self.ptr, curl.as_ptr()) })
162    }
163
164    /// `select` picks the next healthy server according to the pool strategy.
165    ///
166    /// # Errors
167    ///
168    /// Returns `Err` if all servers are unhealthy or the pool is empty.
169    pub fn select(&mut self) -> Result<PoolServer, Error> {
170        let mut out = unsafe { std::mem::zeroed::<ffi::nwep_pool_server>() };
171        check(unsafe { ffi::nwep_log_server_pool_select(self.ptr, &mut out) })?;
172        Ok(PoolServer::from_ffi(&out))
173    }
174
175    /// `mark_success` records a successful request to `url` and resets its failure counter.
176    pub fn mark_success(&mut self, url: &str, now: Tstamp) {
177        if let Ok(curl) = CString::new(url) {
178            unsafe { ffi::nwep_log_server_pool_mark_success(self.ptr, curl.as_ptr(), now) }
179        }
180    }
181
182    /// `mark_failure` records a failed request to `url`, incrementing its consecutive failure count.
183    ///
184    /// Once `consecutive_failures >= max_failures` the server is marked `Unhealthy` and skipped by [`select`](LogServerPool::select).
185    pub fn mark_failure(&mut self, url: &str, now: Tstamp) {
186        if let Ok(curl) = CString::new(url) {
187            unsafe { ffi::nwep_log_server_pool_mark_failure(self.ptr, curl.as_ptr(), now) }
188        }
189    }
190
191    /// `size` returns the total number of servers registered in the pool (healthy + unhealthy).
192    pub fn size(&self) -> usize {
193        unsafe { ffi::nwep_log_server_pool_size(self.ptr) }
194    }
195
196    /// `healthy_count` returns the number of servers currently marked `Healthy`.
197    pub fn healthy_count(&self) -> usize {
198        unsafe { ffi::nwep_log_server_pool_healthy_count(self.ptr) }
199    }
200
201    /// `get` retrieves the server at position `index` in the pool.
202    ///
203    /// # Errors
204    ///
205    /// Returns `Err` if `index >= size()`.
206    pub fn get(&self, index: usize) -> Result<PoolServer, Error> {
207        let mut out = unsafe { std::mem::zeroed::<ffi::nwep_pool_server>() };
208        check(unsafe { ffi::nwep_log_server_pool_get(self.ptr, index, &mut out) })?;
209        Ok(PoolServer::from_ffi(&out))
210    }
211
212    /// `reset_health` marks all servers in the pool as `Healthy` and clears their failure counters.
213    ///
214    /// Use this after a network outage to allow all servers to be tried again.
215    pub fn reset_health(&mut self) {
216        unsafe { ffi::nwep_log_server_pool_reset_health(self.ptr) }
217    }
218}
219
220impl Drop for LogServerPool {
221    fn drop(&mut self) {
222        if !self.ptr.is_null() {
223            unsafe { ffi::nwep_log_server_pool_free(self.ptr) }
224        }
225    }
226}