use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll};
use std::time::Duration;
use tokio::time::Instant;
use apollo_opentelemetry::metrics::{
FutureExt, HistogramExt, RecordDurationGuard, TrackGuard, UpDownCounterExt,
};
use hyper::client::conn::http2;
use hyper_rustls::HttpsConnector;
use hyper_util::client::legacy::connect::HttpConnector;
use hyper_util::client::pool::singleton::Singleton;
use opentelemetry::KeyValue;
use opentelemetry::metrics::UpDownCounter;
use opentelemetry_semantic_conventions::attribute as semconv;
use tower::BoxError;
use tower::Service;
use tower::ServiceExt as _;
use super::handshake::{FixedOrigin, Http2Handshake, TcpConnect};
use super::{
HostSender, HttpBody, Origin, ProtocolVersionCell, into_http_client_error, retain_predicate,
spawn_eviction_task,
};
use crate::error::HttpClientError;
use crate::metrics::{ConnectionMetrics, STATE_ACTIVE, STATE_IDLE};
struct ConnectionState {
guard: Mutex<TrackGuard<i64>>,
inflight: AtomicI32,
idle_since: Mutex<Option<Instant>>,
}
impl ConnectionState {
fn new(counter: &UpDownCounter<i64>, mut base_attrs: Vec<KeyValue>) -> Self {
base_attrs.push(KeyValue::new(semconv::HTTP_CONNECTION_STATE, STATE_IDLE));
Self {
guard: Mutex::new(counter.track(base_attrs)),
inflight: AtomicI32::new(0),
idle_since: Mutex::new(Some(Instant::now())),
}
}
fn request_start(&self) {
if self.inflight.fetch_add(1, Ordering::AcqRel) == 0 {
*self.idle_since.lock().unwrap() = None;
self.guard
.lock()
.unwrap()
.set(KeyValue::new(semconv::HTTP_CONNECTION_STATE, STATE_ACTIVE));
}
}
fn request_done(&self) {
if self.inflight.fetch_sub(1, Ordering::AcqRel) == 1 {
*self.idle_since.lock().unwrap() = Some(Instant::now());
self.guard
.lock()
.unwrap()
.set(KeyValue::new(semconv::HTTP_CONNECTION_STATE, STATE_IDLE));
}
}
fn idle_since(&self) -> Option<Instant> {
*self.idle_since.lock().unwrap()
}
}
struct RequestGuard(Arc<H2ConnectionInner>);
impl RequestGuard {
fn new(inner: Arc<H2ConnectionInner>) -> Self {
inner.state.request_start();
Self(inner)
}
}
impl Drop for RequestGuard {
fn drop(&mut self) {
self.0.state.request_done();
}
}
struct H2ConnectionInner {
state: ConnectionState,
_connection_duration: RecordDurationGuard,
}
#[derive(Clone)]
pub(crate) struct H2Connection {
sender: http2::SendRequest<HttpBody>,
inner: Arc<H2ConnectionInner>,
}
impl H2Connection {
pub(crate) fn idle_since(&self) -> Option<Instant> {
self.inner.state.idle_since()
}
pub(crate) fn new(
sender: http2::SendRequest<HttpBody>,
metrics: &ConnectionMetrics,
base_attrs: Vec<KeyValue>,
) -> Self {
Self {
sender,
inner: Arc::new(H2ConnectionInner {
state: ConnectionState::new(metrics.counter(), base_attrs.clone()),
_connection_duration: metrics
.duration_histogram()
.record_duration_on_drop(base_attrs),
}),
}
}
}
impl Service<http::Request<HttpBody>> for H2Connection {
type Response = http::Response<hyper::body::Incoming>;
type Error = hyper::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.sender.poll_ready(cx)
}
fn call(&mut self, req: http::Request<HttpBody>) -> Self::Future {
Box::pin(
self.sender
.send_request(req)
.with_guard(RequestGuard::new(self.inner.clone())),
)
}
}
#[derive(Clone)]
pub(crate) struct H2Connector {
inner: HttpsConnector<HttpConnector>,
metrics: ConnectionMetrics,
connect_timeout: Duration,
keep_alive_interval: Duration,
keep_alive_timeout: Duration,
keep_alive_while_idle: bool,
}
impl H2Connector {
pub(crate) fn new(
inner: HttpsConnector<HttpConnector>,
metrics: ConnectionMetrics,
connect_timeout: Duration,
keep_alive_interval: Duration,
keep_alive_timeout: Duration,
keep_alive_while_idle: bool,
) -> Self {
Self {
inner,
metrics,
connect_timeout,
keep_alive_interval,
keep_alive_timeout,
keep_alive_while_idle,
}
}
pub(crate) fn keep_alive_interval(&self) -> Duration {
self.keep_alive_interval
}
pub(crate) fn keep_alive_timeout(&self) -> Duration {
self.keep_alive_timeout
}
pub(crate) fn keep_alive_while_idle(&self) -> bool {
self.keep_alive_while_idle
}
}
impl Service<Origin> for H2Connector {
type Response = H2Connection;
type Error = BoxError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx).map_err(BoxError::from)
}
fn call(&mut self, origin: Origin) -> Self::Future {
let attrs = self
.metrics
.connection_attrs(Some(http::Version::HTTP_2), &origin);
let clone = self.inner.clone();
let inner = std::mem::replace(&mut self.inner, clone);
let mut handshake = Http2Handshake {
inner: FixedOrigin {
inner: TcpConnect::new(inner, self.connect_timeout),
origin,
},
metrics: self.metrics.clone(),
attrs,
keep_alive_interval: self.keep_alive_interval,
keep_alive_timeout: self.keep_alive_timeout,
keep_alive_while_idle: self.keep_alive_while_idle,
};
handshake.call(())
}
}
pub(crate) fn new_sender<C, K, D>(
connector: C,
idle_timeout: Duration,
key: K,
decorate_request: D,
) -> HostSender
where
C: Service<K, Response = H2Connection> + Clone + Send + 'static,
C::Error: Into<BoxError> + Send + 'static,
C::Future: Send + 'static,
K: Clone + Send + Sync + 'static,
D: Fn(&mut http::Request<HttpBody>) + Send + Sync + 'static,
{
let singleton = Arc::new(Mutex::new(Singleton::new(connector)));
let drop_guard = spawn_eviction_task(&singleton, idle_timeout, move |s, now| {
let mut idle_count = 0;
s.retain(|conn| retain_predicate(conn.idle_since(), idle_timeout, now, &mut idle_count, 1));
});
Arc::new(move |mut req, version: ProtocolVersionCell| {
let _guard = &drop_guard;
let mut s = singleton.lock().unwrap().clone();
let k = key.clone();
decorate_request(&mut req);
Box::pin(async move {
version.set(http::Version::HTTP_2);
let mut conn = s
.ready()
.await
.map_err(|e| into_http_client_error(e.into()))?
.call(k)
.await
.map_err(|e| into_http_client_error(e.into()))?;
let resp = conn
.ready()
.await
.map_err(|source| HttpClientError::Request { source })?
.call(req)
.await
.map_err(|source| HttpClientError::Request { source })?;
Ok(resp)
})
})
}
#[cfg(test)]
mod tests {
use apollo_opentelemetry_test::{TelemetryContext, assert_metrics_snapshot};
use opentelemetry::KeyValue;
use opentelemetry::global;
use super::ConnectionState;
fn make_counter() -> opentelemetry::metrics::UpDownCounter<i64> {
global::meter_provider()
.meter("test")
.i64_up_down_counter("test.connections")
.build()
}
fn base_attrs() -> Vec<KeyValue> {
vec![KeyValue::new("server.address", "test-host")]
}
#[test]
fn new_connection_is_idle() {
let ctx = TelemetryContext::new();
let _state = ConnectionState::new(&make_counter(), base_attrs());
assert_metrics_snapshot!(ctx, @r"
- name: test.connections
data:
type: Sum
data_points:
- attributes:
http.connection.state: idle
server.address: test-host
value: 1
is_monotonic: false
temporality: Cumulative
");
}
#[test]
fn first_request_transitions_to_active() {
let ctx = TelemetryContext::new();
let state = ConnectionState::new(&make_counter(), base_attrs());
state.request_start();
assert_metrics_snapshot!(ctx, @r"
- name: test.connections
data:
type: Sum
data_points:
- attributes:
http.connection.state: active
server.address: test-host
value: 1
- attributes:
http.connection.state: idle
server.address: test-host
value: 0
is_monotonic: false
temporality: Cumulative
");
}
#[test]
fn concurrent_requests_stay_active() {
let ctx = TelemetryContext::new();
let state = ConnectionState::new(&make_counter(), base_attrs());
state.request_start();
state.request_start(); assert_metrics_snapshot!(ctx, @r"
- name: test.connections
data:
type: Sum
data_points:
- attributes:
http.connection.state: active
server.address: test-host
value: 1
- attributes:
http.connection.state: idle
server.address: test-host
value: 0
is_monotonic: false
temporality: Cumulative
");
}
#[test]
fn last_request_done_returns_to_idle() {
let ctx = TelemetryContext::new();
let state = ConnectionState::new(&make_counter(), base_attrs());
state.request_start();
state.request_start();
state.request_done(); state.request_done(); assert_metrics_snapshot!(ctx, @r"
- name: test.connections
data:
type: Sum
data_points:
- attributes:
http.connection.state: active
server.address: test-host
value: 0
- attributes:
http.connection.state: idle
server.address: test-host
value: 1
is_monotonic: false
temporality: Cumulative
");
}
#[test]
fn drop_idle_decrements_idle() {
let ctx = TelemetryContext::new();
let state = ConnectionState::new(&make_counter(), base_attrs());
drop(state);
assert_metrics_snapshot!(ctx, @r"
- name: test.connections
data:
type: Sum
data_points:
- attributes:
http.connection.state: idle
server.address: test-host
value: 0
is_monotonic: false
temporality: Cumulative
");
}
#[test]
fn drop_with_inflight_requests_decrements_active() {
let ctx = TelemetryContext::new();
let state = ConnectionState::new(&make_counter(), base_attrs());
state.request_start();
drop(state);
assert_metrics_snapshot!(ctx, @r"
- name: test.connections
data:
type: Sum
data_points:
- attributes:
http.connection.state: active
server.address: test-host
value: 0
- attributes:
http.connection.state: idle
server.address: test-host
value: 0
is_monotonic: false
temporality: Cumulative
");
}
}