AWS SDK

AWS SDK

rev. 174400987dccd7e137fefa96b1143d21c7ddfb78

Files changed:

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/tests/pool_behavior_test.rs

@@ -0,1 +0,2631 @@
           1  +
/*
           2  +
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
           3  +
 * SPDX-License-Identifier: Apache-2.0
           4  +
 */
           5  +
           6  +
//! Pool behavior tests parameterized over HTTP client implementations.
           7  +
//!
           8  +
//! Each test is written once as an `async fn` that takes a `&dyn MakeClient`,
           9  +
//! then invoked for each client implementation.
          10  +
          11  +
#![cfg(all(feature = "wire-mock", feature = "default-client"))]
          12  +
          13  +
use aws_smithy_async::time::SystemTimeSource;
          14  +
use aws_smithy_http_client::pool::{Client as PoolClient, SharedPool};
          15  +
use aws_smithy_http_client::test_util::wire::connection::{
          16  +
    ConnectionBehavior, ConnectionTestHarness,
          17  +
};
          18  +
use aws_smithy_http_client::test_util::wire::{ReplayedEvent, WireMockServer};
          19  +
use aws_smithy_http_client::{ev, match_events, Builder, Connector};
          20  +
use aws_smithy_runtime_api::client::http::{
          21  +
    HttpClient, HttpConnector, HttpConnectorSettings, SharedHttpClient,
          22  +
};
          23  +
use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
          24  +
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
          25  +
use aws_smithy_runtime_api::shared::IntoShared;
          26  +
use std::borrow::Cow;
          27  +
use std::net::{IpAddr, Ipv4Addr};
          28  +
use std::time::Duration;
          29  +
          30  +
const IP1: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);
          31  +
          32  +
// ---------------------------------------------------------------------------
          33  +
// ClientConfig + MakeClient
          34  +
// ---------------------------------------------------------------------------
          35  +
          36  +
#[derive(Clone, Default)]
          37  +
struct ClientConfig {
          38  +
    idle_timeout: Option<Duration>,
          39  +
    max_connections: Option<usize>,
          40  +
    max_connections_per_host: Option<usize>,
          41  +
}
          42  +
          43  +
impl ClientConfig {
          44  +
    fn with_idle_timeout(mut self, timeout: Duration) -> Self {
          45  +
        self.idle_timeout = Some(timeout);
          46  +
        self
          47  +
    }
          48  +
          49  +
    fn with_max_connections(mut self, n: usize) -> Self {
          50  +
        self.max_connections = Some(n);
          51  +
        self
          52  +
    }
          53  +
          54  +
    fn with_max_connections_per_host(mut self, n: usize) -> Self {
          55  +
        self.max_connections_per_host = Some(n);
          56  +
        self
          57  +
    }
          58  +
}
          59  +
          60  +
trait MakeClient: Send + Sync {
          61  +
    fn make(&self, config: ClientConfig) -> SharedHttpClient;
          62  +
}
          63  +
          64  +
/// Current hyper 1.x client via `Builder::new()`
          65  +
struct V1Client;
          66  +
          67  +
impl MakeClient for V1Client {
          68  +
    fn make(&self, config: ClientConfig) -> SharedHttpClient {
          69  +
        Builder::new().build_with_connector_fn(move |_settings, _components| {
          70  +
            let mut builder = Connector::builder();
          71  +
            if let Some(timeout) = config.idle_timeout {
          72  +
                builder = builder.pool_idle_timeout(timeout);
          73  +
            }
          74  +
            // v1 does not support max_connections
          75  +
            builder.build_http()
          76  +
        })
          77  +
    }
          78  +
}
          79  +
          80  +
// V2 client backed by the composable connection pool
          81  +
struct V2Client;
          82  +
          83  +
impl MakeClient for V2Client {
          84  +
    fn make(&self, config: ClientConfig) -> SharedHttpClient {
          85  +
        let mut builder = SharedPool::builder();
          86  +
        if let Some(timeout) = config.idle_timeout {
          87  +
            builder = builder.pool_idle_timeout(timeout);
          88  +
        }
          89  +
        if let Some(n) = config.max_connections {
          90  +
            builder = builder.max_connections(n);
          91  +
        }
          92  +
        if let Some(n) = config.max_connections_per_host {
          93  +
            builder = builder.max_connections_per_host(n);
          94  +
        }
          95  +
        let pool = builder.build_http();
          96  +
        PoolClient::new(&pool).into_shared()
          97  +
    }
          98  +
}
          99  +
         100  +
// ---------------------------------------------------------------------------
         101  +
// Helpers
         102  +
// ---------------------------------------------------------------------------
         103  +
         104  +
fn runtime_components() -> aws_smithy_runtime_api::client::runtime_components::RuntimeComponents {
         105  +
    RuntimeComponentsBuilder::for_tests()
         106  +
        .with_time_source(Some(SystemTimeSource::new()))
         107  +
        .build()
         108  +
        .expect("valid runtime components")
         109  +
}
         110  +
         111  +
async fn send_to(
         112  +
    client: &SharedHttpClient,
         113  +
    url: &str,
         114  +
) -> Result<
         115  +
    aws_smithy_runtime_api::client::orchestrator::HttpResponse,
         116  +
    aws_smithy_runtime_api::client::result::ConnectorError,
         117  +
> {
         118  +
    let settings = HttpConnectorSettings::builder().build();
         119  +
    let components = runtime_components();
         120  +
    let connector = client.http_connector(&settings, &components);
         121  +
    connector
         122  +
        .call(HttpRequest::get(url).expect("valid HTTP request"))
         123  +
        .await
         124  +
}
         125  +
         126  +
/// Send a request and read the response body to completion.
         127  +
///
         128  +
/// This ensures the connection's body guard is released, returning the
         129  +
/// connection to the pool. Returns `(status, body_bytes)`.
         130  +
async fn send_and_read_body(
         131  +
    client: &SharedHttpClient,
         132  +
    url: &str,
         133  +
) -> Result<(u16, Vec<u8>), aws_smithy_runtime_api::client::result::ConnectorError> {
         134  +
    use http_body_util::BodyExt;
         135  +
    let resp = send_to(client, url).await?;
         136  +
    let status = resp.status().as_u16();
         137  +
    let body = resp
         138  +
        .into_body()
         139  +
        .collect()
         140  +
        .await
         141  +
        .expect("body should be readable")
         142  +
        .to_bytes()
         143  +
        .to_vec();
         144  +
    Ok((status, body))
         145  +
}
         146  +
         147  +
fn localhost_url(server: &WireMockServer) -> String {
         148  +
    let endpoint = server.endpoint_url();
         149  +
    let port = endpoint
         150  +
        .rsplit(':')
         151  +
        .next()
         152  +
        .expect("endpoint URL should contain port");
         153  +
    format!("http://127.0.0.1:{port}/")
         154  +
}
         155  +
         156  +
// ---------------------------------------------------------------------------
         157  +
// Test implementations
         158  +
// ---------------------------------------------------------------------------
         159  +
         160  +
/// Connection reuse via HTTP/1.1 keep-alive: sequential requests reuse one TCP connection.
         161  +
async fn connection_reuse(make: &dyn MakeClient) {
         162  +
    let server = WireMockServer::start(vec![
         163  +
        ReplayedEvent::status(200),
         164  +
        ReplayedEvent::status(200),
         165  +
        ReplayedEvent::status(200),
         166  +
    ])
         167  +
    .await;
         168  +
         169  +
    let client = make.make(ClientConfig::default());
         170  +
    let url = localhost_url(&server);
         171  +
         172  +
    for i in 1..=3 {
         173  +
        let resp = send_to(&client, &url)
         174  +
            .await
         175  +
            .unwrap_or_else(|e| panic!("request {i} should succeed: {e}"));
         176  +
        assert_eq!(resp.status().as_u16(), 200, "request {i} should return 200");
         177  +
    }
         178  +
         179  +
    match_events!(ev!(connect), ev!(http(200)), ev!(http(200)), ev!(http(200)))(&server.events());
         180  +
}
         181  +
         182  +
/// Idle timeout eviction: a connection idle past the timeout is discarded.
         183  +
async fn idle_timeout_eviction(make: &dyn MakeClient) {
         184  +
    let server =
         185  +
        WireMockServer::start(vec![ReplayedEvent::status(200), ReplayedEvent::status(200)]).await;
         186  +
         187  +
    let idle_timeout = Duration::from_millis(100);
         188  +
    let client = make.make(ClientConfig::default().with_idle_timeout(idle_timeout));
         189  +
    let url = localhost_url(&server);
         190  +
         191  +
    let status = send_to(&client, &url)
         192  +
        .await
         193  +
        .expect("first request should succeed")
         194  +
        .status()
         195  +
        .as_u16();
         196  +
    assert_eq!(status, 200);
         197  +
    // Response (and its body guard) dropped here — connection returns to pool.
         198  +
         199  +
    tokio::time::sleep(idle_timeout * 2).await;
         200  +
         201  +
    let status = send_to(&client, &url)
         202  +
        .await
         203  +
        .expect("second request should succeed after idle eviction")
         204  +
        .status()
         205  +
        .as_u16();
         206  +
    assert_eq!(status, 200);
         207  +
         208  +
    match_events!(ev!(connect), ev!(http(200)), ev!(connect), ev!(http(200)))(&server.events());
         209  +
}
         210  +
         211  +
/// A server that resets on connect should surface as a connector error.
         212  +
async fn connection_reset_returns_error(make: &dyn MakeClient) {
         213  +
    let harness = ConnectionTestHarness::builder()
         214  +
        .endpoint(IP1, vec![ConnectionBehavior::ResetOnConnect])
         215  +
        .build()
         216  +
        .await;
         217  +
         218  +
    let client = make.make(ClientConfig::default());
         219  +
    let url = format!("http://127.0.0.1:{}/", harness.endpoints[0].port());
         220  +
         221  +
    let err = send_to(&client, &url)
         222  +
        .await
         223  +
        .expect_err("request to a reset-on-connect endpoint should fail");
         224  +
    assert!(err.is_io(), "expected ConnectorError::io, got: {err:?}");
         225  +
}
         226  +
         227  +
/// The client should report correct connector metadata.
         228  +
async fn connector_metadata(make: &dyn MakeClient) {
         229  +
    let client = make.make(ClientConfig::default());
         230  +
    let metadata = client
         231  +
        .connector_metadata()
         232  +
        .expect("connector_metadata should return Some");
         233  +
    assert_eq!(metadata.name(), Cow::Borrowed("hyper"));
         234  +
    assert_eq!(
         235  +
        metadata.version(),
         236  +
        Some(Cow::Borrowed("1.x")),
         237  +
        "expected hyper 1.x connector version"
         238  +
    );
         239  +
}
         240  +
         241  +
// ---------------------------------------------------------------------------
         242  +
// v1 test runners
         243  +
// ---------------------------------------------------------------------------
         244  +
         245  +
#[tokio::test]
         246  +
async fn v1_connection_reuse() {
         247  +
    connection_reuse(&V1Client).await;
         248  +
}
         249  +
         250  +
#[tokio::test]
         251  +
async fn v1_idle_timeout_eviction() {
         252  +
    idle_timeout_eviction(&V1Client).await;
         253  +
}
         254  +
         255  +
#[tokio::test]
         256  +
async fn v1_connection_reset_returns_error() {
         257  +
    connection_reset_returns_error(&V1Client).await;
         258  +
}
         259  +
         260  +
#[tokio::test]
         261  +
async fn v1_connector_metadata() {
         262  +
    connector_metadata(&V1Client).await;
         263  +
}
         264  +
         265  +
// ---------------------------------------------------------------------------
         266  +
// v2 test runners
         267  +
// ---------------------------------------------------------------------------
         268  +
         269  +
#[tokio::test]
         270  +
async fn v2_connection_reuse() {
         271  +
    connection_reuse(&V2Client).await;
         272  +
}
         273  +
         274  +
#[tokio::test]
         275  +
async fn v2_idle_timeout_eviction() {
         276  +
    // An idle connection is evicted after the pool idle timeout, so a request
         277  +
    // arriving after the timeout opens a fresh connection. The body of the
         278  +
    // first response is fully consumed, so its connection returns to the pool
         279  +
    // and idles (the eviction target) rather than staying checked out.
         280  +
    let idle_timeout = Duration::from_millis(100);
         281  +
         282  +
    let harness = ConnectionTestHarness::builder()
         283  +
        .endpoint(
         284  +
            IP1,
         285  +
            vec![
         286  +
                ConnectionBehavior::RespondKeepAlive {
         287  +
                    status: 200,
         288  +
                    body: b"first",
         289  +
                },
         290  +
                ConnectionBehavior::RespondKeepAlive {
         291  +
                    status: 200,
         292  +
                    body: b"second",
         293  +
                },
         294  +
            ],
         295  +
        )
         296  +
        .build()
         297  +
        .await;
         298  +
         299  +
    let pool = SharedPool::builder()
         300  +
        .dns_resolver(harness.dns_resolver())
         301  +
        .pool_idle_timeout(idle_timeout)
         302  +
        .build_http();
         303  +
    let client = PoolClient::new(&pool).into_shared();
         304  +
    let url = format!("http://127.0.0.1:{}/", harness.endpoints[0].port());
         305  +
         306  +
    // First request: body consumed, connection returns to the pool idle. This
         307  +
    // also lazily spawns the eviction task (pool_idle_timeout is set).
         308  +
    let (status, _) = send_and_read_body(&client, &url)
         309  +
        .await
         310  +
        .expect("first request should succeed");
         311  +
    assert_eq!(status, 200);
         312  +
    assert_eq!(harness.tcp_accepted_count(), 1);
         313  +
         314  +
    // Wait past the idle timeout so the eviction task drops the idle connection.
         315  +
    tokio::time::sleep(idle_timeout * 3).await;
         316  +
         317  +
    // The next request cannot reuse the evicted connection and opens a new one.
         318  +
    let (status, _) = send_and_read_body(&client, &url)
         319  +
        .await
         320  +
        .expect("second request should succeed after idle eviction");
         321  +
    assert_eq!(status, 200);
         322  +
    assert_eq!(
         323  +
        harness.tcp_accepted_count(),
         324  +
        2,
         325  +
        "the idle connection was evicted, so the second request reconnects"
         326  +
    );
         327  +
}
         328  +
         329  +
#[tokio::test]
         330  +
async fn v2_connection_reset_returns_error() {
         331  +
    connection_reset_returns_error(&V2Client).await;
         332  +
}
         333  +
         334  +
#[tokio::test]
         335  +
async fn v2_connector_metadata() {
         336  +
    connector_metadata(&V2Client).await;
         337  +
}
         338  +
         339  +
// ---------------------------------------------------------------------------
         340  +
// Test implementations: origin-form URI + Host header
         341  +
// ---------------------------------------------------------------------------
         342  +
         343  +
/// Requests should be sent with origin-form URI (just the path) and a correct Host header.
         344  +
async fn origin_form_and_host_header(make: &dyn MakeClient) {
         345  +
    let harness = ConnectionTestHarness::builder()
         346  +
        .endpoint(
         347  +
            IP1,
         348  +
            vec![ConnectionBehavior::RespondKeepAlive {
         349  +
                status: 200,
         350  +
                body: b"ok",
         351  +
            }],
         352  +
        )
         353  +
        .build()
         354  +
        .await;
         355  +
         356  +
    let client = make.make(ClientConfig::default());
         357  +
    let port = harness.endpoints[0].port();
         358  +
    let url = format!("http://127.0.0.1:{port}/some/path?key=val");
         359  +
         360  +
    let resp = send_to(&client, &url)
         361  +
        .await
         362  +
        .expect("request should succeed");
         363  +
    assert_eq!(resp.status().as_u16(), 200);
         364  +
         365  +
    let requests = harness.http_requests();
         366  +
    assert_eq!(requests.len(), 1, "expected exactly one HTTP request");
         367  +
    let (uri, host) = &requests[0];
         368  +
         369  +
    // URI must be origin-form (path + query only, no scheme/authority)
         370  +
    assert_eq!(uri, "/some/path?key=val", "URI should be origin-form");
         371  +
         372  +
    // Host header must be present with the correct authority
         373  +
    let host = host.as_deref().expect("Host header should be present");
         374  +
    assert_eq!(
         375  +
        host,
         376  +
        format!("127.0.0.1:{port}"),
         377  +
        "Host header should match authority"
         378  +
    );
         379  +
}
         380  +
         381  +
#[tokio::test]
         382  +
async fn v1_origin_form_and_host_header() {
         383  +
    origin_form_and_host_header(&V1Client).await;
         384  +
}
         385  +
         386  +
#[tokio::test]
         387  +
async fn v2_origin_form_and_host_header() {
         388  +
    origin_form_and_host_header(&V2Client).await;
         389  +
}
         390  +
         391  +
// ---------------------------------------------------------------------------
         392  +
// Test implementations: max_connections
         393  +
// ---------------------------------------------------------------------------
         394  +
         395  +
/// With max_connections(2), concurrent requests should not open more than 2 connections,
         396  +
/// and all requests should succeed (waiting requests served via connection reuse).
         397  +
async fn max_connections_limits_concurrency(make: &dyn MakeClient) {
         398  +
    let harness = ConnectionTestHarness::builder()
         399  +
        .endpoint(
         400  +
            IP1,
         401  +
            (0..10)
         402  +
                .map(|_| ConnectionBehavior::RespondKeepAlive {
         403  +
                    status: 200,
         404  +
                    body: b"ok",
         405  +
                })
         406  +
                .collect(),
         407  +
        )
         408  +
        .build()
         409  +
        .await;
         410  +
         411  +
    let client = make.make(ClientConfig::default().with_max_connections(2));
         412  +
    let port = harness.endpoints[0].port();
         413  +
    let url = format!("http://127.0.0.1:{port}/");
         414  +
         415  +
    // Send 5 concurrent requests — all should succeed
         416  +
    let mut tasks = tokio::task::JoinSet::new();
         417  +
    for _ in 0..5 {
         418  +
        let client = client.clone();
         419  +
        let url = url.clone();
         420  +
        tasks.spawn(async move { send_to(&client, &url).await });
         421  +
    }
         422  +
    let mut success_count = 0;
         423  +
    while let Some(result) = tasks.join_next().await {
         424  +
        let resp = result
         425  +
            .expect("task should not panic")
         426  +
            .expect("request should succeed");
         427  +
        assert_eq!(resp.status().as_u16(), 200);
         428  +
        success_count += 1;
         429  +
    }
         430  +
    assert_eq!(success_count, 5, "all 5 requests should complete");
         431  +
         432  +
    let accepted = harness.tcp_accepted_count();
         433  +
    assert!(
         434  +
        accepted <= 2,
         435  +
        "expected at most 2 connections with max_connections(2), got {accepted}"
         436  +
    );
         437  +
}
         438  +
         439  +
/// Cached (reused) connections don't consume permits — sequential requests
         440  +
/// with max_connections(1) all succeed on one connection.
         441  +
async fn max_connections_reuse_does_not_consume_permits(make: &dyn MakeClient) {
         442  +
    let harness = ConnectionTestHarness::builder()
         443  +
        .endpoint(
         444  +
            IP1,
         445  +
            (0..5)
         446  +
                .map(|_| ConnectionBehavior::RespondKeepAlive {
         447  +
                    status: 200,
         448  +
                    body: b"ok",
         449  +
                })
         450  +
                .collect(),
         451  +
        )
         452  +
        .build()
         453  +
        .await;
         454  +
         455  +
    let client = make.make(ClientConfig::default().with_max_connections(1));
         456  +
    let port = harness.endpoints[0].port();
         457  +
    let url = format!("http://127.0.0.1:{port}/");
         458  +
         459  +
    // 5 sequential requests — all reuse the same connection
         460  +
    for i in 0..5 {
         461  +
        let resp = send_to(&client, &url)
         462  +
            .await
         463  +
            .unwrap_or_else(|e| panic!("request {i} should succeed: {e}"));
         464  +
        assert_eq!(resp.status().as_u16(), 200);
         465  +
    }
         466  +
         467  +
    assert_eq!(
         468  +
        harness.tcp_accepted_count(),
         469  +
        1,
         470  +
        "all requests should reuse one connection"
         471  +
    );
         472  +
}
         473  +
         474  +
#[tokio::test]
         475  +
async fn v2_max_connections_limits_concurrency() {
         476  +
    max_connections_limits_concurrency(&V2Client).await;
         477  +
}
         478  +
         479  +
#[tokio::test]
         480  +
async fn v2_max_connections_reuse_does_not_consume_permits() {
         481  +
    max_connections_reuse_does_not_consume_permits(&V2Client).await;
         482  +
}
         483  +
         484  +
// ---------------------------------------------------------------------------
         485  +
// Test implementations: max_connections_per_host
         486  +
// ---------------------------------------------------------------------------
         487  +
         488  +
async fn is_bindable(ip: IpAddr) -> bool {
         489  +
    tokio::net::TcpListener::bind((ip, 0u16)).await.is_ok()
         490  +
}
         491  +
         492  +
const IP2: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2));
         493  +
         494  +
/// Per-host limit: each host gets its own budget.
         495  +
async fn max_connections_per_host_limits_per_host(make: &dyn MakeClient) {
         496  +
    if !is_bindable(IP2).await {
         497  +
        eprintln!("skipping test: 127.0.0.2 not bindable");
         498  +
        return;
         499  +
    }
         500  +
         501  +
    let harness = ConnectionTestHarness::builder()
         502  +
        .endpoint(
         503  +
            IP1,
         504  +
            (0..10)
         505  +
                .map(|_| ConnectionBehavior::RespondKeepAlive {
         506  +
                    status: 200,
         507  +
                    body: b"ok",
         508  +
                })
         509  +
                .collect(),
         510  +
        )
         511  +
        .endpoint(
         512  +
            IP2,
         513  +
            (0..10)
         514  +
                .map(|_| ConnectionBehavior::RespondKeepAlive {
         515  +
                    status: 200,
         516  +
                    body: b"ok",
         517  +
                })
         518  +
                .collect(),
         519  +
        )
         520  +
        .build()
         521  +
        .await;
         522  +
         523  +
    let client = make.make(ClientConfig::default().with_max_connections_per_host(2));
         524  +
    let port = harness.endpoints[0].port();
         525  +
         526  +
    // Send 4 concurrent requests to each host
         527  +
    let mut tasks = tokio::task::JoinSet::new();
         528  +
    for ip in ["127.0.0.1", "127.0.0.2"] {
         529  +
        for _ in 0..4 {
         530  +
            let client = client.clone();
         531  +
            let url = format!("http://{ip}:{port}/");
         532  +
            tasks.spawn(async move { send_to(&client, &url).await });
         533  +
        }
         534  +
    }
         535  +
    while let Some(result) = tasks.join_next().await {
         536  +
        result
         537  +
            .expect("task should not panic")
         538  +
            .expect("request should succeed");
         539  +
    }
         540  +
         541  +
    let host1 = harness.tcp_accepted_by(IP1);
         542  +
    let host2 = harness.tcp_accepted_by(IP2);
         543  +
    assert!(host1 <= 2, "host 1: expected ≤2 connections, got {host1}");
         544  +
    assert!(host2 <= 2, "host 2: expected ≤2 connections, got {host2}");
         545  +
    // Total exceeds per-host limit (no global limit set)
         546  +
    assert!(host1 + host2 > 2, "total should exceed per-host limit");
         547  +
}
         548  +
         549  +
/// Global and per-host limits compose: global(3) + per_host(2) with 2 hosts.
         550  +
async fn max_connections_global_and_per_host_compose(make: &dyn MakeClient) {
         551  +
    if !is_bindable(IP2).await {
         552  +
        eprintln!("skipping test: 127.0.0.2 not bindable");
         553  +
        return;
         554  +
    }
         555  +
         556  +
    let harness = ConnectionTestHarness::builder()
         557  +
        .endpoint(
         558  +
            IP1,
         559  +
            (0..10)
         560  +
                .map(|_| ConnectionBehavior::RespondKeepAlive {
         561  +
                    status: 200,
         562  +
                    body: b"ok",
         563  +
                })
         564  +
                .collect(),
         565  +
        )
         566  +
        .endpoint(
         567  +
            IP2,
         568  +
            (0..10)
         569  +
                .map(|_| ConnectionBehavior::RespondKeepAlive {
         570  +
                    status: 200,
         571  +
                    body: b"ok",
         572  +
                })
         573  +
                .collect(),
         574  +
        )
         575  +
        .build()
         576  +
        .await;
         577  +
         578  +
    let client = make.make(
         579  +
        ClientConfig::default()
         580  +
            .with_max_connections(3)
         581  +
            .with_max_connections_per_host(2),
         582  +
    );
         583  +
    let port = harness.endpoints[0].port();
         584  +
         585  +
    // Send 4 concurrent requests to each host (8 total)
         586  +
    let mut tasks = tokio::task::JoinSet::new();
         587  +
    for ip in ["127.0.0.1", "127.0.0.2"] {
         588  +
        for _ in 0..4 {
         589  +
            let client = client.clone();
         590  +
            let url = format!("http://{ip}:{port}/");
         591  +
            tasks.spawn(async move { send_to(&client, &url).await });
         592  +
        }
         593  +
    }
         594  +
    while let Some(result) = tasks.join_next().await {
         595  +
        result
         596  +
            .expect("task should not panic")
         597  +
            .expect("request should succeed");
         598  +
    }
         599  +
         600  +
    let host1 = harness.tcp_accepted_by(IP1);
         601  +
    let host2 = harness.tcp_accepted_by(IP2);
         602  +
    // Per-host: each ≤2
         603  +
    assert!(host1 <= 2, "host 1: expected ≤2, got {host1}");
         604  +
    assert!(host2 <= 2, "host 2: expected ≤2, got {host2}");
         605  +
    // Global: total ≤3
         606  +
    assert!(
         607  +
        host1 + host2 <= 3,
         608  +
        "total: expected ≤3 (global limit), got {}",
         609  +
        host1 + host2
         610  +
    );
         611  +
}
         612  +
         613  +
#[tokio::test]
         614  +
async fn v2_max_connections_per_host_limits_per_host() {
         615  +
    max_connections_per_host_limits_per_host(&V2Client).await;
         616  +
}
         617  +
         618  +
#[tokio::test]
         619  +
async fn v2_max_connections_global_and_per_host_compose() {
         620  +
    max_connections_global_and_per_host_compose(&V2Client).await;
         621  +
}
         622  +
         623  +
// ---------------------------------------------------------------------------
         624  +
// Test implementations: connection lifecycle
         625  +
// ---------------------------------------------------------------------------
         626  +
         627  +
/// Without max_connections, connections scale freely under concurrency.
         628  +
async fn no_limit_allows_unbounded_connections(make: &dyn MakeClient) {
         629  +
    let harness = ConnectionTestHarness::builder()
         630  +
        .endpoint(
         631  +
            IP1,
         632  +
            (0..20)
         633  +
                .map(|_| ConnectionBehavior::HoldThenClose(Duration::from_secs(2)))
         634  +
                .collect(),
         635  +
        )
         636  +
        .build()
         637  +
        .await;
         638  +
         639  +
    let client = make.make(ClientConfig::default());
         640  +
    let port = harness.endpoints[0].port();
         641  +
    let url = format!("http://127.0.0.1:{port}/");
         642  +
         643  +
    // Send 5 concurrent requests — server holds each connection open,
         644  +
    // so the client must open 5 separate connections.
         645  +
    let mut tasks = tokio::task::JoinSet::new();
         646  +
    for _ in 0..5 {
         647  +
        let client = client.clone();
         648  +
        let url = url.clone();
         649  +
        tasks.spawn(async move {
         650  +
            // These will fail (server doesn't send HTTP response), but
         651  +
            // the point is that 5 TCP connections are opened concurrently.
         652  +
            let _ = send_to(&client, &url).await;
         653  +
        });
         654  +
    }
         655  +
    // Give time for all connections to be established
         656  +
    tokio::time::sleep(Duration::from_millis(200)).await;
         657  +
         658  +
    let accepted = harness.tcp_accepted_count();
         659  +
    assert!(
         660  +
        accepted >= 4,
         661  +
        "without max_connections, expected ≥4 concurrent connections, got {accepted}"
         662  +
    );
         663  +
         664  +
    // Clean up tasks
         665  +
    tasks.shutdown().await;
         666  +
}
         667  +
         668  +
/// Sequential requests reuse the same connection (body fully consumed between requests).
         669  +
async fn connection_reuse_after_body_consumed(make: &dyn MakeClient) {
         670  +
    let harness = ConnectionTestHarness::builder()
         671  +
        .endpoint(
         672  +
            IP1,
         673  +
            (0..3)
         674  +
                .map(|_| ConnectionBehavior::RespondKeepAlive {
         675  +
                    status: 200,
         676  +
                    body: b"hello world response body",
         677  +
                })
         678  +
                .collect(),
         679  +
        )
         680  +
        .build()
         681  +
        .await;
         682  +
         683  +
    let client = make.make(ClientConfig::default());
         684  +
    let port = harness.endpoints[0].port();
         685  +
    let url = format!("http://127.0.0.1:{port}/");
         686  +
         687  +
    for i in 0..3 {
         688  +
        let (status, body) = send_and_read_body(&client, &url)
         689  +
            .await
         690  +
            .unwrap_or_else(|e| panic!("request {i} should succeed: {e}"));
         691  +
        assert_eq!(status, 200);
         692  +
        assert_eq!(body, b"hello world response body");
         693  +
    }
         694  +
         695  +
    assert_eq!(
         696  +
        harness.tcp_accepted_count(),
         697  +
        1,
         698  +
        "all requests should reuse one connection after body is consumed"
         699  +
    );
         700  +
}
         701  +
         702  +
/// Back-to-back requests reuse one connection despite a short idle timeout:
         703  +
/// each re-checkout beats the eviction tick, so no reconnect occurs.
         704  +
async fn active_connection_survives_idle_timeout(make: &dyn MakeClient) {
         705  +
    let harness = ConnectionTestHarness::builder()
         706  +
        .endpoint(
         707  +
            IP1,
         708  +
            vec![
         709  +
                ConnectionBehavior::RespondKeepAlive {
         710  +
                    status: 200,
         711  +
                    body: b"first",
         712  +
                },
         713  +
                ConnectionBehavior::RespondKeepAlive {
         714  +
                    status: 200,
         715  +
                    body: b"second",
         716  +
                },
         717  +
                ConnectionBehavior::RespondKeepAlive {
         718  +
                    status: 200,
         719  +
                    body: b"third",
         720  +
                },
         721  +
            ],
         722  +
        )
         723  +
        .build()
         724  +
        .await;
         725  +
         726  +
    // Use a very short idle timeout — but sequential requests should still
         727  +
    // reuse the connection because the pool doesn't evict between requests
         728  +
    // that happen back-to-back.
         729  +
    let client = make.make(ClientConfig::default().with_idle_timeout(Duration::from_millis(100)));
         730  +
    let port = harness.endpoints[0].port();
         731  +
    let url = format!("http://127.0.0.1:{port}/");
         732  +
         733  +
    // Three back-to-back requests — all should reuse the same connection
         734  +
    let expected = [b"first".as_slice(), b"second", b"third"];
         735  +
    for (i, expected_body) in expected.iter().enumerate() {
         736  +
        let (status, body) = send_and_read_body(&client, &url)
         737  +
            .await
         738  +
            .unwrap_or_else(|e| panic!("request {i} should succeed: {e}"));
         739  +
        assert_eq!(status, 200);
         740  +
        assert_eq!(body, *expected_body, "request {i} body mismatch");
         741  +
    }
         742  +
         743  +
    assert_eq!(
         744  +
        harness.tcp_accepted_count(),
         745  +
        1,
         746  +
        "back-to-back requests should reuse one connection even with short idle timeout"
         747  +
    );
         748  +
}
         749  +
         750  +
#[tokio::test]
         751  +
async fn v2_no_limit_allows_unbounded_connections() {
         752  +
    no_limit_allows_unbounded_connections(&V2Client).await;
         753  +
}
         754  +
         755  +
#[tokio::test]
         756  +
async fn v2_connection_reuse_after_body_consumed() {
         757  +
    connection_reuse_after_body_consumed(&V2Client).await;
         758  +
}
         759  +
         760  +
#[tokio::test]
         761  +
async fn v2_active_connection_survives_idle_timeout() {
         762  +
    active_connection_survives_idle_timeout(&V2Client).await;
         763  +
}
         764  +
         765  +
// ---------------------------------------------------------------------------
         766  +
// Test implementations: connection lifecycle
         767  +
// ---------------------------------------------------------------------------
         768  +
         769  +
/// Server closes an idle connection (simulating server-side idle timeout).
         770  +
/// The connection returns to the pool clean, then dies while idle. On next
         771  +
/// checkout, poll_ready detects the dead connection, the checkout loop
         772  +
/// discards it, and a fresh connection is created.
         773  +
async fn stale_connection_detected_at_checkout(make: &dyn MakeClient) {
         774  +
    let harness = ConnectionTestHarness::builder()
         775  +
        .endpoint(
         776  +
            IP1,
         777  +
            vec![
         778  +
                // First connection: respond with keep-alive, then close after 30ms
         779  +
                ConnectionBehavior::RespondThenIdleClose {
         780  +
                    status: 200,
         781  +
                    body: b"first",
         782  +
                    idle: Duration::from_millis(30),
         783  +
                },
         784  +
                // Second connection (after stale one is discarded)
         785  +
                ConnectionBehavior::RespondKeepAlive {
         786  +
                    status: 200,
         787  +
                    body: b"second",
         788  +
                },
         789  +
            ],
         790  +
        )
         791  +
        .build()
         792  +
        .await;
         793  +
         794  +
    let client = make.make(ClientConfig::default());
         795  +
    let port = harness.endpoints[0].port();
         796  +
    let url = format!("http://127.0.0.1:{port}/");
         797  +
         798  +
    // First request succeeds, body consumed, connection returns to pool
         799  +
    let (status, body) = send_and_read_body(&client, &url)
         800  +
        .await
         801  +
        .expect("first request");
         802  +
    assert_eq!(status, 200);
         803  +
    assert_eq!(body, b"first");
         804  +
         805  +
    // Wait for server to close the idle connection (30ms idle + margin)
         806  +
    tokio::time::sleep(Duration::from_millis(80)).await;
         807  +
         808  +
    // Second request: checkout finds stale connection, discards, creates new
         809  +
    let (status, body) = send_and_read_body(&client, &url)
         810  +
        .await
         811  +
        .expect("second request should succeed on fresh connection");
         812  +
    assert_eq!(status, 200);
         813  +
    assert_eq!(body, b"second");
         814  +
         815  +
    assert_eq!(
         816  +
        harness.tcp_accepted_count(),
         817  +
        2,
         818  +
        "should have opened 2 connections (first died while idle in pool)"
         819  +
    );
         820  +
}
         821  +
         822  +
/// When the response body is not consumed, the connection is held by the
         823  +
/// body guard and unavailable for reuse. A concurrent request must open
         824  +
/// a new connection.
         825  +
async fn unconsumed_body_holds_connection(make: &dyn MakeClient) {
         826  +
    let harness = ConnectionTestHarness::builder()
         827  +
        .endpoint(
         828  +
            IP1,
         829  +
            vec![
         830  +
                ConnectionBehavior::RespondKeepAlive {
         831  +
                    status: 200,
         832  +
                    body: b"first",
         833  +
                },
         834  +
                ConnectionBehavior::RespondKeepAlive {
         835  +
                    status: 200,
         836  +
                    body: b"second",
         837  +
                },
         838  +
            ],
         839  +
        )
         840  +
        .build()
         841  +
        .await;
         842  +
         843  +
    let client = make.make(ClientConfig::default());
         844  +
    let port = harness.endpoints[0].port();
         845  +
    let url = format!("http://127.0.0.1:{port}/");
         846  +
         847  +
    // First request — hold the response (body unconsumed, connection held)
         848  +
    let _held_resp = send_to(&client, &url).await.expect("first request");
         849  +
         850  +
    // Second request while first response is still held — must open new connection
         851  +
    let (status, body) = send_and_read_body(&client, &url)
         852  +
        .await
         853  +
        .expect("second request");
         854  +
    assert_eq!(status, 200);
         855  +
    assert_eq!(body, b"second");
         856  +
         857  +
    assert_eq!(
         858  +
        harness.tcp_accepted_count(),
         859  +
        2,
         860  +
        "second request should open a new connection while first body is held"
         861  +
    );
         862  +
         863  +
    // Drop the held response — connection guard released
         864  +
    drop(_held_resp);
         865  +
}
         866  +
         867  +
/// Server sends Connection: close — client should not reuse the connection.
         868  +
async fn connection_close_header_prevents_reuse(make: &dyn MakeClient) {
         869  +
    let harness = ConnectionTestHarness::builder()
         870  +
        .endpoint(
         871  +
            IP1,
         872  +
            vec![
         873  +
                ConnectionBehavior::RespondThenClose {
         874  +
                    status: 200,
         875  +
                    body: b"closing",
         876  +
                },
         877  +
                ConnectionBehavior::RespondKeepAlive {
         878  +
                    status: 200,
         879  +
                    body: b"fresh",
         880  +
                },
         881  +
            ],
         882  +
        )
         883  +
        .build()
         884  +
        .await;
         885  +
         886  +
    let client = make.make(ClientConfig::default());
         887  +
    let port = harness.endpoints[0].port();
         888  +
    let url = format!("http://127.0.0.1:{port}/");
         889  +
         890  +
    let (status, body) = send_and_read_body(&client, &url)
         891  +
        .await
         892  +
        .expect("first request");
         893  +
    assert_eq!(status, 200);
         894  +
    assert_eq!(body, b"closing");
         895  +
         896  +
    // Connection: close means the client should not attempt to reuse
         897  +
    let (status, body) = send_and_read_body(&client, &url)
         898  +
        .await
         899  +
        .expect("second request");
         900  +
    assert_eq!(status, 200);
         901  +
    assert_eq!(body, b"fresh");
         902  +
         903  +
    assert_eq!(
         904  +
        harness.tcp_accepted_count(),
         905  +
        2,
         906  +
        "Connection: close should prevent reuse"
         907  +
    );
         908  +
}
         909  +
         910  +
#[tokio::test]
         911  +
async fn v2_stale_connection_detected_at_checkout() {
         912  +
    stale_connection_detected_at_checkout(&V2Client).await;
         913  +
}
         914  +
         915  +
#[tokio::test]
         916  +
async fn v2_unconsumed_body_holds_connection() {
         917  +
    unconsumed_body_holds_connection(&V2Client).await;
         918  +
}
         919  +
         920  +
#[tokio::test]
         921  +
async fn v2_connection_close_header_prevents_reuse() {
         922  +
    connection_close_header_prevents_reuse(&V2Client).await;
         923  +
}
         924  +
         925  +
// ---------------------------------------------------------------------------
         926  +
// Test implementations: connection poisoning
         927  +
// ---------------------------------------------------------------------------
         928  +
         929  +
/// Send a request and return both the response body and the `ConnectionMetadata`
         930  +
/// captured by a `CaptureSmithyConnection` attached to the request. This is the
         931  +
/// integration surface the `ConnectionPoisoningInterceptor` uses in real SDK flows
         932  +
/// — the adapter must populate a retriever that returns live metadata pointing
         933  +
/// at the connection selected for this request.
         934  +
async fn send_with_capture(
         935  +
    client: &SharedHttpClient,
         936  +
    url: &str,
         937  +
) -> (
         938  +
    u16,
         939  +
    Vec<u8>,
         940  +
    Option<aws_smithy_runtime_api::client::connection::ConnectionMetadata>,
         941  +
) {
         942  +
    use aws_smithy_runtime_api::client::connection::CaptureSmithyConnection;
         943  +
    use http_body_util::BodyExt;
         944  +
         945  +
    let settings = HttpConnectorSettings::builder().build();
         946  +
    let components = runtime_components();
         947  +
    let connector = client.http_connector(&settings, &components);
         948  +
         949  +
    let capture = CaptureSmithyConnection::new();
         950  +
    let mut request = HttpRequest::get(url).expect("valid HTTP request");
         951  +
    request.add_extension(capture.clone());
         952  +
         953  +
    let resp = connector
         954  +
        .call(request)
         955  +
        .await
         956  +
        .expect("request should succeed");
         957  +
    let status = resp.status().as_u16();
         958  +
    let body = resp
         959  +
        .into_body()
         960  +
        .collect()
         961  +
        .await
         962  +
        .expect("body should be readable")
         963  +
        .to_bytes()
         964  +
        .to_vec();
         965  +
    (status, body, capture.get())
         966  +
}
         967  +
         968  +
/// Poisoning a connection prevents it from being reused.
         969  +
///
         970  +
/// Mirrors the production flow: `ConnectionPoisoningInterceptor` attaches a
         971  +
/// `CaptureSmithyConnection` to the request, the adapter populates it with
         972  +
/// metadata pointing at the selected connection, and on a transient error
         973  +
/// the interceptor calls `ConnectionMetadata::poison()`. The next request
         974  +
/// for the same host must establish a new TCP connection instead of reusing
         975  +
/// the poisoned one.
         976  +
async fn poisoned_connection_not_reused(make: &dyn MakeClient) {
         977  +
    let harness = ConnectionTestHarness::builder()
         978  +
        .endpoint(
         979  +
            IP1,
         980  +
            vec![
         981  +
                ConnectionBehavior::RespondKeepAlive {
         982  +
                    status: 200,
         983  +
                    body: b"first",
         984  +
                },
         985  +
                ConnectionBehavior::RespondKeepAlive {
         986  +
                    status: 200,
         987  +
                    body: b"second",
         988  +
                },
         989  +
            ],
         990  +
        )
         991  +
        .build()
         992  +
        .await;
         993  +
         994  +
    let url = format!("http://127.0.0.1:{}/", harness.endpoints[0].port());
         995  +
    let client = make.make(ClientConfig::default());
         996  +
         997  +
    // First request: establish connection, capture metadata.
         998  +
    let (status, body, metadata) = send_with_capture(&client, &url).await;
         999  +
    assert_eq!(status, 200);
        1000  +
    assert_eq!(body, b"first");
        1001  +
    let metadata = metadata.expect("adapter should populate CaptureSmithyConnection");
        1002  +
    assert_eq!(
        1003  +
        harness.tcp_accepted_count(),
        1004  +
        1,
        1005  +
        "first request opens one connection"
        1006  +
    );
        1007  +
        1008  +
    // Poison the connection — what the orchestrator does on a transient error.
        1009  +
    metadata.poison();
        1010  +
        1011  +
    // Next request to the same host must open a NEW connection; the poisoned
        1012  +
    // one is skipped on checkout and dropped on return.
        1013  +
    let (status, body, _) = send_with_capture(&client, &url).await;
        1014  +
    assert_eq!(status, 200);
        1015  +
    assert_eq!(body, b"second");
        1016  +
    assert_eq!(
        1017  +
        harness.tcp_accepted_count(),
        1018  +
        2,
        1019  +
        "poisoned connection must not be reused"
        1020  +
    );
        1021  +
}
        1022  +
        1023  +
/// When no transient error occurs, the captured metadata is handed out but
        1024  +
/// never poisoned — the connection returns to the pool and is reused.
        1025  +
/// This is the non-poison control for `poisoned_connection_not_reused`.
        1026  +
async fn capture_without_poison_allows_reuse(make: &dyn MakeClient) {
        1027  +
    let harness = ConnectionTestHarness::builder()
        1028  +
        .endpoint(
        1029  +
            IP1,
        1030  +
            vec![
        1031  +
                ConnectionBehavior::RespondKeepAlive {
        1032  +
                    status: 200,
        1033  +
                    body: b"first",
        1034  +
                },
        1035  +
                ConnectionBehavior::RespondKeepAlive {
        1036  +
                    status: 200,
        1037  +
                    body: b"second",
        1038  +
                },
        1039  +
            ],
        1040  +
        )
        1041  +
        .build()
        1042  +
        .await;
        1043  +
        1044  +
    let url = format!("http://127.0.0.1:{}/", harness.endpoints[0].port());
        1045  +
    let client = make.make(ClientConfig::default());
        1046  +
        1047  +
    let (status, body, metadata) = send_with_capture(&client, &url).await;
        1048  +
    assert_eq!(status, 200);
        1049  +
    assert_eq!(body, b"first");
        1050  +
    assert!(
        1051  +
        metadata.is_some(),
        1052  +
        "adapter should populate CaptureSmithyConnection"
        1053  +
    );
        1054  +
    // Deliberately not calling poison().
        1055  +
    drop(metadata);
        1056  +
        1057  +
    let (status, body, _) = send_with_capture(&client, &url).await;
        1058  +
    assert_eq!(status, 200);
        1059  +
    assert_eq!(body, b"second");
        1060  +
    assert_eq!(
        1061  +
        harness.tcp_accepted_count(),
        1062  +
        1,
        1063  +
        "without poison the connection should be reused"
        1064  +
    );
        1065  +
}
        1066  +
        1067  +
#[tokio::test]
        1068  +
async fn v2_poisoned_connection_not_reused() {
        1069  +
    poisoned_connection_not_reused(&V2Client).await;
        1070  +
}
        1071  +
        1072  +
#[tokio::test]
        1073  +
async fn v2_capture_without_poison_allows_reuse() {
        1074  +
    capture_without_poison_allows_reuse(&V2Client).await;
        1075  +
}
        1076  +
        1077  +
// ---------------------------------------------------------------------------
        1078  +
// Test implementations: per-operation timeouts
        1079  +
// ---------------------------------------------------------------------------
        1080  +
        1081  +
/// Read timeout fires when the server accepts TCP but never sends a response.
        1082  +
///
        1083  +
/// Exercises the v2 adapter's per-op timeout wrapping end-to-end: the
        1084  +
/// `HttpConnectorSettings::read_timeout` flows through
        1085  +
/// `HttpClient::http_connector` into `PooledConnector`, wraps
        1086  +
/// `pool.send_request`, fires because `HoldThenClose` never replies, and
        1087  +
/// produces a `ConnectorError::timeout` classified by `downcast_error`.
        1088  +
async fn v2_read_timeout_fires_on_silent_server() {
        1089  +
    use aws_smithy_async::rt::sleep::{SharedAsyncSleep, TokioSleep};
        1090  +
        1091  +
    let harness = ConnectionTestHarness::builder()
        1092  +
        .endpoint(
        1093  +
            IP1,
        1094  +
            vec![ConnectionBehavior::HoldThenClose(Duration::from_secs(30))],
        1095  +
        )
        1096  +
        .build()
        1097  +
        .await;
        1098  +
        1099  +
    let client = V2Client.make(ClientConfig::default());
        1100  +
    let components =
        1101  +
        aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder::for_tests()
        1102  +
            .with_time_source(Some(SystemTimeSource::new()))
        1103  +
            .with_sleep_impl(Some(SharedAsyncSleep::new(TokioSleep::new())))
        1104  +
            .build()
        1105  +
            .expect("valid runtime components");
        1106  +
    let settings = HttpConnectorSettings::builder()
        1107  +
        .read_timeout(Duration::from_millis(200))
        1108  +
        .build();
        1109  +
    let connector = client.http_connector(&settings, &components);
        1110  +
        1111  +
    let url = format!("http://127.0.0.1:{}/", harness.endpoints[0].port());
        1112  +
    let start = std::time::Instant::now();
        1113  +
    let err = connector
        1114  +
        .call(HttpRequest::get(&url).expect("valid HTTP request"))
        1115  +
        .await
        1116  +
        .expect_err("read timeout should fire against a non-responsive server");
        1117  +
    let elapsed = start.elapsed();
        1118  +
        1119  +
    assert!(
        1120  +
        err.is_timeout(),
        1121  +
        "expected timeout classification, got {err:?}"
        1122  +
    );
        1123  +
    assert!(
        1124  +
        elapsed < Duration::from_secs(2),
        1125  +
        "read timeout did not fire in time (took {elapsed:?})"
        1126  +
    );
        1127  +
}
        1128  +
        1129  +
#[tokio::test]
        1130  +
async fn v2_read_timeout() {
        1131  +
    v2_read_timeout_fires_on_silent_server().await;
        1132  +
}
        1133  +
        1134  +
// ---------------------------------------------------------------------------
        1135  +
// Tracing output assertions
        1136  +
// ---------------------------------------------------------------------------
        1137  +
//
        1138  +
// These tests verify the pool emits useful structured tracing events.
        1139  +
// They use a thread-local subscriber and `current_thread` tokio so spawned
        1140  +
// tasks (eviction) run on the same thread. Must be run with
        1141  +
// `--test-threads=1` to avoid subscriber interference from parallel tests.
        1142  +
        1143  +
fn capture_pool_logs() -> (
        1144  +
    tracing::subscriber::DefaultGuard,
        1145  +
    std::sync::Arc<std::sync::Mutex<Vec<u8>>>,
        1146  +
) {
        1147  +
    use std::io::Write;
        1148  +
    use std::sync::{Arc, Mutex};
        1149  +
        1150  +
    struct BufWriter(Arc<Mutex<Vec<u8>>>);
        1151  +
    impl Write for BufWriter {
        1152  +
        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        1153  +
            self.0.lock().unwrap().extend_from_slice(buf);
        1154  +
            Ok(buf.len())
        1155  +
        }
        1156  +
        fn flush(&mut self) -> std::io::Result<()> {
        1157  +
            Ok(())
        1158  +
        }
        1159  +
    }
        1160  +
        1161  +
    let buf = Arc::new(Mutex::new(Vec::<u8>::new()));
        1162  +
    let buf_clone = buf.clone();
        1163  +
    let subscriber = tracing_subscriber::fmt()
        1164  +
        .with_ansi(false)
        1165  +
        .with_max_level(tracing::Level::TRACE)
        1166  +
        .with_writer(move || BufWriter(buf_clone.clone()))
        1167  +
        .finish();
        1168  +
    let guard = tracing::subscriber::set_default(subscriber);
        1169  +
    (guard, buf)
        1170  +
}
        1171  +
        1172  +
fn captured_str(buf: &std::sync::Arc<std::sync::Mutex<Vec<u8>>>) -> String {
        1173  +
    String::from_utf8(buf.lock().unwrap().clone()).expect("captured logs are utf-8")
        1174  +
}
        1175  +
        1176  +
/// Background eviction task emits structured tracing events including
        1177  +
/// pool init, connection established, eviction with reason, and host
        1178  +
/// entry removal.
        1179  +
///
        1180  +
/// Requires `--test-threads=1` due to thread-local subscriber; run via:
        1181  +
/// `cargo test --features default-client,test-util,wire-mock --test pool_behavior_test -- --ignored --test-threads=1`
        1182  +
#[tokio::test(flavor = "current_thread")]
        1183  +
#[serial_test::serial(tracing)]
        1184  +
#[ignore]
        1185  +
async fn v2_background_eviction_emits_tracing_events() {
        1186  +
    let (_guard, logs) = capture_pool_logs();
        1187  +
        1188  +
    let server = WireMockServer::start(vec![ReplayedEvent::status(200)]).await;
        1189  +
        1190  +
    let idle_timeout = Duration::from_millis(100);
        1191  +
    let client = V2Client.make(ClientConfig::default().with_idle_timeout(idle_timeout));
        1192  +
    let url = localhost_url(&server);
        1193  +
        1194  +
    let status = send_to(&client, &url)
        1195  +
        .await
        1196  +
        .expect("first request should succeed")
        1197  +
        .status()
        1198  +
        .as_u16();
        1199  +
    assert_eq!(status, 200);
        1200  +
        1201  +
    // Wait past idle timeout + eviction tick intervals.
        1202  +
    tokio::time::sleep(idle_timeout * 5).await;
        1203  +
        1204  +
    let captured = captured_str(&logs);
        1205  +
    assert!(
        1206  +
        captured.contains("pool: initialized"),
        1207  +
        "expected pool init log. captured:\n{captured}"
        1208  +
    );
        1209  +
    assert!(
        1210  +
        captured.contains("pool: eviction task spawned"),
        1211  +
        "expected eviction task spawn log. captured:\n{captured}"
        1212  +
    );
        1213  +
    assert!(
        1214  +
        captured.contains("pool: connection established"),
        1215  +
        "expected connection-established log. captured:\n{captured}"
        1216  +
    );
        1217  +
    assert!(
        1218  +
        captured.contains("pool: connection evicted"),
        1219  +
        "expected connection-evicted log. captured:\n{captured}"
        1220  +
    );
        1221  +
    assert!(
        1222  +
        captured.contains("pool: host entry removed"),
        1223  +
        "expected host-entry-removed log. captured:\n{captured}"
        1224  +
    );
        1225  +
}
        1226  +
        1227  +
/// conn_id is stable (same across a connection's lifetime) and monotonically
        1228  +
/// increasing (new connection after eviction gets the next id).
        1229  +
///
        1230  +
/// Requires `--test-threads=1`; see `v2_background_eviction_emits_tracing_events`.
        1231  +
#[tokio::test(flavor = "current_thread")]
        1232  +
#[serial_test::serial(tracing)]
        1233  +
#[ignore]
        1234  +
async fn v2_conn_id_is_stable_and_monotonic() {
        1235  +
    let (_guard, logs) = capture_pool_logs();
        1236  +
        1237  +
    let server =
        1238  +
        WireMockServer::start(vec![ReplayedEvent::status(200), ReplayedEvent::status(200)]).await;
        1239  +
        1240  +
    let idle_timeout = Duration::from_millis(100);
        1241  +
    let client = V2Client.make(ClientConfig::default().with_idle_timeout(idle_timeout));
        1242  +
    let url = localhost_url(&server);
        1243  +
        1244  +
    send_to(&client, &url).await.expect("first request");
        1245  +
    tokio::time::sleep(idle_timeout * 5).await;
        1246  +
    send_to(&client, &url).await.expect("second request");
        1247  +
        1248  +
    let captured = captured_str(&logs);
        1249  +
    assert!(
        1250  +
        captured.contains("conn_id=0"),
        1251  +
        "first connection should be conn_id=0. captured:\n{captured}"
        1252  +
    );
        1253  +
    assert!(
        1254  +
        captured.contains("conn_id=1"),
        1255  +
        "second connection (after eviction) should be conn_id=1. captured:\n{captured}"
        1256  +
    );
        1257  +
}
        1258  +
        1259  +
// ---------------------------------------------------------------------------
        1260  +
// Connect timeout
        1261  +
// ---------------------------------------------------------------------------
        1262  +
        1263  +
/// Proves that `connect_timeout` from `HttpConnectorSettings` fires when the
        1264  +
/// TCP connection cannot be established within the deadline. Uses TEST-NET-1
        1265  +
/// (192.0.2.1), a non-routable address guaranteed to black-hole SYN packets.
        1266  +
#[tokio::test]
        1267  +
async fn v2_connect_timeout() {
        1268  +
    use aws_smithy_async::rt::sleep::{SharedAsyncSleep, TokioSleep};
        1269  +
        1270  +
    let client = V2Client.make(ClientConfig::default());
        1271  +
    let components =
        1272  +
        aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder::for_tests()
        1273  +
            .with_time_source(Some(SystemTimeSource::new()))
        1274  +
            .with_sleep_impl(Some(SharedAsyncSleep::new(TokioSleep::new())))
        1275  +
            .build()
        1276  +
            .expect("valid runtime components");
        1277  +
    let settings = HttpConnectorSettings::builder()
        1278  +
        .connect_timeout(Duration::from_millis(500))
        1279  +
        .build();
        1280  +
    let connector = client.http_connector(&settings, &components);
        1281  +
        1282  +
    let start = std::time::Instant::now();
        1283  +
    let err = connector
        1284  +
        .call(HttpRequest::get("http://192.0.2.1:1234/unreachable").expect("valid request"))
        1285  +
        .await
        1286  +
        .expect_err("connect timeout should fire against non-routable address");
        1287  +
    let elapsed = start.elapsed();
        1288  +
        1289  +
    assert!(
        1290  +
        err.is_timeout(),
        1291  +
        "expected timeout classification, got {err:?}"
        1292  +
    );
        1293  +
    assert!(
        1294  +
        elapsed >= Duration::from_millis(450),
        1295  +
        "timeout fired too early ({elapsed:?})"
        1296  +
    );
        1297  +
    assert!(
        1298  +
        elapsed < Duration::from_secs(5),
        1299  +
        "timeout took too long ({elapsed:?})"
        1300  +
    );
        1301  +
}
        1302  +
        1303  +
/// `ConnectionMetadata::connection_id()` surfaces the pool-assigned id
        1304  +
/// through `CaptureSmithyConnection`. Sequential requests on the same
        1305  +
/// connection share the same id; a new connection after eviction gets a
        1306  +
/// different id.
        1307  +
#[tokio::test]
        1308  +
async fn v2_connection_id_surfaced_through_metadata() {
        1309  +
    use aws_smithy_runtime_api::client::connection::ConnectionId;
        1310  +
        1311  +
    let harness = ConnectionTestHarness::builder()
        1312  +
        .endpoint(
        1313  +
            IP1,
        1314  +
            vec![
        1315  +
                ConnectionBehavior::RespondKeepAlive {
        1316  +
                    status: 200,
        1317  +
                    body: b"a",
        1318  +
                },
        1319  +
                ConnectionBehavior::RespondKeepAlive {
        1320  +
                    status: 200,
        1321  +
                    body: b"b",
        1322  +
                },
        1323  +
                ConnectionBehavior::RespondKeepAlive {
        1324  +
                    status: 200,
        1325  +
                    body: b"c",
        1326  +
                },
        1327  +
            ],
        1328  +
        )
        1329  +
        .build()
        1330  +
        .await;
        1331  +
        1332  +
    let client = V2Client.make(ClientConfig {
        1333  +
        idle_timeout: Some(Duration::from_millis(80)),
        1334  +
        ..Default::default()
        1335  +
    });
        1336  +
    let url = format!("http://127.0.0.1:{}/", harness.endpoints[0].port());
        1337  +
        1338  +
    // First two requests reuse the same connection.
        1339  +
    let (_, _, meta1) = send_with_capture(&client, &url).await;
        1340  +
    let (_, _, meta2) = send_with_capture(&client, &url).await;
        1341  +
    let id1 = meta1
        1342  +
        .unwrap()
        1343  +
        .connection_id()
        1344  +
        .expect("v2 sets connection_id");
        1345  +
    let id2 = meta2
        1346  +
        .unwrap()
        1347  +
        .connection_id()
        1348  +
        .expect("v2 sets connection_id");
        1349  +
    assert_eq!(id1, id2, "same connection should have same id");
        1350  +
    assert_eq!(id1, ConnectionId::new(0));
        1351  +
        1352  +
    // Wait for eviction, then the next request gets a new connection.
        1353  +
    tokio::time::sleep(Duration::from_millis(200)).await;
        1354  +
        1355  +
    let (_, _, meta3) = send_with_capture(&client, &url).await;
        1356  +
    let id3 = meta3
        1357  +
        .unwrap()
        1358  +
        .connection_id()
        1359  +
        .expect("v2 sets connection_id");
        1360  +
    assert_ne!(
        1361  +
        id1, id3,
        1362  +
        "new connection after eviction should have different id"
        1363  +
    );
        1364  +
    assert_eq!(id3, ConnectionId::new(1));
        1365  +
}
        1366  +
        1367  +
// ---- ConnectionEventListener tests ----
        1368  +
        1369  +
mod listener_tests {
        1370  +
    use super::*;
        1371  +
    use aws_smithy_http_client::pool::{
        1372  +
        CloseReason, ConnectionClosedEvent, ConnectionCreatedEvent, ConnectionEventListener,
        1373  +
        ConnectionFailedEvent, ConnectionReusedEvent,
        1374  +
    };
        1375  +
    use std::sync::{Arc, Mutex};
        1376  +
        1377  +
    /// Records connection lifecycle events for test assertions.
        1378  +
    #[derive(Debug, Clone, Default)]
        1379  +
    struct RecordingListener {
        1380  +
        /// (conn_id, authority) for each connection created
        1381  +
        created: Arc<Mutex<Vec<(u64, String)>>>,
        1382  +
        /// (conn_id, authority) for each connection reused from idle
        1383  +
        reused: Arc<Mutex<Vec<(u64, String)>>>,
        1384  +
        /// (conn_id, authority, reason) for each connection closed
        1385  +
        closed: Arc<Mutex<Vec<(u64, String, CloseReason)>>>,
        1386  +
        /// authority for each failed connection attempt
        1387  +
        failed: Arc<Mutex<Vec<String>>>,
        1388  +
    }
        1389  +
        1390  +
    impl ConnectionEventListener for RecordingListener {
        1391  +
        fn on_created(&self, event: &ConnectionCreatedEvent) {
        1392  +
            self.created.lock().unwrap().push((
        1393  +
                event.conn_id().to_string().parse().unwrap(),
        1394  +
                event.authority().as_str().to_string(),
        1395  +
            ));
        1396  +
        }
        1397  +
        fn on_reused(&self, event: &ConnectionReusedEvent) {
        1398  +
            self.reused.lock().unwrap().push((
        1399  +
                event.conn_id().to_string().parse().unwrap(),
        1400  +
                event.authority().as_str().to_string(),
        1401  +
            ));
        1402  +
        }
        1403  +
        fn on_closed(&self, event: &ConnectionClosedEvent) {
        1404  +
            self.closed.lock().unwrap().push((
        1405  +
                event.conn_id().to_string().parse().unwrap(),
        1406  +
                event.authority().as_str().to_string(),
        1407  +
                event.reason(),
        1408  +
            ));
        1409  +
        }
        1410  +
        fn on_connection_failed(&self, event: &ConnectionFailedEvent) {
        1411  +
            self.failed
        1412  +
                .lock()
        1413  +
                .unwrap()
        1414  +
                .push(event.authority().as_str().to_string());
        1415  +
        }
        1416  +
    }
        1417  +
        1418  +
    fn make_v2_with_listener(
        1419  +
        harness: &ConnectionTestHarness,
        1420  +
        idle_timeout: Option<Duration>,
        1421  +
        listener: Arc<dyn ConnectionEventListener>,
        1422  +
    ) -> SharedHttpClient {
        1423  +
        let mut builder = aws_smithy_http_client::pool::SharedPool::builder()
        1424  +
            .connection_event_listener(listener)
        1425  +
            .dns_resolver(harness.dns_resolver());
        1426  +
        if let Some(timeout) = idle_timeout {
        1427  +
            builder = builder.pool_idle_timeout(timeout);
        1428  +
        }
        1429  +
        let pool = builder.build_http();
        1430  +
        PoolClient::new(&pool).into_shared()
        1431  +
    }
        1432  +
        1433  +
    /// Listener receives created on first request, reused on second, and
        1434  +
    /// closed(IdleTimeout) after eviction.
        1435  +
    #[tokio::test]
        1436  +
    async fn listener_lifecycle_created_reused_closed() {
        1437  +
        let harness = ConnectionTestHarness::builder()
        1438  +
            .endpoint(
        1439  +
                IP1,
        1440  +
                vec![
        1441  +
                    ConnectionBehavior::RespondKeepAlive {
        1442  +
                        status: 200,
        1443  +
                        body: b"a",
        1444  +
                    },
        1445  +
                    ConnectionBehavior::RespondKeepAlive {
        1446  +
                        status: 200,
        1447  +
                        body: b"b",
        1448  +
                    },
        1449  +
                ],
        1450  +
            )
        1451  +
            .build()
        1452  +
            .await;
        1453  +
        1454  +
        let listener = Arc::new(RecordingListener::default());
        1455  +
        let client =
        1456  +
            make_v2_with_listener(&harness, Some(Duration::from_millis(80)), listener.clone());
        1457  +
        let url = format!("http://127.0.0.1:{}/", harness.endpoints[0].port());
        1458  +
        1459  +
        // First request: on_created
        1460  +
        send_and_read_body(&client, &url).await;
        1461  +
        assert_eq!(listener.created.lock().unwrap().len(), 1);
        1462  +
        assert_eq!(listener.created.lock().unwrap()[0].0, 0);
        1463  +
        assert!(listener.created.lock().unwrap()[0].1.contains("127.0.0.1"));
        1464  +
        1465  +
        // Second request: on_reused
        1466  +
        send_and_read_body(&client, &url).await;
        1467  +
        assert_eq!(listener.reused.lock().unwrap().len(), 1);
        1468  +
        assert_eq!(listener.reused.lock().unwrap()[0].0, 0);
        1469  +
        1470  +
        // Wait for eviction
        1471  +
        tokio::time::sleep(Duration::from_millis(200)).await;
        1472  +
        1473  +
        let closed = listener.closed.lock().unwrap();
        1474  +
        assert_eq!(closed.len(), 1);
        1475  +
        assert_eq!(closed[0].0, 0);
        1476  +
        assert_eq!(closed[0].2, CloseReason::IdleTimeout);
        1477  +
    }
        1478  +
        1479  +
    /// Poisoning a connection fires on_closed with Poisoned reason.
        1480  +
    #[tokio::test]
        1481  +
    async fn listener_poisoned_connection_fires_on_closed() {
        1482  +
        let harness = ConnectionTestHarness::builder()
        1483  +
            .endpoint(
        1484  +
                IP1,
        1485  +
                vec![
        1486  +
                    ConnectionBehavior::RespondKeepAlive {
        1487  +
                        status: 200,
        1488  +
                        body: b"ok",
        1489  +
                    },
        1490  +
                    ConnectionBehavior::RespondKeepAlive {
        1491  +
                        status: 200,
        1492  +
                        body: b"ok",
        1493  +
                    },
        1494  +
                ],
        1495  +
            )
        1496  +
            .build()
        1497  +
            .await;
        1498  +
        1499  +
        let listener = Arc::new(RecordingListener::default());
        1500  +
        let client = make_v2_with_listener(&harness, None, listener.clone());
        1501  +
        let url = format!("http://127.0.0.1:{}/", harness.endpoints[0].port());
        1502  +
        1503  +
        // Make a request and poison the connection
        1504  +
        let (_, _, meta) = super::send_with_capture(&client, &url).await;
        1505  +
        meta.unwrap().poison();
        1506  +
        1507  +
        // The poisoned connection is discarded on next checkout attempt
        1508  +
        send_and_read_body(&client, &url).await;
        1509  +
        1510  +
        let closed = listener.closed.lock().unwrap();
        1511  +
        assert_eq!(closed.len(), 1);
        1512  +
        assert_eq!(closed[0].2, CloseReason::Poisoned);
        1513  +
    }
        1514  +
        1515  +
    /// Connection failure fires on_connection_failed with the authority.
        1516  +
    #[tokio::test]
        1517  +
    async fn listener_connection_failed() {
        1518  +
        let listener = Arc::new(RecordingListener::default());
        1519  +
        1520  +
        // Use a harness with no endpoints so the connection will fail
        1521  +
        let harness = ConnectionTestHarness::builder()
        1522  +
            .endpoint(IP1, vec![ConnectionBehavior::ResetOnConnect])
        1523  +
            .build()
        1524  +
            .await;
        1525  +
        1526  +
        let client = make_v2_with_listener(&harness, None, listener.clone());
        1527  +
        let url = format!("http://127.0.0.1:{}/", harness.endpoints[0].port());
        1528  +
        1529  +
        let settings = HttpConnectorSettings::builder().build();
        1530  +
        let components = runtime_components();
        1531  +
        let connector = client.http_connector(&settings, &components);
        1532  +
        let request = HttpRequest::get(&url).expect("valid request");
        1533  +
        let _ = connector.call(request).await;
        1534  +
        1535  +
        let failed = listener.failed.lock().unwrap();
        1536  +
        assert_eq!(failed.len(), 1);
        1537  +
        assert!(failed[0].contains("127.0.0.1"));
        1538  +
    }
        1539  +
        1540  +
    /// Authority is correctly populated on events.
        1541  +
    #[tokio::test]
        1542  +
    async fn listener_authority_populated() {
        1543  +
        let harness = ConnectionTestHarness::builder()
        1544  +
            .endpoint(
        1545  +
                IP1,
        1546  +
                vec![ConnectionBehavior::RespondKeepAlive {
        1547  +
                    status: 200,
        1548  +
                    body: b"x",
        1549  +
                }],
        1550  +
            )
        1551  +
            .build()
        1552  +
            .await;
        1553  +
        1554  +
        let listener = Arc::new(RecordingListener::default());
        1555  +
        let client = make_v2_with_listener(&harness, None, listener.clone());
        1556  +
        let port = harness.endpoints[0].port();
        1557  +
        let url = format!("http://127.0.0.1:{}/path", port);
        1558  +
        1559  +
        send_and_read_body(&client, &url).await;
        1560  +
        1561  +
        let created = listener.created.lock().unwrap();
        1562  +
        assert_eq!(created[0].1, format!("127.0.0.1:{}", port));
        1563  +
    }
        1564  +
        1565  +
    /// A connection that the server closed while idle fires on_closed
        1566  +
    /// with Unusable reason when detected at checkout.
        1567  +
    #[tokio::test]
        1568  +
    async fn listener_unusable_connection_fires_on_closed() {
        1569  +
        let harness = ConnectionTestHarness::builder()
        1570  +
            .endpoint(
        1571  +
                IP1,
        1572  +
                vec![
        1573  +
                    // First request succeeds, then server closes the connection.
        1574  +
                    ConnectionBehavior::RespondThenClose {
        1575  +
                        status: 200,
        1576  +
                        body: b"ok",
        1577  +
                    },
        1578  +
                    // Second connection for the retry.
        1579  +
                    ConnectionBehavior::RespondKeepAlive {
        1580  +
                        status: 200,
        1581  +
                        body: b"ok",
        1582  +
                    },
        1583  +
                ],
        1584  +
            )
        1585  +
            .build()
        1586  +
            .await;
        1587  +
        1588  +
        let listener = Arc::new(RecordingListener::default());
        1589  +
        let client = make_v2_with_listener(&harness, None, listener.clone());
        1590  +
        let url = format!("http://127.0.0.1:{}/", harness.endpoints[0].port());
        1591  +
        1592  +
        // First request succeeds; connection returns to pool.
        1593  +
        send_and_read_body(&client, &url).await;
        1594  +
        1595  +
        // Brief pause for the server-side close to propagate.
        1596  +
        tokio::time::sleep(Duration::from_millis(50)).await;
        1597  +
        1598  +
        // Second request: pool checks out the dead connection, detects it
        1599  +
        // in poll_ready, fires on_closed(Unusable), then creates a new one.
        1600  +
        send_and_read_body(&client, &url).await;
        1601  +
        1602  +
        let closed = listener.closed.lock().unwrap();
        1603  +
        let unusable = closed.iter().find(|c| c.2 == CloseReason::Unusable);
        1604  +
        assert!(
        1605  +
            unusable.is_some(),
        1606  +
            "expected on_closed with Unusable reason, got: {:?}",
        1607  +
            *closed
        1608  +
        );
        1609  +
        assert_eq!(unusable.unwrap().0, 0, "should be the first connection");
        1610  +
    }
        1611  +
        1612  +
    /// ConnectionCreatedEvent carries non-zero connect_duration.
        1613  +
    #[tokio::test]
        1614  +
    async fn listener_timing_populated_on_created() {
        1615  +
        let timing: Arc<Mutex<Option<Duration>>> = Arc::new(Mutex::new(None));
        1616  +
        1617  +
        struct TimingListener(Arc<Mutex<Option<Duration>>>);
        1618  +
        impl ConnectionEventListener for TimingListener {
        1619  +
            fn on_created(&self, event: &ConnectionCreatedEvent) {
        1620  +
                *self.0.lock().unwrap() = Some(event.timing().connect_duration());
        1621  +
            }
        1622  +
        }
        1623  +
        1624  +
        let harness = ConnectionTestHarness::builder()
        1625  +
            .endpoint(
        1626  +
                IP1,
        1627  +
                vec![ConnectionBehavior::RespondKeepAlive {
        1628  +
                    status: 200,
        1629  +
                    body: b"ok",
        1630  +
                }],
        1631  +
            )
        1632  +
            .build()
        1633  +
            .await;
        1634  +
        1635  +
        let listener: Arc<dyn ConnectionEventListener> = Arc::new(TimingListener(timing.clone()));
        1636  +
        let client = make_v2_with_listener(&harness, None, listener);
        1637  +
        let url = format!("http://127.0.0.1:{}/", harness.endpoints[0].port());
        1638  +
        1639  +
        send_and_read_body(&client, &url).await;
        1640  +
        1641  +
        let duration = timing.lock().unwrap().expect("timing should be set");
        1642  +
        assert!(duration > Duration::ZERO, "connect_duration should be > 0");
        1643  +
    }
        1644  +
}
        1645  +
        1646  +
/// Prove that a partition's declared spawner is used to spawn the connection
        1647  +
/// driver, not the free `TokioExecutor::new().execute(…)` helper that would
        1648  +
/// target whatever runtime is current at connect time.
        1649  +
#[tokio::test]
        1650  +
async fn partition_spawners_are_isolated() {
        1651  +
    use aws_smithy_http_client::pool::{DriverSpawner, Partition, PartitionId, TokioDriverSpawner};
        1652  +
    use std::sync::atomic::{AtomicUsize, Ordering};
        1653  +
    use std::sync::Arc;
        1654  +
        1655  +
    /// A spawner that records how many times `spawn` was called, then
        1656  +
    /// delegates to an inner `TokioDriverSpawner` so the driver runs.
        1657  +
    #[derive(Debug)]
        1658  +
    struct RecordingSpawner {
        1659  +
        inner: TokioDriverSpawner,
        1660  +
        spawned: Arc<AtomicUsize>,
        1661  +
    }
        1662  +
        1663  +
    impl RecordingSpawner {
        1664  +
        fn new(spawned: Arc<AtomicUsize>) -> Self {
        1665  +
            Self {
        1666  +
                inner: TokioDriverSpawner::current(),
        1667  +
                spawned,
        1668  +
            }
        1669  +
        }
        1670  +
    }
        1671  +
        1672  +
    impl DriverSpawner for RecordingSpawner {
        1673  +
        fn spawn(
        1674  +
            &self,
        1675  +
            driver: std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + 'static>>,
        1676  +
        ) {
        1677  +
            self.spawned.fetch_add(1, Ordering::Relaxed);
        1678  +
            self.inner.spawn(driver);
        1679  +
        }
        1680  +
    }
        1681  +
        1682  +
    let harness = ConnectionTestHarness::builder()
        1683  +
        .endpoint(
        1684  +
            IP1,
        1685  +
            vec![
        1686  +
                ConnectionBehavior::RespondKeepAlive {
        1687  +
                    status: 200,
        1688  +
                    body: b"hello",
        1689  +
                },
        1690  +
                ConnectionBehavior::RespondKeepAlive {
        1691  +
                    status: 200,
        1692  +
                    body: b"hello",
        1693  +
                },
        1694  +
            ],
        1695  +
        )
        1696  +
        .build()
        1697  +
        .await;
        1698  +
        1699  +
    // Two partitions, each with its own recording spawner.
        1700  +
    let count0 = Arc::new(AtomicUsize::new(0));
        1701  +
    let count1 = Arc::new(AtomicUsize::new(0));
        1702  +
    let p0 = Partition::new(
        1703  +
        PartitionId::from_index(0),
        1704  +
        RecordingSpawner::new(count0.clone()),
        1705  +
    );
        1706  +
    let p1 = Partition::new(
        1707  +
        PartitionId::from_index(1),
        1708  +
        RecordingSpawner::new(count1.clone()),
        1709  +
    );
        1710  +
        1711  +
    let pool = SharedPool::builder()
        1712  +
        .dns_resolver(harness.dns_resolver())
        1713  +
        .partitions([p0, p1])
        1714  +
        .build_http();
        1715  +
        1716  +
    let url = format!("http://127.0.0.1:{}/", harness.endpoints[0].port());
        1717  +
        1718  +
    // A request on partition 0 must drive its connection through spawner 0
        1719  +
    // only — spawner 1 stays untouched (no cross-partition leakage).
        1720  +
    let client0 = PoolClient::from_partition(&pool, PartitionId::from_index(0)).into_shared();
        1721  +
    let (status, _) = send_and_read_body(&client0, &url)
        1722  +
        .await
        1723  +
        .expect("p0 request should succeed");
        1724  +
    assert_eq!(status, 200);
        1725  +
    assert!(
        1726  +
        count0.load(Ordering::Relaxed) >= 1,
        1727  +
        "partition 0's spawner should have driven its connection"
        1728  +
    );
        1729  +
    assert_eq!(
        1730  +
        count1.load(Ordering::Relaxed),
        1731  +
        0,
        1732  +
        "partition 1's spawner must not be touched by a partition 0 request"
        1733  +
    );
        1734  +
        1735  +
    // A request on partition 1 drives through spawner 1; spawner 0's count
        1736  +
    // does not change (each partition opens its own connection).
        1737  +
    let count0_before = count0.load(Ordering::Relaxed);
        1738  +
    let client1 = PoolClient::from_partition(&pool, PartitionId::from_index(1)).into_shared();
        1739  +
    let (status, _) = send_and_read_body(&client1, &url)
        1740  +
        .await
        1741  +
        .expect("p1 request should succeed");
        1742  +
    assert_eq!(status, 200);
        1743  +
    assert!(
        1744  +
        count1.load(Ordering::Relaxed) >= 1,
        1745  +
        "partition 1's spawner should have driven its connection"
        1746  +
    );
        1747  +
    assert_eq!(
        1748  +
        count0.load(Ordering::Relaxed),
        1749  +
        count0_before,
        1750  +
        "partition 0's spawner must not be touched by a partition 1 request"
        1751  +
    );
        1752  +
}
        1753  +
        1754  +
// ---------------------------------------------------------------------------
        1755  +
// Test implementations: cross-partition active reclaim (Never policy)
        1756  +
// ---------------------------------------------------------------------------
        1757  +
        1758  +
/// Under cap pressure, a starved partition reclaims an over-supplied peer's
        1759  +
/// idle connection (freeing its permit) and connects locally — rather than
        1760  +
/// waiting for passive idle eviction.
        1761  +
///
        1762  +
/// Setup distinguishes *active* reclaim from passive eviction: the pool's
        1763  +
/// idle timeout is set very long, so the eviction tick will not fire during
        1764  +
/// the test. P1 establishes an idle connection holding the single global
        1765  +
/// permit; P0 (same authority, same NIC group) then requests, finds the cap
        1766  +
/// bound, reclaims P1's *non-expired* idle, and connects on a fresh
        1767  +
/// connection. Proven via the `ConnectionEventListener`:
        1768  +
///   - P1's connection closes with `CloseReason::Reclaimed` (not
        1769  +
///     `IdleTimeout` — the tick never ran),
        1770  +
///   - P0's connection is freshly created (distinct `conn_id`),
        1771  +
///   - the server accepted two connections (P1's reclaimed, P0's new),
        1772  +
///   - P0 completed well under the idle timeout.
        1773  +
#[tokio::test]
        1774  +
async fn v2_cross_partition_reclaim_frees_peer_idle() {
        1775  +
    use aws_smithy_http_client::pool::{
        1776  +
        CloseReason, ConnectionClosedEvent, ConnectionCreatedEvent, ConnectionEventListener,
        1777  +
        ConnectionFailedEvent, ConnectionReusedEvent, Partition, PartitionId, TokioDriverSpawner,
        1778  +
    };
        1779  +
    use std::sync::{Arc, Mutex};
        1780  +
        1781  +
    #[derive(Debug, Clone, Default)]
        1782  +
    struct RecordingListener {
        1783  +
        created: Arc<Mutex<Vec<(u64, String)>>>,
        1784  +
        closed: Arc<Mutex<Vec<(u64, String, CloseReason)>>>,
        1785  +
    }
        1786  +
    impl ConnectionEventListener for RecordingListener {
        1787  +
        fn on_created(&self, event: &ConnectionCreatedEvent) {
        1788  +
            self.created.lock().unwrap().push((
        1789  +
                event.conn_id().to_string().parse().unwrap(),
        1790  +
                event.authority().as_str().to_string(),
        1791  +
            ));
        1792  +
        }
        1793  +
        fn on_reused(&self, _event: &ConnectionReusedEvent) {}
        1794  +
        fn on_closed(&self, event: &ConnectionClosedEvent) {
        1795  +
            self.closed.lock().unwrap().push((
        1796  +
                event.conn_id().to_string().parse().unwrap(),
        1797  +
                event.authority().as_str().to_string(),
        1798  +
                event.reason(),
        1799  +
            ));
        1800  +
        }
        1801  +
        fn on_connection_failed(&self, _event: &ConnectionFailedEvent) {}
        1802  +
    }
        1803  +
        1804  +
    // One endpoint, two connections total: P1's (later reclaimed) and P0's.
        1805  +
    let harness = ConnectionTestHarness::builder()
        1806  +
        .endpoint(
        1807  +
            IP1,
        1808  +
            vec![
        1809  +
                ConnectionBehavior::RespondKeepAlive {
        1810  +
                    status: 200,
        1811  +
                    body: b"p1",
        1812  +
                },
        1813  +
                ConnectionBehavior::RespondKeepAlive {
        1814  +
                    status: 200,
        1815  +
                    body: b"p0",
        1816  +
                },
        1817  +
            ],
        1818  +
        )
        1819  +
        .build()
        1820  +
        .await;
        1821  +
        1822  +
    let listener = Arc::new(RecordingListener::default());
        1823  +
        1824  +
    // Two partitions sharing one NIC group (so they are reclaim peers),
        1825  +
    // global cap of 1 (so the second partition is cap-bound), and a long
        1826  +
    // idle timeout so passive eviction never fires during the test.
        1827  +
    let p0 = Partition::new(PartitionId::from_index(0), TokioDriverSpawner::current())
        1828  +
        .interface("eth-test");
        1829  +
    let p1 = Partition::new(PartitionId::from_index(1), TokioDriverSpawner::current())
        1830  +
        .interface("eth-test");
        1831  +
    let pool = SharedPool::builder()
        1832  +
        .dns_resolver(harness.dns_resolver())
        1833  +
        .connection_event_listener(listener.clone() as Arc<dyn ConnectionEventListener>)
        1834  +
        .max_connections(1)
        1835  +
        .pool_idle_timeout(Duration::from_secs(3600))
        1836  +
        .partitions([p0, p1])
        1837  +
        .build_http();
        1838  +
        1839  +
    let url = format!("http://127.0.0.1:{}/", harness.endpoints[0].port());
        1840  +
    let client0 = PoolClient::from_partition(&pool, PartitionId::from_index(0)).into_shared();
        1841  +
    let client1 = PoolClient::from_partition(&pool, PartitionId::from_index(1)).into_shared();
        1842  +
        1843  +
    // P1 establishes a connection and returns it idle to its cache, holding
        1844  +
    // the single global permit.
        1845  +
    let (_, _, meta1) = send_with_capture(&client1, &url).await;
        1846  +
    let p1_conn_id: u64 = meta1
        1847  +
        .expect("p1 metadata")
        1848  +
        .connection_id()
        1849  +
        .expect("p1 conn id")
        1850  +
        .to_string()
        1851  +
        .parse()
        1852  +
        .unwrap();
        1853  +
        1854  +
    // P0 requests the same authority. The global permit is held by P1's
        1855  +
    // idle connection, so P0 is cap-bound; it reclaims P1's idle inline,
        1856  +
    // freeing the permit, then connects locally. Bounded so a hang (failed
        1857  +
    // reclaim → indefinite block) surfaces as a test timeout rather than
        1858  +
    // a stall.
        1859  +
    let started = std::time::Instant::now();
        1860  +
    let (status, body, meta0) =
        1861  +
        tokio::time::timeout(Duration::from_secs(5), send_with_capture(&client0, &url))
        1862  +
            .await
        1863  +
            .expect("p0 must not block indefinitely — reclaim should free a permit");
        1864  +
    assert_eq!(status, 200);
        1865  +
    assert_eq!(body, b"p0");
        1866  +
    let p0_conn_id: u64 = meta0
        1867  +
        .expect("p0 metadata")
        1868  +
        .connection_id()
        1869  +
        .expect("p0 conn id")
        1870  +
        .to_string()
        1871  +
        .parse()
        1872  +
        .unwrap();
        1873  +
        1874  +
    // P0 completed promptly — far under the 3600s idle timeout, so this was
        1875  +
    // active reclaim, not passive eviction.
        1876  +
    assert!(
        1877  +
        started.elapsed() < Duration::from_secs(60),
        1878  +
        "p0 should complete promptly via reclaim, took {:?}",
        1879  +
        started.elapsed()
        1880  +
    );
        1881  +
        1882  +
    // P0 ran on a *fresh local* connection, not P1's (that is reclaim, not
        1883  +
    // borrow — borrow would reuse P1's exact connection).
        1884  +
    assert_ne!(
        1885  +
        p0_conn_id, p1_conn_id,
        1886  +
        "reclaim gives P0 its own fresh connection, distinct from P1's"
        1887  +
    );
        1888  +
        1889  +
    // The server accepted two connections: P1's (reclaimed) and P0's (new).
        1890  +
    assert_eq!(
        1891  +
        harness.tcp_accepted_count(),
        1892  +
        2,
        1893  +
        "expected P1's connection + P0's fresh connection"
        1894  +
    );
        1895  +
        1896  +
    // The listener proves the mechanism directly: P1's connection closed
        1897  +
    // with `Reclaimed` (not `IdleTimeout` — the tick never ran), and P0's
        1898  +
    // connection was created fresh.
        1899  +
    let closed = listener.closed.lock().unwrap();
        1900  +
    let reclaimed: Vec<_> = closed
        1901  +
        .iter()
        1902  +
        .filter(|(_, _, reason)| matches!(reason, CloseReason::Reclaimed))
        1903  +
        .collect();
        1904  +
    assert_eq!(
        1905  +
        reclaimed.len(),
        1906  +
        1,
        1907  +
        "exactly one connection should close with Reclaimed, got: {closed:?}"
        1908  +
    );
        1909  +
    assert_eq!(
        1910  +
        reclaimed[0].0, p1_conn_id,
        1911  +
        "the reclaimed connection must be P1's"
        1912  +
    );
        1913  +
    assert!(
        1914  +
        closed
        1915  +
            .iter()
        1916  +
            .all(|(_, _, reason)| !matches!(reason, CloseReason::IdleTimeout)),
        1917  +
        "no IdleTimeout close — the long idle timeout means reclaim, not passive eviction"
        1918  +
    );
        1919  +
        1920  +
    let created = listener.created.lock().unwrap();
        1921  +
    assert!(
        1922  +
        created.iter().any(|(id, _)| *id == p0_conn_id),
        1923  +
        "P0's fresh connection should fire on_created"
        1924  +
    );
        1925  +
}
        1926  +
        1927  +
// ---------------------------------------------------------------------------
        1928  +
// Test implementations: cross-partition borrow (PreferLocal policy)
        1929  +
// ---------------------------------------------------------------------------
        1930  +
        1931  +
/// Under cap pressure with `PreferLocal`, a starved partition borrows a
        1932  +
/// same-NIC peer's idle connection and dispatches its request through it —
        1933  +
/// no new connection, no permit. This is the opposite disposition from
        1934  +
/// reclaim: the connection stays the peer's (P0 runs on P1's exact
        1935  +
/// connection), proven by `p0_conn_id == p1_conn_id` and a single TCP
        1936  +
/// accept (P1's connection serves both requests via keep-alive).
        1937  +
#[tokio::test]
        1938  +
async fn v2_cross_partition_borrow_reuses_peer_connection() {
        1939  +
    use aws_smithy_http_client::pool::{
        1940  +
        CrossPartitionPolicy, Partition, PartitionId, TokioDriverSpawner,
        1941  +
    };
        1942  +
        1943  +
    // One endpoint, ONE connection serving TWO requests (P1's, then P0's
        1944  +
    // borrowed request via the keep-alive loop).
        1945  +
    let harness = ConnectionTestHarness::builder()
        1946  +
        .endpoint(
        1947  +
            IP1,
        1948  +
            vec![
        1949  +
                ConnectionBehavior::RespondKeepAlive {
        1950  +
                    status: 200,
        1951  +
                    body: b"p1",
        1952  +
                },
        1953  +
                ConnectionBehavior::RespondKeepAlive {
        1954  +
                    status: 200,
        1955  +
                    body: b"p0",
        1956  +
                },
        1957  +
            ],
        1958  +
        )
        1959  +
        .build()
        1960  +
        .await;
        1961  +
        1962  +
    // Two partitions, same NIC group (borrow peers), global cap 1, long
        1963  +
    // idle timeout so the eviction tick never interferes.
        1964  +
    let p0 = Partition::new(PartitionId::from_index(0), TokioDriverSpawner::current())
        1965  +
        .interface("eth-test");
        1966  +
    let p1 = Partition::new(PartitionId::from_index(1), TokioDriverSpawner::current())
        1967  +
        .interface("eth-test");
        1968  +
    let pool = SharedPool::builder()
        1969  +
        .dns_resolver(harness.dns_resolver())
        1970  +
        .cross_partition_policy(CrossPartitionPolicy::PreferLocal)
        1971  +
        .max_connections(1)
        1972  +
        .pool_idle_timeout(Duration::from_secs(3600))
        1973  +
        .partitions([p0, p1])
        1974  +
        .build_http();
        1975  +
        1976  +
    let url = format!("http://127.0.0.1:{}/", harness.endpoints[0].port());
        1977  +
    let client0 = PoolClient::from_partition(&pool, PartitionId::from_index(0)).into_shared();
        1978  +
    let client1 = PoolClient::from_partition(&pool, PartitionId::from_index(1)).into_shared();
        1979  +
        1980  +
    // P1 establishes a connection, drains it, and returns it idle to its
        1981  +
    // cache holding the single global permit.
        1982  +
    let (_, _, meta1) = send_with_capture(&client1, &url).await;
        1983  +
    let p1_conn_id: u64 = meta1
        1984  +
        .expect("p1 metadata")
        1985  +
        .connection_id()
        1986  +
        .expect("p1 conn id")
        1987  +
        .to_string()
        1988  +
        .parse()
        1989  +
        .unwrap();
        1990  +
        1991  +
    // P0 requests the same authority. The permit is held by P1's idle
        1992  +
    // connection, so P0 is cap-bound; under PreferLocal it borrows P1's
        1993  +
    // connection and dispatches through it. Bounded so a hang surfaces as
        1994  +
    // a failure.
        1995  +
    let (status, body, meta0) =
        1996  +
        tokio::time::timeout(Duration::from_secs(5), send_with_capture(&client0, &url))
        1997  +
            .await
        1998  +
            .expect("p0 must not block — borrow should reuse P1's connection");
        1999  +
    assert_eq!(status, 200);
        2000  +
    assert_eq!(body, b"p0");
        2001  +
    let p0_conn_id: u64 = meta0
        2002  +
        .expect("p0 metadata")
        2003  +
        .connection_id()
        2004  +
        .expect("p0 conn id")
        2005  +
        .to_string()
        2006  +
        .parse()
        2007  +
        .unwrap();
        2008  +
        2009  +
    // The direct proof of borrow: P0 ran on P1's *exact* connection.
        2010  +
    assert_eq!(
        2011  +
        p0_conn_id, p1_conn_id,
        2012  +
        "PreferLocal borrow dispatches P0's request through P1's connection"
        2013  +
    );
        2014  +
        2015  +
    // One TCP accept total — P1's connection served both requests. A fresh
        2016  +
    // local connection for P0 (reclaim, or no borrow) would be 2.
        2017  +
    assert_eq!(
        2018  +
        harness.tcp_accepted_count(),
        2019  +
        1,
        2020  +
        "borrow reuses the peer's connection; no new connection is opened"
        2021  +
    );
        2022  +
}
        2023  +
        2024  +
/// Storage residency under borrow: a borrowed connection never changes
        2025  +
/// partitions. When P0 borrows P1's connection under `PreferLocal`, the
        2026  +
/// connection stays resident in P1's storage — `stats()` shows P1 owning the
        2027  +
/// one established connection and P0 owning zero, even though P0's request
        2028  +
/// was served. Borrow dispatches *through* the peer's connection; it does
        2029  +
/// not migrate it (connections never move). This is the wiring counterpart
        2030  +
/// to the borrow test's `conn_id` equality: the id matches because the
        2031  +
/// connection is P1's, and the residency confirms it stayed P1's.
        2032  +
#[tokio::test]
        2033  +
async fn v2_borrowed_connection_stays_resident_in_peer() {
        2034  +
    use aws_smithy_http_client::pool::{
        2035  +
        Authority, CrossPartitionPolicy, Partition, PartitionId, TokioDriverSpawner,
        2036  +
    };
        2037  +
        2038  +
    let harness = ConnectionTestHarness::builder()
        2039  +
        .endpoint(
        2040  +
            IP1,
        2041  +
            vec![
        2042  +
                ConnectionBehavior::RespondKeepAlive {
        2043  +
                    status: 200,
        2044  +
                    body: b"p1",
        2045  +
                },
        2046  +
                ConnectionBehavior::RespondKeepAlive {
        2047  +
                    status: 200,
        2048  +
                    body: b"p0",
        2049  +
                },
        2050  +
            ],
        2051  +
        )
        2052  +
        .build()
        2053  +
        .await;
        2054  +
        2055  +
    // Same setup as the borrow test: two same-NIC partitions, cap 1, long
        2056  +
    // idle timeout. The borrow is forced by P1 holding the only permit.
        2057  +
    let p0 = Partition::new(PartitionId::from_index(0), TokioDriverSpawner::current())
        2058  +
        .interface("eth-test");
        2059  +
    let p1 = Partition::new(PartitionId::from_index(1), TokioDriverSpawner::current())
        2060  +
        .interface("eth-test");
        2061  +
    let pool = SharedPool::builder()
        2062  +
        .dns_resolver(harness.dns_resolver())
        2063  +
        .cross_partition_policy(CrossPartitionPolicy::PreferLocal)
        2064  +
        .max_connections(1)
        2065  +
        .pool_idle_timeout(Duration::from_secs(3600))
        2066  +
        .partitions([p0, p1])
        2067  +
        .build_http();
        2068  +
        2069  +
    let port = harness.endpoints[0].port();
        2070  +
    let url = format!("http://127.0.0.1:{port}/");
        2071  +
    let authority = Authority::from_host(format!("127.0.0.1:{port}"));
        2072  +
    let client0 = PoolClient::from_partition(&pool, PartitionId::from_index(0)).into_shared();
        2073  +
    let client1 = PoolClient::from_partition(&pool, PartitionId::from_index(1)).into_shared();
        2074  +
        2075  +
    // P1 establishes and returns its connection idle (holding the permit).
        2076  +
    let _ = send_with_capture(&client1, &url).await;
        2077  +
        2078  +
    // P0 borrows P1's connection (cap-bound, PreferLocal) and its request
        2079  +
    // completes through it. send_with_capture drains the body, so all active
        2080  +
    // counts settle before we read stats.
        2081  +
    let (status, _, _) =
        2082  +
        tokio::time::timeout(Duration::from_secs(5), send_with_capture(&client0, &url))
        2083  +
            .await
        2084  +
            .expect("p0 must not block — borrow should reuse P1's connection");
        2085  +
    assert_eq!(status, 200);
        2086  +
        2087  +
    // Residency: the single established connection lives in P1's cell. P0
        2088  +
    // borrowed rather than created, so its cell holds zero established.
        2089  +
    let stats = pool.stats(&authority);
        2090  +
    let p1_stats = stats
        2091  +
        .get(PartitionId::from_index(1))
        2092  +
        .expect("P1 owns the established connection");
        2093  +
    assert_eq!(
        2094  +
        p1_stats.established, 1,
        2095  +
        "the connection is resident in P1's storage"
        2096  +
    );
        2097  +
    assert_eq!(
        2098  +
        stats
        2099  +
            .get(PartitionId::from_index(0))
        2100  +
            .map(|s| s.established)
        2101  +
            .unwrap_or(0),
        2102  +
        0,
        2103  +
        "P0 borrowed P1's connection; it added nothing to P0's established"
        2104  +
    );
        2105  +
        2106  +
    // Exactly one connection exists across both partitions.
        2107  +
    let total_established: usize = stats.iter().map(|(_, s)| s.established).sum();
        2108  +
    assert_eq!(
        2109  +
        total_established, 1,
        2110  +
        "one connection total — borrow does not create or duplicate"
        2111  +
    );
        2112  +
}
        2113  +
        2114  +
/// Borrow is NIC-bounded: a peer on a different NIC is not a borrow
        2115  +
/// candidate. With `PreferLocal` but P0 and P1 on different NICs, P0
        2116  +
/// cannot borrow P1's connection — nor reclaim its permit (reclaim
        2117  +
/// candidates are also drawn from the NIC group). P0's cap-bound wait is
        2118  +
/// instead released when P1's idle connection is evicted (scenario B), and
        2119  +
/// P0 then connects locally. Proven by `p0_conn_id != p1_conn_id` and two
        2120  +
/// TCP accepts — the opposite of the same-NIC borrow case, which reuses
        2121  +
/// P1's exact connection.
        2122  +
#[tokio::test]
        2123  +
async fn v2_cross_partition_borrow_respects_nic_boundary() {
        2124  +
    use aws_smithy_http_client::pool::{
        2125  +
        CrossPartitionPolicy, Partition, PartitionId, TokioDriverSpawner,
        2126  +
    };
        2127  +
        2128  +
    // One endpoint, two connections: P1's, then P0's fresh local one.
        2129  +
    let harness = ConnectionTestHarness::builder()
        2130  +
        .endpoint(
        2131  +
            IP1,
        2132  +
            vec![
        2133  +
                ConnectionBehavior::RespondKeepAlive {
        2134  +
                    status: 200,
        2135  +
                    body: b"p1",
        2136  +
                },
        2137  +
                ConnectionBehavior::RespondKeepAlive {
        2138  +
                    status: 200,
        2139  +
                    body: b"p0",
        2140  +
                },
        2141  +
            ],
        2142  +
        )
        2143  +
        .build()
        2144  +
        .await;
        2145  +
        2146  +
    // Two partitions on DIFFERENT NICs — not borrow peers (and not reclaim
        2147  +
    // peers). A short idle timeout lets P0's cap-bound wait be released by
        2148  +
    // eviction of P1's idle connection, rather than hanging.
        2149  +
    let p0 = Partition::new(PartitionId::from_index(0), TokioDriverSpawner::current())
        2150  +
        .interface("eth-zero");
        2151  +
    let p1 = Partition::new(PartitionId::from_index(1), TokioDriverSpawner::current())
        2152  +
        .interface("eth-one");
        2153  +
    let pool = SharedPool::builder()
        2154  +
        .dns_resolver(harness.dns_resolver())
        2155  +
        .cross_partition_policy(CrossPartitionPolicy::PreferLocal)
        2156  +
        .max_connections(1)
        2157  +
        .pool_idle_timeout(Duration::from_millis(150))
        2158  +
        .partitions([p0, p1])
        2159  +
        .build_http();
        2160  +
        2161  +
    let url = format!("http://127.0.0.1:{}/", harness.endpoints[0].port());
        2162  +
    let client0 = PoolClient::from_partition(&pool, PartitionId::from_index(0)).into_shared();
        2163  +
    let client1 = PoolClient::from_partition(&pool, PartitionId::from_index(1)).into_shared();
        2164  +
        2165  +
    let (_, _, meta1) = send_with_capture(&client1, &url).await;
        2166  +
    let p1_conn_id: u64 = meta1
        2167  +
        .expect("p1 metadata")
        2168  +
        .connection_id()
        2169  +
        .expect("p1 conn id")
        2170  +
        .to_string()
        2171  +
        .parse()
        2172  +
        .unwrap();
        2173  +
        2174  +
    // P0 is cap-bound and cannot borrow across the NIC boundary; it waits
        2175  +
    // for P1's idle to be evicted, then connects locally. Bounded so a true
        2176  +
    // hang still surfaces as a failure.
        2177  +
    let (status, body, meta0) =
        2178  +
        tokio::time::timeout(Duration::from_secs(5), send_with_capture(&client0, &url))
        2179  +
            .await
        2180  +
            .expect("p0 should proceed once P1's idle is evicted");
        2181  +
    assert_eq!(status, 200);
        2182  +
    assert_eq!(body, b"p0");
        2183  +
    let p0_conn_id: u64 = meta0
        2184  +
        .expect("p0 metadata")
        2185  +
        .connection_id()
        2186  +
        .expect("p0 conn id")
        2187  +
        .to_string()
        2188  +
        .parse()
        2189  +
        .unwrap();
        2190  +
        2191  +
    // P1 is on a different NIC → not a borrow candidate. P0 does NOT run on
        2192  +
    // P1's connection; it gets its own.
        2193  +
    assert_ne!(
        2194  +
        p0_conn_id, p1_conn_id,
        2195  +
        "no borrow across NICs — P0 runs on its own connection"
        2196  +
    );
        2197  +
        2198  +
    // Two TCP accepts: P1's connection plus P0's fresh local one.
        2199  +
    assert_eq!(
        2200  +
        harness.tcp_accepted_count(),
        2201  +
        2,
        2202  +
        "NIC boundary blocks borrow; P0 opens its own connection"
        2203  +
    );
        2204  +
}
        2205  +
        2206  +
// ---------------------------------------------------------------------------
        2207  +
// Test implementations: cross-partition concurrency stress (TSan target)
        2208  +
// ---------------------------------------------------------------------------
        2209  +
        2210  +
/// Drives concurrent cross-partition borrow, reclaim, and eviction against
        2211  +
/// a shared authority under a binding cap. The assertion is intentionally
        2212  +
/// weak (every request completes); the value is the interleaving:
        2213  +
/// concurrent `try_borrow_on` / `try_reclaim_on` touch a peer's
        2214  +
/// `authorities` and cache locks while that peer serves its own requests
        2215  +
/// and the eviction task runs `retain` on the same caches. `additional-ci`
        2216  +
/// additionally runs it under ThreadSanitizer.
        2217  +
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
        2218  +
async fn v2_cross_partition_concurrency_stress() {
        2219  +
    use aws_smithy_http_client::pool::{
        2220  +
        CrossPartitionPolicy, Partition, PartitionId, TokioDriverSpawner,
        2221  +
    };
        2222  +
        2223  +
    const PARTITIONS: usize = 4;
        2224  +
    const ROUNDS: usize = 8;
        2225  +
    const REQUESTS_PER_ROUND: usize = 16;
        2226  +
        2227  +
    // Single loopback endpoint serving many keep-alive requests. The
        2228  +
    // cross-partition contention comes from multiple partitions sharing
        2229  +
    // one authority under a binding cap, not from IP spread.
        2230  +
    let harness = ConnectionTestHarness::builder()
        2231  +
        .endpoint(
        2232  +
            IP1,
        2233  +
            (0..512)
        2234  +
                .map(|_| ConnectionBehavior::RespondKeepAlive {
        2235  +
                    status: 200,
        2236  +
                    body: b"ok",
        2237  +
                })
        2238  +
                .collect(),
        2239  +
        )
        2240  +
        .build()
        2241  +
        .await;
        2242  +
        2243  +
    // No NIC binding (the common case): all partitions land in the single
        2244  +
    // implicit NIC group, so they are borrow + reclaim peers. A small
        2245  +
    // global cap forces cross-partition contention; a short idle timeout
        2246  +
    // makes the eviction task churn concurrently with borrow/reclaim.
        2247  +
    let parts = (0..PARTITIONS)
        2248  +
        .map(|i| Partition::new(PartitionId::from_index(i), TokioDriverSpawner::current()));
        2249  +
    let pool = SharedPool::builder()
        2250  +
        .dns_resolver(harness.dns_resolver())
        2251  +
        .cross_partition_policy(CrossPartitionPolicy::PreferLocal)
        2252  +
        .max_connections(PARTITIONS) // bind the cap below the offered load
        2253  +
        .pool_idle_timeout(Duration::from_millis(20))
        2254  +
        .partitions(parts)
        2255  +
        .build_http();
        2256  +
        2257  +
    let clients: Vec<SharedHttpClient> = (0..PARTITIONS)
        2258  +
        .map(|i| PoolClient::from_partition(&pool, PartitionId::from_index(i)).into_shared())
        2259  +
        .collect();
        2260  +
    let url = format!("http://127.0.0.1:{}/", harness.endpoints[0].port());
        2261  +
        2262  +
    for _ in 0..ROUNDS {
        2263  +
        let mut tasks = tokio::task::JoinSet::new();
        2264  +
        for r in 0..REQUESTS_PER_ROUND {
        2265  +
            // Spread requests across partitions so borrow/reclaim peers
        2266  +
            // are all live at once.
        2267  +
            let client = clients[r % PARTITIONS].clone();
        2268  +
            let url = url.clone();
        2269  +
            tasks.spawn(async move { send_and_read_body(&client, &url).await });
        2270  +
        }
        2271  +
        while let Some(result) = tasks.join_next().await {
        2272  +
            let (status, _) = result
        2273  +
                .expect("task should not panic")
        2274  +
                .expect("request should succeed under cross-partition contention");
        2275  +
            assert_eq!(status, 200);
        2276  +
        }
        2277  +
        // Let the eviction tick fire between rounds so the next round
        2278  +
        // races fresh connects against reclaim/borrow on partly-evicted
        2279  +
        // caches.
        2280  +
        tokio::time::sleep(Duration::from_millis(30)).await;
        2281  +
    }
        2282  +
}
        2283  +
        2284  +
// ---------------------------------------------------------------------------
        2285  +
// Test implementations: stats read API
        2286  +
// ---------------------------------------------------------------------------
        2287  +
        2288  +
/// SharedPool::stats returns sparse per-partition snapshots: only partitions
        2289  +
/// that have touched an authority appear. After a request completes and the
        2290  +
/// body is consumed, counters reflect the idle state.
        2291  +
#[tokio::test]
        2292  +
async fn v2_stats_reports_per_partition_sparse() {
        2293  +
    use aws_smithy_http_client::pool::{Authority, Partition, PartitionId, TokioDriverSpawner};
        2294  +
        2295  +
    let harness = ConnectionTestHarness::builder()
        2296  +
        .endpoint(
        2297  +
            IP1,
        2298  +
            vec![ConnectionBehavior::RespondKeepAlive {
        2299  +
                status: 200,
        2300  +
                body: b"stats-test",
        2301  +
            }],
        2302  +
        )
        2303  +
        .build()
        2304  +
        .await;
        2305  +
        2306  +
    let p0 = Partition::new(PartitionId::from_index(0), TokioDriverSpawner::current());
        2307  +
    let p1 = Partition::new(PartitionId::from_index(1), TokioDriverSpawner::current());
        2308  +
        2309  +
    let pool = SharedPool::builder()
        2310  +
        .dns_resolver(harness.dns_resolver())
        2311  +
        .partitions([p0, p1])
        2312  +
        .build_http();
        2313  +
        2314  +
    let port = harness.endpoints[0].port();
        2315  +
    let url = format!("http://127.0.0.1:{port}/");
        2316  +
    let authority = Authority::from_host(format!("127.0.0.1:{port}"));
        2317  +
        2318  +
    // Before any request, stats are empty for this authority.
        2319  +
    let stats = pool.stats(&authority);
        2320  +
    assert!(
        2321  +
        stats.is_empty(),
        2322  +
        "no partition should have touched this authority yet"
        2323  +
    );
        2324  +
        2325  +
    // Send a request on partition 0, consume the body so the connection idles.
        2326  +
    let client0 = PoolClient::from_partition(&pool, PartitionId::from_index(0)).into_shared();
        2327  +
    let (status, body) = send_and_read_body(&client0, &url)
        2328  +
        .await
        2329  +
        .expect("p0 request should succeed");
        2330  +
    assert_eq!(status, 200);
        2331  +
    assert_eq!(body, b"stats-test");
        2332  +
        2333  +
    // After the request idles, partition 0 should appear, partition 1 should not.
        2334  +
    let stats = pool.stats(&authority);
        2335  +
    assert_eq!(stats.len(), 1, "only partition 0 should appear (sparse)");
        2336  +
        2337  +
    let p0_stats = stats
        2338  +
        .get(PartitionId::from_index(0))
        2339  +
        .expect("partition 0 should have stats");
        2340  +
    assert_eq!(p0_stats.established, 1, "one connection established");
        2341  +
    assert_eq!(p0_stats.establishing, 0, "no handshakes in flight");
        2342  +
    assert_eq!(p0_stats.active, 0, "connection is idle after body consumed");
        2343  +
    assert_eq!(p0_stats.idle(), 1, "one idle connection");
        2344  +
    // H1 cell: capacity_hint is Some(idle)
        2345  +
    assert_eq!(p0_stats.capacity_hint(), Some(1));
        2346  +
        2347  +
    assert!(
        2348  +
        stats.get(PartitionId::from_index(1)).is_none(),
        2349  +
        "partition 1 has not touched this authority"
        2350  +
    );
        2351  +
}
        2352  +
        2353  +
/// `active` tracks an in-flight request end-to-end. The H1 checkout guard
        2354  +
/// rides the response body (`GuardedBody`): it is held while the response
        2355  +
/// value is alive and releases when the body is consumed/dropped, returning
        2356  +
/// the connection to the pool. So `active == 1` is observable for as long as
        2357  +
/// the caller holds the response, and drops to 0 once the body is drained.
        2358  +
#[tokio::test]
        2359  +
async fn v2_stats_active_tracks_in_flight_request() {
        2360  +
    use aws_smithy_http_client::pool::{Authority, PartitionId};
        2361  +
    use http_body_util::BodyExt;
        2362  +
        2363  +
    let harness = ConnectionTestHarness::builder()
        2364  +
        .endpoint(
        2365  +
            IP1,
        2366  +
            vec![ConnectionBehavior::RespondKeepAlive {
        2367  +
                status: 200,
        2368  +
                body: b"in-flight",
        2369  +
            }],
        2370  +
        )
        2371  +
        .build()
        2372  +
        .await;
        2373  +
        2374  +
    let pool = SharedPool::builder()
        2375  +
        .dns_resolver(harness.dns_resolver())
        2376  +
        .build_http();
        2377  +
        2378  +
    let port = harness.endpoints[0].port();
        2379  +
    let url = format!("http://127.0.0.1:{port}/");
        2380  +
    let authority = Authority::from_host(format!("127.0.0.1:{port}"));
        2381  +
    let partition = PartitionId::default();
        2382  +
        2383  +
    let client = PoolClient::new(&pool).into_shared();
        2384  +
        2385  +
    // Issue the request but hold the response without draining the body. The
        2386  +
    // connection is checked out: its guard is alive inside `resp`'s body.
        2387  +
    let resp = send_to(&client, &url)
        2388  +
        .await
        2389  +
        .expect("request should succeed");
        2390  +
    assert_eq!(resp.status().as_u16(), 200);
        2391  +
        2392  +
    let stats = pool.stats(&authority);
        2393  +
    let in_flight = stats
        2394  +
        .get(partition)
        2395  +
        .expect("partition should have touched this authority");
        2396  +
    assert_eq!(in_flight.established, 1, "one connection established");
        2397  +
    assert_eq!(
        2398  +
        in_flight.active, 1,
        2399  +
        "request is in flight, connection checked out"
        2400  +
    );
        2401  +
    assert_eq!(in_flight.idle(), 0, "no idle connection while in flight");
        2402  +
        2403  +
    // Drain the body: the GuardedBody drops, CachedConnection::Drop fires,
        2404  +
    // active decrements and the connection returns to the pool as idle.
        2405  +
    let body = resp
        2406  +
        .into_body()
        2407  +
        .collect()
        2408  +
        .await
        2409  +
        .expect("body should be readable")
        2410  +
        .to_bytes()
        2411  +
        .to_vec();
        2412  +
    assert_eq!(body, b"in-flight");
        2413  +
        2414  +
    let stats = pool.stats(&authority);
        2415  +
    let idle = stats
        2416  +
        .get(partition)
        2417  +
        .expect("partition still present after request completes");
        2418  +
    assert_eq!(idle.established, 1, "connection still established (idle)");
        2419  +
    assert_eq!(idle.active, 0, "no in-flight request after body drained");
        2420  +
    assert_eq!(idle.idle(), 1, "connection is idle and reusable");
        2421  +
}
        2422  +
        2423  +
/// Eviction decrements `established` and prunes the stats-index cell through
        2424  +
/// its real trigger — the background eviction task — not a direct prune call.
        2425  +
/// After an idle connection is evicted, the host entry is removed and the
        2426  +
/// eviction-triggered prune drops the now-dead cell from the index, so
        2427  +
/// `stats()` reports the authority as untracked.
        2428  +
#[tokio::test]
        2429  +
async fn v2_stats_pruned_after_eviction() {
        2430  +
    use aws_smithy_http_client::pool::{Authority, PartitionId};
        2431  +
        2432  +
    let idle_timeout = Duration::from_millis(100);
        2433  +
        2434  +
    let harness = ConnectionTestHarness::builder()
        2435  +
        .endpoint(
        2436  +
            IP1,
        2437  +
            vec![
        2438  +
                ConnectionBehavior::RespondKeepAlive {
        2439  +
                    status: 200,
        2440  +
                    body: b"evict-me",
        2441  +
                },
        2442  +
                // A second connection is available if eviction forces a reconnect;
        2443  +
                // the test asserts on stats, not connection count.
        2444  +
                ConnectionBehavior::RespondKeepAlive {
        2445  +
                    status: 200,
        2446  +
                    body: b"evict-me",
        2447  +
                },
        2448  +
            ],
        2449  +
        )
        2450  +
        .build()
        2451  +
        .await;
        2452  +
        2453  +
    let pool = SharedPool::builder()
        2454  +
        .dns_resolver(harness.dns_resolver())
        2455  +
        .pool_idle_timeout(idle_timeout)
        2456  +
        .build_http();
        2457  +
        2458  +
    let port = harness.endpoints[0].port();
        2459  +
    let url = format!("http://127.0.0.1:{port}/");
        2460  +
    let authority = Authority::from_host(format!("127.0.0.1:{port}"));
        2461  +
        2462  +
    let client = PoolClient::new(&pool).into_shared();
        2463  +
        2464  +
    // Request completes and the connection idles. This also lazily spawns the
        2465  +
    // eviction task (pool_idle_timeout is set).
        2466  +
    let (status, _) = send_and_read_body(&client, &url)
        2467  +
        .await
        2468  +
        .expect("request should succeed");
        2469  +
    assert_eq!(status, 200);
        2470  +
        2471  +
    let stats = pool.stats(&authority);
        2472  +
    assert_eq!(
        2473  +
        stats
        2474  +
            .get(PartitionId::default())
        2475  +
            .expect("partition present after request")
        2476  +
            .established,
        2477  +
        1,
        2478  +
        "one established idle connection before eviction"
        2479  +
    );
        2480  +
        2481  +
    // Wait past the idle timeout: the eviction task drops the idle connection,
        2482  +
    // removes the host entry, and prunes the now-dead index cell.
        2483  +
    tokio::time::sleep(idle_timeout * 3).await;
        2484  +
        2485  +
    let stats = pool.stats(&authority);
        2486  +
    assert!(
        2487  +
        stats.is_empty(),
        2488  +
        "eviction should have decremented established and pruned the cell"
        2489  +
    );
        2490  +
}
        2491  +
        2492  +
// ---------------------------------------------------------------------------
        2493  +
// Test implementations: at-the-limit scenarios
        2494  +
// ---------------------------------------------------------------------------
        2495  +
        2496  +
/// A request blocked at the connection cap proceeds once an in-flight
        2497  +
/// request releases its permit. With `max_connections(1)`, the first
        2498  +
/// request pins the only permit by holding its response; a second request
        2499  +
/// cannot acquire and stays pending until the first is released, then
        2500  +
/// completes.
        2501  +
#[tokio::test]
        2502  +
async fn v2_cap_bound_request_waits_then_proceeds() {
        2503  +
    let harness = ConnectionTestHarness::builder()
        2504  +
        .endpoint(
        2505  +
            IP1,
        2506  +
            (0..2)
        2507  +
                .map(|_| ConnectionBehavior::RespondKeepAlive {
        2508  +
                    status: 200,
        2509  +
                    body: b"ok",
        2510  +
                })
        2511  +
                .collect(),
        2512  +
        )
        2513  +
        .build()
        2514  +
        .await;
        2515  +
        2516  +
    let pool = SharedPool::builder()
        2517  +
        .dns_resolver(harness.dns_resolver())
        2518  +
        .max_connections(1)
        2519  +
        .build_http();
        2520  +
    let client = PoolClient::new(&pool).into_shared();
        2521  +
    let url = format!("http://127.0.0.1:{}/", harness.endpoints[0].port());
        2522  +
        2523  +
    // First request holds the only permit: the response is kept, body
        2524  +
    // undrained, so the connection stays checked out.
        2525  +
    let held = send_to(&client, &url)
        2526  +
        .await
        2527  +
        .expect("first request succeeds");
        2528  +
    assert_eq!(held.status().as_u16(), 200);
        2529  +
        2530  +
    // Second request cannot acquire a permit; it must not complete while the
        2531  +
    // first is held.
        2532  +
    let mut second = tokio::spawn({
        2533  +
        let client = client.clone();
        2534  +
        let url = url.clone();
        2535  +
        async move { send_and_read_body(&client, &url).await }
        2536  +
    });
        2537  +
    assert!(
        2538  +
        tokio::time::timeout(Duration::from_millis(200), &mut second)
        2539  +
            .await
        2540  +
            .is_err(),
        2541  +
        "second request must stay pending while the cap is held"
        2542  +
    );
        2543  +
        2544  +
    // Release the permit by dropping the first response (its body guard
        2545  +
    // drops, returning the connection to the pool).
        2546  +
    drop(held);
        2547  +
        2548  +
    // The second request now acquires the freed permit and completes.
        2549  +
    let (status, body) = tokio::time::timeout(Duration::from_secs(5), second)
        2550  +
        .await
        2551  +
        .expect("second request completes after permit release")
        2552  +
        .expect("spawned task does not panic")
        2553  +
        .expect("second request succeeds");
        2554  +
    assert_eq!(status, 200);
        2555  +
    assert_eq!(body, b"ok");
        2556  +
        2557  +
    // At most two connections: dropping the first response with its body
        2558  +
    // undrained closes that H1 connection (an unconsumed body cannot be
        2559  +
    // reused), so the second request may open a fresh one. The scenario
        2560  +
    // under test is the permit wait-then-proceed, not connection reuse.
        2561  +
    assert!(harness.tcp_accepted_count() <= 2);
        2562  +
}
        2563  +
        2564  +
/// A host saturated at its per-host cap does not block requests to a
        2565  +
/// different host. With `max_connections_per_host(1)` and global headroom,
        2566  +
/// holding host X's only permit leaves a request to X pending while a
        2567  +
/// request to host Y proceeds — the per-host-before-global acquire order.
        2568  +
#[tokio::test]
        2569  +
async fn v2_per_host_cap_isolates_hosts() {
        2570  +
    // Two distinct authorities, same loopback endpoint (the per-host cap is
        2571  +
    // keyed by authority, so distinct hostnames are distinct hosts even on
        2572  +
    // one listener).
        2573  +
    let harness = ConnectionTestHarness::builder()
        2574  +
        .endpoint(
        2575  +
            IP1,
        2576  +
            (0..4)
        2577  +
                .map(|_| ConnectionBehavior::RespondKeepAlive {
        2578  +
                    status: 200,
        2579  +
                    body: b"ok",
        2580  +
                })
        2581  +
                .collect(),
        2582  +
        )
        2583  +
        .dns("host-x.test", vec![IP1])
        2584  +
        .dns("host-y.test", vec![IP1])
        2585  +
        .build()
        2586  +
        .await;
        2587  +
        2588  +
    let pool = SharedPool::builder()
        2589  +
        .dns_resolver(harness.dns_resolver())
        2590  +
        .max_connections_per_host(1)
        2591  +
        .build_http();
        2592  +
    let client = PoolClient::new(&pool).into_shared();
        2593  +
    let port = harness.endpoints[0].port();
        2594  +
    let url_x = format!("http://host-x.test:{port}/");
        2595  +
    let url_y = format!("http://host-y.test:{port}/");
        2596  +
        2597  +
    // Pin host X at its per-host cap by holding the response.
        2598  +
    let held_x = send_to(&client, &url_x).await.expect("x request succeeds");
        2599  +
    assert_eq!(held_x.status().as_u16(), 200);
        2600  +
        2601  +
    // A second request to X is blocked on X's per-host permit.
        2602  +
    let mut x2 = tokio::spawn({
        2603  +
        let client = client.clone();
        2604  +
        let url_x = url_x.clone();
        2605  +
        async move { send_and_read_body(&client, &url_x).await }
        2606  +
    });
        2607  +
    assert!(
        2608  +
        tokio::time::timeout(Duration::from_millis(200), &mut x2)
        2609  +
            .await
        2610  +
            .is_err(),
        2611  +
        "second X request must wait on X's saturated per-host cap"
        2612  +
    );
        2613  +
        2614  +
    // A request to host Y proceeds: Y's per-host cap is independent and
        2615  +
    // global has headroom.
        2616  +
    let (status, body) =
        2617  +
        tokio::time::timeout(Duration::from_secs(5), send_and_read_body(&client, &url_y))
        2618  +
            .await
        2619  +
            .expect("Y request must not be blocked by X's saturation")
        2620  +
            .expect("y request succeeds");
        2621  +
    assert_eq!(status, 200);
        2622  +
    assert_eq!(body, b"ok");
        2623  +
        2624  +
    // Release X and let its waiter finish so the spawned task is not leaked.
        2625  +
    drop(held_x);
        2626  +
    let _ = tokio::time::timeout(Duration::from_secs(5), x2)
        2627  +
        .await
        2628  +
        .expect("x2 completes after release")
        2629  +
        .expect("x2 task does not panic")
        2630  +
        .expect("x2 succeeds");
        2631  +
}