use std::time::Duration;
pub const MAX_CAPACITY: usize = 1 << 20;
#[derive(Debug, Clone)]
pub struct BackoffConfig {
pub initial: Duration,
pub max: Duration,
pub factor: f64,
pub jitter: f64,
}
impl Default for BackoffConfig {
fn default() -> Self {
Self { initial: Duration::from_millis(50), max: Duration::from_secs(5), factor: 2.0, jitter: 0.2 }
}
}
#[derive(Debug, Clone)]
pub struct AppConfig {
pub handler_queue_capacity: usize,
pub max_in_flight_per_handler: usize,
pub ack_retry_queue_capacity: usize,
pub ack_max_attempts: u32,
pub handler_max_attempts: u32,
pub reclaim_interval: Duration,
pub poll_idle_sleep: Duration,
pub backoff: BackoffConfig,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
handler_queue_capacity: 1_024,
max_in_flight_per_handler: 256,
ack_retry_queue_capacity: 1_024,
ack_max_attempts: 8,
handler_max_attempts: 5,
reclaim_interval: Duration::from_secs(30),
poll_idle_sleep: Duration::from_millis(50),
backoff: BackoffConfig::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum ConfigError {
#[error("`{field}` must be at least 1")]
TooSmall {
field: &'static str,
},
#[error("`{field}` ({value}) exceeds the maximum of {max}")]
TooLarge {
field: &'static str,
value: usize,
max: usize,
},
#[error("`backoff.factor` must be >= 1.0 (got {0})")]
BackoffFactor(f64),
#[error("`backoff.jitter` must be within [0.0, 1.0] (got {0})")]
BackoffJitter(f64),
#[error("`backoff.initial` must be <= `backoff.max`")]
BackoffRange,
}
impl AppConfig {
pub fn validate(&self) -> Result<(), ConfigError> {
check_capacity("handler_queue_capacity", self.handler_queue_capacity)?;
check_capacity("max_in_flight_per_handler", self.max_in_flight_per_handler)?;
check_capacity("ack_retry_queue_capacity", self.ack_retry_queue_capacity)?;
if self.ack_max_attempts < 1 {
return Err(ConfigError::TooSmall { field: "ack_max_attempts" });
}
if self.handler_max_attempts < 1 {
return Err(ConfigError::TooSmall { field: "handler_max_attempts" });
}
if self.backoff.factor.is_nan() || self.backoff.factor < 1.0 {
return Err(ConfigError::BackoffFactor(self.backoff.factor));
}
if !(0.0..=1.0).contains(&self.backoff.jitter) {
return Err(ConfigError::BackoffJitter(self.backoff.jitter));
}
if self.backoff.initial > self.backoff.max {
return Err(ConfigError::BackoffRange);
}
Ok(())
}
}
fn check_capacity(field: &'static str, value: usize) -> Result<(), ConfigError> {
if value < 1 {
return Err(ConfigError::TooSmall { field });
}
if value > MAX_CAPACITY {
return Err(ConfigError::TooLarge { field, value, max: MAX_CAPACITY });
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_is_valid() {
assert!(AppConfig::default().validate().is_ok());
}
#[test]
fn rejects_zero_capacity() {
let config = AppConfig { handler_queue_capacity: 0, ..AppConfig::default() };
assert_eq!(config.validate(), Err(ConfigError::TooSmall { field: "handler_queue_capacity" }));
}
#[test]
fn rejects_oversized_capacity() {
let config = AppConfig { ack_retry_queue_capacity: MAX_CAPACITY + 1, ..AppConfig::default() };
assert!(matches!(config.validate(), Err(ConfigError::TooLarge { .. })));
}
#[test]
fn rejects_bad_backoff() {
let bad_factor =
AppConfig { backoff: BackoffConfig { factor: 0.5, ..BackoffConfig::default() }, ..AppConfig::default() };
assert_eq!(bad_factor.validate(), Err(ConfigError::BackoffFactor(0.5)));
let bad_range = AppConfig {
backoff: BackoffConfig {
initial: Duration::from_secs(10),
max: Duration::from_secs(1),
..BackoffConfig::default()
},
..AppConfig::default()
};
assert_eq!(bad_range.validate(), Err(ConfigError::BackoffRange));
}
#[test]
fn rejects_zero_attempts() {
let config = AppConfig { handler_max_attempts: 0, ..AppConfig::default() };
assert_eq!(config.validate(), Err(ConfigError::TooSmall { field: "handler_max_attempts" }));
}
}