use anyhow::Result;
use std::time::Duration;
use super::types::{Config, Server};
use crate::constants::pool::{MAX_RECOMMENDED_KEEPALIVE_SECS, MIN_RECOMMENDED_KEEPALIVE_SECS};
const MIN_RECOMMENDED_KEEPALIVE: Duration = Duration::from_secs(MIN_RECOMMENDED_KEEPALIVE_SECS);
const MAX_RECOMMENDED_KEEPALIVE: Duration = Duration::from_secs(MAX_RECOMMENDED_KEEPALIVE_SECS);
const MAX_SOCKET_BUFFER_SIZE: usize = u32::MAX as usize;
impl Config {
pub fn validate(&self) -> Result<()> {
if self.servers.is_empty() {
return Err(anyhow::anyhow!(
"Configuration must have at least one server"
));
}
if self.servers.len() > 8 {
return Err(anyhow::anyhow!(
"Configuration cannot have more than 8 servers (current limitation: u8 bitset for article availability tracking). \
Found {} servers. Consider running multiple proxy instances or file an issue if you need more backends.",
self.servers.len()
));
}
validate_socket_buffer_size(
"memory.socket_recv_buffer_size",
self.memory.socket_recv_buffer_size,
)?;
validate_socket_buffer_size(
"memory.socket_send_buffer_size",
self.memory.socket_send_buffer_size,
)?;
for server in &self.servers {
validate_server(server);
}
Ok(())
}
}
fn validate_socket_buffer_size(field: &str, size: usize) -> Result<()> {
if size > MAX_SOCKET_BUFFER_SIZE {
return Err(anyhow::anyhow!(
"{field} must be <= {MAX_SOCKET_BUFFER_SIZE} bytes because OS socket APIs accept u32 buffer sizes; found {size}"
));
}
Ok(())
}
fn validate_server(server: &Server) {
if let Some(keepalive) = server.connection_keepalive {
if keepalive < MIN_RECOMMENDED_KEEPALIVE {
tracing::warn!(
"Server '{}' has connection_keepalive set to {:?} (< {:?}). \
This may cause excessive health check traffic and connection churn. \
Consider using at least {:?} or None to disable.",
server.name.as_str(),
keepalive,
MIN_RECOMMENDED_KEEPALIVE,
MIN_RECOMMENDED_KEEPALIVE
);
} else if keepalive > MAX_RECOMMENDED_KEEPALIVE {
tracing::warn!(
"Server '{}' has connection_keepalive set to {:?} (> {:?} / 5 minutes). \
This may not detect stale connections quickly enough. Consider a lower value.",
server.name.as_str(),
keepalive,
MAX_RECOMMENDED_KEEPALIVE
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Port;
fn create_test_server(name: &str, keepalive: Option<Duration>) -> Server {
let mut builder = Server::builder("localhost", Port::try_new(119).unwrap()).name(name);
if let Some(ka) = keepalive {
builder = builder.connection_keepalive(ka);
}
builder.build().unwrap()
}
#[test]
fn test_validate_empty_config_fails() {
let config = Config {
servers: vec![],
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_validate_single_server_succeeds() {
let config = Config {
servers: vec![create_test_server("test", None)],
..Default::default()
};
assert!(config.validate().is_ok());
}
#[test]
fn test_validate_eight_servers_succeeds() {
let config = Config {
servers: (0..8)
.map(|i| create_test_server(&format!("server{i}"), None))
.collect(),
..Default::default()
};
assert!(config.validate().is_ok());
}
#[test]
fn test_validate_nine_servers_fails() {
let config = Config {
servers: (0..9)
.map(|i| create_test_server(&format!("server{i}"), None))
.collect(),
..Default::default()
};
let result = config.validate();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("cannot have more than 8 servers")
);
}
#[test]
fn test_validate_multiple_servers_succeeds() {
let config = Config {
servers: vec![
create_test_server("server1", None),
create_test_server("server2", None),
],
..Default::default()
};
assert!(config.validate().is_ok());
}
#[test]
fn test_validate_oversized_socket_recv_buffer_fails() {
let mut config = Config {
servers: vec![create_test_server("test", None)],
..Default::default()
};
config.memory.socket_recv_buffer_size = MAX_SOCKET_BUFFER_SIZE + 1;
let result = config.validate();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("memory.socket_recv_buffer_size")
);
}
#[test]
fn test_validate_oversized_socket_send_buffer_fails() {
let mut config = Config {
servers: vec![create_test_server("test", None)],
..Default::default()
};
config.memory.socket_send_buffer_size = MAX_SOCKET_BUFFER_SIZE + 1;
let result = config.validate();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("memory.socket_send_buffer_size")
);
}
#[test]
fn test_validate_server_with_recommended_keepalive() {
let server = create_test_server(
"test",
Some(crate::constants::duration_polyfill::from_minutes(1)),
);
validate_server(&server);
}
#[test]
fn test_validate_server_with_low_keepalive_warns() {
let server = create_test_server("test", Some(Duration::from_secs(5)));
validate_server(&server);
}
#[test]
fn test_validate_server_with_high_keepalive_warns() {
let server = create_test_server(
"test",
Some(crate::constants::duration_polyfill::from_minutes(10)),
);
validate_server(&server);
}
#[test]
fn test_validate_server_with_no_keepalive() {
let server = create_test_server("test", None);
validate_server(&server);
}
#[test]
fn test_validate_server_at_min_boundary() {
let server = create_test_server("test", Some(MIN_RECOMMENDED_KEEPALIVE));
validate_server(&server);
}
#[test]
fn test_validate_server_at_max_boundary() {
let server = create_test_server("test", Some(MAX_RECOMMENDED_KEEPALIVE));
validate_server(&server);
}
}