use std::{net::SocketAddr, sync::Arc, time::Duration};
use http::HeaderMap;
use metrics::{counter, histogram};
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
use tracing::debug;
use super::types::{SubRequestError, SubResponse};
use crate::{
backend,
circuit::{CircuitBreakerConfig, CircuitBreakerRegistry, CircuitToken, PeerKey},
egress::{Connector, EgressError},
};
pub(super) const SUBREQUEST_STREAMS_TOTAL: &str = "praxis_subrequest_streams_total";
pub(super) const SUBREQUEST_STREAM_DURATION_SECONDS: &str = "praxis_subrequest_stream_duration_seconds";
pub(super) const SUBREQUEST_STREAM_BYTES_TOTAL: &str = "praxis_subrequest_stream_bytes_total";
pub(super) const SUBREQUEST_HEADER_DURATION_SECONDS: &str = "praxis_subrequest_header_duration_seconds";
#[derive(Debug)]
pub struct SubRequestConnectorOptions {
pub keepalive_pool_size: usize,
pub max_connections: Option<usize>,
pub circuit_breaker: Option<CircuitBreakerConfig>,
}
#[derive(Clone)]
pub struct SubRequestConnector<C: Connector = backend::Connector> {
pub(super) inner: Arc<C>,
pub(super) admission: Option<Arc<Semaphore>>,
pub(super) configured_max_connections: Option<usize>,
pub(super) circuit_breakers: Option<Arc<CircuitBreakerRegistry>>,
}
impl SubRequestConnector {
pub fn new(keepalive_pool_size: usize, max_connections: Option<usize>) -> Self {
Self::with_connector(backend::Connector::new(keepalive_pool_size), max_connections, None)
}
pub fn with_options(opts: SubRequestConnectorOptions) -> Self {
Self::with_connector(
backend::Connector::new(opts.keepalive_pool_size),
opts.max_connections,
opts.circuit_breaker,
)
}
}
impl<C: Connector> SubRequestConnector<C> {
pub fn with_connector(
inner: C,
max_connections: Option<usize>,
circuit_breaker: Option<CircuitBreakerConfig>,
) -> Self {
Self {
inner: Arc::new(inner),
admission: max_connections.map(|limit| Arc::new(Semaphore::new(limit))),
configured_max_connections: max_connections,
circuit_breakers: circuit_breaker.map(|cfg| Arc::new(CircuitBreakerRegistry::new(cfg))),
}
}
pub fn connector(&self) -> &C {
&self.inner
}
#[must_use]
pub fn has_circuit_breaker(&self) -> bool {
self.circuit_breakers.is_some()
}
#[must_use]
pub fn configured_max_connections(&self) -> Option<usize> {
self.configured_max_connections
}
pub async fn acquire_permit(&self) -> Option<OwnedSemaphorePermit> {
let semaphore = self.admission.as_ref()?;
Arc::clone(semaphore).acquire_owned().await.ok()
}
pub async fn try_acquire_permit(&self, timeout: Duration) -> Result<Option<OwnedSemaphorePermit>, SubRequestError> {
let Some(semaphore) = self.admission.as_ref() else {
return Ok(None);
};
let configured = self.configured_max_connections.unwrap_or(0);
match tokio::time::timeout(timeout, Arc::clone(semaphore).acquire_owned()).await {
Ok(Ok(permit)) => Ok(Some(permit)),
Ok(Err(_closed)) => Err(SubRequestError::AdmissionTimeout {
max_connections: configured,
}),
Err(_elapsed) => Err(SubRequestError::AdmissionTimeout {
max_connections: configured,
}),
}
}
}
impl<C: Connector> std::fmt::Debug for SubRequestConnector<C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SubRequestConnector")
.field("pool", &std::any::type_name::<C>())
.field("max_connections", &self.configured_max_connections)
.field("circuit_breakers", &self.circuit_breakers.is_some())
.finish()
}
}
pub(super) struct RawExchange<'conn, 'reg, C: Connector> {
pub(super) session: C::Session,
pub(super) read_timeout: Option<Duration>,
pub(super) connector: &'conn SubRequestConnector<C>,
pub(super) status: u16,
pub(super) version: http::Version,
pub(super) headers: HeaderMap,
pub(super) circuit_guard: Option<CircuitGuard<'reg>>,
pub(super) permit: Option<OwnedSemaphorePermit>,
pub(super) deadline: tokio::time::Instant,
pub(super) client_span: tracing::Span,
}
pub(super) struct CircuitGuard<'reg> {
registry: &'reg CircuitBreakerRegistry,
peer: PeerKey,
token: Option<CircuitToken>,
neutral_on_drop: bool,
}
impl<'reg> CircuitGuard<'reg> {
pub(super) fn new(registry: &'reg CircuitBreakerRegistry, peer: PeerKey, token: CircuitToken) -> Self {
Self {
registry,
peer,
token: Some(token),
neutral_on_drop: false,
}
}
pub(super) fn abandon_as_neutral(&mut self) {
self.neutral_on_drop = true;
}
pub(super) fn fail(mut self, err: &SubRequestError) {
let Some(token) = self.token.take() else {
return;
};
if matches!(
err,
SubRequestError::Connect(_) | SubRequestError::Io(_) | SubRequestError::DeadlineExceeded
) {
self.registry.record_failure(&self.peer, token);
} else {
self.registry.record_success(&self.peer, token);
}
}
pub(super) fn finalize_success(mut self) {
if let Some(token) = self.token.take() {
self.registry.record_success(&self.peer, token);
}
}
pub(super) fn finalize(mut self, result: &Result<SubResponse, SubRequestError>) {
let Some(token) = self.token.take() else {
return;
};
match result {
Err(SubRequestError::Connect(_) | SubRequestError::Io(_) | SubRequestError::DeadlineExceeded) => {
self.registry.record_failure(&self.peer, token);
},
Ok(_) | Err(_) => {
self.registry.record_success(&self.peer, token);
},
}
}
}
impl Drop for CircuitGuard<'_> {
fn drop(&mut self) {
if let Some(token) = self.token.take() {
if self.neutral_on_drop {
self.registry.release(&self.peer, token);
} else {
self.registry.record_failure(&self.peer, token);
}
}
}
}
pub(super) fn record_header_termination(termination: &'static str) {
counter!(SUBREQUEST_STREAMS_TOTAL, "termination" => termination).increment(1);
histogram!(SUBREQUEST_STREAM_DURATION_SECONDS).record(0.0);
debug!(termination, "sub-request: stream terminated at header phase");
}
pub(super) use crate::reserved_headers::HOP_BY_HOP_HEADERS;
pub(super) fn connection_nominated_tokens(headers: &HeaderMap) -> Vec<&str> {
headers
.get_all(http::header::CONNECTION)
.iter()
.flat_map(crate::reserved_headers::connection_tokens)
.collect()
}
pub(super) fn is_boundary_stripped(name: &http::header::HeaderName, nominated: &[&str]) -> bool {
let name = name.as_str();
HOP_BY_HOP_HEADERS.contains(&name)
|| crate::reserved_headers::is_reserved(name)
|| (nominated.iter().any(|token| token.eq_ignore_ascii_case(name))
&& !crate::reserved_headers::is_connection_token_protected(name))
}
pub(super) fn is_request_stripped(name: &http::header::HeaderName, nominated: &[&str]) -> bool {
let lower = name.as_str();
lower == "content-length" || lower == "transfer-encoding" || is_boundary_stripped(name, nominated)
}
pub(super) fn is_transport_header(name: &http::header::HeaderName) -> bool {
HOP_BY_HOP_HEADERS.iter().any(|header| *header == name.as_str()) || name == http::header::CONTENT_LENGTH
}
pub(super) fn empty_body_needs_framing(method: &http::Method) -> bool {
matches!(*method, http::Method::POST | http::Method::PUT | http::Method::PATCH)
}
pub(super) fn ensure_host_header(headers: &mut HeaderMap, address: SocketAddr) -> Result<(), SubRequestError> {
if !headers.contains_key(http::header::HOST) {
let host = http::HeaderValue::from_str(&address.to_string())
.map_err(|error| SubRequestError::InvalidRequest(error.to_string()))?;
headers.insert(http::header::HOST, host);
}
Ok(())
}
pub(super) fn transport_error(err: EgressError) -> SubRequestError {
match err {
EgressError::InvalidRequest(message) => SubRequestError::InvalidRequest(message),
EgressError::Connect(message) => SubRequestError::Connect(message),
EgressError::Io(message) => SubRequestError::Io(message),
unavailable @ EgressError::Unavailable => SubRequestError::Connect(unavailable.to_string()),
}
}
pub(super) fn min_timeout(configured: Option<Duration>, deadline: Duration) -> Duration {
configured.map_or(deadline, |configured| configured.min(deadline))
}
pub(super) fn classify_timeout(
remaining_budget: Duration,
configured_timeout: Option<Duration>,
phase: &str,
) -> SubRequestError {
if configured_timeout.is_none_or(|timeout| timeout >= remaining_budget) {
SubRequestError::DeadlineExceeded
} else {
SubRequestError::Io(format!("upstream {phase} timeout"))
}
}