use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DrainState {
Active,
Draining,
Drained,
}
#[derive(Debug, Clone)]
pub struct DrainableConnection {
pub conn_id: u64,
pub peer_id: String,
pub pending_requests: u32,
pub state: DrainState,
pub drain_started_at: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct DrainerConfig {
pub drain_timeout_ms: u64,
pub max_drain_wait_ms: u64,
pub reject_new_requests: bool,
pub graceful_close_delay_ms: u64,
}
impl Default for DrainerConfig {
fn default() -> Self {
Self {
drain_timeout_ms: 30_000,
max_drain_wait_ms: 60_000,
reject_new_requests: true,
graceful_close_delay_ms: 500,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct DrainerStats {
pub total_drained: u64,
pub total_timed_out: u64,
pub total_rejected: u64,
pub avg_drain_time_ms: f64,
pub currently_draining: u64,
}
pub struct ConnectionDrainer {
config: DrainerConfig,
connections: HashMap<u64, DrainableConnection>,
drain_order: Vec<u64>,
next_conn_id: u64,
stats: DrainerStats,
drain_duration_sum_ms: u64,
}
impl ConnectionDrainer {
pub fn new(config: DrainerConfig) -> Self {
Self {
config,
connections: HashMap::new(),
drain_order: Vec::new(),
next_conn_id: 1,
stats: DrainerStats::default(),
drain_duration_sum_ms: 0,
}
}
pub fn register_connection(&mut self, peer_id: &str) -> u64 {
let conn_id = self.next_conn_id;
self.next_conn_id = self.next_conn_id.wrapping_add(1);
let conn = DrainableConnection {
conn_id,
peer_id: peer_id.to_string(),
pending_requests: 0,
state: DrainState::Active,
drain_started_at: None,
};
self.connections.insert(conn_id, conn);
conn_id
}
pub fn start_drain(&mut self, conn_id: u64) -> Result<(), String> {
let conn = self
.connections
.get_mut(&conn_id)
.ok_or_else(|| format!("connection {conn_id} not found"))?;
match conn.state {
DrainState::Active => {
conn.state = DrainState::Draining;
conn.drain_started_at = Some(0);
self.drain_order.push(conn_id);
self.stats.currently_draining += 1;
if conn.pending_requests == 0 {
conn.state = DrainState::Drained;
self.stats.currently_draining = self.stats.currently_draining.saturating_sub(1);
self.stats.total_drained += 1;
}
Ok(())
}
DrainState::Draining => Err(format!("connection {conn_id} is already draining")),
DrainState::Drained => Err(format!("connection {conn_id} is already drained")),
}
}
pub fn start_drain_all(&mut self) {
let active_ids: Vec<u64> = self
.connections
.iter()
.filter(|(_, c)| c.state == DrainState::Active)
.map(|(id, _)| *id)
.collect();
for id in active_ids {
let _ = self.start_drain(id);
}
}
pub fn complete_request(&mut self, conn_id: u64) -> Result<(), String> {
let conn = self
.connections
.get_mut(&conn_id)
.ok_or_else(|| format!("connection {conn_id} not found"))?;
if conn.pending_requests == 0 {
return Err(format!("connection {conn_id} has no pending requests"));
}
conn.pending_requests -= 1;
if conn.state == DrainState::Draining && conn.pending_requests == 0 {
conn.state = DrainState::Drained;
self.stats.currently_draining = self.stats.currently_draining.saturating_sub(1);
self.stats.total_drained += 1;
if let Some(start) = conn.drain_started_at {
if start > 0 {
self.drain_duration_sum_ms += 0; }
}
}
Ok(())
}
pub fn add_request(&mut self, conn_id: u64) -> Result<(), String> {
let conn = self
.connections
.get_mut(&conn_id)
.ok_or_else(|| format!("connection {conn_id} not found"))?;
match conn.state {
DrainState::Active => {
conn.pending_requests += 1;
Ok(())
}
DrainState::Draining => {
if self.config.reject_new_requests {
self.stats.total_rejected += 1;
Err(format!(
"connection {conn_id} is draining; new requests rejected"
))
} else {
conn.pending_requests += 1;
Ok(())
}
}
DrainState::Drained => {
self.stats.total_rejected += 1;
Err(format!("connection {conn_id} is already drained"))
}
}
}
pub fn check_drained(&mut self, conn_id: u64) -> Option<DrainState> {
let conn = self.connections.get_mut(&conn_id)?;
if conn.state == DrainState::Draining && conn.pending_requests == 0 {
conn.state = DrainState::Drained;
self.stats.currently_draining = self.stats.currently_draining.saturating_sub(1);
self.stats.total_drained += 1;
}
Some(conn.state.clone())
}
pub fn check_timeouts(&mut self, now: u64) -> Vec<u64> {
let timeout = self.config.drain_timeout_ms;
let timed_out: Vec<u64> = self
.connections
.iter()
.filter(|(_, c)| c.state == DrainState::Draining)
.filter(|(_, c)| {
c.drain_started_at
.map(|s| now.saturating_sub(s) >= timeout)
.unwrap_or(false)
})
.map(|(id, _)| *id)
.collect();
for id in &timed_out {
if let Some(conn) = self.connections.get_mut(id) {
conn.state = DrainState::Drained;
self.stats.currently_draining = self.stats.currently_draining.saturating_sub(1);
self.stats.total_timed_out += 1;
if let Some(start) = conn.drain_started_at {
let elapsed = now.saturating_sub(start);
self.drain_duration_sum_ms += elapsed;
let total_completed = self.stats.total_drained + self.stats.total_timed_out;
if total_completed > 0 {
self.stats.avg_drain_time_ms =
self.drain_duration_sum_ms as f64 / total_completed as f64;
}
}
}
}
timed_out
}
pub fn remove_drained(&mut self) -> usize {
let before = self.connections.len();
self.connections
.retain(|_, c| c.state != DrainState::Drained);
self.drain_order
.retain(|id| self.connections.contains_key(id));
before - self.connections.len()
}
pub fn get_connection(&self, conn_id: u64) -> Option<&DrainableConnection> {
self.connections.get(&conn_id)
}
pub fn active_count(&self) -> usize {
self.connections
.values()
.filter(|c| c.state == DrainState::Active)
.count()
}
pub fn draining_count(&self) -> usize {
self.connections
.values()
.filter(|c| c.state == DrainState::Draining)
.count()
}
pub fn is_fully_drained(&self) -> bool {
self.connections
.values()
.all(|c| c.state == DrainState::Drained)
}
pub fn stats(&self) -> &DrainerStats {
&self.stats
}
pub fn set_drain_start_time(&mut self, conn_id: u64, timestamp_ms: u64) {
if let Some(conn) = self.connections.get_mut(&conn_id) {
if conn.state == DrainState::Draining || conn.state == DrainState::Drained {
conn.drain_started_at = Some(timestamp_ms);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_config() -> DrainerConfig {
DrainerConfig::default()
}
#[test]
fn test_register_returns_unique_ids() {
let mut d = ConnectionDrainer::new(default_config());
let a = d.register_connection("peer-a");
let b = d.register_connection("peer-b");
assert_ne!(a, b);
}
#[test]
fn test_registered_connection_is_active() {
let mut d = ConnectionDrainer::new(default_config());
let id = d.register_connection("peer-a");
let conn = d.get_connection(id).expect("should exist");
assert_eq!(conn.state, DrainState::Active);
assert_eq!(conn.pending_requests, 0);
}
#[test]
fn test_start_drain_active() {
let mut d = ConnectionDrainer::new(default_config());
let id = d.register_connection("peer-a");
d.add_request(id).expect("add ok");
d.start_drain(id).expect("drain ok");
let conn = d.get_connection(id).expect("exists");
assert_eq!(conn.state, DrainState::Draining);
}
#[test]
fn test_start_drain_zero_pending_immediate() {
let mut d = ConnectionDrainer::new(default_config());
let id = d.register_connection("peer-a");
d.start_drain(id).expect("drain ok");
let conn = d.get_connection(id).expect("exists");
assert_eq!(conn.state, DrainState::Drained);
}
#[test]
fn test_duplicate_drain_fails() {
let mut d = ConnectionDrainer::new(default_config());
let id = d.register_connection("peer-a");
d.add_request(id).expect("add ok");
d.start_drain(id).expect("first ok");
assert!(d.start_drain(id).is_err());
}
#[test]
fn test_drain_nonexistent() {
let mut d = ConnectionDrainer::new(default_config());
assert!(d.start_drain(999).is_err());
}
#[test]
fn test_start_drain_all() {
let mut d = ConnectionDrainer::new(default_config());
let a = d.register_connection("peer-a");
let b = d.register_connection("peer-b");
d.add_request(a).expect("ok");
d.add_request(b).expect("ok");
d.start_drain_all();
assert_eq!(d.active_count(), 0);
assert_eq!(d.draining_count(), 2);
}
#[test]
fn test_reject_request_during_drain() {
let mut d = ConnectionDrainer::new(default_config());
let id = d.register_connection("peer-a");
d.add_request(id).expect("ok");
d.start_drain(id).expect("ok");
assert!(d.add_request(id).is_err());
assert_eq!(d.stats().total_rejected, 1);
}
#[test]
fn test_allow_request_during_drain_when_not_rejecting() {
let cfg = DrainerConfig {
reject_new_requests: false,
..default_config()
};
let mut d = ConnectionDrainer::new(cfg);
let id = d.register_connection("peer-a");
d.add_request(id).expect("ok");
d.start_drain(id).expect("ok");
d.add_request(id).expect("should be allowed");
let conn = d.get_connection(id).expect("exists");
assert_eq!(conn.pending_requests, 2);
}
#[test]
fn test_complete_request_decrements() {
let mut d = ConnectionDrainer::new(default_config());
let id = d.register_connection("peer-a");
d.add_request(id).expect("ok");
d.add_request(id).expect("ok");
d.complete_request(id).expect("ok");
let conn = d.get_connection(id).expect("exists");
assert_eq!(conn.pending_requests, 1);
}
#[test]
fn test_complete_request_auto_drain() {
let mut d = ConnectionDrainer::new(default_config());
let id = d.register_connection("peer-a");
d.add_request(id).expect("ok");
d.start_drain(id).expect("ok");
d.complete_request(id).expect("ok");
let conn = d.get_connection(id).expect("exists");
assert_eq!(conn.state, DrainState::Drained);
}
#[test]
fn test_complete_request_zero_pending_error() {
let mut d = ConnectionDrainer::new(default_config());
let id = d.register_connection("peer-a");
assert!(d.complete_request(id).is_err());
}
#[test]
fn test_complete_request_nonexistent() {
let mut d = ConnectionDrainer::new(default_config());
assert!(d.complete_request(999).is_err());
}
#[test]
fn test_check_drained_transitions() {
let mut d = ConnectionDrainer::new(default_config());
let id = d.register_connection("peer-a");
d.add_request(id).expect("ok");
d.start_drain(id).expect("ok");
d.complete_request(id).expect("ok");
let state = d.check_drained(id);
assert_eq!(state, Some(DrainState::Drained));
}
#[test]
fn test_check_drained_unknown() {
let mut d = ConnectionDrainer::new(default_config());
assert!(d.check_drained(999).is_none());
}
#[test]
fn test_check_timeouts() {
let cfg = DrainerConfig {
drain_timeout_ms: 100,
..default_config()
};
let mut d = ConnectionDrainer::new(cfg);
let id = d.register_connection("peer-a");
d.add_request(id).expect("ok");
d.start_drain(id).expect("ok");
d.set_drain_start_time(id, 1000);
let timed = d.check_timeouts(1050);
assert!(timed.is_empty());
let timed = d.check_timeouts(1100);
assert_eq!(timed, vec![id]);
let conn = d.get_connection(id).expect("exists");
assert_eq!(conn.state, DrainState::Drained);
assert_eq!(d.stats().total_timed_out, 1);
}
#[test]
fn test_remove_drained() {
let mut d = ConnectionDrainer::new(default_config());
let a = d.register_connection("peer-a");
let b = d.register_connection("peer-b");
d.start_drain(a).expect("ok"); let removed = d.remove_drained();
assert_eq!(removed, 1);
assert!(d.get_connection(a).is_none());
assert!(d.get_connection(b).is_some());
}
#[test]
fn test_active_count() {
let mut d = ConnectionDrainer::new(default_config());
d.register_connection("a");
d.register_connection("b");
assert_eq!(d.active_count(), 2);
}
#[test]
fn test_draining_count() {
let mut d = ConnectionDrainer::new(default_config());
let a = d.register_connection("a");
let b = d.register_connection("b");
d.add_request(a).expect("ok");
d.add_request(b).expect("ok");
d.start_drain(a).expect("ok");
assert_eq!(d.draining_count(), 1);
}
#[test]
fn test_is_fully_drained_empty() {
let d = ConnectionDrainer::new(default_config());
assert!(d.is_fully_drained());
}
#[test]
fn test_is_fully_drained_with_active() {
let mut d = ConnectionDrainer::new(default_config());
d.register_connection("a");
assert!(!d.is_fully_drained());
}
#[test]
fn test_is_fully_drained_all_drained() {
let mut d = ConnectionDrainer::new(default_config());
let a = d.register_connection("a");
let b = d.register_connection("b");
d.start_drain(a).expect("ok");
d.start_drain(b).expect("ok");
assert!(d.is_fully_drained());
}
#[test]
fn test_stats_total_drained() {
let mut d = ConnectionDrainer::new(default_config());
let a = d.register_connection("a");
let b = d.register_connection("b");
d.start_drain(a).expect("ok");
d.start_drain(b).expect("ok");
assert_eq!(d.stats().total_drained, 2);
}
#[test]
fn test_stats_currently_draining() {
let mut d = ConnectionDrainer::new(default_config());
let a = d.register_connection("a");
d.add_request(a).expect("ok");
d.start_drain(a).expect("ok");
assert_eq!(d.stats().currently_draining, 1);
d.complete_request(a).expect("ok");
assert_eq!(d.stats().currently_draining, 0);
}
#[test]
fn test_avg_drain_time_on_timeout() {
let cfg = DrainerConfig {
drain_timeout_ms: 50,
..default_config()
};
let mut d = ConnectionDrainer::new(cfg);
let id = d.register_connection("a");
d.add_request(id).expect("ok");
d.start_drain(id).expect("ok");
d.set_drain_start_time(id, 100);
d.check_timeouts(200);
assert!(d.stats().avg_drain_time_ms > 0.0);
}
#[test]
fn test_peer_id_stored() {
let mut d = ConnectionDrainer::new(default_config());
let id = d.register_connection("QmPeer123");
let conn = d.get_connection(id).expect("exists");
assert_eq!(conn.peer_id, "QmPeer123");
}
#[test]
fn test_full_lifecycle() {
let mut d = ConnectionDrainer::new(default_config());
let id = d.register_connection("peer");
d.add_request(id).expect("ok");
d.add_request(id).expect("ok");
d.add_request(id).expect("ok");
d.start_drain(id).expect("ok");
assert_eq!(d.draining_count(), 1);
d.complete_request(id).expect("ok");
d.complete_request(id).expect("ok");
assert_eq!(d.get_connection(id).expect("e").state, DrainState::Draining);
d.complete_request(id).expect("ok");
assert_eq!(d.get_connection(id).expect("e").state, DrainState::Drained);
assert!(d.is_fully_drained());
let removed = d.remove_drained();
assert_eq!(removed, 1);
assert!(d.get_connection(id).is_none());
}
#[test]
fn test_add_request_on_drained() {
let mut d = ConnectionDrainer::new(default_config());
let id = d.register_connection("peer");
d.start_drain(id).expect("ok");
assert!(d.add_request(id).is_err());
assert_eq!(d.stats().total_rejected, 1);
}
#[test]
fn test_add_request_nonexistent() {
let mut d = ConnectionDrainer::new(default_config());
assert!(d.add_request(999).is_err());
}
#[test]
fn test_drain_all_idempotent() {
let mut d = ConnectionDrainer::new(default_config());
let a = d.register_connection("a");
d.add_request(a).expect("ok");
d.start_drain(a).expect("ok");
d.start_drain_all();
assert_eq!(d.draining_count(), 1);
}
#[test]
fn test_check_timeouts_empty() {
let mut d = ConnectionDrainer::new(default_config());
d.register_connection("a");
let timed = d.check_timeouts(999_999);
assert!(timed.is_empty());
}
#[test]
fn test_remove_drained_empty() {
let mut d = ConnectionDrainer::new(default_config());
assert_eq!(d.remove_drained(), 0);
}
#[test]
fn test_default_config() {
let cfg = DrainerConfig::default();
assert_eq!(cfg.drain_timeout_ms, 30_000);
assert_eq!(cfg.max_drain_wait_ms, 60_000);
assert!(cfg.reject_new_requests);
assert_eq!(cfg.graceful_close_delay_ms, 500);
}
#[test]
fn test_drainer_stats_default() {
let s = DrainerStats::default();
assert_eq!(s.total_drained, 0);
assert_eq!(s.total_timed_out, 0);
assert_eq!(s.total_rejected, 0);
assert_eq!(s.currently_draining, 0);
assert!((s.avg_drain_time_ms - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_set_drain_start_time_active_noop() {
let mut d = ConnectionDrainer::new(default_config());
let id = d.register_connection("peer");
d.set_drain_start_time(id, 12345);
let conn = d.get_connection(id).expect("exists");
assert!(conn.drain_started_at.is_none());
}
}