Skip to main content

ipfrs_network/
connection_drainer.rs

1//! Graceful connection draining for orderly node shutdown.
2//!
3//! When a node is shutting down, it needs to stop accepting new requests while
4//! allowing in-flight requests to complete. The [`ConnectionDrainer`] manages
5//! this lifecycle for all active connections, tracking pending request counts,
6//! enforcing drain timeouts, and reporting statistics.
7
8use std::collections::HashMap;
9
10/// The drain state of a connection.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum DrainState {
13    /// Connection is actively accepting requests.
14    Active,
15    /// Connection is draining — no new requests accepted, waiting for pending to finish.
16    Draining,
17    /// Connection has been fully drained (pending requests == 0 or timed out).
18    Drained,
19}
20
21/// A connection that participates in the draining lifecycle.
22#[derive(Debug, Clone)]
23pub struct DrainableConnection {
24    /// Unique identifier for this connection.
25    pub conn_id: u64,
26    /// The remote peer identifier.
27    pub peer_id: String,
28    /// Number of requests still in flight on this connection.
29    pub pending_requests: u32,
30    /// Current drain state.
31    pub state: DrainState,
32    /// Timestamp (ms) when draining started, if applicable.
33    pub drain_started_at: Option<u64>,
34}
35
36/// Configuration for the [`ConnectionDrainer`].
37#[derive(Debug, Clone)]
38pub struct DrainerConfig {
39    /// Per-connection timeout (ms) after which a draining connection is force-drained.
40    pub drain_timeout_ms: u64,
41    /// Maximum total time (ms) to wait for all connections to drain.
42    pub max_drain_wait_ms: u64,
43    /// Whether to reject new requests on draining connections.
44    pub reject_new_requests: bool,
45    /// Delay (ms) before closing a fully drained connection.
46    pub graceful_close_delay_ms: u64,
47}
48
49impl Default for DrainerConfig {
50    fn default() -> Self {
51        Self {
52            drain_timeout_ms: 30_000,
53            max_drain_wait_ms: 60_000,
54            reject_new_requests: true,
55            graceful_close_delay_ms: 500,
56        }
57    }
58}
59
60/// Aggregate statistics for the drainer.
61#[derive(Debug, Clone, Default)]
62pub struct DrainerStats {
63    /// Total connections that completed draining gracefully.
64    pub total_drained: u64,
65    /// Total connections that were force-drained due to timeout.
66    pub total_timed_out: u64,
67    /// Total requests rejected because the connection was draining.
68    pub total_rejected: u64,
69    /// Average drain time in milliseconds (over gracefully drained connections).
70    pub avg_drain_time_ms: f64,
71    /// Number of connections currently in the Draining state.
72    pub currently_draining: u64,
73}
74
75/// Manages graceful connection draining for orderly node shutdown.
76///
77/// Tracks all registered connections, transitions them through the
78/// `Active → Draining → Drained` lifecycle, and collects statistics.
79pub struct ConnectionDrainer {
80    config: DrainerConfig,
81    connections: HashMap<u64, DrainableConnection>,
82    drain_order: Vec<u64>,
83    next_conn_id: u64,
84    stats: DrainerStats,
85    /// Sum of drain durations for computing the running average.
86    drain_duration_sum_ms: u64,
87}
88
89impl ConnectionDrainer {
90    /// Create a new drainer with the given configuration.
91    pub fn new(config: DrainerConfig) -> Self {
92        Self {
93            config,
94            connections: HashMap::new(),
95            drain_order: Vec::new(),
96            next_conn_id: 1,
97            stats: DrainerStats::default(),
98            drain_duration_sum_ms: 0,
99        }
100    }
101
102    /// Register a new active connection and return its unique id.
103    pub fn register_connection(&mut self, peer_id: &str) -> u64 {
104        let conn_id = self.next_conn_id;
105        self.next_conn_id = self.next_conn_id.wrapping_add(1);
106
107        let conn = DrainableConnection {
108            conn_id,
109            peer_id: peer_id.to_string(),
110            pending_requests: 0,
111            state: DrainState::Active,
112            drain_started_at: None,
113        };
114        self.connections.insert(conn_id, conn);
115        conn_id
116    }
117
118    /// Begin draining a single connection.
119    ///
120    /// Returns an error if the connection does not exist or is already
121    /// draining/drained.
122    pub fn start_drain(&mut self, conn_id: u64) -> Result<(), String> {
123        let conn = self
124            .connections
125            .get_mut(&conn_id)
126            .ok_or_else(|| format!("connection {conn_id} not found"))?;
127
128        match conn.state {
129            DrainState::Active => {
130                conn.state = DrainState::Draining;
131                // We use a placeholder timestamp; callers that need real wall-clock
132                // time should use `check_timeouts(now)`.
133                conn.drain_started_at = Some(0);
134                self.drain_order.push(conn_id);
135                self.stats.currently_draining += 1;
136
137                // If there are no pending requests, transition immediately.
138                if conn.pending_requests == 0 {
139                    conn.state = DrainState::Drained;
140                    self.stats.currently_draining = self.stats.currently_draining.saturating_sub(1);
141                    self.stats.total_drained += 1;
142                }
143                Ok(())
144            }
145            DrainState::Draining => Err(format!("connection {conn_id} is already draining")),
146            DrainState::Drained => Err(format!("connection {conn_id} is already drained")),
147        }
148    }
149
150    /// Begin draining every currently-active connection.
151    pub fn start_drain_all(&mut self) {
152        let active_ids: Vec<u64> = self
153            .connections
154            .iter()
155            .filter(|(_, c)| c.state == DrainState::Active)
156            .map(|(id, _)| *id)
157            .collect();
158
159        for id in active_ids {
160            // Errors here mean the connection is already draining/drained, which is fine.
161            let _ = self.start_drain(id);
162        }
163    }
164
165    /// Record the completion of one request on a connection.
166    ///
167    /// If the connection is draining and pending drops to zero it transitions
168    /// to `Drained`.
169    pub fn complete_request(&mut self, conn_id: u64) -> Result<(), String> {
170        let conn = self
171            .connections
172            .get_mut(&conn_id)
173            .ok_or_else(|| format!("connection {conn_id} not found"))?;
174
175        if conn.pending_requests == 0 {
176            return Err(format!("connection {conn_id} has no pending requests"));
177        }
178
179        conn.pending_requests -= 1;
180
181        if conn.state == DrainState::Draining && conn.pending_requests == 0 {
182            conn.state = DrainState::Drained;
183            self.stats.currently_draining = self.stats.currently_draining.saturating_sub(1);
184            self.stats.total_drained += 1;
185            // We don't know the real elapsed time without a clock; drain_started_at
186            // is set to 0 by start_drain and real timestamps come from check_timeouts.
187            if let Some(start) = conn.drain_started_at {
188                if start > 0 {
189                    // Approximate: caller should supply wall-clock via check_timeouts.
190                    self.drain_duration_sum_ms += 0; // placeholder
191                }
192            }
193        }
194
195        Ok(())
196    }
197
198    /// Add a new request to a connection.
199    ///
200    /// If the connection is draining and `reject_new_requests` is set, the
201    /// request is rejected and the rejection counter incremented.
202    pub fn add_request(&mut self, conn_id: u64) -> Result<(), String> {
203        let conn = self
204            .connections
205            .get_mut(&conn_id)
206            .ok_or_else(|| format!("connection {conn_id} not found"))?;
207
208        match conn.state {
209            DrainState::Active => {
210                conn.pending_requests += 1;
211                Ok(())
212            }
213            DrainState::Draining => {
214                if self.config.reject_new_requests {
215                    self.stats.total_rejected += 1;
216                    Err(format!(
217                        "connection {conn_id} is draining; new requests rejected"
218                    ))
219                } else {
220                    conn.pending_requests += 1;
221                    Ok(())
222                }
223            }
224            DrainState::Drained => {
225                self.stats.total_rejected += 1;
226                Err(format!("connection {conn_id} is already drained"))
227            }
228        }
229    }
230
231    /// Check whether a connection has been fully drained.
232    ///
233    /// Returns `Some(DrainState::Drained)` when pending requests reach zero
234    /// while draining, `Some(current_state)` otherwise, or `None` if the
235    /// connection does not exist.
236    pub fn check_drained(&mut self, conn_id: u64) -> Option<DrainState> {
237        let conn = self.connections.get_mut(&conn_id)?;
238
239        if conn.state == DrainState::Draining && conn.pending_requests == 0 {
240            conn.state = DrainState::Drained;
241            self.stats.currently_draining = self.stats.currently_draining.saturating_sub(1);
242            self.stats.total_drained += 1;
243        }
244
245        Some(conn.state.clone())
246    }
247
248    /// Force-drain connections whose drain timeout has elapsed.
249    ///
250    /// `now` is a monotonic timestamp in milliseconds. Returns the ids of
251    /// connections that were force-drained.
252    pub fn check_timeouts(&mut self, now: u64) -> Vec<u64> {
253        let timeout = self.config.drain_timeout_ms;
254        let timed_out: Vec<u64> = self
255            .connections
256            .iter()
257            .filter(|(_, c)| c.state == DrainState::Draining)
258            .filter(|(_, c)| {
259                c.drain_started_at
260                    .map(|s| now.saturating_sub(s) >= timeout)
261                    .unwrap_or(false)
262            })
263            .map(|(id, _)| *id)
264            .collect();
265
266        for id in &timed_out {
267            if let Some(conn) = self.connections.get_mut(id) {
268                conn.state = DrainState::Drained;
269                self.stats.currently_draining = self.stats.currently_draining.saturating_sub(1);
270                self.stats.total_timed_out += 1;
271
272                if let Some(start) = conn.drain_started_at {
273                    let elapsed = now.saturating_sub(start);
274                    self.drain_duration_sum_ms += elapsed;
275                    let total_completed = self.stats.total_drained + self.stats.total_timed_out;
276                    if total_completed > 0 {
277                        self.stats.avg_drain_time_ms =
278                            self.drain_duration_sum_ms as f64 / total_completed as f64;
279                    }
280                }
281            }
282        }
283
284        timed_out
285    }
286
287    /// Remove all connections in the `Drained` state and return how many were removed.
288    pub fn remove_drained(&mut self) -> usize {
289        let before = self.connections.len();
290        self.connections
291            .retain(|_, c| c.state != DrainState::Drained);
292        self.drain_order
293            .retain(|id| self.connections.contains_key(id));
294        before - self.connections.len()
295    }
296
297    /// Look up a connection by id.
298    pub fn get_connection(&self, conn_id: u64) -> Option<&DrainableConnection> {
299        self.connections.get(&conn_id)
300    }
301
302    /// Number of connections in the `Active` state.
303    pub fn active_count(&self) -> usize {
304        self.connections
305            .values()
306            .filter(|c| c.state == DrainState::Active)
307            .count()
308    }
309
310    /// Number of connections in the `Draining` state.
311    pub fn draining_count(&self) -> usize {
312        self.connections
313            .values()
314            .filter(|c| c.state == DrainState::Draining)
315            .count()
316    }
317
318    /// Returns `true` when there are no active or draining connections.
319    pub fn is_fully_drained(&self) -> bool {
320        self.connections
321            .values()
322            .all(|c| c.state == DrainState::Drained)
323    }
324
325    /// Reference to the current statistics.
326    pub fn stats(&self) -> &DrainerStats {
327        &self.stats
328    }
329
330    /// Set a real wall-clock start time on a draining connection.
331    ///
332    /// This is a helper so callers that use `start_drain` followed by
333    /// `check_timeouts(now)` can record accurate timestamps.
334    pub fn set_drain_start_time(&mut self, conn_id: u64, timestamp_ms: u64) {
335        if let Some(conn) = self.connections.get_mut(&conn_id) {
336            if conn.state == DrainState::Draining || conn.state == DrainState::Drained {
337                conn.drain_started_at = Some(timestamp_ms);
338            }
339        }
340    }
341}
342
343// ---------------------------------------------------------------------------
344// Tests
345// ---------------------------------------------------------------------------
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    fn default_config() -> DrainerConfig {
351        DrainerConfig::default()
352    }
353
354    // 1. Registration returns unique ids
355    #[test]
356    fn test_register_returns_unique_ids() {
357        let mut d = ConnectionDrainer::new(default_config());
358        let a = d.register_connection("peer-a");
359        let b = d.register_connection("peer-b");
360        assert_ne!(a, b);
361    }
362
363    // 2. Registered connection is Active
364    #[test]
365    fn test_registered_connection_is_active() {
366        let mut d = ConnectionDrainer::new(default_config());
367        let id = d.register_connection("peer-a");
368        let conn = d.get_connection(id).expect("should exist");
369        assert_eq!(conn.state, DrainState::Active);
370        assert_eq!(conn.pending_requests, 0);
371    }
372
373    // 3. Start drain on active connection
374    #[test]
375    fn test_start_drain_active() {
376        let mut d = ConnectionDrainer::new(default_config());
377        let id = d.register_connection("peer-a");
378        d.add_request(id).expect("add ok");
379        d.start_drain(id).expect("drain ok");
380        let conn = d.get_connection(id).expect("exists");
381        assert_eq!(conn.state, DrainState::Draining);
382    }
383
384    // 4. Start drain with zero pending transitions immediately to Drained
385    #[test]
386    fn test_start_drain_zero_pending_immediate() {
387        let mut d = ConnectionDrainer::new(default_config());
388        let id = d.register_connection("peer-a");
389        d.start_drain(id).expect("drain ok");
390        let conn = d.get_connection(id).expect("exists");
391        assert_eq!(conn.state, DrainState::Drained);
392    }
393
394    // 5. Duplicate drain attempt fails
395    #[test]
396    fn test_duplicate_drain_fails() {
397        let mut d = ConnectionDrainer::new(default_config());
398        let id = d.register_connection("peer-a");
399        d.add_request(id).expect("add ok");
400        d.start_drain(id).expect("first ok");
401        assert!(d.start_drain(id).is_err());
402    }
403
404    // 6. Drain on nonexistent connection
405    #[test]
406    fn test_drain_nonexistent() {
407        let mut d = ConnectionDrainer::new(default_config());
408        assert!(d.start_drain(999).is_err());
409    }
410
411    // 7. drain_all drains all active connections
412    #[test]
413    fn test_start_drain_all() {
414        let mut d = ConnectionDrainer::new(default_config());
415        let a = d.register_connection("peer-a");
416        let b = d.register_connection("peer-b");
417        d.add_request(a).expect("ok");
418        d.add_request(b).expect("ok");
419        d.start_drain_all();
420        assert_eq!(d.active_count(), 0);
421        assert_eq!(d.draining_count(), 2);
422    }
423
424    // 8. Request rejection during drain
425    #[test]
426    fn test_reject_request_during_drain() {
427        let mut d = ConnectionDrainer::new(default_config());
428        let id = d.register_connection("peer-a");
429        d.add_request(id).expect("ok");
430        d.start_drain(id).expect("ok");
431        assert!(d.add_request(id).is_err());
432        assert_eq!(d.stats().total_rejected, 1);
433    }
434
435    // 9. Allow request during drain when reject_new_requests is false
436    #[test]
437    fn test_allow_request_during_drain_when_not_rejecting() {
438        let cfg = DrainerConfig {
439            reject_new_requests: false,
440            ..default_config()
441        };
442        let mut d = ConnectionDrainer::new(cfg);
443        let id = d.register_connection("peer-a");
444        d.add_request(id).expect("ok");
445        d.start_drain(id).expect("ok");
446        d.add_request(id).expect("should be allowed");
447        let conn = d.get_connection(id).expect("exists");
448        assert_eq!(conn.pending_requests, 2);
449    }
450
451    // 10. complete_request decrements pending
452    #[test]
453    fn test_complete_request_decrements() {
454        let mut d = ConnectionDrainer::new(default_config());
455        let id = d.register_connection("peer-a");
456        d.add_request(id).expect("ok");
457        d.add_request(id).expect("ok");
458        d.complete_request(id).expect("ok");
459        let conn = d.get_connection(id).expect("exists");
460        assert_eq!(conn.pending_requests, 1);
461    }
462
463    // 11. complete_request auto-drains when draining and pending hits 0
464    #[test]
465    fn test_complete_request_auto_drain() {
466        let mut d = ConnectionDrainer::new(default_config());
467        let id = d.register_connection("peer-a");
468        d.add_request(id).expect("ok");
469        d.start_drain(id).expect("ok");
470        d.complete_request(id).expect("ok");
471        let conn = d.get_connection(id).expect("exists");
472        assert_eq!(conn.state, DrainState::Drained);
473    }
474
475    // 12. complete_request on zero pending errors
476    #[test]
477    fn test_complete_request_zero_pending_error() {
478        let mut d = ConnectionDrainer::new(default_config());
479        let id = d.register_connection("peer-a");
480        assert!(d.complete_request(id).is_err());
481    }
482
483    // 13. complete_request on nonexistent connection
484    #[test]
485    fn test_complete_request_nonexistent() {
486        let mut d = ConnectionDrainer::new(default_config());
487        assert!(d.complete_request(999).is_err());
488    }
489
490    // 14. check_drained transitions draining to drained
491    #[test]
492    fn test_check_drained_transitions() {
493        let mut d = ConnectionDrainer::new(default_config());
494        let id = d.register_connection("peer-a");
495        d.add_request(id).expect("ok");
496        d.start_drain(id).expect("ok");
497        d.complete_request(id).expect("ok");
498        // pending is 0 but complete_request already transitioned it
499        let state = d.check_drained(id);
500        assert_eq!(state, Some(DrainState::Drained));
501    }
502
503    // 15. check_drained returns None for unknown id
504    #[test]
505    fn test_check_drained_unknown() {
506        let mut d = ConnectionDrainer::new(default_config());
507        assert!(d.check_drained(999).is_none());
508    }
509
510    // 16. Timeout handling
511    #[test]
512    fn test_check_timeouts() {
513        let cfg = DrainerConfig {
514            drain_timeout_ms: 100,
515            ..default_config()
516        };
517        let mut d = ConnectionDrainer::new(cfg);
518        let id = d.register_connection("peer-a");
519        d.add_request(id).expect("ok");
520        d.start_drain(id).expect("ok");
521        d.set_drain_start_time(id, 1000);
522
523        // Not timed out yet
524        let timed = d.check_timeouts(1050);
525        assert!(timed.is_empty());
526
527        // Timed out
528        let timed = d.check_timeouts(1100);
529        assert_eq!(timed, vec![id]);
530        let conn = d.get_connection(id).expect("exists");
531        assert_eq!(conn.state, DrainState::Drained);
532        assert_eq!(d.stats().total_timed_out, 1);
533    }
534
535    // 17. remove_drained removes only drained connections
536    #[test]
537    fn test_remove_drained() {
538        let mut d = ConnectionDrainer::new(default_config());
539        let a = d.register_connection("peer-a");
540        let b = d.register_connection("peer-b");
541        d.start_drain(a).expect("ok"); // immediate drained (0 pending)
542        let removed = d.remove_drained();
543        assert_eq!(removed, 1);
544        assert!(d.get_connection(a).is_none());
545        assert!(d.get_connection(b).is_some());
546    }
547
548    // 18. active_count
549    #[test]
550    fn test_active_count() {
551        let mut d = ConnectionDrainer::new(default_config());
552        d.register_connection("a");
553        d.register_connection("b");
554        assert_eq!(d.active_count(), 2);
555    }
556
557    // 19. draining_count
558    #[test]
559    fn test_draining_count() {
560        let mut d = ConnectionDrainer::new(default_config());
561        let a = d.register_connection("a");
562        let b = d.register_connection("b");
563        d.add_request(a).expect("ok");
564        d.add_request(b).expect("ok");
565        d.start_drain(a).expect("ok");
566        assert_eq!(d.draining_count(), 1);
567    }
568
569    // 20. is_fully_drained on empty drainer
570    #[test]
571    fn test_is_fully_drained_empty() {
572        let d = ConnectionDrainer::new(default_config());
573        assert!(d.is_fully_drained());
574    }
575
576    // 21. is_fully_drained with active connections
577    #[test]
578    fn test_is_fully_drained_with_active() {
579        let mut d = ConnectionDrainer::new(default_config());
580        d.register_connection("a");
581        assert!(!d.is_fully_drained());
582    }
583
584    // 22. is_fully_drained after all drained
585    #[test]
586    fn test_is_fully_drained_all_drained() {
587        let mut d = ConnectionDrainer::new(default_config());
588        let a = d.register_connection("a");
589        let b = d.register_connection("b");
590        d.start_drain(a).expect("ok");
591        d.start_drain(b).expect("ok");
592        assert!(d.is_fully_drained());
593    }
594
595    // 23. Stats track total_drained
596    #[test]
597    fn test_stats_total_drained() {
598        let mut d = ConnectionDrainer::new(default_config());
599        let a = d.register_connection("a");
600        let b = d.register_connection("b");
601        d.start_drain(a).expect("ok");
602        d.start_drain(b).expect("ok");
603        assert_eq!(d.stats().total_drained, 2);
604    }
605
606    // 24. Stats currently_draining updates
607    #[test]
608    fn test_stats_currently_draining() {
609        let mut d = ConnectionDrainer::new(default_config());
610        let a = d.register_connection("a");
611        d.add_request(a).expect("ok");
612        d.start_drain(a).expect("ok");
613        assert_eq!(d.stats().currently_draining, 1);
614        d.complete_request(a).expect("ok");
615        assert_eq!(d.stats().currently_draining, 0);
616    }
617
618    // 25. avg_drain_time computed on timeout
619    #[test]
620    fn test_avg_drain_time_on_timeout() {
621        let cfg = DrainerConfig {
622            drain_timeout_ms: 50,
623            ..default_config()
624        };
625        let mut d = ConnectionDrainer::new(cfg);
626        let id = d.register_connection("a");
627        d.add_request(id).expect("ok");
628        d.start_drain(id).expect("ok");
629        d.set_drain_start_time(id, 100);
630        d.check_timeouts(200);
631        // elapsed = 100ms
632        assert!(d.stats().avg_drain_time_ms > 0.0);
633    }
634
635    // 26. Peer id is stored correctly
636    #[test]
637    fn test_peer_id_stored() {
638        let mut d = ConnectionDrainer::new(default_config());
639        let id = d.register_connection("QmPeer123");
640        let conn = d.get_connection(id).expect("exists");
641        assert_eq!(conn.peer_id, "QmPeer123");
642    }
643
644    // 27. Multiple requests then drain lifecycle
645    #[test]
646    fn test_full_lifecycle() {
647        let mut d = ConnectionDrainer::new(default_config());
648        let id = d.register_connection("peer");
649        d.add_request(id).expect("ok");
650        d.add_request(id).expect("ok");
651        d.add_request(id).expect("ok");
652        d.start_drain(id).expect("ok");
653        assert_eq!(d.draining_count(), 1);
654
655        d.complete_request(id).expect("ok");
656        d.complete_request(id).expect("ok");
657        assert_eq!(d.get_connection(id).expect("e").state, DrainState::Draining);
658
659        d.complete_request(id).expect("ok");
660        assert_eq!(d.get_connection(id).expect("e").state, DrainState::Drained);
661        assert!(d.is_fully_drained());
662
663        let removed = d.remove_drained();
664        assert_eq!(removed, 1);
665        assert!(d.get_connection(id).is_none());
666    }
667
668    // 28. add_request on drained connection is rejected
669    #[test]
670    fn test_add_request_on_drained() {
671        let mut d = ConnectionDrainer::new(default_config());
672        let id = d.register_connection("peer");
673        d.start_drain(id).expect("ok");
674        assert!(d.add_request(id).is_err());
675        assert_eq!(d.stats().total_rejected, 1);
676    }
677
678    // 29. add_request on nonexistent connection
679    #[test]
680    fn test_add_request_nonexistent() {
681        let mut d = ConnectionDrainer::new(default_config());
682        assert!(d.add_request(999).is_err());
683    }
684
685    // 30. drain_all is idempotent (already draining connections ignored)
686    #[test]
687    fn test_drain_all_idempotent() {
688        let mut d = ConnectionDrainer::new(default_config());
689        let a = d.register_connection("a");
690        d.add_request(a).expect("ok");
691        d.start_drain(a).expect("ok");
692        // calling drain_all should not error even though `a` is already draining
693        d.start_drain_all();
694        assert_eq!(d.draining_count(), 1);
695    }
696
697    // 31. check_timeouts with no draining connections returns empty
698    #[test]
699    fn test_check_timeouts_empty() {
700        let mut d = ConnectionDrainer::new(default_config());
701        d.register_connection("a");
702        let timed = d.check_timeouts(999_999);
703        assert!(timed.is_empty());
704    }
705
706    // 32. remove_drained on empty drainer
707    #[test]
708    fn test_remove_drained_empty() {
709        let mut d = ConnectionDrainer::new(default_config());
710        assert_eq!(d.remove_drained(), 0);
711    }
712
713    // 33. Default config values
714    #[test]
715    fn test_default_config() {
716        let cfg = DrainerConfig::default();
717        assert_eq!(cfg.drain_timeout_ms, 30_000);
718        assert_eq!(cfg.max_drain_wait_ms, 60_000);
719        assert!(cfg.reject_new_requests);
720        assert_eq!(cfg.graceful_close_delay_ms, 500);
721    }
722
723    // 34. DrainerStats default
724    #[test]
725    fn test_drainer_stats_default() {
726        let s = DrainerStats::default();
727        assert_eq!(s.total_drained, 0);
728        assert_eq!(s.total_timed_out, 0);
729        assert_eq!(s.total_rejected, 0);
730        assert_eq!(s.currently_draining, 0);
731        assert!((s.avg_drain_time_ms - 0.0).abs() < f64::EPSILON);
732    }
733
734    // 35. set_drain_start_time on active connection is no-op
735    #[test]
736    fn test_set_drain_start_time_active_noop() {
737        let mut d = ConnectionDrainer::new(default_config());
738        let id = d.register_connection("peer");
739        d.set_drain_start_time(id, 12345);
740        // Should remain None because connection is Active
741        let conn = d.get_connection(id).expect("exists");
742        assert!(conn.drain_started_at.is_none());
743    }
744}