268 273 | }
|
269 274 |
|
270 275 | #[tokio::test]
|
271 276 | async fn test_concurrent_cold_start_converges_on_one_h2_connection_with_partitioned_pool() {
|
272 277 | concurrent_cold_start_converges_on_one_h2_connection(&PartitionedConnectionPool).await;
|
273 278 | }
|
274 279 | }
|
275 280 |
|
276 281 | mod connection_metadata {
|
277 282 | use super::*;
|
283 + | use aws_smithy_async::test_util::ManualTimeSource;
|
284 + | use std::time::{Duration, UNIX_EPOCH};
|
285 + |
|
286 + | async fn capture_attempt(
|
287 + | connector: &SharedHttpConnector,
|
288 + | url: &str,
|
289 + | ) -> aws_smithy_runtime_api::client::http::telemetry::HttpAttemptTelemetry {
|
290 + | let capture = CaptureHttpAttemptTelemetry::new();
|
291 + | let mut request = HttpRequest::get(url).expect("valid HTTP request");
|
292 + | request.add_extension(capture.clone());
|
293 + | let (status, body) = test_client::send_and_collect(connector, request).await;
|
294 + | assert_eq!((status, body.as_slice()), (200, b"ok".as_slice()));
|
295 + | capture.get()
|
296 + | }
|
297 + |
|
298 + | async fn attempt_telemetry_matches_backend_capability(
|
299 + | backend: &dyn HttpsClientBackend,
|
300 + | captures_selection: bool,
|
301 + | ) {
|
302 + | let server = H2TestServer::builder()
|
303 + | .connections(H2ConnectionPlan::queue([
|
304 + | H2ConnectionScript::new().fallback(H2StreamScript::respond(H2Response::ok("ok")))
|
305 + | ]))
|
306 + | .start()
|
307 + | .await
|
308 + | .expect("H2 server should start");
|
309 + | let client = h2_client(backend);
|
310 + | let connector = test_client::connector(&client);
|
311 + |
|
312 + | let first = capture_attempt(&connector, &server.url("/first")).await;
|
313 + | let second = capture_attempt(&connector, &server.url("/second")).await;
|
314 + | assert!(first.connector_call_duration().is_some());
|
315 + | assert!(second.connector_call_duration().is_some());
|
316 + |
|
317 + | if captures_selection {
|
318 + | assert_eq!(
|
319 + | first.acquisition().expect("first acquisition").usage(),
|
320 + | ConnectionUsage::Fresh
|
321 + | );
|
322 + | assert_eq!(
|
323 + | second.acquisition().expect("second acquisition").usage(),
|
324 + | ConnectionUsage::Reused
|
325 + | );
|
326 + | let first_connection = first.connection().expect("first connection metadata");
|
327 + | let second_connection = second.connection().expect("second connection metadata");
|
328 + | assert_eq!(
|
329 + | first_connection.connection_id(),
|
330 + | second_connection.connection_id()
|
331 + | );
|
332 + | assert_eq!(
|
333 + | first_connection.establishment(),
|
334 + | second_connection.establishment(),
|
335 + | "reuse must retain the establishment that created the connection"
|
336 + | );
|
337 + | assert!(first_connection.establishment().is_some());
|
338 + | } else {
|
339 + | assert!(first.acquisition().is_none());
|
340 + | assert!(first.connection().is_none());
|
341 + | assert!(second.acquisition().is_none());
|
342 + | assert!(second.connection().is_none());
|
343 + | }
|
344 + |
|
345 + | drop(connector);
|
346 + | drop(client);
|
347 + | server.shutdown().await.expect("clean H2 server shutdown");
|
348 + | }
|
349 + |
|
350 + | #[tokio::test]
|
351 + | async fn test_attempt_telemetry_with_hyper_util_legacy_pool() {
|
352 + | attempt_telemetry_matches_backend_capability(&HyperUtilLegacyPool, false).await;
|
353 + | }
|
354 + |
|
355 + | #[tokio::test]
|
356 + | async fn test_attempt_telemetry_with_partitioned_pool() {
|
357 + | attempt_telemetry_matches_backend_capability(&PartitionedConnectionPool, true).await;
|
358 + | }
|
359 + |
|
360 + | #[tokio::test]
|
361 + | async fn h2_acquisition_ends_before_response_headers() {
|
362 + | let response_gate = ManualGate::new();
|
363 + | let server = H2TestServer::builder()
|
364 + | .connections(H2ConnectionPlan::queue([H2ConnectionScript::new()
|
365 + | .fallback(H2StreamScript::respond_after(
|
366 + | H2Response::ok("ok"),
|
367 + | response_gate.waiter(),
|
368 + | ))]))
|
369 + | .start()
|
370 + | .await
|
371 + | .expect("H2 server should start");
|
372 + | let time = ManualTimeSource::new(UNIX_EPOCH);
|
373 + | let pool = ConnectionPool::builder()
|
374 + | .tls_provider(rustls_aws_lc())
|
375 + | .tls_context(test_tls::SERVER_IDENTITY.client_context())
|
376 + | .time_source(time.clone())
|
377 + | .build_https()
|
378 + | .expect("valid partitioned HTTPS pool");
|
379 + | let client =
|
380 + | SharedHttpClient::new(PoolClient::new(&pool).expect("anonymous partition exists"));
|
381 + | let connector = test_client::connector_with_time_source(&client, time.clone());
|
382 + | let capture = CaptureHttpAttemptTelemetry::new();
|
383 + | let mut request = HttpRequest::get(server.url("/gated")).expect("valid HTTP request");
|
384 + | request.add_extension(capture.clone());
|
385 + | let send = tokio::spawn({
|
386 + | let connector = connector.clone();
|
387 + | async move { test_client::send_and_collect(&connector, request).await }
|
388 + | });
|
389 + |
|
390 + | response_gate
|
391 + | .wait_until_reached(test_client::WAIT)
|
392 + | .await
|
393 + | .expect("server should accept the H2 stream");
|
394 + | time.advance(Duration::from_secs(10));
|
395 + | response_gate.release();
|
396 + | let (status, body) = send.await.expect("request task");
|
397 + | assert_eq!((status, body.as_slice()), (200, b"ok".as_slice()));
|
398 + |
|
399 + | let telemetry = capture.get();
|
400 + | assert_eq!(
|
401 + | telemetry.acquisition().expect("acquisition").duration(),
|
402 + | Some(Duration::ZERO)
|
403 + | );
|
404 + | assert_eq!(
|
405 + | telemetry.connector_call_duration(),
|
406 + | Some(Duration::from_secs(10))
|
407 + | );
|
408 + |
|
409 + | drop(connector);
|
410 + | drop(client);
|
411 + | drop(pool);
|
412 + | server.shutdown().await.expect("clean H2 server shutdown");
|
413 + | }
|
278 414 |
|
279 415 | /// Poisoning captured H2 connection metadata moves later streams to a new connection.
|
280 416 | async fn poisoned_connection_is_not_reused(backend: &dyn HttpsClientBackend) {
|
281 417 | let script =
|
282 418 | H2ConnectionScript::new().fallback(H2StreamScript::respond(H2Response::ok("ok")));
|
283 419 | let server = H2TestServer::builder()
|
284 420 | .connections(H2ConnectionPlan::queue([script.clone(), script]))
|
285 421 | .start()
|
286 422 | .await
|
287 423 | .expect("H2 server should start");
|
632 768 | "GOAWAY before dispatch created {} connections",
|
633 769 | server.connection_count()
|
634 770 | );
|
635 771 |
|
636 772 | drop(connector);
|
637 773 | drop(client);
|
638 774 | server.shutdown().await.expect("clean H2 server shutdown");
|
639 775 | }
|
640 776 | }
|
641 777 |
|
778 + | mod abrupt_transport_failure {
|
779 + | use super::*;
|
780 + | use aws_smithy_http_client::pool::ConnectionEvent;
|
781 + | use aws_smithy_runtime_api::client::connection::ConnectionId;
|
782 + | use std::sync::{Arc, Mutex};
|
783 + | use std::time::Duration;
|
784 + |
|
785 + | #[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
786 + | enum PoolEvent {
|
787 + | Opened(ConnectionId),
|
788 + | LogicalClose(ConnectionId),
|
789 + | PhysicalClose(ConnectionId),
|
790 + | }
|
791 + |
|
792 + | /// Raw transport loss terminates every accepted stream and later demand uses
|
793 + | /// a replacement connection.
|
794 + | async fn abrupt_transport_failure_terminates_streams_and_replaces_connection(
|
795 + | client: SharedHttpClient,
|
796 + | ) -> aws_smithy_http_client::pool::OriginKey {
|
797 + | let body_gate = ManualGate::new();
|
798 + | let held = |body| {
|
799 + | H2StreamScript::respond(
|
800 + | H2Response::new(http_1x::StatusCode::OK).body(H2BodyPlan::gated(
|
801 + | body,
|
802 + | body_gate.waiter(),
|
803 + | "-never",
|
804 + | )),
|
805 + | )
|
806 + | };
|
807 + | let first = H2ConnectionScript::new()
|
808 + | .route("/warm", H2StreamScript::respond(H2Response::ok("warm")))
|
809 + | .route("/one", held("one"))
|
810 + | .route("/two", held("two"))
|
811 + | .route("/three", held("three"));
|
812 + | let second = H2ConnectionScript::new().route(
|
813 + | "/replacement",
|
814 + | H2StreamScript::respond(H2Response::ok("replacement")),
|
815 + | );
|
816 + | let server = H2TestServer::builder()
|
817 + | .connections(H2ConnectionPlan::queue([first, second]))
|
818 + | .start()
|
819 + | .await
|
820 + | .expect("H2 server should start");
|
821 + | let connector = test_client::connector(&client);
|
822 + |
|
823 + | let (status, body) = test_client::get_and_collect(&connector, &server.url("/warm")).await;
|
824 + | assert_eq!((status, body.as_slice()), (200, b"warm".as_slice()));
|
825 + | let (one, two, three) = tokio::join!(
|
826 + | test_client::send_request(
|
827 + | &connector,
|
828 + | HttpRequest::get(server.url("/one")).expect("valid HTTP request"),
|
829 + | ),
|
830 + | test_client::send_request(
|
831 + | &connector,
|
832 + | HttpRequest::get(server.url("/two")).expect("valid HTTP request"),
|
833 + | ),
|
834 + | test_client::send_request(
|
835 + | &connector,
|
836 + | HttpRequest::get(server.url("/three")).expect("valid HTTP request"),
|
837 + | ),
|
838 + | );
|
839 + | let responses = [
|
840 + | one.expect("first response headers should arrive"),
|
841 + | two.expect("second response headers should arrive"),
|
842 + | three.expect("third response headers should arrive"),
|
843 + | ];
|
844 + | body_gate
|
845 + | .wait_for_arrivals(3, test_client::WAIT)
|
846 + | .await
|
847 + | .expect("all response streams should reach the body gate");
|
848 + |
|
849 + | let original = single_stream_connection(&server, "/warm");
|
850 + | assert_eq!(original, single_stream_connection(&server, "/one"));
|
851 + | assert_eq!(original, single_stream_connection(&server, "/two"));
|
852 + | assert_eq!(original, single_stream_connection(&server, "/three"));
|
853 + | server
|
854 + | .abort_transport(original)
|
855 + | .await
|
856 + | .expect("the original transport should abort");
|
857 + |
|
858 + | for response in responses {
|
859 + | let body_result =
|
860 + | tokio::time::timeout(test_client::WAIT, response.into_body().collect())
|
861 + | .await
|
862 + | .expect("aborted response body should terminate");
|
863 + | assert!(
|
864 + | body_result.is_err(),
|
865 + | "abrupt transport loss completed an accepted response body"
|
866 + | );
|
867 + | }
|
868 + | server
|
869 + | .wait_for_event(test_client::WAIT, |event| {
|
870 + | matches!(
|
871 + | event,
|
872 + | H2Event::ConnectionClosed {
|
873 + | connection_id,
|
874 + | reason: ConnectionCloseReason::ScriptedTransportAbort,
|
875 + | } if *connection_id == original
|
876 + | )
|
877 + | })
|
878 + | .await
|
879 + | .expect("the harness should record raw transport loss");
|
880 + |
|
881 + | let replacement_url = server.url("/replacement");
|
882 + | let origin = aws_smithy_http_client::pool::OriginKey::from_uri(
|
883 + | &replacement_url.parse().expect("valid replacement URI"),
|
884 + | )
|
885 + | .expect("replacement URI should name an origin");
|
886 + | let (status, body) = test_client::get_and_collect(&connector, &replacement_url).await;
|
887 + | assert_eq!((status, body.as_slice()), (200, b"replacement".as_slice()));
|
888 + | assert_ne!(
|
889 + | original,
|
890 + | single_stream_connection(&server, "/replacement"),
|
891 + | "later demand reused the aborted connection"
|
892 + | );
|
893 + | assert_eq!(2, server.connection_count());
|
894 + |
|
895 + | drop(connector);
|
896 + | drop(client);
|
897 + | server.shutdown().await.expect("clean H2 server shutdown");
|
898 + | origin
|
899 + | }
|
900 + |
|
901 + | async fn wait_for_pool_events(
|
902 + | events: &Arc<Mutex<Vec<PoolEvent>>>,
|
903 + | predicate: impl Fn(&[PoolEvent]) -> bool,
|
904 + | ) {
|
905 + | tokio::time::timeout(test_client::WAIT, async {
|
906 + | loop {
|
907 + | if predicate(&events.lock().unwrap()) {
|
908 + | return;
|
909 + | }
|
910 + | tokio::time::sleep(Duration::from_millis(10)).await;
|
911 + | }
|
912 + | })
|
913 + | .await
|
914 + | .expect("pool lifecycle events should converge");
|
915 + | }
|
916 + |
|
917 + | #[tokio::test]
|
918 + | async fn test_abrupt_transport_failure_terminates_streams_and_replaces_connection_with_hyper_util_legacy_pool(
|
919 + | ) {
|
920 + | abrupt_transport_failure_terminates_streams_and_replaces_connection(h2_client(
|
921 + | &HyperUtilLegacyPool,
|
922 + | ))
|
923 + | .await;
|
924 + | }
|
925 + |
|
926 + | #[tokio::test]
|
927 + | async fn test_abrupt_transport_failure_terminates_streams_and_replaces_connection_with_partitioned_pool(
|
928 + | ) {
|
929 + | let events = Arc::new(Mutex::new(Vec::new()));
|
930 + | let observed = events.clone();
|
931 + | let pool = ConnectionPool::builder()
|
932 + | .tls_provider(rustls_aws_lc())
|
933 + | .tls_context(test_tls::SERVER_IDENTITY.client_context())
|
934 + | .max_connections_per_host(1)
|
935 + | .event_listener(move |event: &ConnectionEvent<'_>| {
|
936 + | let event = match event {
|
937 + | ConnectionEvent::Opened(opened) => {
|
938 + | Some(PoolEvent::Opened(opened.connection().id()))
|
939 + | }
|
940 + | ConnectionEvent::LogicalClose(closed) => {
|
941 + | Some(PoolEvent::LogicalClose(closed.connection().id()))
|
942 + | }
|
943 + | ConnectionEvent::PhysicalClose(closed) => {
|
944 + | Some(PoolEvent::PhysicalClose(closed.connection().id()))
|
945 + | }
|
946 + | ConnectionEvent::EstablishmentFailed(_) => None,
|
947 + | _ => None,
|
948 + | };
|
949 + | if let Some(event) = event {
|
950 + | observed.lock().unwrap().push(event);
|
951 + | }
|
952 + | })
|
953 + | .build_https()
|
954 + | .expect("valid partitioned HTTPS pool");
|
955 + | let client = SharedHttpClient::new(
|
956 + | PoolClient::new(&pool).expect("anonymous partition should resolve"),
|
957 + | );
|
958 + |
|
959 + | let origin =
|
960 + | abrupt_transport_failure_terminates_streams_and_replaces_connection(client).await;
|
961 + | wait_for_pool_events(&events, |events| {
|
962 + | events
|
963 + | .iter()
|
964 + | .filter(|event| matches!(event, PoolEvent::PhysicalClose(_)))
|
965 + | .count()
|
966 + | >= 1
|
967 + | })
|
968 + | .await;
|
969 + |
|
970 + | let events = events.lock().unwrap();
|
971 + | let opened: Vec<_> = events
|
972 + | .iter()
|
973 + | .filter_map(|event| match event {
|
974 + | PoolEvent::Opened(connection) => Some(*connection),
|
975 + | _ => None,
|
976 + | })
|
977 + | .collect();
|
978 + | assert_eq!(2, opened.len(), "expected original and replacement opens");
|
979 + | assert_ne!(opened[0], opened[1]);
|
980 + | assert_eq!(
|
981 + | 1,
|
982 + | events
|
983 + | .iter()
|
984 + | .filter(|event| matches!(event, PoolEvent::LogicalClose(id) if *id == opened[0]))
|
985 + | .count(),
|
986 + | "the aborted generation should close logically once"
|
987 + | );
|
988 + | assert_eq!(
|
989 + | 1,
|
990 + | events
|
991 + | .iter()
|
992 + | .filter(|event| matches!(event, PoolEvent::PhysicalClose(id) if *id == opened[0]))
|
993 + | .count(),
|
994 + | "the aborted transport should close physically once"
|
995 + | );
|
996 + | drop(events);
|
997 + |
|
998 + | let stats = pool
|
999 + | .partition_stats(PartitionId::ANONYMOUS, &origin)
|
1000 + | .expect("anonymous partition should retain origin statistics");
|
1001 + | assert_eq!(0, stats.pending_acquisitions());
|
1002 + | assert_eq!(0, stats.establishing_connections());
|
1003 + | assert_eq!(0, stats.h2().active_requests());
|
1004 + | assert_eq!(1, stats.h2().accepting());
|
1005 + | assert_eq!(0, stats.h2().draining());
|
1006 + | assert_eq!(1, stats.physically_live_connections());
|
1007 + | let capacity = pool
|
1008 + | .origin_stats(&origin)
|
1009 + | .capacity()
|
1010 + | .copied()
|
1011 + | .expect("origin should have bounded capacity");
|
1012 + | assert_eq!(1, capacity.limit());
|
1013 + | assert_eq!(
|
1014 + | 1,
|
1015 + | capacity.in_use(),
|
1016 + | "replacement connection should own the returned slot"
|
1017 + | );
|
1018 + | }
|
1019 + | }
|
1020 + |
|
642 1021 | #[cfg(feature = "s2n-tls")]
|
643 1022 | mod protocol_negotiation {
|
644 1023 | use super::*;
|
645 1024 |
|
646 1025 | /// The s2n-tls provider negotiates `h2` with ALPN and reuses that connection.
|
647 1026 | async fn s2n_negotiates_h2_and_reuses_connection(backend: &dyn HttpsClientBackend) {
|
648 1027 | let server = H2TestServer::builder()
|
649 1028 | .connections(H2ConnectionPlan::queue([H2ConnectionScript::new()
|
650 1029 | .fallback(H2StreamScript::respond(H2Response::ok("s2n-h2")))]))
|
651 1030 | .start()
|
1072 1451 | let second_connection = single_stream_connection(&server, "/second");
|
1073 1452 | assert_ne!(first_connection, second_connection);
|
1074 1453 | assert_eq!(server.connection_count(), 2);
|
1075 1454 |
|
1076 1455 | drop(metadata);
|
1077 1456 | drop(first);
|
1078 1457 | drop(pool);
|
1079 1458 | server.shutdown().await.expect("clean H2 server shutdown");
|
1080 1459 | }
|
1081 1460 | }
|
1461 + |
|
1462 + | mod runtime_placement {
|
1463 + | use super::*;
|
1464 + | use aws_smithy_http_client::pool::ConnectionEvent;
|
1465 + | use aws_smithy_runtime_api::client::connection::ConnectionId;
|
1466 + | use runtime::DrivenRuntime;
|
1467 + | use std::sync::{Arc, Mutex};
|
1468 + | use std::time::Duration;
|
1469 + | use tokio::runtime::{Handle, Id};
|
1470 + |
|
1471 + | #[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
1472 + | enum EventKind {
|
1473 + | Opened,
|
1474 + | LogicalClose,
|
1475 + | PhysicalClose,
|
1476 + | }
|
1477 + |
|
1478 + | #[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
1479 + | struct ObservedEvent {
|
1480 + | kind: EventKind,
|
1481 + | connection: ConnectionId,
|
1482 + | partition: PartitionId,
|
1483 + | runtime: Option<Id>,
|
1484 + | }
|
1485 + |
|
1486 + | async fn wait_for_event(
|
1487 + | events: &Arc<Mutex<Vec<ObservedEvent>>>,
|
1488 + | predicate: impl Fn(&ObservedEvent) -> bool,
|
1489 + | ) -> ObservedEvent {
|
1490 + | tokio::time::timeout(test_client::WAIT, async {
|
1491 + | loop {
|
1492 + | if let Some(event) = events.lock().unwrap().iter().copied().find(&predicate) {
|
1493 + | return event;
|
1494 + | }
|
1495 + | tokio::time::sleep(Duration::from_millis(10)).await;
|
1496 + | }
|
1497 + | })
|
1498 + | .await
|
1499 + | .expect("connection event should arrive")
|
1500 + | }
|
1501 + |
|
1502 + | #[tokio::test]
|
1503 + | async fn peer_h2_reuse_keeps_connection_work_on_the_owner_runtime() {
|
1504 + | let first_id = PartitionId::from_index(1);
|
1505 + | let second_id = PartitionId::from_index(2);
|
1506 + | let first_runtime = DrivenRuntime::start("h2-owner-one");
|
1507 + | let first_runtime_id = first_runtime.id();
|
1508 + | let second_runtime = DrivenRuntime::start("h2-owner-two");
|
1509 + | let second_runtime_id = second_runtime.id();
|
1510 + | let peer_body_gate = ManualGate::new();
|
1511 + | let first_script = H2ConnectionScript::new()
|
1512 + | .abort_streams_on_client_close()
|
1513 + | .route("/first", H2StreamScript::respond(H2Response::ok("first")))
|
1514 + | .route(
|
1515 + | "/peer",
|
1516 + | H2StreamScript::respond(
|
1517 + | H2Response::new(http_1x::StatusCode::OK).body(H2BodyPlan::gated(
|
1518 + | "peer-",
|
1519 + | peer_body_gate.waiter(),
|
1520 + | "never",
|
1521 + | )),
|
1522 + | ),
|
1523 + | );
|
1524 + | let second_script = H2ConnectionScript::new().route(
|
1525 + | "/replacement",
|
1526 + | H2StreamScript::respond(H2Response::ok("replacement")),
|
1527 + | );
|
1528 + | let server = H2TestServer::builder()
|
1529 + | .connections(H2ConnectionPlan::queue([first_script, second_script]))
|
1530 + | .start()
|
1531 + | .await
|
1532 + | .expect("H2 server should start");
|
1533 + |
|
1534 + | let events = Arc::new(Mutex::new(Vec::new()));
|
1535 + | let observed = events.clone();
|
1536 + | let pool = ConnectionPool::builder()
|
1537 + | .tls_provider(rustls_aws_lc())
|
1538 + | .tls_context(test_tls::SERVER_IDENTITY.client_context())
|
1539 + | .partitions([
|
1540 + | Partition::new(first_id, first_runtime.driver_spawner()),
|
1541 + | Partition::new(second_id, second_runtime.driver_spawner()),
|
1542 + | ])
|
1543 + | .connection_reuse_scope(ConnectionReuseScope::Pool)
|
1544 + | .max_connections_per_host(1)
|
1545 + | .event_listener(move |event: &ConnectionEvent<'_>| {
|
1546 + | let observed_event = match event {
|
1547 + | ConnectionEvent::Opened(opened) => {
|
1548 + | Some((EventKind::Opened, opened.connection()))
|
1549 + | }
|
1550 + | ConnectionEvent::LogicalClose(closed) => {
|
1551 + | Some((EventKind::LogicalClose, closed.connection()))
|
1552 + | }
|
1553 + | ConnectionEvent::PhysicalClose(closed) => {
|
1554 + | Some((EventKind::PhysicalClose, closed.connection()))
|
1555 + | }
|
1556 + | ConnectionEvent::EstablishmentFailed(_) => None,
|
1557 + | _ => None,
|
1558 + | };
|
1559 + | if let Some((kind, connection)) = observed_event {
|
1560 + | observed.lock().unwrap().push(ObservedEvent {
|
1561 + | kind,
|
1562 + | connection: connection.id(),
|
1563 + | partition: connection.owner_partition(),
|
1564 + | runtime: Handle::try_current().ok().map(|handle| handle.id()),
|
1565 + | });
|
1566 + | }
|
1567 + | })
|
1568 + | .build_https()
|
1569 + | .expect("valid partitioned HTTPS pool");
|
1570 + | let first_client = SharedHttpClient::new(
|
1571 + | PoolClient::from_partition(&pool, first_id).expect("first partition should resolve"),
|
1572 + | );
|
1573 + | let second_client = SharedHttpClient::new(
|
1574 + | PoolClient::from_partition(&pool, second_id).expect("second partition should resolve"),
|
1575 + | );
|
1576 + | let first = test_client::connector(&first_client);
|
1577 + | let second = test_client::connector(&second_client);
|
1578 + |
|
1579 + | let first_request = first.clone();
|
1580 + | let first_url = server.url("/first");
|
1581 + | let (status, body) = first_runtime
|
1582 + | .spawn(async move { test_client::get_and_collect(&first_request, &first_url).await })
|
1583 + | .await
|
1584 + | .expect("first partition request task should not panic");
|
1585 + | assert_eq!((status, body.as_slice()), (200, b"first".as_slice()));
|
1586 + | let original_server_connection = single_stream_connection(&server, "/first");
|
1587 + | let original_open = wait_for_event(&events, |event| {
|
1588 + | event.kind == EventKind::Opened && event.partition == first_id
|
1589 + | })
|
1590 + | .await;
|
1591 + | assert_eq!(Some(first_runtime_id), original_open.runtime);
|
1592 + | assert!(first_runtime.submitted_tasks() > 0);
|
1593 + |
|
1594 + | let peer_request = second.clone();
|
1595 + | let peer_url = server.url("/peer");
|
1596 + | let peer_response = second_runtime
|
1597 + | .spawn(async move {
|
1598 + | test_client::send_request(
|
1599 + | &peer_request,
|
1600 + | HttpRequest::get(peer_url).expect("valid HTTP request"),
|
1601 + | )
|
1602 + | .await
|
1603 + | })
|
1604 + | .await
|
1605 + | .expect("peer request task should not panic")
|
1606 + | .expect("peer response headers should arrive");
|
1607 + | peer_body_gate
|
1608 + | .wait_until_reached(test_client::WAIT)
|
1609 + | .await
|
1610 + | .expect("peer response should reach its body gate");
|
1611 + | assert_eq!(
|
1612 + | original_server_connection,
|
1613 + | single_stream_connection(&server, "/peer"),
|
1614 + | "peer demand should use the first partition's generation"
|
1615 + | );
|
1616 + | assert!(
|
1617 + | !events
|
1618 + | .lock()
|
1619 + | .unwrap()
|
1620 + | .iter()
|
1621 + | .any(|event| { event.kind == EventKind::Opened && event.partition == second_id }),
|
1622 + | "peer reuse should not establish a requester-owned connection"
|
1623 + | );
|
1624 + |
|
1625 + | first_runtime.shutdown();
|
1626 + | let body_result =
|
1627 + | tokio::time::timeout(test_client::WAIT, peer_response.into_body().collect())
|
1628 + | .await
|
1629 + | .expect("owner-runtime shutdown should terminate the accepted response");
|
1630 + | assert!(
|
1631 + | body_result.is_err(),
|
1632 + | "accepted response completed after its owner runtime stopped"
|
1633 + | );
|
1634 + | let logical_close = wait_for_event(&events, |event| {
|
1635 + | event.kind == EventKind::LogicalClose && event.connection == original_open.connection
|
1636 + | })
|
1637 + | .await;
|
1638 + | assert_eq!(first_id, logical_close.partition);
|
1639 + | let physical_close = wait_for_event(&events, |event| {
|
1640 + | event.kind == EventKind::PhysicalClose && event.connection == original_open.connection
|
1641 + | })
|
1642 + | .await;
|
1643 + | assert_eq!(first_id, physical_close.partition);
|
1644 + |
|
1645 + | let replacement_request = second.clone();
|
1646 + | let replacement_url = server.url("/replacement");
|
1647 + | let (status, body) = second_runtime
|
1648 + | .spawn(async move {
|
1649 + | test_client::get_and_collect(&replacement_request, &replacement_url).await
|
1650 + | })
|
1651 + | .await
|
1652 + | .expect("replacement request task should not panic");
|
1653 + | assert_eq!((status, body.as_slice()), (200, b"replacement".as_slice()));
|
1654 + | assert_ne!(
|
1655 + | original_server_connection,
|
1656 + | single_stream_connection(&server, "/replacement")
|
1657 + | );
|
1658 + | let replacement_open = wait_for_event(&events, |event| {
|
1659 + | event.kind == EventKind::Opened && event.partition == second_id
|
1660 + | })
|
1661 + | .await;
|
1662 + | assert_eq!(Some(second_runtime_id), replacement_open.runtime);
|
1663 + | assert_ne!(original_open.connection, replacement_open.connection);
|
1664 + | assert!(second_runtime.submitted_tasks() > 0);
|
1665 + |
|
1666 + | let origin = aws_smithy_http_client::pool::OriginKey::from_uri(
|
1667 + | &server.url("/").parse().expect("valid server URI"),
|
1668 + | )
|
1669 + | .expect("server URI should name an origin");
|
1670 + | let first_stats = pool
|
1671 + | .partition_stats(first_id, &origin)
|
1672 + | .expect("first partition should retain origin statistics");
|
1673 + | assert_eq!(0, first_stats.h2().active_requests());
|
1674 + | assert_eq!(0, first_stats.physically_live_connections());
|
1675 + | let second_stats = pool
|
1676 + | .partition_stats(second_id, &origin)
|
1677 + | .expect("second partition should retain origin statistics");
|
1678 + | assert_eq!(1, second_stats.h2().accepting());
|
1679 + | assert_eq!(0, second_stats.h2().active_requests());
|
1680 + | assert_eq!(1, second_stats.physically_live_connections());
|
1681 + | let capacity = pool
|
1682 + | .origin_stats(&origin)
|
1683 + | .capacity()
|
1684 + | .copied()
|
1685 + | .expect("origin should have bounded capacity");
|
1686 + | assert_eq!(1, capacity.limit());
|
1687 + | assert_eq!(1, capacity.in_use());
|
1688 + |
|
1689 + | drop(first);
|
1690 + | drop(second);
|
1691 + | drop(first_client);
|
1692 + | drop(second_client);
|
1693 + | drop(pool);
|
1694 + | second_runtime.shutdown();
|
1695 + | server.shutdown().await.expect("clean H2 server shutdown");
|
1696 + | }
|
1697 + | }
|