Skip to main content

aws_smithy_runtime/client/http/
connection_poisoning.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use crate::client::retries::classifiers::run_classifiers_on_ctx;
7use aws_smithy_runtime_api::box_error::BoxError;
8use aws_smithy_runtime_api::client::interceptors::context::{
9    AfterDeserializationInterceptorContextRef, BeforeTransmitInterceptorContextMut,
10};
11use aws_smithy_runtime_api::client::interceptors::{dyn_dispatch_hint, Intercept};
12use aws_smithy_runtime_api::client::retries::classifiers::RetryAction;
13use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
14use aws_smithy_types::config_bag::ConfigBag;
15use aws_smithy_types::retry::{ReconnectMode, RetryConfig, RetrySpec};
16use tracing::{debug, error};
17
18// re-export relocated struct that used to live here
19pub use aws_smithy_runtime_api::client::connection::CaptureSmithyConnection;
20
21/// An interceptor for poisoning connections in response to certain events.
22///
23/// This interceptor, when paired with a compatible connection, allows the connection to be
24/// poisoned in reaction to certain events *(like receiving a transient error.)* This allows users
25/// to avoid sending requests to a server that isn't responding. This can increase the load on a
26/// server, because more connections will be made overall.
27///
28/// **In order for this interceptor to work,** the configured connection must interact with the
29/// "connection retriever" stored in an HTTP request's `extensions` map. For an example of this,
30/// see [`HyperConnector`]. When a connection is made available to the retriever, this interceptor
31/// will call a `.poison` method on it, signalling that the connection should be dropped. It is
32/// up to the connection implementer to handle this.
33///
34/// [`HyperConnector`]: https://github.com/smithy-lang/smithy-rs/blob/26a914ece072bba2dd9b5b49003204b70e7666ac/rust-runtime/aws-smithy-runtime/src/client/http/hyper_014.rs#L347
35#[non_exhaustive]
36#[derive(Debug, Default)]
37pub struct ConnectionPoisoningInterceptor {}
38
39impl ConnectionPoisoningInterceptor {
40    /// Create a new `ConnectionPoisoningInterceptor`.
41    pub fn new() -> Self {
42        Self::default()
43    }
44}
45
46#[dyn_dispatch_hint]
47impl Intercept for ConnectionPoisoningInterceptor {
48    fn name(&self) -> &'static str {
49        "ConnectionPoisoningInterceptor"
50    }
51
52    fn modify_before_transmit(
53        &self,
54        context: &mut BeforeTransmitInterceptorContextMut<'_>,
55        _runtime_components: &RuntimeComponents,
56        cfg: &mut ConfigBag,
57    ) -> Result<(), BoxError> {
58        let capture_smithy_connection = CaptureSmithyConnection::new();
59        context
60            .request_mut()
61            .add_extension(capture_smithy_connection.clone());
62        cfg.interceptor_state().store_put(capture_smithy_connection);
63
64        Ok(())
65    }
66
67    fn read_after_deserialization(
68        &self,
69        context: &AfterDeserializationInterceptorContextRef<'_>,
70        runtime_components: &RuntimeComponents,
71        cfg: &mut ConfigBag,
72    ) -> Result<(), BoxError> {
73        let reconnect_mode = cfg
74            .load::<RetryConfig>()
75            .map(|rc| {
76                if rc
77                    .retry_spec()
78                    .is_some_and(|s| s.is_at_least(RetrySpec::V2_1))
79                {
80                    ReconnectMode::ReuseAllConnections
81                } else {
82                    rc.reconnect_mode()
83                }
84            })
85            .unwrap_or(ReconnectMode::ReconnectOnTransientError);
86        let captured_connection = cfg.load::<CaptureSmithyConnection>().cloned();
87        let retry_classifier_result =
88            run_classifiers_on_ctx(runtime_components.retry_classifiers(), context.inner());
89        let error_is_transient = retry_classifier_result == RetryAction::transient_error();
90        let connection_poisoning_is_enabled =
91            reconnect_mode == ReconnectMode::ReconnectOnTransientError;
92
93        if error_is_transient && connection_poisoning_is_enabled {
94            debug!("received a transient error, marking the connection for closure...");
95
96            if let Some(captured_connection) = captured_connection.and_then(|conn| conn.get()) {
97                captured_connection.poison();
98                debug!("the connection was marked for closure")
99            } else {
100                error!(
101                    "unable to mark the connection for closure because no connection was found! The underlying HTTP connector never set a connection."
102                );
103            }
104        }
105
106        Ok(())
107    }
108}