use std::sync::Arc;
use bytes::Bytes;
use http::HeaderMap;
use pingora_core::connectors::{ConnectorOptions, http::Connector};
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
#[derive(Clone, Debug)]
pub struct SubRequest {
pub method: http::Method,
pub uri: http::Uri,
pub headers: HeaderMap,
pub body: Bytes,
}
#[derive(Clone, Debug)]
pub struct SubResponse {
pub status: u16,
pub headers: HeaderMap,
pub body: Bytes,
}
#[derive(Clone)]
pub struct SubRequestConnector {
inner: Arc<Connector<()>>,
admission: Option<Arc<Semaphore>>,
}
impl SubRequestConnector {
pub fn new(keepalive_pool_size: usize, max_connections: Option<usize>) -> Self {
let options = ConnectorOptions::new(keepalive_pool_size);
Self {
inner: Arc::new(Connector::new(Some(options))),
admission: max_connections.map(|n| Arc::new(Semaphore::new(n))),
}
}
pub fn connector(&self) -> &Connector<()> {
&self.inner
}
pub async fn acquire_permit(&self) -> Option<OwnedSemaphorePermit> {
let semaphore = self.admission.as_ref()?;
Arc::clone(semaphore).acquire_owned().await.ok()
}
}
impl std::fmt::Debug for SubRequestConnector {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SubRequestConnector")
.field("pool", &"Connector<()>")
.field(
"max_connections",
&self.admission.as_ref().map(|s| s.available_permits()),
)
.finish()
}
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(clippy::unwrap_used, clippy::expect_used, reason = "tests")]
mod tests {
use super::*;
#[test]
fn clone_shares_same_arc() {
let a = SubRequestConnector::new(16, None);
let b = a.clone();
assert!(
Arc::ptr_eq(&a.inner, &b.inner),
"cloned connectors should share the same Arc"
);
}
#[test]
fn debug_impl_does_not_panic() {
let connector = SubRequestConnector::new(8, None);
let debug = format!("{connector:?}");
assert!(
debug.contains("SubRequestConnector"),
"debug output should contain type name"
);
}
#[test]
fn unbounded_connector_has_no_admission() {
let connector = SubRequestConnector::new(8, None);
assert!(
connector.admission.is_none(),
"no max_connections should mean no semaphore"
);
}
#[test]
fn bounded_connector_has_admission_semaphore() {
let connector = SubRequestConnector::new(8, Some(16));
let semaphore = connector
.admission
.as_ref()
.expect("max_connections should create semaphore");
assert_eq!(
semaphore.available_permits(),
16,
"semaphore should have the configured permits"
);
}
#[tokio::test]
async fn acquire_permit_returns_none_without_limit() {
let connector = SubRequestConnector::new(4, None);
assert!(
connector.acquire_permit().await.is_none(),
"unbounded connector should return None"
);
}
#[tokio::test]
async fn acquire_permit_returns_some_with_limit() {
let connector = SubRequestConnector::new(4, Some(2));
assert!(
connector.acquire_permit().await.is_some(),
"bounded connector should return a permit"
);
}
#[tokio::test]
async fn dropping_permit_restores_capacity() {
let connector = SubRequestConnector::new(4, Some(1));
let permit = connector.acquire_permit().await.unwrap();
assert_eq!(
connector.admission.as_ref().unwrap().available_permits(),
0,
"all permits should be taken"
);
drop(permit);
assert_eq!(
connector.admission.as_ref().unwrap().available_permits(),
1,
"dropping permit should restore capacity"
);
}
#[test]
fn clone_shares_admission_semaphore() {
let a = SubRequestConnector::new(4, Some(8));
let b = a.clone();
assert!(
Arc::ptr_eq(a.admission.as_ref().unwrap(), b.admission.as_ref().unwrap()),
"cloned connectors should share the semaphore"
);
}
#[test]
fn subrequest_clone_preserves_fields() {
let req = SubRequest {
method: http::Method::POST,
uri: "/v1/chat".parse().unwrap(),
headers: HeaderMap::new(),
body: Bytes::from_static(b"hello"),
};
let cloned = req.clone();
assert_eq!(cloned.method, http::Method::POST);
assert_eq!(cloned.body, Bytes::from_static(b"hello"));
}
#[test]
fn subresponse_clone_preserves_fields() {
let resp = SubResponse {
status: 200,
headers: HeaderMap::new(),
body: Bytes::from_static(b"world"),
};
let cloned = resp.clone();
assert_eq!(cloned.status, 200);
assert_eq!(cloned.body, Bytes::from_static(b"world"));
}
}