use super::partition::PartitionId;
use super::registry::PartitionState;
use super::ConnectionPool;
use crate::client::timeout::{self, TimeoutKind};
use crate::client::{downcast_error, ConnectorCallTimer};
use crate::sync::Arc;
use aws_smithy_async::rt::sleep::{default_async_sleep, SharedAsyncSleep};
use aws_smithy_async::time::SharedTimeSource;
use aws_smithy_runtime_api::box_error::BoxError;
use aws_smithy_runtime_api::client::connector_metadata::ConnectorMetadata;
use aws_smithy_runtime_api::client::http::telemetry::CaptureHttpAttemptTelemetry;
use aws_smithy_runtime_api::client::http::{
HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpConnector,
};
use aws_smithy_runtime_api::client::orchestrator::{HttpRequest, HttpResponse};
use aws_smithy_runtime_api::client::result::ConnectorError;
use aws_smithy_runtime_api::client::runtime_components::{
RuntimeComponents, RuntimeComponentsBuilder,
};
use aws_smithy_types::config_bag::ConfigBag;
use std::borrow::Cow;
use std::error::Error;
use std::fmt;
use std::time::Duration;
#[derive(Clone)]
pub struct Client {
pool: ConnectionPool,
partition: Arc<PartitionState>,
}
impl Client {
pub fn new(pool: &ConnectionPool) -> Result<Self, ClientBuildError> {
Self::from_partition(pool, PartitionId::ANONYMOUS)
}
pub fn from_partition(
pool: &ConnectionPool,
id: PartitionId,
) -> Result<Self, ClientBuildError> {
let partition = pool
.inner
.registry
.partition(id)
.ok_or_else(|| ClientBuildError::invalid_partition(id))?;
Ok(Self {
pool: pool.clone(),
partition,
})
}
}
impl fmt::Debug for Client {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Client")
.field("partition", &self.partition.id())
.field("pool", &self.pool)
.finish_non_exhaustive()
}
}
impl HttpClient for Client {
fn validate_base_client_config(
&self,
_: &RuntimeComponentsBuilder,
_: &ConfigBag,
) -> Result<(), BoxError> {
self.pool
.inner
.transport
.initialize_for_partition(&self.partition)
}
fn http_connector(
&self,
settings: &HttpConnectorSettings,
components: &RuntimeComponents,
) -> SharedHttpConnector {
let connect_timeout = settings.connect_timeout();
let read_timeout = settings.read_timeout();
let sleep = components.sleep_impl().or_else(default_async_sleep);
let time_source = components.time_source().unwrap_or_default();
SharedHttpConnector::new(PoolConnector {
pool: self.pool.clone(),
partition: self.partition.clone(),
connect_timeout,
read_timeout,
sleep,
time_source,
})
}
fn connector_metadata(&self) -> Option<ConnectorMetadata> {
Some(ConnectorMetadata::new("hyper", Some(Cow::Borrowed("1.x"))))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClientBuildError {
kind: ClientBuildErrorKind,
}
impl ClientBuildError {
fn invalid_partition(partition: PartitionId) -> Self {
Self {
kind: ClientBuildErrorKind::InvalidPartition(partition),
}
}
pub fn partition(&self) -> Option<PartitionId> {
match &self.kind {
ClientBuildErrorKind::InvalidPartition(partition) => Some(*partition),
}
}
}
impl fmt::Display for ClientBuildError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
ClientBuildErrorKind::InvalidPartition(partition) => {
write!(
f,
"connection pool does not contain partition {partition:?}"
)
}
}
}
}
impl Error for ClientBuildError {}
#[derive(Clone, Debug, Eq, PartialEq)]
enum ClientBuildErrorKind {
InvalidPartition(PartitionId),
}
struct PoolConnector {
pool: ConnectionPool,
partition: Arc<PartitionState>,
connect_timeout: Option<Duration>,
read_timeout: Option<Duration>,
sleep: Option<SharedAsyncSleep>,
time_source: SharedTimeSource,
}
impl fmt::Debug for PoolConnector {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PoolConnector")
.field("partition", &self.partition.id())
.field("connect_timeout", &self.connect_timeout)
.field("read_timeout", &self.read_timeout)
.finish_non_exhaustive()
}
}
async fn send_pool_request(
pool: &ConnectionPool,
partition: Arc<PartitionState>,
request: HttpRequest,
connect_timeout: Option<Duration>,
read_timeout: Option<Duration>,
sleep: Option<SharedAsyncSleep>,
attempt_telemetry: Option<super::dispatch::AttemptTelemetryInput>,
) -> Result<HttpResponse, ConnectorError> {
if (connect_timeout.is_some() || read_timeout.is_some()) && sleep.is_none() {
return Err(ConnectorError::user(MissingAsyncSleep.into()));
}
let request = request
.try_into_http1x()
.map_err(|error| ConnectorError::user(error.into()))?;
let options = super::dispatch::RequestOptions::new(
connect_timeout
.zip(sleep.clone())
.map(|(duration, sleep)| super::establish::TransportTimeout::new(duration, sleep)),
attempt_telemetry,
);
let send = pool.send_request(partition, request, options);
let response =
timeout::maybe_timeout_future(send, read_timeout, sleep.as_ref(), TimeoutKind::Read)
.await
.map_err(downcast_error)?;
HttpResponse::try_from(response).map_err(|error| ConnectorError::other(error.into(), None))
}
impl HttpConnector for PoolConnector {
fn call(&self, request: HttpRequest) -> HttpConnectorFuture {
let attempt_capture = request.extension::<CaptureHttpAttemptTelemetry>().cloned();
let connector_call_timer = ConnectorCallTimer::start(&request, &self.time_source);
let attempt_telemetry = attempt_capture.as_ref().map(|capture| {
super::dispatch::AttemptTelemetryInput::new(capture.clone(), self.time_source.clone())
});
let pool = self.pool.clone();
let partition = self.partition.clone();
let connect_timeout = self.connect_timeout;
let read_timeout = self.read_timeout;
let sleep = self.sleep.clone();
HttpConnectorFuture::new(async move {
let result = send_pool_request(
&pool,
partition,
request,
connect_timeout,
read_timeout,
sleep,
attempt_telemetry,
)
.await;
if let Some(timer) = connector_call_timer {
timer.finish();
}
result
})
}
}
#[derive(Debug)]
struct MissingAsyncSleep;
impl fmt::Display for MissingAsyncSleep {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("an async sleep implementation is required for HTTP connect or read timeouts")
}
}
impl Error for MissingAsyncSleep {}
#[cfg(all(test, feature = "rt-tokio", not(smithy_http_client_loom)))]
mod tests {
use super::*;
#[tokio::test]
async fn configured_timeout_without_sleep_returns_a_user_error() {
let pool = ConnectionPool::builder()
.idle_timeout(None)
.build_http()
.unwrap();
let client = Client::new(&pool).unwrap();
for (connect_timeout, read_timeout) in [
(Some(Duration::from_secs(1)), None),
(None, Some(Duration::from_secs(1))),
] {
let connector = PoolConnector {
pool: client.pool.clone(),
partition: client.partition.clone(),
connect_timeout,
read_timeout,
sleep: None,
time_source: SharedTimeSource::default(),
};
let error = connector
.call(HttpRequest::get("http://example.com/").unwrap())
.await
.expect_err("a timeout without sleep unexpectedly started a request");
assert!(error.is_user(), "unexpected connector error: {error:?}");
assert_eq!(
"an async sleep implementation is required for HTTP connect or read timeouts",
std::error::Error::source(&error).unwrap().to_string()
);
}
}
}