156 lines
5.5 KiB
Rust
156 lines
5.5 KiB
Rust
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
use std::time::{Duration, Instant};
|
|
|
|
/// Per-connection tracking record with atomics for lock-free updates.
|
|
///
|
|
/// Each field uses atomics so that the forwarding tasks can update
|
|
/// bytes_received / bytes_sent / last_activity without holding any lock,
|
|
/// while the zombie scanner reads them concurrently.
|
|
pub struct ConnectionRecord {
|
|
/// Unique connection ID assigned by the ConnectionTracker.
|
|
pub id: u64,
|
|
/// Wall-clock instant when this connection was created.
|
|
pub created_at: Instant,
|
|
/// Milliseconds since `created_at` when the last activity occurred.
|
|
/// Updated atomically by the forwarding loops.
|
|
pub last_activity: AtomicU64,
|
|
/// Total bytes received from the client (inbound).
|
|
pub bytes_received: AtomicU64,
|
|
/// Total bytes sent to the client (outbound / from backend).
|
|
pub bytes_sent: AtomicU64,
|
|
/// True once the client side of the connection has closed.
|
|
pub client_closed: AtomicBool,
|
|
/// True once the backend side of the connection has closed.
|
|
pub backend_closed: AtomicBool,
|
|
/// Whether this connection uses TLS (affects zombie thresholds).
|
|
pub is_tls: AtomicBool,
|
|
/// Whether this connection has keep-alive semantics.
|
|
pub has_keep_alive: AtomicBool,
|
|
}
|
|
|
|
impl ConnectionRecord {
|
|
/// Create a new connection record with the given ID.
|
|
/// All counters start at zero, all flags start as false.
|
|
pub fn new(id: u64) -> Self {
|
|
Self {
|
|
id,
|
|
created_at: Instant::now(),
|
|
last_activity: AtomicU64::new(0),
|
|
bytes_received: AtomicU64::new(0),
|
|
bytes_sent: AtomicU64::new(0),
|
|
client_closed: AtomicBool::new(false),
|
|
backend_closed: AtomicBool::new(false),
|
|
is_tls: AtomicBool::new(false),
|
|
has_keep_alive: AtomicBool::new(false),
|
|
}
|
|
}
|
|
|
|
/// Update `last_activity` to reflect the current elapsed time.
|
|
pub fn touch(&self) {
|
|
let elapsed_ms = self.created_at.elapsed().as_millis() as u64;
|
|
self.last_activity.store(elapsed_ms, Ordering::Relaxed);
|
|
}
|
|
|
|
/// Record `n` bytes received from the client (inbound).
|
|
pub fn record_bytes_in(&self, n: u64) {
|
|
self.bytes_received.fetch_add(n, Ordering::Relaxed);
|
|
self.touch();
|
|
}
|
|
|
|
/// Record `n` bytes sent to the client (outbound / from backend).
|
|
pub fn record_bytes_out(&self, n: u64) {
|
|
self.bytes_sent.fetch_add(n, Ordering::Relaxed);
|
|
self.touch();
|
|
}
|
|
|
|
/// How long since the last activity on this connection.
|
|
pub fn idle_duration(&self) -> Duration {
|
|
let last_ms = self.last_activity.load(Ordering::Relaxed);
|
|
let age_ms = self.created_at.elapsed().as_millis() as u64;
|
|
Duration::from_millis(age_ms.saturating_sub(last_ms))
|
|
}
|
|
|
|
/// Total age of this connection (time since creation).
|
|
pub fn age(&self) -> Duration {
|
|
self.created_at.elapsed()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::thread;
|
|
|
|
#[test]
|
|
fn test_new_record() {
|
|
let record = ConnectionRecord::new(42);
|
|
assert_eq!(record.id, 42);
|
|
assert_eq!(record.bytes_received.load(Ordering::Relaxed), 0);
|
|
assert_eq!(record.bytes_sent.load(Ordering::Relaxed), 0);
|
|
assert!(!record.client_closed.load(Ordering::Relaxed));
|
|
assert!(!record.backend_closed.load(Ordering::Relaxed));
|
|
assert!(!record.is_tls.load(Ordering::Relaxed));
|
|
assert!(!record.has_keep_alive.load(Ordering::Relaxed));
|
|
}
|
|
|
|
#[test]
|
|
fn test_record_bytes() {
|
|
let record = ConnectionRecord::new(1);
|
|
record.record_bytes_in(100);
|
|
record.record_bytes_in(200);
|
|
assert_eq!(record.bytes_received.load(Ordering::Relaxed), 300);
|
|
|
|
record.record_bytes_out(50);
|
|
record.record_bytes_out(75);
|
|
assert_eq!(record.bytes_sent.load(Ordering::Relaxed), 125);
|
|
}
|
|
|
|
#[test]
|
|
fn test_touch_updates_activity() {
|
|
let record = ConnectionRecord::new(1);
|
|
assert_eq!(record.last_activity.load(Ordering::Relaxed), 0);
|
|
|
|
// Sleep briefly so elapsed time is nonzero
|
|
thread::sleep(Duration::from_millis(10));
|
|
record.touch();
|
|
|
|
let activity = record.last_activity.load(Ordering::Relaxed);
|
|
assert!(activity >= 10, "last_activity should be at least 10ms, got {}", activity);
|
|
}
|
|
|
|
#[test]
|
|
fn test_idle_duration() {
|
|
let record = ConnectionRecord::new(1);
|
|
// Initially idle_duration ~ age since last_activity is 0
|
|
thread::sleep(Duration::from_millis(20));
|
|
let idle = record.idle_duration();
|
|
assert!(idle >= Duration::from_millis(20));
|
|
|
|
// After touch, idle should be near zero
|
|
record.touch();
|
|
let idle = record.idle_duration();
|
|
assert!(idle < Duration::from_millis(10));
|
|
}
|
|
|
|
#[test]
|
|
fn test_age() {
|
|
let record = ConnectionRecord::new(1);
|
|
thread::sleep(Duration::from_millis(20));
|
|
let age = record.age();
|
|
assert!(age >= Duration::from_millis(20));
|
|
}
|
|
|
|
#[test]
|
|
fn test_flags() {
|
|
let record = ConnectionRecord::new(1);
|
|
record.client_closed.store(true, Ordering::Relaxed);
|
|
record.is_tls.store(true, Ordering::Relaxed);
|
|
record.has_keep_alive.store(true, Ordering::Relaxed);
|
|
|
|
assert!(record.client_closed.load(Ordering::Relaxed));
|
|
assert!(!record.backend_closed.load(Ordering::Relaxed));
|
|
assert!(record.is_tls.load(Ordering::Relaxed));
|
|
assert!(record.has_keep_alive.load(Ordering::Relaxed));
|
|
}
|
|
}
|