From 1bfc40b2254ce57b632d2ffefaaf5e9a0e6147c5 Mon Sep 17 00:00:00 2001 From: cbaugus Date: Mon, 13 Apr 2026 20:08:01 -0500 Subject: [PATCH 1/4] feat: replace heuristic connection metrics with accurate port tracking (#119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the unreliable latency-based heuristic (likely_new/likely_reused) and replace with deterministic local TCP port tracking. Each response's local SocketAddr is extracted from hyper's HttpInfo — a new local port means a new TCP connection, same port means reused. Breaking metric changes: - Removed: connection_pool_likely_new_total - Removed: connection_pool_likely_reused_total - Added: connection_pool_new_total - Added: connection_pool_reused_total - connection_pool_reuse_rate_percent now uses accurate tracking - Removed: metricsReuseThresholdMs YAML field (no longer needed) Closes #119 Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.toml | 1 + docs/CONNECTION_POOL.md | 60 ++++------ src/config.rs | 35 ++---- src/connection_pool.rs | 252 +++++++++++++++++++--------------------- src/executor.rs | 9 +- src/main.rs | 10 +- src/metrics.rs | 14 +-- src/worker.rs | 11 +- src/yaml_config.rs | 6 - 9 files changed, 183 insertions(+), 215 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e7f4e59..90c6141 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] reqwest = { version = "0.12", features = ["json", "rustls-tls-native-roots", "cookies"], default-features = false } # Using rustls-tls-native-roots with cookie support +hyper-util = "0.1" # For HttpInfo — accurate connection tracking (Issue #119) tokio = { version = "1", features = ["full"] } # "full" includes everything you need for async main prometheus = "0.13" hyper = { version = "0.14", features = ["full"] } # For the HTTP server diff --git a/docs/CONNECTION_POOL.md b/docs/CONNECTION_POOL.md index fe12542..3aa07d2 100644 --- a/docs/CONNECTION_POOL.md +++ b/docs/CONNECTION_POOL.md @@ -52,14 +52,12 @@ config: pool: maxIdlePerHost: 32 idleTimeoutSecs: 30 - metricsReuseThresholdMs: 100 ``` -| Field | Default | Description | -|--------------------------|---------|--------------------------------------------------| -| `maxIdlePerHost` | `32` | Max idle connections per host. Set to `0` to disable pooling. | -| `idleTimeoutSecs` | `30` | Seconds before idle connections are closed. Set to `0` to close immediately. | -| `metricsReuseThresholdMs`| `100` | Latency threshold (ms) for the Prometheus metrics heuristic. Does **not** affect actual connection behavior — only how metrics classify requests as "new" vs "reused". | +| Field | Default | Description | +|------------------|---------|----------------------------------------------------------------------| +| `maxIdlePerHost` | `32` | Max idle connections per host. Set to `0` to disable pooling. | +| `idleTimeoutSecs`| `30` | Seconds before idle connections are closed. Set to `0` to close immediately. | ## Use Case: Force New Connection Per Request @@ -180,41 +178,29 @@ will transparently open a new connection when this happens. ## Monitoring Connection Reuse -Prometheus metrics are available on port 9090: - -| Metric | Type | Description | -|-----------------------------------------|------------|------------------------------------------| -| `connection_pool_likely_new_total` | Counter | Requests classified as new connections | -| `connection_pool_likely_reused_total` | Counter | Requests classified as reused connections| -| `connection_pool_reuse_rate_percent` | Gauge | Current reuse percentage | -| `connection_pool_requests_total` | Counter | Total requests tracked | -| `connection_pool_max_idle_per_host` | Gauge | Configured max idle setting | -| `connection_pool_idle_timeout_seconds` | Gauge | Configured idle timeout setting | - -### Important: Metrics Are Heuristic-Based - -The "new" vs "reused" classification uses a **latency heuristic**, not actual -connection state (reqwest does not expose this). Requests slower than -`metricsReuseThresholdMs` (default: 100ms) are classified as "likely new -connection" because a TLS handshake typically adds 50-150ms. - -This means: - -- Fast targets where TLS completes in <100ms will **undercount** new connections -- Slow targets where reused requests take >100ms will **overcount** new connections - -Tune `metricsReuseThresholdMs` in the YAML to match your target's typical TLS -handshake time for more accurate classification. For definitive connection -tracking, check server-side access logs. +Prometheus metrics are available on port 9090. Connection tracking uses +**local TCP port comparison** — each response's local socket address is +checked. A new local port means a new TCP connection was established. +Same port means the connection was reused from the pool. This is +deterministic and accurate at any RPS. + +| Metric | Type | Description | +|-------------------------------------|---------|------------------------------------------| +| `connection_pool_new_total` | Counter | Requests that used a new TCP connection | +| `connection_pool_reused_total` | Counter | Requests that reused a pooled connection | +| `connection_pool_reuse_rate_percent`| Gauge | Current reuse percentage | +| `connection_pool_requests_total` | Counter | Total requests tracked | +| `connection_pool_max_idle_per_host` | Gauge | Configured max idle setting | +| `connection_pool_idle_timeout_seconds`| Gauge | Configured idle timeout setting | ### Grafana Queries **New vs reused connections over time (time series panel):** -| Query | Legend | -|-------------------------------------------------|----------| -| `rate(connection_pool_likely_reused_total[1m])` | Reused | -| `rate(connection_pool_likely_new_total[1m])` | New | +| Query | Legend | Color | +|--------------------------------------------|--------|-------| +| `rate(connection_pool_reused_total[1m])` | Reused | Green | +| `rate(connection_pool_new_total[1m])` | New | Red | **Reuse rate (single stat panel):** @@ -225,5 +211,5 @@ connection_pool_reuse_rate_percent **Percentage of new connections (single stat panel):** ```promql -connection_pool_likely_new_total / connection_pool_requests_total * 100 +connection_pool_new_total / connection_pool_requests_total * 100 ``` diff --git a/src/config.rs b/src/config.rs index ff5cca0..9fa53fd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -101,7 +101,6 @@ pub struct Config { // When Some, these override env-var defaults when building the HTTP client. pub pool_max_idle_per_host: Option, pub pool_idle_timeout_secs: Option, - pub pool_metrics_reuse_threshold_ms: Option, } /// Helper to get a required environment variable. @@ -236,15 +235,10 @@ impl Config { let auto_disable_percentiles_on_warning = env_bool("AUTO_DISABLE_PERCENTILES_ON_WARNING", true); - let (pool_max_idle_per_host, pool_idle_timeout_secs, pool_metrics_reuse_threshold_ms) = - match &yaml_config.config.pool { - Some(p) => ( - p.max_idle_per_host, - p.idle_timeout_secs, - p.metrics_reuse_threshold_ms, - ), - None => (None, None, None), - }; + let (pool_max_idle_per_host, pool_idle_timeout_secs) = match &yaml_config.config.pool { + Some(p) => (p.max_idle_per_host, p.idle_timeout_secs), + None => (None, None), + }; let config = Config { target_url, @@ -269,7 +263,7 @@ impl Config { cluster: ClusterConfig::from_env(), pool_max_idle_per_host, pool_idle_timeout_secs, - pool_metrics_reuse_threshold_ms, + }; config.validate()?; @@ -337,15 +331,10 @@ impl Config { let auto_disable_percentiles_on_warning = env_bool("AUTO_DISABLE_PERCENTILES_ON_WARNING", true); - let (pool_max_idle_per_host, pool_idle_timeout_secs, pool_metrics_reuse_threshold_ms) = - match &yaml_config.config.pool { - Some(p) => ( - p.max_idle_per_host, - p.idle_timeout_secs, - p.metrics_reuse_threshold_ms, - ), - None => (None, None, None), - }; + let (pool_max_idle_per_host, pool_idle_timeout_secs) = match &yaml_config.config.pool { + Some(p) => (p.max_idle_per_host, p.idle_timeout_secs), + None => (None, None), + }; let config = Config { target_url, @@ -370,7 +359,7 @@ impl Config { cluster: ClusterConfig::from_env(), pool_max_idle_per_host, pool_idle_timeout_secs, - pool_metrics_reuse_threshold_ms, + }; config.validate()?; @@ -538,7 +527,7 @@ impl Config { cluster: ClusterConfig::from_env(), pool_max_idle_per_host: None, pool_idle_timeout_secs: None, - pool_metrics_reuse_threshold_ms: None, + }; config.validate()?; @@ -744,7 +733,7 @@ impl Config { cluster: ClusterConfig::for_testing(), pool_max_idle_per_host: None, pool_idle_timeout_secs: None, - pool_metrics_reuse_threshold_ms: None, + } } diff --git a/src/connection_pool.rs b/src/connection_pool.rs index ffd16c0..833b595 100644 --- a/src/connection_pool.rs +++ b/src/connection_pool.rs @@ -1,15 +1,18 @@ //! Connection pool configuration and monitoring. //! -//! This module provides connection pool statistics tracking and configuration. -//! Since reqwest doesn't expose internal pool metrics, we track connection -//! behavior patterns and configuration to provide insights into pool utilization. +//! Tracks connection reuse accurately using local TCP port comparison +//! (Issue #119). Each response's local SocketAddr is extracted from hyper's +//! HttpInfo extension — a new local port means a new TCP connection, same +//! port means the connection was reused from the pool. +use std::collections::HashSet; +use std::net::SocketAddr; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use tracing::debug; use crate::metrics::{ - CONNECTION_POOL_LIKELY_NEW, CONNECTION_POOL_LIKELY_REUSED, CONNECTION_POOL_REQUESTS_TOTAL, + CONNECTION_POOL_NEW_TOTAL, CONNECTION_POOL_REQUESTS_TOTAL, CONNECTION_POOL_REUSED_TOTAL, CONNECTION_POOL_REUSE_RATE, }; @@ -36,10 +39,10 @@ impl Default for PoolConfig { fn default() -> Self { Self { max_idle_per_host: 32, - idle_timeout: Duration::from_secs(30), // Reduced from 90s to free kernel buffers sooner + idle_timeout: Duration::from_secs(30), tcp_keepalive: Some(Duration::from_secs(60)), - tcp_nodelay: true, // Disable Nagle for lower latency at high RPS - request_timeout: Duration::from_secs(30), // Prevent hung connections accumulating memory + tcp_nodelay: true, + request_timeout: Duration::from_secs(30), } } } @@ -129,11 +132,11 @@ pub struct ConnectionStats { /// Total requests made pub total_requests: u64, - /// Requests that likely used a new connection (slow initial handshake) - pub likely_new_connections: u64, + /// Requests that used a new TCP connection (by local port) + pub new_connections: u64, - /// Requests that likely reused a connection (fast, no TLS handshake) - pub likely_reused_connections: u64, + /// Requests that reused a pooled TCP connection (by local port) + pub reused_connections: u64, /// First request timestamp (for rate calculations) pub first_request: Option, @@ -145,18 +148,20 @@ pub struct ConnectionStats { impl ConnectionStats { /// Calculate the connection reuse rate. pub fn reuse_rate(&self) -> f64 { - if self.total_requests == 0 { + let tracked = self.new_connections + self.reused_connections; + if tracked == 0 { return 0.0; } - (self.likely_reused_connections as f64 / self.total_requests as f64) * 100.0 + (self.reused_connections as f64 / tracked as f64) * 100.0 } /// Calculate the new connection rate. pub fn new_connection_rate(&self) -> f64 { - if self.total_requests == 0 { + let tracked = self.new_connections + self.reused_connections; + if tracked == 0 { return 0.0; } - (self.likely_new_connections as f64 / self.total_requests as f64) * 100.0 + (self.new_connections as f64 / tracked as f64) * 100.0 } /// Get the duration over which requests were tracked. @@ -172,9 +177,9 @@ impl ConnectionStats { format!( "Total: {}, Reused: {} ({:.1}%), New: {} ({:.1}%)", self.total_requests, - self.likely_reused_connections, + self.reused_connections, self.reuse_rate(), - self.likely_new_connections, + self.new_connections, self.new_connection_rate() ) } @@ -182,77 +187,57 @@ impl ConnectionStats { /// Tracker for connection pool statistics. /// -/// This tracker monitors connection behavior patterns to provide insights -/// into connection reuse. It uses timing heuristics to infer whether a -/// connection was likely reused or newly established. +/// Uses local TCP port tracking to deterministically identify new vs reused +/// connections. A new local port = new TCP connection. Same port = reused. #[derive(Clone)] pub struct PoolStatsTracker { stats: Arc>, - - /// Threshold for considering a connection "likely new" (milliseconds) - /// Requests slower than this are likely establishing new connections - new_connection_threshold_ms: Arc>, + seen_ports: Arc>>, } impl PoolStatsTracker { /// Create a new pool statistics tracker. - /// - /// # Arguments - /// * `new_connection_threshold_ms` - Latency threshold (ms) above which we - /// consider a connection likely new (includes TLS handshake time) - pub fn new(new_connection_threshold_ms: u64) -> Self { + pub fn new() -> Self { Self { stats: Arc::new(Mutex::new(ConnectionStats::default())), - new_connection_threshold_ms: Arc::new(Mutex::new(new_connection_threshold_ms)), + seen_ports: Arc::new(Mutex::new(HashSet::new())), } } - /// Update the latency threshold used to classify new vs reused connections. - pub fn set_threshold_ms(&self, threshold_ms: u64) { - *self.new_connection_threshold_ms.lock().unwrap() = threshold_ms; - } - - /// Record a request with timing information. + /// Record a request with its local socket address for connection tracking. /// - /// Uses latency to infer connection reuse. Requests with very low latency - /// (<50ms typically) likely reused an existing connection. Slower requests - /// may have established a new connection (including TLS handshake). - pub fn record_request(&self, latency_ms: u64) { + /// `local_addr` is obtained from `response.extensions().get::()`. + /// When `None` (e.g. request failed before a connection was established), + /// only the total request counter is incremented. + pub fn record_request(&self, local_addr: Option) { let now = Instant::now(); - let threshold = *self.new_connection_threshold_ms.lock().unwrap(); let mut stats = self.stats.lock().unwrap(); stats.total_requests += 1; CONNECTION_POOL_REQUESTS_TOTAL.inc(); - // Track timing if stats.first_request.is_none() { stats.first_request = Some(now); } stats.last_request = Some(now); - // Infer connection type based on latency - // Fast requests (= threshold { - stats.likely_new_connections += 1; - CONNECTION_POOL_LIKELY_NEW.inc(); - debug!( - latency_ms, - threshold, "Request latency suggests new connection" - ); - } else { - stats.likely_reused_connections += 1; - CONNECTION_POOL_LIKELY_REUSED.inc(); - debug!( - latency_ms, - threshold, "Request latency suggests reused connection" - ); + if let Some(addr) = local_addr { + let port = addr.port(); + let mut ports = self.seen_ports.lock().unwrap(); + if ports.insert(port) { + stats.new_connections += 1; + CONNECTION_POOL_NEW_TOTAL.inc(); + debug!(local_port = port, "New TCP connection (new local port)"); + } else { + stats.reused_connections += 1; + CONNECTION_POOL_REUSED_TOTAL.inc(); + debug!( + local_port = port, + "Reused TCP connection (seen local port)" + ); + } + CONNECTION_POOL_REUSE_RATE.set(stats.reuse_rate()); } - - // Update reuse rate gauge - let reuse_rate = stats.reuse_rate(); - CONNECTION_POOL_REUSE_RATE.set(reuse_rate); } /// Get current connection statistics. @@ -264,14 +249,14 @@ impl PoolStatsTracker { pub fn reset(&self) { let mut stats = self.stats.lock().unwrap(); *stats = ConnectionStats::default(); + let mut ports = self.seen_ports.lock().unwrap(); + ports.clear(); } } impl Default for PoolStatsTracker { fn default() -> Self { - // Default threshold of 100ms to distinguish new vs reused connections - // TLS handshake typically adds 50-150ms depending on network conditions - Self::new(100) + Self::new() } } @@ -319,127 +304,130 @@ mod tests { fn test_connection_stats_rates() { let stats = ConnectionStats { total_requests: 100, - likely_new_connections: 20, - likely_reused_connections: 80, + new_connections: 10, + reused_connections: 90, first_request: Some(Instant::now()), last_request: Some(Instant::now()), }; - assert_eq!(stats.reuse_rate(), 80.0); - assert_eq!(stats.new_connection_rate(), 20.0); + assert_eq!(stats.reuse_rate(), 90.0); + assert_eq!(stats.new_connection_rate(), 10.0); } #[test] - fn test_pool_stats_tracker_fast_requests() { - let tracker = PoolStatsTracker::new(100); + fn test_port_tracking_new_connections() { + let tracker = PoolStatsTracker::new(); - // Simulate 10 fast requests (likely reused connections) - for _ in 0..10 { - tracker.record_request(20); // 20ms - fast - } + let addr1: SocketAddr = "127.0.0.1:50001".parse().unwrap(); + let addr2: SocketAddr = "127.0.0.1:50002".parse().unwrap(); + let addr3: SocketAddr = "127.0.0.1:50003".parse().unwrap(); + + tracker.record_request(Some(addr1)); + tracker.record_request(Some(addr2)); + tracker.record_request(Some(addr3)); let stats = tracker.stats(); - assert_eq!(stats.total_requests, 10); - assert_eq!(stats.likely_reused_connections, 10); - assert_eq!(stats.likely_new_connections, 0); - assert_eq!(stats.reuse_rate(), 100.0); + assert_eq!(stats.total_requests, 3); + assert_eq!(stats.new_connections, 3); + assert_eq!(stats.reused_connections, 0); + assert_eq!(stats.reuse_rate(), 0.0); } #[test] - fn test_pool_stats_tracker_slow_requests() { - let tracker = PoolStatsTracker::new(100); + fn test_port_tracking_reused_connections() { + let tracker = PoolStatsTracker::new(); - // Simulate 10 slow requests (likely new connections) - for _ in 0..10 { - tracker.record_request(150); // 150ms - slow (includes TLS handshake) - } + let addr: SocketAddr = "127.0.0.1:50001".parse().unwrap(); + + tracker.record_request(Some(addr)); + tracker.record_request(Some(addr)); + tracker.record_request(Some(addr)); let stats = tracker.stats(); - assert_eq!(stats.total_requests, 10); - assert_eq!(stats.likely_reused_connections, 0); - assert_eq!(stats.likely_new_connections, 10); - assert_eq!(stats.new_connection_rate(), 100.0); + assert_eq!(stats.total_requests, 3); + assert_eq!(stats.new_connections, 1); + assert_eq!(stats.reused_connections, 2); } #[test] - fn test_pool_stats_tracker_mixed() { - let tracker = PoolStatsTracker::new(100); + fn test_port_tracking_mixed() { + let tracker = PoolStatsTracker::new(); + + let addr1: SocketAddr = "127.0.0.1:50001".parse().unwrap(); + let addr2: SocketAddr = "127.0.0.1:50002".parse().unwrap(); - // Simulate mixed requests - tracker.record_request(150); // New connection (slow) - tracker.record_request(30); // Reused (fast) - tracker.record_request(25); // Reused (fast) - tracker.record_request(120); // New connection (slow) - tracker.record_request(40); // Reused (fast) + tracker.record_request(Some(addr1)); // New + tracker.record_request(Some(addr1)); // Reused + tracker.record_request(Some(addr2)); // New + tracker.record_request(Some(addr1)); // Reused + tracker.record_request(Some(addr2)); // Reused let stats = tracker.stats(); - assert_eq!(stats.total_requests, 5); - assert_eq!(stats.likely_reused_connections, 3); - assert_eq!(stats.likely_new_connections, 2); + assert_eq!(stats.new_connections, 2); + assert_eq!(stats.reused_connections, 3); assert_eq!(stats.reuse_rate(), 60.0); - assert_eq!(stats.new_connection_rate(), 40.0); } #[test] - fn test_pool_stats_tracker_reset() { - let tracker = PoolStatsTracker::new(100); + fn test_port_tracking_none_addr() { + let tracker = PoolStatsTracker::new(); - tracker.record_request(50); - tracker.record_request(150); + tracker.record_request(None); + tracker.record_request(None); let stats = tracker.stats(); assert_eq!(stats.total_requests, 2); + assert_eq!(stats.new_connections, 0); + assert_eq!(stats.reused_connections, 0); + } + + #[test] + fn test_reset_clears_ports() { + let tracker = PoolStatsTracker::new(); + + let addr: SocketAddr = "127.0.0.1:50001".parse().unwrap(); + tracker.record_request(Some(addr)); + tracker.record_request(Some(addr)); + + assert_eq!(tracker.stats().new_connections, 1); + assert_eq!(tracker.stats().reused_connections, 1); tracker.reset(); - let stats = tracker.stats(); - assert_eq!(stats.total_requests, 0); - assert_eq!(stats.likely_reused_connections, 0); - assert_eq!(stats.likely_new_connections, 0); + tracker.record_request(Some(addr)); + assert_eq!(tracker.stats().new_connections, 1); + assert_eq!(tracker.stats().reused_connections, 0); } #[test] fn test_connection_stats_format() { let stats = ConnectionStats { total_requests: 100, - likely_new_connections: 25, - likely_reused_connections: 75, + new_connections: 20, + reused_connections: 80, first_request: Some(Instant::now()), last_request: Some(Instant::now()), }; let formatted = stats.format(); assert!(formatted.contains("Total: 100")); - assert!(formatted.contains("Reused: 75")); - assert!(formatted.contains("75.0%")); - assert!(formatted.contains("New: 25")); - assert!(formatted.contains("25.0%")); + assert!(formatted.contains("Reused: 80")); + assert!(formatted.contains("80.0%")); + assert!(formatted.contains("New: 20")); + assert!(formatted.contains("20.0%")); } #[test] fn test_pool_stats_timing() { - let tracker = PoolStatsTracker::new(100); + let tracker = PoolStatsTracker::new(); - tracker.record_request(50); + tracker.record_request(None); std::thread::sleep(Duration::from_millis(100)); - tracker.record_request(50); + tracker.record_request(None); let stats = tracker.stats(); let duration = stats.duration().unwrap(); - assert!(duration >= Duration::from_millis(100)); assert!(duration < Duration::from_millis(200)); } - - #[test] - fn test_custom_threshold() { - let tracker = PoolStatsTracker::new(200); // Higher threshold - - tracker.record_request(150); // Under threshold - reused - tracker.record_request(250); // Over threshold - new - - let stats = tracker.stats(); - assert_eq!(stats.likely_reused_connections, 1); - assert_eq!(stats.likely_new_connections, 1); - } } diff --git a/src/executor.rs b/src/executor.rs index ae480ff..f1ac279 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -7,6 +7,7 @@ use crate::assertions; use crate::connection_pool::GLOBAL_POOL_STATS; use crate::extractor; +use hyper_util::client::legacy::connect::HttpInfo; use crate::metrics::{ CONCURRENT_SCENARIOS, SCENARIO_ASSERTIONS_TOTAL, SCENARIO_DURATION_SECONDS, SCENARIO_EXECUTIONS_TOTAL, SCENARIO_STEPS_TOTAL, SCENARIO_STEP_DURATION_SECONDS, @@ -348,10 +349,15 @@ impl ScenarioExecutor { let response_result = request_builder.send().await; let response_time_ms = step_start.elapsed().as_millis() as u64; - GLOBAL_POOL_STATS.record_request(response_time_ms); match response_result { Ok(response) => { + let local_addr = response + .extensions() + .get::() + .map(|info| info.local_addr()); + GLOBAL_POOL_STATS.record_request(local_addr); + let status = response.status(); let headers = response.headers().clone(); @@ -569,6 +575,7 @@ impl ScenarioExecutor { } } Err(e) => { + GLOBAL_POOL_STATS.record_request(None); error!( step = %step.name, error = %e, diff --git a/src/main.rs b/src/main.rs index 09060d4..259c934 100644 --- a/src/main.rs +++ b/src/main.rs @@ -175,9 +175,8 @@ fn print_pool_report() { info!(" Check: pool configuration, connection timeouts, load patterns."); } - info!("\nNote: Connection classification is based on latency patterns:"); - info!(" - Fast requests (<100ms) likely reused pooled connections"); - info!(" - Slow requests (≥100ms) likely established new connections (TLS handshake)"); + info!("\nNote: Connection classification is based on local TCP port tracking."); + info!(" A new local port = new TCP connection. Same port = reused."); } else { info!("\nNo connection pool data collected.\n"); } @@ -1136,10 +1135,7 @@ async fn main() -> Result<(), Box> { h.abort(); } - // Apply pool stats threshold from YAML and reset counters for new test. - if let Some(threshold_ms) = new_cfg.pool_metrics_reuse_threshold_ms { - GLOBAL_POOL_STATS.set_threshold_ms(threshold_ms); - } + // Reset pool stats counters for new test. GLOBAL_POOL_STATS.reset(); // Rebuild HTTP client in case TLS/pool config changed. diff --git a/src/metrics.rs b/src/metrics.rs index a20ea61..e79a546 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -142,21 +142,21 @@ lazy_static::lazy_static! { .namespace(METRIC_NAMESPACE.as_str()) ).unwrap(); - pub static ref CONNECTION_POOL_LIKELY_REUSED: IntCounter = + pub static ref CONNECTION_POOL_NEW_TOTAL: IntCounter = IntCounter::with_opts( - Opts::new("connection_pool_likely_reused_total", "Requests that likely reused existing connections") + Opts::new("connection_pool_new_total", "Requests that used a new TCP connection (by local port)") .namespace(METRIC_NAMESPACE.as_str()) ).unwrap(); - pub static ref CONNECTION_POOL_LIKELY_NEW: IntCounter = + pub static ref CONNECTION_POOL_REUSED_TOTAL: IntCounter = IntCounter::with_opts( - Opts::new("connection_pool_likely_new_total", "Requests that likely established new connections") + Opts::new("connection_pool_reused_total", "Requests that reused an existing TCP connection (by local port)") .namespace(METRIC_NAMESPACE.as_str()) ).unwrap(); pub static ref CONNECTION_POOL_REUSE_RATE: Gauge = Gauge::with_opts( - Opts::new("connection_pool_reuse_rate_percent", "Percentage of requests reusing connections") + Opts::new("connection_pool_reuse_rate_percent", "Accurate reuse percentage based on local port tracking") .namespace(METRIC_NAMESPACE.as_str()) ).unwrap(); @@ -295,8 +295,8 @@ pub fn register_metrics() -> Result<(), Box prometheus::default_registry() .register(Box::new(CONNECTION_POOL_IDLE_TIMEOUT_SECONDS.clone()))?; prometheus::default_registry().register(Box::new(CONNECTION_POOL_REQUESTS_TOTAL.clone()))?; - prometheus::default_registry().register(Box::new(CONNECTION_POOL_LIKELY_REUSED.clone()))?; - prometheus::default_registry().register(Box::new(CONNECTION_POOL_LIKELY_NEW.clone()))?; + prometheus::default_registry().register(Box::new(CONNECTION_POOL_NEW_TOTAL.clone()))?; + prometheus::default_registry().register(Box::new(CONNECTION_POOL_REUSED_TOTAL.clone()))?; prometheus::default_registry().register(Box::new(CONNECTION_POOL_REUSE_RATE.clone()))?; // Memory usage metrics diff --git a/src/worker.rs b/src/worker.rs index b442787..99100ee 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -23,6 +23,7 @@ fn should_sample(rate: u8) -> bool { use crate::client::{build_client, ClientConfig}; use crate::connection_pool::GLOBAL_POOL_STATS; use crate::errors::ErrorCategory; +use hyper_util::client::legacy::connect::HttpInfo; use crate::executor::{ScenarioExecutor, SessionStore}; use crate::load_models::LoadModel; use crate::memory_guard::is_percentile_tracking_active; @@ -172,8 +173,14 @@ pub async fn run_worker(client: reqwest::Client, config: WorkerConfig, start_tim // Build and send request let req = build_request(&client, &config); + let mut local_addr = None; match req.send().await { Ok(mut response) => { + local_addr = response + .extensions() + .get::() + .map(|info| info.local_addr()); + let status = response.status().as_u16(); // Use static strings to avoid a heap allocation on every request let status_str = status_code_label(status); @@ -276,8 +283,8 @@ pub async fn run_worker(client: reqwest::Client, config: WorkerConfig, start_tim GLOBAL_REQUEST_PERCENTILES.record_ms(actual_latency_ms); } - // Record connection pool statistics (Issue #36) - GLOBAL_POOL_STATS.record_request(actual_latency_ms); + // Record connection pool statistics (Issue #36, #119) + GLOBAL_POOL_STATS.record_request(local_addr); // No explicit sleep here — sleep_until(next_fire) at the top of the next // iteration handles all timing with sub-millisecond precision. diff --git a/src/yaml_config.rs b/src/yaml_config.rs index 67462a5..b7cfc5e 100644 --- a/src/yaml_config.rs +++ b/src/yaml_config.rs @@ -119,12 +119,6 @@ pub struct YamlPoolConfig { #[serde(rename = "idleTimeoutSecs")] pub idle_timeout_secs: Option, - /// Latency threshold in milliseconds used by Prometheus metrics to classify - /// a request as a new connection vs a reused one (default: 100). Requests - /// slower than this are counted as "likely new connection". Does NOT affect - /// actual connection behavior — only the metrics heuristic. - #[serde(rename = "metricsReuseThresholdMs")] - pub metrics_reuse_threshold_ms: Option, } fn default_timeout() -> YamlDuration { From 3dee2544554e6f260ce1e444a6a332eb997f7c39 Mon Sep 17 00:00:00 2001 From: cbaugus Date: Mon, 13 Apr 2026 20:13:10 -0500 Subject: [PATCH 2/4] =?UTF-8?q?style:=20rustfmt=20=E2=80=94=20fix=20import?= =?UTF-8?q?=20ordering,=20blank=20lines,=20and=20debug!=20macro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- src/config.rs | 4 ---- src/connection_pool.rs | 5 +---- src/executor.rs | 2 +- src/worker.rs | 2 +- src/yaml_config.rs | 1 - 5 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/config.rs b/src/config.rs index 9fa53fd..5e5d65d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -263,7 +263,6 @@ impl Config { cluster: ClusterConfig::from_env(), pool_max_idle_per_host, pool_idle_timeout_secs, - }; config.validate()?; @@ -359,7 +358,6 @@ impl Config { cluster: ClusterConfig::from_env(), pool_max_idle_per_host, pool_idle_timeout_secs, - }; config.validate()?; @@ -527,7 +525,6 @@ impl Config { cluster: ClusterConfig::from_env(), pool_max_idle_per_host: None, pool_idle_timeout_secs: None, - }; config.validate()?; @@ -733,7 +730,6 @@ impl Config { cluster: ClusterConfig::for_testing(), pool_max_idle_per_host: None, pool_idle_timeout_secs: None, - } } diff --git a/src/connection_pool.rs b/src/connection_pool.rs index 833b595..a6f8093 100644 --- a/src/connection_pool.rs +++ b/src/connection_pool.rs @@ -231,10 +231,7 @@ impl PoolStatsTracker { } else { stats.reused_connections += 1; CONNECTION_POOL_REUSED_TOTAL.inc(); - debug!( - local_port = port, - "Reused TCP connection (seen local port)" - ); + debug!(local_port = port, "Reused TCP connection (seen local port)"); } CONNECTION_POOL_REUSE_RATE.set(stats.reuse_rate()); } diff --git a/src/executor.rs b/src/executor.rs index f1ac279..ae14cf5 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -7,13 +7,13 @@ use crate::assertions; use crate::connection_pool::GLOBAL_POOL_STATS; use crate::extractor; -use hyper_util::client::legacy::connect::HttpInfo; use crate::metrics::{ CONCURRENT_SCENARIOS, SCENARIO_ASSERTIONS_TOTAL, SCENARIO_DURATION_SECONDS, SCENARIO_EXECUTIONS_TOTAL, SCENARIO_STEPS_TOTAL, SCENARIO_STEP_DURATION_SECONDS, SCENARIO_STEP_STATUS_CODES, }; use crate::scenario::{Scenario, ScenarioContext, Step}; +use hyper_util::client::legacy::connect::HttpInfo; use rand::Rng; use std::collections::HashMap; use std::time::Instant; diff --git a/src/worker.rs b/src/worker.rs index 99100ee..aa1a4e3 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -23,7 +23,6 @@ fn should_sample(rate: u8) -> bool { use crate::client::{build_client, ClientConfig}; use crate::connection_pool::GLOBAL_POOL_STATS; use crate::errors::ErrorCategory; -use hyper_util::client::legacy::connect::HttpInfo; use crate::executor::{ScenarioExecutor, SessionStore}; use crate::load_models::LoadModel; use crate::memory_guard::is_percentile_tracking_active; @@ -36,6 +35,7 @@ use crate::percentiles::{ }; use crate::scenario::{Scenario, ScenarioContext}; use crate::throughput::GLOBAL_THROUGHPUT_TRACKER; +use hyper_util::client::legacy::connect::HttpInfo; /// Configuration for a worker task. pub struct WorkerConfig { diff --git a/src/yaml_config.rs b/src/yaml_config.rs index b7cfc5e..1e72c1a 100644 --- a/src/yaml_config.rs +++ b/src/yaml_config.rs @@ -118,7 +118,6 @@ pub struct YamlPoolConfig { /// Set to 0 to immediately close connections after each request. #[serde(rename = "idleTimeoutSecs")] pub idle_timeout_secs: Option, - } fn default_timeout() -> YamlDuration { From 4690f4d8e1a0bba3b9572b4e5370388a8034494e Mon Sep 17 00:00:00 2001 From: cbaugus Date: Mon, 13 Apr 2026 20:15:42 -0500 Subject: [PATCH 3/4] fix: update integration tests for port-based connection tracking API Tests now use SocketAddr-based record_request() and the new ConnectionStats fields (new_connections, reused_connections). Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/connection_pool_tests.rs | 274 +++++++++++---------------------- 1 file changed, 86 insertions(+), 188 deletions(-) diff --git a/tests/connection_pool_tests.rs b/tests/connection_pool_tests.rs index 1f167ec..1e54881 100644 --- a/tests/connection_pool_tests.rs +++ b/tests/connection_pool_tests.rs @@ -1,10 +1,12 @@ -//! Integration tests for connection pool statistics (Issue #36). +//! Integration tests for connection pool statistics (Issue #36, #119). //! -//! These tests validate connection pool configuration and statistics tracking. +//! These tests validate connection pool configuration and accurate +//! port-based connection tracking. use rust_loadtest::connection_pool::{ ConnectionStats, PoolConfig, PoolStatsTracker, GLOBAL_POOL_STATS, }; +use std::net::SocketAddr; use std::time::Duration; #[test] @@ -16,8 +18,6 @@ fn test_pool_config_default() { assert_eq!(config.tcp_keepalive, Some(Duration::from_secs(60))); assert!(config.tcp_nodelay); assert_eq!(config.request_timeout, Duration::from_secs(30)); - - println!("✅ Pool configuration defaults work"); } #[test] @@ -30,17 +30,12 @@ fn test_pool_config_builder_pattern() { assert_eq!(config.max_idle_per_host, 64); assert_eq!(config.idle_timeout, Duration::from_secs(120)); assert_eq!(config.tcp_keepalive, Some(Duration::from_secs(30))); - - println!("✅ Pool configuration builder pattern works"); } #[test] fn test_pool_config_disable_keepalive() { let config = PoolConfig::new().with_tcp_keepalive(None); - assert_eq!(config.tcp_keepalive, None); - - println!("✅ TCP keepalive can be disabled"); } #[test] @@ -48,21 +43,19 @@ fn test_connection_stats_empty() { let stats = ConnectionStats::default(); assert_eq!(stats.total_requests, 0); - assert_eq!(stats.likely_new_connections, 0); - assert_eq!(stats.likely_reused_connections, 0); + assert_eq!(stats.new_connections, 0); + assert_eq!(stats.reused_connections, 0); assert_eq!(stats.reuse_rate(), 0.0); assert_eq!(stats.new_connection_rate(), 0.0); assert!(stats.duration().is_none()); - - println!("✅ Empty connection stats handled correctly"); } #[test] fn test_connection_stats_calculations() { let stats = ConnectionStats { total_requests: 100, - likely_new_connections: 20, - likely_reused_connections: 80, + new_connections: 20, + reused_connections: 80, first_request: Some(std::time::Instant::now()), last_request: Some(std::time::Instant::now()), }; @@ -76,135 +69,120 @@ fn test_connection_stats_calculations() { assert!(formatted.contains("80.0%")); assert!(formatted.contains("New: 20")); assert!(formatted.contains("20.0%")); - - println!("✅ Connection stats calculations work"); - println!(" {}", formatted); } #[test] -fn test_pool_stats_tracker_fast_requests() { - let tracker = PoolStatsTracker::new(100); +fn test_port_tracking_all_new_connections() { + let tracker = PoolStatsTracker::new(); - // Simulate 10 fast requests (reused connections) - for _ in 0..10 { - tracker.record_request(30); // 30ms - very fast + // Each request from a different port = new connection + for port in 50001..50011 { + let addr: SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap(); + tracker.record_request(Some(addr)); } let stats = tracker.stats(); assert_eq!(stats.total_requests, 10); - assert_eq!(stats.likely_reused_connections, 10); - assert_eq!(stats.likely_new_connections, 0); - assert_eq!(stats.reuse_rate(), 100.0); - - println!("✅ Fast requests classified as reused connections"); - println!(" {}", stats.format()); + assert_eq!(stats.new_connections, 10); + assert_eq!(stats.reused_connections, 0); + assert_eq!(stats.reuse_rate(), 0.0); } #[test] -fn test_pool_stats_tracker_slow_requests() { - let tracker = PoolStatsTracker::new(100); +fn test_port_tracking_all_reused_connections() { + let tracker = PoolStatsTracker::new(); - // Simulate 10 slow requests (new connections with TLS handshake) + // Same port every time = reused connection (after first) + let addr: SocketAddr = "127.0.0.1:50001".parse().unwrap(); for _ in 0..10 { - tracker.record_request(150); // 150ms - includes TLS handshake + tracker.record_request(Some(addr)); } let stats = tracker.stats(); assert_eq!(stats.total_requests, 10); - assert_eq!(stats.likely_reused_connections, 0); - assert_eq!(stats.likely_new_connections, 10); - assert_eq!(stats.new_connection_rate(), 100.0); - - println!("✅ Slow requests classified as new connections"); - println!(" {}", stats.format()); + assert_eq!(stats.new_connections, 1); // First use of port + assert_eq!(stats.reused_connections, 9); } #[test] -fn test_pool_stats_tracker_mixed_patterns() { - let tracker = PoolStatsTracker::new(100); - - // Simulate realistic mixed pattern - tracker.record_request(150); // New connection (slow) - tracker.record_request(25); // Reused (fast) - tracker.record_request(30); // Reused (fast) - tracker.record_request(120); // New connection (slow) - tracker.record_request(20); // Reused (fast) - tracker.record_request(35); // Reused (fast) - tracker.record_request(110); // New connection (slow) - tracker.record_request(28); // Reused (fast) +fn test_port_tracking_mixed_pattern() { + let tracker = PoolStatsTracker::new(); + + let addr1: SocketAddr = "127.0.0.1:50001".parse().unwrap(); + let addr2: SocketAddr = "127.0.0.1:50002".parse().unwrap(); + let addr3: SocketAddr = "127.0.0.1:50003".parse().unwrap(); + + tracker.record_request(Some(addr1)); // New + tracker.record_request(Some(addr1)); // Reused + tracker.record_request(Some(addr2)); // New + tracker.record_request(Some(addr1)); // Reused + tracker.record_request(Some(addr2)); // Reused + tracker.record_request(Some(addr3)); // New + tracker.record_request(Some(addr3)); // Reused + tracker.record_request(Some(addr1)); // Reused let stats = tracker.stats(); assert_eq!(stats.total_requests, 8); - assert_eq!(stats.likely_reused_connections, 5); - assert_eq!(stats.likely_new_connections, 3); + assert_eq!(stats.new_connections, 3); + assert_eq!(stats.reused_connections, 5); assert_eq!(stats.reuse_rate(), 62.5); assert_eq!(stats.new_connection_rate(), 37.5); - - println!("✅ Mixed request patterns tracked correctly"); - println!(" {}", stats.format()); } #[test] -fn test_pool_stats_tracker_custom_threshold() { - let tracker = PoolStatsTracker::new(200); // Higher threshold +fn test_port_tracking_none_addr() { + let tracker = PoolStatsTracker::new(); - tracker.record_request(150); // Under threshold - reused - tracker.record_request(180); // Under threshold - reused - tracker.record_request(210); // Over threshold - new - tracker.record_request(250); // Over threshold - new + // No local_addr (failed requests) — only total counted + tracker.record_request(None); + tracker.record_request(None); + tracker.record_request(None); let stats = tracker.stats(); - assert_eq!(stats.total_requests, 4); - assert_eq!(stats.likely_reused_connections, 2); - assert_eq!(stats.likely_new_connections, 2); - - println!("✅ Custom threshold works correctly"); - println!(" {}", stats.format()); + assert_eq!(stats.total_requests, 3); + assert_eq!(stats.new_connections, 0); + assert_eq!(stats.reused_connections, 0); } #[test] fn test_pool_stats_tracker_reset() { - let tracker = PoolStatsTracker::new(100); + let tracker = PoolStatsTracker::new(); - // Record some requests - tracker.record_request(50); - tracker.record_request(150); - tracker.record_request(30); + let addr: SocketAddr = "127.0.0.1:50001".parse().unwrap(); + tracker.record_request(Some(addr)); + tracker.record_request(Some(addr)); + tracker.record_request(None); let stats = tracker.stats(); assert_eq!(stats.total_requests, 3); - // Reset tracker.reset(); let stats = tracker.stats(); assert_eq!(stats.total_requests, 0); - assert_eq!(stats.likely_reused_connections, 0); - assert_eq!(stats.likely_new_connections, 0); + assert_eq!(stats.new_connections, 0); + assert_eq!(stats.reused_connections, 0); - println!("✅ Pool stats tracker reset works"); + // After reset, same port should be new again + tracker.record_request(Some(addr)); + let stats = tracker.stats(); + assert_eq!(stats.new_connections, 1); + assert_eq!(stats.reused_connections, 0); } #[test] fn test_pool_stats_timing_accuracy() { - let tracker = PoolStatsTracker::new(100); - - tracker.record_request(50); + let tracker = PoolStatsTracker::new(); - // Wait a known duration + tracker.record_request(None); std::thread::sleep(Duration::from_millis(100)); - - tracker.record_request(50); + tracker.record_request(None); let stats = tracker.stats(); let duration = stats.duration().unwrap(); - // Duration should be at least 100ms but less than 200ms assert!(duration >= Duration::from_millis(100)); assert!(duration < Duration::from_millis(200)); - - println!("✅ Timing accuracy validated"); - println!(" Duration: {:?}", duration); } #[test] @@ -217,8 +195,8 @@ fn test_connection_stats_duration_calculation() { let stats = ConnectionStats { total_requests: 10, - likely_new_connections: 2, - likely_reused_connections: 8, + new_connections: 2, + reused_connections: 8, first_request: Some(start), last_request: Some(end), }; @@ -226,32 +204,23 @@ fn test_connection_stats_duration_calculation() { let duration = stats.duration().unwrap(); assert!(duration >= Duration::from_millis(50)); assert!(duration < Duration::from_millis(100)); - - println!("✅ Duration calculation works"); - println!(" Duration: {:.3}s", duration.as_secs_f64()); } #[test] -fn test_pool_stats_high_reuse_scenario() { - let tracker = PoolStatsTracker::new(100); - - // Simulate high connection reuse (ideal scenario) - // First request is slow (new connection) - tracker.record_request(150); +fn test_port_tracking_high_reuse_scenario() { + let tracker = PoolStatsTracker::new(); - // Following 99 requests are fast (reused) - for _ in 0..99 { - tracker.record_request(30); + // One connection reused 99 times + let addr: SocketAddr = "127.0.0.1:50001".parse().unwrap(); + for _ in 0..100 { + tracker.record_request(Some(addr)); } let stats = tracker.stats(); assert_eq!(stats.total_requests, 100); - assert_eq!(stats.likely_reused_connections, 99); - assert_eq!(stats.likely_new_connections, 1); + assert_eq!(stats.new_connections, 1); + assert_eq!(stats.reused_connections, 99); assert_eq!(stats.reuse_rate(), 99.0); - - println!("✅ High reuse scenario validated"); - println!(" {}", stats.format()); } #[test] @@ -259,94 +228,36 @@ fn test_pool_stats_concurrent_access() { use std::sync::Arc; use std::thread; - let tracker = Arc::new(PoolStatsTracker::new(100)); + let tracker = Arc::new(PoolStatsTracker::new()); let mut handles = vec![]; - // Spawn 5 threads, each recording 20 requests - for thread_id in 0..5 { + // 5 threads, each using a unique port (simulating new connections) + for thread_id in 0..5u16 { let tracker_clone = Arc::clone(&tracker); let handle = thread::spawn(move || { - for i in 0..20 { - // Alternate between fast and slow requests - if (thread_id + i) % 3 == 0 { - tracker_clone.record_request(150); // Slow (new) - } else { - tracker_clone.record_request(30); // Fast (reused) - } + let addr: SocketAddr = + format!("127.0.0.1:{}", 50000 + thread_id).parse().unwrap(); + for _ in 0..20 { + tracker_clone.record_request(Some(addr)); } }); handles.push(handle); } - // Wait for all threads for handle in handles { handle.join().unwrap(); } let stats = tracker.stats(); assert_eq!(stats.total_requests, 100); // 5 threads * 20 requests - - println!("✅ Concurrent access handled correctly"); - println!(" {}", stats.format()); -} - -#[test] -fn test_pool_stats_boundary_values() { - let tracker = PoolStatsTracker::new(100); - - // Test exact threshold - tracker.record_request(99); // Just below threshold - reused - tracker.record_request(100); // Exactly at threshold - new - tracker.record_request(101); // Just above threshold - new - - let stats = tracker.stats(); - assert_eq!(stats.total_requests, 3); - assert_eq!(stats.likely_reused_connections, 1); - assert_eq!(stats.likely_new_connections, 2); - - println!("✅ Boundary values handled correctly"); -} - -#[test] -fn test_pool_stats_zero_latency() { - let tracker = PoolStatsTracker::new(100); - - // Edge case: zero latency (shouldn't happen in practice) - tracker.record_request(0); - - let stats = tracker.stats(); - assert_eq!(stats.total_requests, 1); - assert_eq!(stats.likely_reused_connections, 1); // Zero is below threshold - - println!("✅ Zero latency handled correctly"); -} - -#[test] -fn test_pool_stats_extreme_latency() { - let tracker = PoolStatsTracker::new(100); - - // Edge case: very high latency (network issues) - tracker.record_request(5000); // 5 seconds - definitely new connection or error - - let stats = tracker.stats(); - assert_eq!(stats.total_requests, 1); - assert_eq!(stats.likely_new_connections, 1); - - println!("✅ Extreme latency handled correctly"); + assert_eq!(stats.new_connections, 5); // 5 unique ports + assert_eq!(stats.reused_connections, 95); // rest are reuses } #[test] fn test_global_pool_stats_singleton() { - // Note: GLOBAL_POOL_STATS is shared across tests, so we just verify it exists - // and can be called without testing specific values - let stats = GLOBAL_POOL_STATS.stats(); - - // Should be able to get stats (may have data from other tests) - // Just verify we can access it without panicking let _ = stats.total_requests; - - println!("✅ Global pool stats singleton accessible"); } #[test] @@ -356,13 +267,8 @@ fn test_pool_config_apply_to_builder() { .with_idle_timeout(Duration::from_secs(120)) .with_tcp_keepalive(Some(Duration::from_secs(30))); - // Create a reqwest client builder let builder = reqwest::Client::builder(); - - // Apply pool config (this should not panic) let _builder = config.apply_to_builder(builder); - - println!("✅ Pool config can be applied to reqwest ClientBuilder"); } #[tokio::test] @@ -373,19 +279,13 @@ async fn test_pool_with_real_client() { let builder = reqwest::Client::builder(); let builder = config.apply_to_builder(builder); - let client = builder.build().expect("Failed to build client"); - // Just verify we can create a client with pool config - // We won't make actual requests in unit tests assert!(client.get("http://example.com").build().is_ok()); - - println!("✅ Real HTTP client with pool config works"); } #[test] fn test_connection_stats_format_variations() { - // Test different percentage scenarios let test_cases = vec![ (100, 0, 100), // 100% reuse (100, 100, 0), // 0% reuse (all new) @@ -396,8 +296,8 @@ fn test_connection_stats_format_variations() { for (total, new, reused) in test_cases { let stats = ConnectionStats { total_requests: total, - likely_new_connections: new, - likely_reused_connections: reused, + new_connections: new, + reused_connections: reused, first_request: Some(std::time::Instant::now()), last_request: Some(std::time::Instant::now()), }; @@ -407,6 +307,4 @@ fn test_connection_stats_format_variations() { assert!(formatted.contains(&format!("New: {}", new))); assert!(formatted.contains(&format!("Reused: {}", reused))); } - - println!("✅ Connection stats formatting works for all scenarios"); } From 691815b1c5e0bf1a1167e16f7ee1ac88a0697552 Mon Sep 17 00:00:00 2001 From: cbaugus Date: Mon, 13 Apr 2026 20:16:35 -0500 Subject: [PATCH 4/4] =?UTF-8?q?style:=20rustfmt=20=E2=80=94=20collapse=20S?= =?UTF-8?q?ocketAddr=20parse=20onto=20one=20line?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/connection_pool_tests.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/connection_pool_tests.rs b/tests/connection_pool_tests.rs index 1e54881..3c7b2b6 100644 --- a/tests/connection_pool_tests.rs +++ b/tests/connection_pool_tests.rs @@ -235,8 +235,7 @@ fn test_pool_stats_concurrent_access() { for thread_id in 0..5u16 { let tracker_clone = Arc::clone(&tracker); let handle = thread::spawn(move || { - let addr: SocketAddr = - format!("127.0.0.1:{}", 50000 + thread_id).parse().unwrap(); + let addr: SocketAddr = format!("127.0.0.1:{}", 50000 + thread_id).parse().unwrap(); for _ in 0..20 { tracker_clone.record_request(Some(addr)); }