use std::collections::HashMap;
use std::net::{IpAddr, SocketAddr};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, OnceLock, PoisonError};
use std::time::Duration;
use pingora_core::apps::{HttpServerApp, ServerApp};
use pingora_core::protocols::GetSocketDigest;
use pingora_core::protocols::l4::listener::Listener;
use pingora_core::protocols::l4::stream::Stream;
use pingora_proxy::HttpProxy;
use tokio::sync::{OwnedSemaphorePermit, Semaphore, watch};
use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken;
use crate::error::PingoraError;
use crate::lifecycle::{Lifecycle, LifecycleCell, LifecycleEvent};
use crate::metrics::Metrics;
use crate::proxy::Gateway;
use crate::upstream::Pool;
const ACCEPT_BACKOFF: Duration = Duration::from_millis(100);
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UpstreamHealth {
pub route: String,
pub healthy: usize,
pub total: usize,
}
#[derive(Debug)]
pub struct Shared {
pub lifecycle: LifecycleCell,
pub local_addr: OnceLock<SocketAddr>,
pub metrics: OnceLock<Arc<Metrics>>,
pools: OnceLock<(Vec<String>, Arc<Vec<Pool>>)>,
active: AtomicUsize,
stop: CancellationToken,
done: watch::Sender<bool>,
}
impl Default for Shared {
fn default() -> Self {
Self {
lifecycle: LifecycleCell::new(),
local_addr: OnceLock::new(),
metrics: OnceLock::new(),
pools: OnceLock::new(),
active: AtomicUsize::new(0),
stop: CancellationToken::new(),
done: watch::channel(false).0,
}
}
}
impl Shared {
pub fn active_connections(&self) -> usize {
self.active.load(Ordering::Acquire)
}
pub fn upstream_health(&self) -> Vec<UpstreamHealth> {
self.pools.get().map_or_else(Vec::new, |(names, pools)| {
names
.iter()
.zip(pools.iter())
.map(|(name, pool)| UpstreamHealth {
route: name.clone(),
healthy: pool.healthy(),
total: pool.total(),
})
.collect()
})
}
pub fn fail(&self) {
if self.lifecycle.apply(LifecycleEvent::StartFailed).is_ok() {
self.done.send_replace(true);
}
}
fn finish(&self, event: LifecycleEvent) {
let _ = self.lifecycle.apply(event);
if self.lifecycle.get().is_terminal() {
self.done.send_replace(true);
}
}
}
struct Active(Arc<Shared>);
impl Active {
fn new(shared: &Arc<Shared>) -> Self {
shared.active.fetch_add(1, Ordering::AcqRel);
Self(shared.clone())
}
}
impl Drop for Active {
fn drop(&mut self) {
self.0.active.fetch_sub(1, Ordering::AcqRel);
}
}
pub struct Launch {
pub listener: tokio::net::TcpListener,
pub proxy: HttpProxy<Gateway>,
pub pools: Arc<Vec<Pool>>,
pub route_names: Vec<String>,
pub max_connections: usize,
pub max_connections_per_ip: usize,
pub grace: Duration,
pub health_interval: Option<Duration>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Started {
Serving(SocketAddr),
Stopped,
}
pub fn start(shared: &Arc<Shared>, launch: Launch) -> Result<Started, PingoraError> {
let addr = launch
.listener
.local_addr()
.map_err(|source| PingoraError::Bind {
addr: SocketAddr::from(([0, 0, 0, 0], 0)),
source,
})?;
match shared.lifecycle.apply(LifecycleEvent::Bound) {
Ok(_) => {}
Err(Lifecycle::Stopped) => return Ok(Started::Stopped),
Err(_) => return Err(PingoraError::AlreadyStarted),
}
let _ = shared.local_addr.set(addr);
let _ = shared.pools.set((launch.route_names, launch.pools.clone()));
if let Some(interval) = launch.health_interval {
tokio::spawn(check_health(launch.pools, interval, shared.stop.clone()));
}
let limit =
(launch.max_connections > 0).then(|| Arc::new(Semaphore::new(launch.max_connections)));
let proxy = Arc::new(launch.proxy);
let per_ip = PerIp::new(launch.max_connections_per_ip);
let exit = ExitGuard(Some(shared.clone()));
tokio::spawn(serve(
exit,
Listener::from(launch.listener),
proxy,
Limits {
total: limit,
per_ip,
},
launch.grace,
));
Ok(Started::Serving(addr))
}
async fn check_health(pools: Arc<Vec<Pool>>, interval: Duration, stop: CancellationToken) {
let mut ticker = tokio::time::interval(interval);
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
tokio::select! {
() = stop.cancelled() => return,
_ = ticker.tick() => {
futures_util::future::join_all(pools.iter().map(Pool::check)).await;
}
}
}
}
async fn slot(
limit: Option<&Arc<Semaphore>>,
stop: &CancellationToken,
) -> Option<Option<OwnedSemaphorePermit>> {
let Some(limit) = limit else {
return Some(None);
};
tokio::select! {
() = stop.cancelled() => None,
permit = limit.clone().acquire_owned() => permit.ok().map(Some),
}
}
#[derive(Debug)]
struct PerIp {
limit: usize,
open: Mutex<HashMap<IpAddr, usize>>,
}
impl PerIp {
fn new(limit: usize) -> Option<Arc<Self>> {
(limit > 0).then(|| {
Arc::new(Self {
limit,
open: Mutex::new(HashMap::new()),
})
})
}
fn take(self: &Arc<Self>, ip: IpAddr) -> Option<IpSlot> {
let mut open = self.open.lock().unwrap_or_else(PoisonError::into_inner);
let count = open.entry(ip).or_insert(0);
let admitted = *count < self.limit;
if admitted {
*count += 1;
}
drop(open);
admitted.then(|| IpSlot(self.clone(), ip))
}
}
struct IpSlot(Arc<PerIp>, IpAddr);
impl Drop for IpSlot {
fn drop(&mut self) {
let mut open = self.0.open.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(count) = open.get_mut(&self.1) {
*count -= 1;
if *count == 0 {
open.remove(&self.1);
}
}
}
}
struct Limits {
total: Option<Arc<Semaphore>>,
per_ip: Option<Arc<PerIp>>,
}
fn ip_slot(per_ip: Option<&Arc<PerIp>>, stream: &Stream) -> Result<Option<IpSlot>, ()> {
let Some(per_ip) = per_ip else {
return Ok(None);
};
let ip = stream.get_socket_digest().and_then(|digest| {
digest
.peer_addr()
.and_then(|a| a.as_inet())
.map(SocketAddr::ip)
});
ip.map_or(Ok(None), |ip| per_ip.take(ip).map(Some).ok_or(()))
}
async fn serve(
mut exit: ExitGuard,
listener: Listener,
proxy: Arc<HttpProxy<Gateway>>,
limits: Limits,
grace: Duration,
) {
let Some(shared) = exit.0.clone() else {
return;
};
let (shutdown_tx, shutdown_rx) = watch::channel(false);
let mut connections = JoinSet::new();
loop {
let Some(permit) = slot(limits.total.as_ref(), &shared.stop).await else {
break;
};
tokio::select! {
() = shared.stop.cancelled() => break,
accepted = listener.accept() => match accepted {
Ok(stream) => {
let Ok(ip) = ip_slot(limits.per_ip.as_ref(), &stream) else {
tracing::debug!("pingora proxy: client at max_connections_per_ip");
continue;
};
let active = Active::new(&shared);
let proxy = proxy.clone();
let shutdown = shutdown_rx.clone();
connections.spawn(async move {
serve_connection(proxy, stream, shutdown).await;
drop((active, permit, ip));
});
}
Err(error) => {
tracing::warn!(%error, "pingora proxy: accept failed");
tokio::select! {
() = shared.stop.cancelled() => {}
() = tokio::time::sleep(ACCEPT_BACKOFF) => {}
}
}
},
Some(_) = connections.join_next(), if !connections.is_empty() => {}
}
}
drop(listener);
drain(&proxy, &shutdown_tx, &mut connections, grace).await;
exit.0 = None;
shared.finish(LifecycleEvent::Drained);
}
struct ExitGuard(Option<Arc<Shared>>);
impl Drop for ExitGuard {
fn drop(&mut self) {
if let Some(shared) = self.0.take() {
shared.stop.cancel();
shared.finish(LifecycleEvent::ServerExited);
}
}
}
async fn drain(
proxy: &HttpProxy<Gateway>,
shutdown: &watch::Sender<bool>,
connections: &mut JoinSet<()>,
grace: Duration,
) {
shutdown.send_replace(true);
proxy.http_cleanup().await;
let finished = tokio::time::timeout(grace, async {
while connections.join_next().await.is_some() {}
})
.await;
if finished.is_err() {
tracing::warn!(
open = connections.len(),
"pingora proxy: the grace period ended; the proxy closes the open connections"
);
connections.abort_all();
while connections.join_next().await.is_some() {}
}
}
async fn serve_connection(
proxy: Arc<HttpProxy<Gateway>>,
stream: Stream,
shutdown: watch::Receiver<bool>,
) {
let mut next: Option<pingora_core::protocols::Stream> = Some(Box::new(stream));
while let Some(stream) = next {
next = proxy.process_new(stream, &shutdown).await;
}
}
#[derive(Debug, Clone)]
pub struct PingoraHandle {
shared: Arc<Shared>,
}
impl PingoraHandle {
pub(crate) const fn new(shared: Arc<Shared>) -> Self {
Self { shared }
}
#[must_use]
pub fn state(&self) -> Lifecycle {
self.shared.lifecycle.get()
}
#[must_use]
pub fn local_addr(&self) -> Option<SocketAddr> {
self.shared.local_addr.get().copied()
}
#[must_use]
pub fn active_connections(&self) -> usize {
self.shared.active_connections()
}
#[must_use]
pub fn upstream_health(&self) -> Vec<UpstreamHealth> {
self.shared.upstream_health()
}
#[must_use]
pub fn metric_families(&self) -> Vec<autumn_web::actuator::MetricFamily> {
crate::metrics::families(&self.shared)
}
pub async fn shutdown(&self) {
match self
.shared
.lifecycle
.apply(LifecycleEvent::ShutdownRequested)
{
Ok(Lifecycle::Draining) => self.shared.stop.cancel(),
Ok(_) => {
self.shared.stop.cancel();
self.shared.done.send_replace(true);
}
Err(_) => {}
}
let mut done = self.shared.done.subscribe();
let _ = done.wait_for(|finished| *finished).await;
}
}