Skip to main content

astraea_server/
connection.rs

1//! Connection management: pooling, backpressure, timeouts, and graceful shutdown.
2
3use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
5use std::time::Duration;
6
7use tokio::sync::Semaphore;
8
9/// Configuration for connection management.
10#[derive(Debug, Clone)]
11pub struct ConnectionConfig {
12    /// Maximum concurrent connections. New connections beyond this are rejected.
13    pub max_connections: usize,
14    /// Maximum concurrent request processing. Requests beyond this wait in queue.
15    pub max_concurrent_requests: usize,
16    /// Close connections idle for longer than this duration.
17    pub idle_timeout: Duration,
18    /// Abort requests that take longer than this duration.
19    pub request_timeout: Duration,
20    /// Time to wait for in-flight requests during shutdown.
21    pub drain_timeout: Duration,
22}
23
24impl Default for ConnectionConfig {
25    fn default() -> Self {
26        Self {
27            max_connections: 1024,
28            max_concurrent_requests: 256,
29            idle_timeout: Duration::from_secs(300), // 5 minutes
30            request_timeout: Duration::from_secs(30), // 30 seconds
31            drain_timeout: Duration::from_secs(10), // 10 seconds
32        }
33    }
34}
35
36/// Manages connection limits, request queuing, and shutdown coordination.
37pub struct ConnectionManager {
38    config: ConnectionConfig,
39    /// Semaphore for limiting concurrent connections.
40    connection_semaphore: Arc<Semaphore>,
41    /// Semaphore for limiting concurrent request processing.
42    request_semaphore: Arc<Semaphore>,
43    /// Flag indicating the server is shutting down.
44    shutting_down: Arc<AtomicBool>,
45    /// Count of currently active connections.
46    active_connections: Arc<AtomicU64>,
47    /// Total connections rejected due to limits.
48    rejected_connections: AtomicU64,
49}
50
51impl ConnectionManager {
52    /// Create a new connection manager with the given configuration.
53    pub fn new(config: ConnectionConfig) -> Self {
54        Self {
55            connection_semaphore: Arc::new(Semaphore::new(config.max_connections)),
56            request_semaphore: Arc::new(Semaphore::new(config.max_concurrent_requests)),
57            shutting_down: Arc::new(AtomicBool::new(false)),
58            active_connections: Arc::new(AtomicU64::new(0)),
59            rejected_connections: AtomicU64::new(0),
60            config,
61        }
62    }
63
64    /// Try to accept a new connection. Returns a guard that releases the slot on drop.
65    /// Returns None if the connection limit is reached or server is shutting down.
66    pub fn try_accept(&self) -> Option<ConnectionGuard> {
67        if self.shutting_down.load(Ordering::Relaxed) {
68            return None;
69        }
70
71        match self.connection_semaphore.clone().try_acquire_owned() {
72            Ok(permit) => {
73                self.active_connections.fetch_add(1, Ordering::Relaxed);
74                Some(ConnectionGuard {
75                    _permit: permit,
76                    active_connections: Arc::clone(&self.active_connections),
77                })
78            }
79            Err(_) => {
80                self.rejected_connections.fetch_add(1, Ordering::Relaxed);
81                None
82            }
83        }
84    }
85
86    /// Get a permit for processing a request. Blocks if at capacity.
87    /// Returns None if the request times out waiting for a slot.
88    pub async fn acquire_request_permit(&self) -> Option<tokio::sync::OwnedSemaphorePermit> {
89        tokio::time::timeout(
90            self.config.request_timeout,
91            self.request_semaphore.clone().acquire_owned(),
92        )
93        .await
94        .ok()
95        .and_then(|r| r.ok())
96    }
97
98    /// Get the idle timeout duration.
99    pub fn idle_timeout(&self) -> Duration {
100        self.config.idle_timeout
101    }
102
103    /// Get the request timeout duration.
104    pub fn request_timeout(&self) -> Duration {
105        self.config.request_timeout
106    }
107
108    /// Check if the server is shutting down.
109    pub fn is_shutting_down(&self) -> bool {
110        self.shutting_down.load(Ordering::Relaxed)
111    }
112
113    /// Initiate graceful shutdown.
114    pub fn initiate_shutdown(&self) {
115        self.shutting_down.store(true, Ordering::Relaxed);
116    }
117
118    /// Get current active connection count.
119    pub fn active_connections(&self) -> u64 {
120        self.active_connections.load(Ordering::Relaxed)
121    }
122
123    /// Get total rejected connections.
124    pub fn rejected_connections(&self) -> u64 {
125        self.rejected_connections.load(Ordering::Relaxed)
126    }
127
128    /// Get the drain timeout for graceful shutdown.
129    pub fn drain_timeout(&self) -> Duration {
130        self.config.drain_timeout
131    }
132
133    /// Wait until all connections are closed or drain timeout expires.
134    pub async fn wait_for_drain(&self) {
135        let start = tokio::time::Instant::now();
136        while self.active_connections() > 0 {
137            if start.elapsed() >= self.config.drain_timeout {
138                tracing::warn!(
139                    "Drain timeout expired with {} active connections",
140                    self.active_connections()
141                );
142                break;
143            }
144            tokio::time::sleep(Duration::from_millis(50)).await;
145        }
146    }
147}
148
149/// RAII guard that tracks an active connection. Decrements the counter on drop.
150pub struct ConnectionGuard {
151    _permit: tokio::sync::OwnedSemaphorePermit,
152    active_connections: Arc<AtomicU64>,
153}
154
155impl Drop for ConnectionGuard {
156    fn drop(&mut self) {
157        self.active_connections.fetch_sub(1, Ordering::Relaxed);
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn default_config() {
167        let config = ConnectionConfig::default();
168        assert_eq!(config.max_connections, 1024);
169        assert_eq!(config.max_concurrent_requests, 256);
170        assert_eq!(config.idle_timeout, Duration::from_secs(300));
171    }
172
173    #[test]
174    fn connection_limit_enforced() {
175        let config = ConnectionConfig {
176            max_connections: 2,
177            max_concurrent_requests: 10,
178            ..Default::default()
179        };
180        let mgr = ConnectionManager::new(config);
181
182        let _g1 = mgr.try_accept().expect("first connection should succeed");
183        let _g2 = mgr.try_accept().expect("second connection should succeed");
184        assert!(
185            mgr.try_accept().is_none(),
186            "third connection should be rejected"
187        );
188        assert_eq!(mgr.active_connections(), 2);
189        assert_eq!(mgr.rejected_connections(), 1);
190    }
191
192    #[test]
193    fn connection_released_on_drop() {
194        let config = ConnectionConfig {
195            max_connections: 1,
196            ..Default::default()
197        };
198        let mgr = ConnectionManager::new(config);
199
200        {
201            let _g = mgr.try_accept().expect("should succeed");
202            assert_eq!(mgr.active_connections(), 1);
203        }
204        // Guard dropped, slot freed.
205        assert_eq!(mgr.active_connections(), 0);
206        let _g = mgr.try_accept().expect("should succeed after release");
207    }
208
209    #[test]
210    fn shutdown_rejects_new_connections() {
211        let mgr = ConnectionManager::new(ConnectionConfig::default());
212        assert!(!mgr.is_shutting_down());
213
214        mgr.initiate_shutdown();
215        assert!(mgr.is_shutting_down());
216        assert!(mgr.try_accept().is_none());
217    }
218
219    #[tokio::test]
220    async fn request_permit_works() {
221        let config = ConnectionConfig {
222            max_concurrent_requests: 2,
223            ..Default::default()
224        };
225        let mgr = ConnectionManager::new(config);
226
227        let _p1 = mgr
228            .acquire_request_permit()
229            .await
230            .expect("should get permit");
231        let _p2 = mgr
232            .acquire_request_permit()
233            .await
234            .expect("should get permit");
235        // Third should timeout quickly in test but we don't want to wait long
236    }
237
238    #[tokio::test]
239    async fn drain_completes_when_no_connections() {
240        let mgr = ConnectionManager::new(ConnectionConfig {
241            drain_timeout: Duration::from_millis(100),
242            ..Default::default()
243        });
244        // No active connections, drain should complete immediately.
245        mgr.wait_for_drain().await;
246    }
247}