use std::borrow::Cow;
use std::fmt::Debug;
use std::sync::Arc;
use bytesbuf::mem::GlobalPool;
use http_extensions::{HttpBodyBuilder, RequestHandler};
use opentelemetry::metrics::Meter;
use thread_aware::{PerCore, ThreadAware, unaware};
use tick::Clock;
use crate::handlers::TransportHandler;
use crate::options::{ClientOptions, PoolIndex, TransportOptions};
use crate::tls::TlsOptions;
use crate::{HttpClient, HttpClientBuilder};
#[derive(Debug, Clone, Copy, PartialEq, Eq, ThreadAware)]
pub enum Isolation {
Isolated,
Shared,
}
#[derive(Debug, Clone, ThreadAware)]
pub struct CustomDeps<Extras = ()>
where
Extras: ThreadAware + Send + Sync + Clone + 'static,
{
pub clock: Clock,
pub global_pool: GlobalPool,
pub extras: Extras,
}
#[derive(Debug)]
#[non_exhaustive]
pub struct CustomContext<Extras = ()> {
pub body_builder: HttpBodyBuilder,
pub clock: Clock,
pub pool_index: PoolIndex,
pub extras: Extras,
pub options: TransportOptions,
pub tls: TlsOptions,
pub meter: Meter,
}
pub fn create_builder<F, R, Extras>(
runtime: impl Into<Cow<'static, str>>,
transport: impl Into<Cow<'static, str>>,
factory: F,
isolation: Isolation,
deps: impl Into<CustomDeps<Extras>>,
) -> HttpClientBuilder
where
F: Fn(CustomContext<Extras>) -> R + Send + Sync + 'static,
R: RequestHandler + 'static,
Extras: ThreadAware + Send + Sync + Clone + 'static,
{
HttpClient::builder_custom_internal(
runtime,
transport,
move |cx| TransportHandler::new(factory(cx)),
isolation,
deps.into(),
)
}
impl HttpClient {
pub(crate) fn builder_custom_internal<F, Extras>(
runtime: impl Into<Cow<'static, str>>,
transport: impl Into<Cow<'static, str>>,
factory: F,
isolation: Isolation,
deps: CustomDeps<Extras>,
) -> HttpClientBuilder
where
F: Fn(CustomContext<Extras>) -> TransportHandler + Send + Sync + 'static,
Extras: ThreadAware + Send + Sync + Clone + 'static,
{
let factory = Arc::new(factory);
let transport = Transport {
runtime_name: runtime.into(),
name: transport.into(),
clock: deps.clock.clone(),
global_pool: deps.global_pool.clone(),
isolation,
inner: thread_aware::Arc::new_with((deps, unaware(factory)), |(deps, factory)| {
Arc::new(move |options, meter, pool_index| {
let context = CustomContext {
body_builder: create_body_builder(&deps.global_pool, &deps.clock, &options),
clock: deps.clock.clone(),
pool_index,
extras: deps.extras.clone(),
options: options.transport.clone(),
tls: options.tls.clone(),
meter,
};
factory.0(context)
})
}),
};
HttpClientBuilder::new(transport)
}
}
type TransportFn = Arc<dyn Fn(ClientOptions, Meter, PoolIndex) -> TransportHandler + Send + Sync>;
#[derive(Clone, ThreadAware)]
pub(crate) struct Transport {
#[thread_aware(skip)]
runtime_name: Cow<'static, str>,
#[thread_aware(skip)]
name: Cow<'static, str>,
inner: thread_aware::Arc<TransportFn, PerCore>,
clock: Clock,
global_pool: GlobalPool,
isolation: Isolation,
}
impl Transport {
pub(crate) fn create_transport_handler(&self, options: ClientOptions, meter: Meter, index: PoolIndex) -> TransportHandler {
self.inner.as_ref()(options, meter, index)
}
pub(crate) fn runtime(&self) -> &Cow<'static, str> {
&self.runtime_name
}
pub(crate) fn name(&self) -> &Cow<'static, str> {
&self.name
}
pub(crate) fn clock(&self) -> &Clock {
&self.clock
}
pub(crate) fn isolation(&self) -> Isolation {
self.isolation
}
pub(crate) fn create_body_builder(&self, options: &ClientOptions) -> HttpBodyBuilder {
create_body_builder(&self.global_pool, &self.clock, options)
}
}
impl Debug for Transport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(std::any::type_name::<Self>()).finish()
}
}
pub(crate) fn create_body_builder(pool: &GlobalPool, clock: &Clock, options: &ClientOptions) -> HttpBodyBuilder {
HttpBodyBuilder::new(pool.clone(), clock).with_options(options.response_body_options)
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use http::StatusCode;
use http_extensions::FakeHandler;
use thread_aware::unaware;
use super::{CustomContext, CustomDeps, Isolation, create_builder};
use crate::HttpResponseBuilder;
use crate::fake::FakeDeps;
use crate::pipeline::Pipeline;
#[mutants::skip]
fn custom_deps() -> CustomDeps {
CustomDeps {
clock: FakeDeps::default().clock,
global_pool: bytesbuf::mem::GlobalPool::new(),
extras: (),
}
}
#[mutants::skip]
fn ok_factory(_ctx: CustomContext) -> FakeHandler {
FakeHandler::from_fn(|_req| HttpResponseBuilder::new_fake().status(StatusCode::OK).build())
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn create_builder_serves_requests_through_custom_pipeline() {
let client = create_builder("test-runtime", "test", ok_factory, Isolation::Shared, custom_deps())
.insecure_allow_http()
.minimal_pipeline()
.build();
assert!(matches!(client.pipeline(), Pipeline::Minimal(_)));
let response = client.get("http://example.com").fetch().await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn isolated_runtime_uses_per_core_handler() {
let client = create_builder("test-runtime", "test", ok_factory, Isolation::Isolated, custom_deps())
.insecure_allow_http()
.build();
let response = client.get("http://example.com").fetch().await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn extras_are_forwarded_to_factory() {
let counter = Arc::new(AtomicUsize::new(0));
let deps = CustomDeps {
clock: FakeDeps::default().clock,
global_pool: bytesbuf::mem::GlobalPool::new(),
extras: unaware(Arc::clone(&counter)),
};
let client = create_builder(
"test-runtime",
"test",
|ctx: CustomContext<thread_aware::Unaware<Arc<AtomicUsize>>>| {
ctx.extras.fetch_add(1, Ordering::Relaxed);
FakeHandler::from_fn(|_req| HttpResponseBuilder::new_fake().status(StatusCode::OK).build())
},
Isolation::Shared,
deps,
)
.insecure_allow_http()
.build();
let response = client.get("http://example.com").fetch().await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert!(counter.load(Ordering::Relaxed) >= 1);
}
#[cfg_attr(miri, ignore)]
#[test]
fn transport_has_type_name_debug_representation() {
let builder = create_builder("test-runtime", "test", ok_factory, Isolation::Shared, custom_deps());
assert!(format!("{builder:?}").contains("Transport"));
}
}