use anyhow::Result;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::time::Duration;
use tokio::net::TcpStream;
use tracing::{debug, info, warn};
use crate::cache::UnifiedCache;
use crate::config::RoutingMode;
use crate::network::NetworkOptimizer;
use crate::pool::ConnectionProvider;
use crate::router;
use crate::session::ClientSession;
use crate::types::{self, ClientAddress, TransferMetrics};
use crate::session::SessionError;
use super::NntpProxy;
#[allow(clippy::cast_possible_truncation)]
fn elapsed_nanos_u64(start: std::time::Instant) -> u64 {
u64::try_from(start.elapsed().as_nanos()).unwrap_or(u64::MAX)
}
impl NntpProxy {
#[inline]
pub(super) fn record_connection_opened(&self) {
self.metrics.connection_opened();
}
#[inline]
pub(super) fn record_connection_closed(&self) {
self.metrics.connection_closed();
}
#[inline]
pub fn increment_active_clients(&self) {
self.active_clients.fetch_add(1, Ordering::Relaxed);
}
#[inline]
pub fn decrement_active_clients(&self) {
let prev = self.active_clients.fetch_sub(1, Ordering::Relaxed);
if prev == 1 {
let nanos = elapsed_nanos_u64(self.start_instant);
self.last_activity_nanos.store(nanos, Ordering::Relaxed);
}
}
pub(crate) fn check_and_clear_stale_pools(&self) -> bool {
if self.active_clients.load(Ordering::Relaxed) > 0 {
return false;
}
let last_activity_nanos = self.last_activity_nanos.load(Ordering::Relaxed);
if last_activity_nanos == 0 {
return false;
}
let last_activity = Duration::from_nanos(last_activity_nanos);
let now = self.start_instant.elapsed();
let idle_duration = now.saturating_sub(last_activity);
let mut stale_backends = Vec::new();
for (server, provider) in self.servers.iter().zip(&self.connection_providers) {
if idle_duration > server.backend_idle_timeout {
provider.clear_idle_connections();
stale_backends.push((
server.name.as_ref().to_string(),
server.backend_idle_timeout.as_secs(),
));
}
}
if stale_backends.is_empty() {
return false;
}
info!(
idle_secs = idle_duration.as_secs(),
backend_count = stale_backends.len(),
stale_backends = ?stale_backends,
"Clearing idle backend connections"
);
true
}
pub(super) fn build_session(
&self,
client_addr: ClientAddress,
router: Option<Arc<router::BackendSelector>>,
routing_mode: RoutingMode,
cache: Arc<UnifiedCache>,
) -> ClientSession {
let builder = ClientSession::builder(
client_addr,
self.buffer_pool.clone(),
self.auth_handler.clone(),
self.metrics.clone(),
)
.with_routing_mode(routing_mode)
.with_connection_stats(self.connection_stats.clone())
.with_cache(cache)
.with_cache_articles(self.store_article_bodies)
.with_adaptive_precheck(self.adaptive_precheck);
let builder = match router {
Some(r) => builder.with_router(r),
None => builder,
};
builder.build()
}
pub(super) fn log_session_completion(
&self,
client_addr: ClientAddress,
session_id: &str,
session: &ClientSession,
routing_mode: crate::config::RoutingMode,
metrics: &types::TransferMetrics,
) {
self.connection_stats.record_session_disconnection(
session.client_id(),
session.username(),
routing_mode.short_name(),
);
debug!(
"Session {} [{}] ↑{} ↓{}",
client_addr,
session_id,
crate::formatting::format_bytes(metrics.client_to_backend.as_u64()),
crate::formatting::format_bytes(metrics.backend_to_client.as_u64())
);
}
#[inline]
pub(super) fn log_routing_selection(
&self,
client_addr: ClientAddress,
backend_id: crate::types::BackendId,
server: &crate::config::Server,
) {
info!(
"Routing client {} to backend {:?} ({}:{})",
client_addr, backend_id, server.host, server.port
);
}
#[inline]
pub(super) fn log_pool_status(&self, server_idx: usize) {
let pool_status = self.connection_providers[server_idx].status();
debug!(
"Pool status for {}: {}/{} available, {} created",
self.servers[server_idx].name,
pool_status.available,
pool_status.max_size,
pool_status.created
);
}
pub(super) async fn prepare_stateful_connection(
&self,
client_stream: &mut TcpStream,
client_addr: ClientAddress,
) -> Result<crate::types::BackendId> {
self.record_connection_opened();
let client_id = types::ClientId::new();
let backend_id = self.router.route_without_availability(client_id)?;
let server_idx = backend_id.as_index();
self.log_routing_selection(client_addr, backend_id, &self.servers[server_idx]);
crate::protocol::send_proxy_greeting(client_stream, client_addr).await?;
self.log_pool_status(server_idx);
self.apply_tcp_optimizations(client_stream);
Ok(backend_id)
}
pub(super) async fn prepare_per_command_connection(
&self,
client_stream: &mut TcpStream,
client_addr: ClientAddress,
) -> Result<()> {
self.record_connection_opened();
crate::protocol::send_proxy_greeting(client_stream, client_addr).await?;
self.apply_tcp_optimizations(client_stream);
Ok(())
}
#[inline]
pub(super) fn create_session(
&self,
client_addr: ClientAddress,
router: Option<Arc<crate::router::BackendSelector>>,
) -> ClientSession {
self.build_session(client_addr, router, self.routing_mode, self.cache.clone())
}
#[inline]
pub(super) fn generate_session_id(session: &ClientSession) -> String {
crate::formatting::short_id(session.client_id().as_uuid())
}
#[inline]
pub(super) fn apply_tcp_optimizations(&self, client_stream: &TcpStream) {
use crate::network::TcpOptimizer;
TcpOptimizer::with_buffer_sizes(
client_stream,
self.memory.socket_recv_buffer_size,
self.memory.socket_send_buffer_size,
)
.optimize()
.map_err(|e| debug!("Failed to optimize client socket: {}", e))
.ok();
}
#[inline]
pub(super) fn routing_mode_display_name(&self) -> &'static str {
if self.cache.entry_count() > 0 {
"caching"
} else {
"per-command"
}
}
pub(super) fn finalize_stateful_session(
&self,
metrics: Result<TransferMetrics, SessionError>,
client_addr: ClientAddress,
session_id: &str,
session: &ClientSession,
backend_id: crate::types::BackendId,
) -> Result<(), SessionError> {
self.record_connection_if_unauthenticated(session);
self.router.complete_command(backend_id);
self.record_session_metrics(metrics, client_addr, session_id, session, Some(backend_id))?;
self.record_connection_closed();
Ok(())
}
pub(super) fn finalize_per_command_session(
&self,
metrics: Result<TransferMetrics, SessionError>,
client_addr: ClientAddress,
session_id: &str,
session: &ClientSession,
) -> Result<(), SessionError> {
self.record_session_metrics(metrics, client_addr, session_id, session, None)?;
self.record_connection_closed();
Ok(())
}
#[inline]
pub(super) fn record_connection_if_unauthenticated(&self, session: &ClientSession) {
if !self.auth_handler.is_enabled() || session.username().is_none() {
let mode = self.session_mode_label(session.mode());
self.connection_stats.record_session_connection(
session.client_id(),
session.username(),
mode,
);
}
}
pub(super) fn record_session_metrics(
&self,
metrics: Result<TransferMetrics, SessionError>,
client_addr: ClientAddress,
session_id: &str,
session: &ClientSession,
backend_id: Option<crate::types::BackendId>,
) -> Result<(), SessionError> {
match metrics {
Ok(m) => {
self.log_session_completion(
client_addr,
session_id,
session,
self.routing_mode,
&m,
);
if let Some(bid) = backend_id {
self.metrics
.record_client_to_backend_bytes_for(bid, m.client_to_backend.as_u64());
self.metrics
.record_backend_to_client_bytes_for(bid, m.backend_to_client.as_u64());
}
Ok(())
}
Err(e) => {
if let Some(bid) = backend_id {
self.metrics.record_error(bid);
}
if let SessionError::Backend(ref inner) = e {
warn!("Session error for client {}: {:?}", client_addr, inner);
}
Err(e)
}
}
}
#[inline]
pub(super) const fn session_mode_label(
&self,
session_mode: crate::session::SessionMode,
) -> &'static str {
use crate::session::SessionMode;
match (session_mode, self.routing_mode) {
(SessionMode::PerCommand, _) => "per-command",
(SessionMode::Stateful, RoutingMode::Stateful) => "standard",
(SessionMode::Stateful, RoutingMode::Hybrid) => "hybrid",
(SessionMode::Stateful, _) => "stateful",
}
}
pub async fn handle_client(
&self,
mut client_stream: TcpStream,
client_addr: ClientAddress,
) -> Result<(), SessionError> {
debug!("New client connection from {}", client_addr);
self.check_and_clear_stale_pools();
self.increment_active_clients();
let result: Result<(), SessionError> = async {
let backend_id = self
.prepare_stateful_connection(&mut client_stream, client_addr)
.await
.map_err(SessionError::from)?;
let server_idx = backend_id.as_index();
let session = self.create_session(client_addr, None);
let session_id = Self::generate_session_id(&session);
debug!("Starting stateful session for client {}", client_addr);
let metrics = session
.handle_stateful_session(
client_stream,
backend_id,
&self.connection_providers[server_idx],
&self.servers[server_idx].name,
)
.await;
self.finalize_stateful_session(metrics, client_addr, &session_id, &session, backend_id)
}
.await;
self.decrement_active_clients();
result
}
pub async fn handle_client_per_command_routing(
&self,
client_stream: TcpStream,
client_addr: ClientAddress,
) -> Result<(), SessionError> {
self.check_and_clear_stale_pools();
self.increment_active_clients();
let result = Box::pin(self.handle_per_command_client(client_stream, client_addr)).await;
self.decrement_active_clients();
result
}
async fn handle_per_command_client(
&self,
mut client_stream: TcpStream,
client_addr: ClientAddress,
) -> Result<(), SessionError> {
let mode_label = self.routing_mode_display_name();
debug!(
"New {} routing client connection from {}",
mode_label, client_addr
);
self.prepare_per_command_connection(&mut client_stream, client_addr)
.await
.map_err(SessionError::from)?;
let session = self.create_session(client_addr, Some(self.router.clone()));
let session_id = Self::generate_session_id(&session);
let metrics = session.handle_per_command_routing(client_stream).await;
self.finalize_per_command_session(metrics, client_addr, &session_id, &session)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::RoutingMode;
use crate::session::SessionMode;
use std::sync::Arc;
fn create_test_config() -> crate::config::Config {
super::super::tests::create_test_config()
}
#[test]
fn test_session_mode_label_per_command() {
let config = create_test_config();
let proxy = NntpProxy::new_sync(config, RoutingMode::PerCommand).unwrap();
let label = proxy.session_mode_label(SessionMode::PerCommand);
assert_eq!(label, "per-command");
}
#[test]
fn test_session_mode_label_stateful_standard() {
let config = create_test_config();
let proxy = NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap();
let label = proxy.session_mode_label(SessionMode::Stateful);
assert_eq!(label, "standard");
}
#[test]
fn test_session_mode_label_stateful_hybrid() {
let config = create_test_config();
let proxy = NntpProxy::new_sync(config, RoutingMode::Hybrid).unwrap();
let label = proxy.session_mode_label(SessionMode::Stateful);
assert_eq!(label, "hybrid");
}
#[test]
fn test_routing_mode_display_name_caching() {
let config = create_test_config();
let proxy = NntpProxy::new_sync(config, RoutingMode::PerCommand).unwrap();
assert_eq!(proxy.routing_mode_display_name(), "per-command");
}
#[test]
fn test_generate_session_id_format() {
let config = create_test_config();
let proxy = NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap();
let session = proxy.create_session(
ClientAddress::from("127.0.0.1:12345".parse::<std::net::SocketAddr>().unwrap()),
None,
);
let session_id = NntpProxy::generate_session_id(&session);
assert_eq!(session_id.len(), 8);
}
#[test]
fn test_create_session_without_router() {
let config = create_test_config();
let proxy = NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap();
let session = proxy.create_session(
ClientAddress::from("127.0.0.1:12345".parse::<std::net::SocketAddr>().unwrap()),
None,
);
assert_eq!(session.mode(), SessionMode::Stateful);
}
#[test]
fn test_create_session_with_router() {
let config = create_test_config();
let proxy = NntpProxy::new_sync(config, RoutingMode::PerCommand).unwrap();
let session = proxy.create_session(
ClientAddress::from("127.0.0.1:12345".parse::<std::net::SocketAddr>().unwrap()),
Some(proxy.router.clone()),
);
assert_eq!(session.mode(), SessionMode::PerCommand);
}
#[test]
fn test_record_connection_if_unauthenticated_no_auth() {
let config = create_test_config();
let proxy = Arc::new(NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap());
let session = proxy.create_session(
ClientAddress::from("127.0.0.1:12345".parse::<std::net::SocketAddr>().unwrap()),
None,
);
proxy.record_connection_if_unauthenticated(&session);
}
#[test]
fn test_record_session_metrics_success() {
let config = create_test_config();
let proxy = Arc::new(NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap());
let session = proxy.create_session(
ClientAddress::from("127.0.0.1:12345".parse::<std::net::SocketAddr>().unwrap()),
None,
);
let session_id = NntpProxy::generate_session_id(&session);
let metrics = TransferMetrics {
client_to_backend: crate::types::ClientToBackendBytes::new(1024),
backend_to_client: crate::types::BackendToClientBytes::new(2048),
};
let result = proxy.record_session_metrics(
Ok(metrics),
ClientAddress::from("127.0.0.1:12345".parse::<std::net::SocketAddr>().unwrap()),
&session_id,
&session,
Some(crate::types::BackendId::from_index(0)),
);
assert!(result.is_ok());
}
#[test]
fn test_record_session_metrics_error() {
let config = create_test_config();
let proxy = Arc::new(NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap());
let session = proxy.create_session(
ClientAddress::from("127.0.0.1:12345".parse::<std::net::SocketAddr>().unwrap()),
None,
);
let session_id = NntpProxy::generate_session_id(&session);
let result = proxy.record_session_metrics(
Err(SessionError::Backend(anyhow::anyhow!("test error"))),
ClientAddress::from("127.0.0.1:12345".parse::<std::net::SocketAddr>().unwrap()),
&session_id,
&session,
Some(crate::types::BackendId::from_index(0)),
);
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "test error");
}
#[tokio::test]
async fn test_prepare_per_command_connection() {
let config = create_test_config();
let proxy = Arc::new(
NntpProxy::new(config, RoutingMode::PerCommand)
.await
.unwrap(),
);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let mut buf = [0u8; 1024];
let _ = stream.try_read(&mut buf); });
let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
let client_addr = ClientAddress::from(stream.peer_addr().unwrap());
let result = proxy
.prepare_per_command_connection(&mut stream, client_addr)
.await;
assert!(result.is_ok());
}
#[test]
fn test_routing_mode_display_name_empty_cache() {
let config = create_test_config();
let proxy = NntpProxy::new_sync(config, RoutingMode::Hybrid).unwrap();
let _empty_cache = Arc::new(crate::cache::UnifiedCache::availability(
std::time::Duration::MAX,
));
assert_eq!(proxy.routing_mode_display_name(), "per-command");
}
#[test]
fn test_log_routing_selection() {
let config = create_test_config();
let proxy = Arc::new(NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap());
let backend_id = crate::types::BackendId::from_index(0);
let client_addr =
ClientAddress::from("127.0.0.1:12345".parse::<std::net::SocketAddr>().unwrap());
proxy.log_routing_selection(client_addr, backend_id, &proxy.servers()[0]);
}
#[test]
fn test_log_pool_status() {
let config = create_test_config();
let proxy = Arc::new(NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap());
proxy.log_pool_status(0);
}
#[tokio::test]
async fn test_apply_tcp_optimizations() {
let config = create_test_config();
let proxy = Arc::new(NntpProxy::new(config, RoutingMode::Stateful).await.unwrap());
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let (_stream, _) = listener.accept().await.unwrap();
});
let stream = tokio::net::TcpStream::connect(addr).await.unwrap();
proxy.apply_tcp_optimizations(&stream);
}
#[test]
fn test_session_mode_labels_all_combinations() {
use crate::session::SessionMode;
let config = create_test_config();
let proxy = NntpProxy::new_sync(config.clone(), RoutingMode::PerCommand).unwrap();
assert_eq!(
proxy.session_mode_label(SessionMode::PerCommand),
"per-command"
);
let proxy = NntpProxy::new_sync(config.clone(), RoutingMode::Stateful).unwrap();
assert_eq!(proxy.session_mode_label(SessionMode::Stateful), "standard");
let proxy = NntpProxy::new_sync(config, RoutingMode::Hybrid).unwrap();
assert_eq!(proxy.session_mode_label(SessionMode::Stateful), "hybrid");
}
#[test]
fn test_finalize_stateful_session_success() {
let config = create_test_config();
let proxy = Arc::new(NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap());
let client_addr =
ClientAddress::from("127.0.0.1:12345".parse::<std::net::SocketAddr>().unwrap());
let session = proxy.create_session(client_addr, None);
let session_id = NntpProxy::generate_session_id(&session);
let backend_id = crate::types::BackendId::from_index(0);
let metrics = TransferMetrics {
client_to_backend: crate::types::ClientToBackendBytes::new(512),
backend_to_client: crate::types::BackendToClientBytes::new(1024),
};
let result = proxy.finalize_stateful_session(
Ok(metrics),
client_addr,
&session_id,
&session,
backend_id,
);
assert!(result.is_ok());
}
#[test]
fn test_finalize_stateful_session_error() {
let config = create_test_config();
let proxy = Arc::new(NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap());
let client_addr =
ClientAddress::from("127.0.0.1:12345".parse::<std::net::SocketAddr>().unwrap());
let session = proxy.create_session(client_addr, None);
let session_id = NntpProxy::generate_session_id(&session);
let backend_id = crate::types::BackendId::from_index(0);
let result = proxy.finalize_stateful_session(
Err(SessionError::Backend(anyhow::anyhow!("connection error"))),
client_addr,
&session_id,
&session,
backend_id,
);
assert!(result.is_err());
}
#[test]
fn test_finalize_per_command_session_success() {
let config = create_test_config();
let proxy = Arc::new(NntpProxy::new_sync(config, RoutingMode::PerCommand).unwrap());
let client_addr =
ClientAddress::from("127.0.0.1:12345".parse::<std::net::SocketAddr>().unwrap());
let session = proxy.create_session(client_addr, Some(proxy.router.clone()));
let session_id = NntpProxy::generate_session_id(&session);
let metrics = TransferMetrics {
client_to_backend: crate::types::ClientToBackendBytes::new(256),
backend_to_client: crate::types::BackendToClientBytes::new(512),
};
let result =
proxy.finalize_per_command_session(Ok(metrics), client_addr, &session_id, &session);
assert!(result.is_ok());
}
#[test]
fn test_finalize_per_command_session_error() {
let config = create_test_config();
let proxy = Arc::new(NntpProxy::new_sync(config, RoutingMode::PerCommand).unwrap());
let client_addr =
ClientAddress::from("127.0.0.1:12345".parse::<std::net::SocketAddr>().unwrap());
let session = proxy.create_session(client_addr, Some(proxy.router.clone()));
let session_id = NntpProxy::generate_session_id(&session);
let result = proxy.finalize_per_command_session(
Err(SessionError::Backend(anyhow::anyhow!("session failed"))),
client_addr,
&session_id,
&session,
);
assert!(result.is_err());
}
#[test]
fn test_record_session_metrics_without_backend() {
let config = create_test_config();
let proxy = Arc::new(NntpProxy::new_sync(config, RoutingMode::PerCommand).unwrap());
let client_addr =
ClientAddress::from("127.0.0.1:12345".parse::<std::net::SocketAddr>().unwrap());
let session = proxy.create_session(client_addr, Some(proxy.router.clone()));
let session_id = NntpProxy::generate_session_id(&session);
let metrics = TransferMetrics {
client_to_backend: crate::types::ClientToBackendBytes::new(128),
backend_to_client: crate::types::BackendToClientBytes::new(256),
};
let result =
proxy.record_session_metrics(Ok(metrics), client_addr, &session_id, &session, None);
assert!(result.is_ok());
}
#[test]
fn test_generate_session_id_uniqueness() {
let config = create_test_config();
let proxy = NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap();
let client_addr =
ClientAddress::from("127.0.0.1:12345".parse::<std::net::SocketAddr>().unwrap());
let session1 = proxy.create_session(client_addr, None);
let session2 = proxy.create_session(client_addr, None);
let id1 = NntpProxy::generate_session_id(&session1);
let id2 = NntpProxy::generate_session_id(&session2);
assert_ne!(id1, id2);
}
#[test]
fn test_per_backend_idle_timeout_defaults() {
let config = create_test_config();
let proxy = NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap();
for server in proxy.servers() {
assert_eq!(
server.backend_idle_timeout,
crate::constants::duration_polyfill::from_minutes(10),
"Server '{}' should have default 10-minute backend_idle_timeout",
server.name.as_ref(),
);
}
}
#[test]
fn test_active_clients_increment_decrement() {
let config = create_test_config();
let proxy = NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap();
assert_eq!(proxy.active_clients.load(Ordering::Relaxed), 0);
proxy.increment_active_clients();
assert_eq!(proxy.active_clients.load(Ordering::Relaxed), 1);
proxy.increment_active_clients();
assert_eq!(proxy.active_clients.load(Ordering::Relaxed), 2);
proxy.decrement_active_clients();
assert_eq!(proxy.active_clients.load(Ordering::Relaxed), 1);
proxy.decrement_active_clients();
assert_eq!(proxy.active_clients.load(Ordering::Relaxed), 0);
}
#[test]
fn test_last_activity_updated_on_last_client_disconnect() {
let config = create_test_config();
let proxy = NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap();
assert_eq!(proxy.last_activity_nanos.load(Ordering::Relaxed), 0);
proxy.increment_active_clients();
proxy.increment_active_clients();
proxy.decrement_active_clients();
assert_eq!(proxy.last_activity_nanos.load(Ordering::Relaxed), 0);
proxy.decrement_active_clients();
assert!(proxy.last_activity_nanos.load(Ordering::Relaxed) > 0);
}
#[test]
fn test_check_and_clear_skips_when_clients_active() {
let config = create_test_config();
let proxy = NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap();
proxy.last_activity_nanos.store(1, Ordering::Relaxed);
proxy.increment_active_clients();
let cleared = proxy.check_and_clear_stale_pools();
assert!(!cleared);
}
#[test]
fn test_check_and_clear_skips_when_never_active() {
let config = create_test_config();
let proxy = NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap();
let cleared = proxy.check_and_clear_stale_pools();
assert!(!cleared);
}
#[test]
fn test_check_and_clear_skips_when_recently_active() {
let config = create_test_config();
let proxy = NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap();
proxy.increment_active_clients();
proxy.decrement_active_clients();
let cleared = proxy.check_and_clear_stale_pools();
assert!(!cleared);
}
#[test]
fn test_check_and_clear_clears_when_timeout_exceeded() {
use crate::config::{Config, Server};
use crate::types::{MaxConnections, Port};
let config = Config {
servers: vec![
Server::builder("server1.example.com", Port::try_new(119).unwrap())
.name("Fast Timeout Server")
.max_connections(MaxConnections::try_new(2).unwrap())
.backend_idle_timeout(Duration::from_nanos(1)) .build()
.unwrap(),
],
..Default::default()
};
let proxy = NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap();
proxy.last_activity_nanos.store(1, Ordering::Relaxed);
std::thread::sleep(std::time::Duration::from_millis(1));
let cleared = proxy.check_and_clear_stale_pools();
assert!(cleared, "Should clear pools when idle timeout exceeded");
}
#[test]
fn test_check_and_clear_per_backend_independent_short_timeout() {
use crate::config::{Config, Server};
use crate::types::{MaxConnections, Port};
let config = Config {
servers: vec![
Server::builder("server1.example.com", Port::try_new(119).unwrap())
.name("Short Timeout")
.max_connections(MaxConnections::try_new(2).unwrap())
.backend_idle_timeout(Duration::from_nanos(1))
.build()
.unwrap(),
],
..Default::default()
};
let proxy = NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap();
proxy.last_activity_nanos.store(1, Ordering::Relaxed);
std::thread::sleep(std::time::Duration::from_millis(1));
assert!(proxy.check_and_clear_stale_pools());
}
#[test]
fn test_check_and_clear_per_backend_independent_long_timeout() {
use crate::config::{Config, Server};
use crate::types::{MaxConnections, Port};
let config = Config {
servers: vec![
Server::builder("server2.example.com", Port::try_new(119).unwrap())
.name("Long Timeout")
.max_connections(MaxConnections::try_new(2).unwrap())
.backend_idle_timeout(crate::constants::duration_polyfill::from_hours(24)) .build()
.unwrap(),
],
..Default::default()
};
let proxy = NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap();
proxy.last_activity_nanos.store(1, Ordering::Relaxed);
let cleared = proxy.check_and_clear_stale_pools();
assert!(
!cleared,
"Should NOT clear pools when timeout not yet exceeded"
);
}
#[test]
fn test_check_and_clear_mixed_timeouts_returns_true_when_any_cleared() {
use crate::config::{Config, Server};
use crate::types::{MaxConnections, Port};
let config = Config {
servers: vec![
Server::builder("server1.example.com", Port::try_new(119).unwrap())
.name("Short Timeout")
.max_connections(MaxConnections::try_new(2).unwrap())
.backend_idle_timeout(Duration::from_nanos(1))
.build()
.unwrap(),
Server::builder("server2.example.com", Port::try_new(119).unwrap())
.name("Long Timeout")
.max_connections(MaxConnections::try_new(2).unwrap())
.backend_idle_timeout(crate::constants::duration_polyfill::from_hours(24))
.build()
.unwrap(),
],
..Default::default()
};
let proxy = NntpProxy::new_sync(config, RoutingMode::Stateful).unwrap();
proxy.last_activity_nanos.store(1, Ordering::Relaxed);
std::thread::sleep(std::time::Duration::from_millis(1));
let cleared = proxy.check_and_clear_stale_pools();
assert!(
cleared,
"Should return true when at least one backend exceeded its timeout"
);
}
}