Skip to main content

autumn_plugin_pingora/
server.rs

1//! The accept loop, the drain and [`PingoraHandle`] (ADR 0001, ADR 0003).
2
3use std::collections::HashMap;
4use std::net::{IpAddr, SocketAddr};
5use std::sync::atomic::{AtomicUsize, Ordering};
6use std::sync::{Arc, Mutex, OnceLock, PoisonError};
7use std::time::Duration;
8
9use pingora_core::apps::{HttpServerApp, ServerApp};
10use pingora_core::protocols::GetSocketDigest;
11use pingora_core::protocols::l4::listener::Listener;
12use pingora_core::protocols::l4::stream::Stream;
13use pingora_proxy::HttpProxy;
14use tokio::sync::{OwnedSemaphorePermit, Semaphore, watch};
15use tokio::task::JoinSet;
16use tokio_util::sync::CancellationToken;
17
18use crate::error::PingoraError;
19use crate::lifecycle::{Lifecycle, LifecycleCell, LifecycleEvent};
20use crate::metrics::Metrics;
21use crate::proxy::Gateway;
22use crate::upstream::Pool;
23
24/// Wait after an `accept` error. Then a full fd table does not use all
25/// the CPU.
26const ACCEPT_BACKOFF: Duration = Duration::from_millis(100);
27
28/// Upstream health for one route.
29#[derive(Debug, Clone, PartialEq, Eq)]
30#[non_exhaustive]
31pub struct UpstreamHealth {
32    /// The route name.
33    pub route: String,
34    /// Upstreams that passed the last health check.
35    pub healthy: usize,
36    /// All upstreams, after name resolution. One entry per address.
37    pub total: usize,
38}
39
40/// State shared by the plugin, the server and the handles.
41#[derive(Debug)]
42pub struct Shared {
43    pub lifecycle: LifecycleCell,
44    pub local_addr: OnceLock<SocketAddr>,
45    pub metrics: OnceLock<Arc<Metrics>>,
46    pools: OnceLock<(Vec<String>, Arc<Vec<Pool>>)>,
47    active: AtomicUsize,
48    /// Fires when shutdown starts.
49    stop: CancellationToken,
50    /// `true` when the proxy reached a terminal state.
51    done: watch::Sender<bool>,
52}
53
54impl Default for Shared {
55    fn default() -> Self {
56        Self {
57            lifecycle: LifecycleCell::new(),
58            local_addr: OnceLock::new(),
59            metrics: OnceLock::new(),
60            pools: OnceLock::new(),
61            active: AtomicUsize::new(0),
62            stop: CancellationToken::new(),
63            done: watch::channel(false).0,
64        }
65    }
66}
67
68impl Shared {
69    /// Open client connections.
70    pub fn active_connections(&self) -> usize {
71        self.active.load(Ordering::Acquire)
72    }
73
74    /// Upstream health for each route, in route order.
75    pub fn upstream_health(&self) -> Vec<UpstreamHealth> {
76        self.pools.get().map_or_else(Vec::new, |(names, pools)| {
77            names
78                .iter()
79                .zip(pools.iter())
80                .map(|(name, pool)| UpstreamHealth {
81                    route: name.clone(),
82                    healthy: pool.healthy(),
83                    total: pool.total(),
84                })
85                .collect()
86        })
87    }
88
89    /// Record a start failure. Waiters on `shutdown` return. A proxy that
90    /// already serves does not change.
91    pub fn fail(&self) {
92        if self.lifecycle.apply(LifecycleEvent::StartFailed).is_ok() {
93            self.done.send_replace(true);
94        }
95    }
96
97    /// Apply `event`. Wake the waiters when the state is terminal.
98    fn finish(&self, event: LifecycleEvent) {
99        let _ = self.lifecycle.apply(event);
100        if self.lifecycle.get().is_terminal() {
101            self.done.send_replace(true);
102        }
103    }
104}
105
106/// Counts one open connection while it lives.
107struct Active(Arc<Shared>);
108
109impl Active {
110    fn new(shared: &Arc<Shared>) -> Self {
111        shared.active.fetch_add(1, Ordering::AcqRel);
112        Self(shared.clone())
113    }
114}
115
116impl Drop for Active {
117    fn drop(&mut self) {
118        self.0.active.fetch_sub(1, Ordering::AcqRel);
119    }
120}
121
122/// What the accept loop needs.
123pub struct Launch {
124    pub listener: tokio::net::TcpListener,
125    pub proxy: HttpProxy<Gateway>,
126    pub pools: Arc<Vec<Pool>>,
127    pub route_names: Vec<String>,
128    pub max_connections: usize,
129    pub max_connections_per_ip: usize,
130    pub grace: Duration,
131    pub health_interval: Option<Duration>,
132}
133
134/// The result of [`start`].
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum Started {
137    /// The proxy serves on this address.
138    Serving(SocketAddr),
139    /// Shutdown came first. Nothing serves.
140    Stopped,
141}
142
143/// Start serving. The caller has bound the listener.
144///
145/// # Errors
146///
147/// [`PingoraError::AlreadyStarted`] when another start won. An I/O error
148/// when the listener has no local address.
149pub fn start(shared: &Arc<Shared>, launch: Launch) -> Result<Started, PingoraError> {
150    let addr = launch
151        .listener
152        .local_addr()
153        .map_err(|source| PingoraError::Bind {
154            addr: SocketAddr::from(([0, 0, 0, 0], 0)),
155            source,
156        })?;
157    match shared.lifecycle.apply(LifecycleEvent::Bound) {
158        Ok(_) => {}
159        Err(Lifecycle::Stopped) => return Ok(Started::Stopped),
160        Err(_) => return Err(PingoraError::AlreadyStarted),
161    }
162    let _ = shared.local_addr.set(addr);
163    let _ = shared.pools.set((launch.route_names, launch.pools.clone()));
164    if let Some(interval) = launch.health_interval {
165        tokio::spawn(check_health(launch.pools, interval, shared.stop.clone()));
166    }
167    let limit =
168        (launch.max_connections > 0).then(|| Arc::new(Semaphore::new(launch.max_connections)));
169    let proxy = Arc::new(launch.proxy);
170    let per_ip = PerIp::new(launch.max_connections_per_ip);
171    // The guard lives in the future, so it drops even when the task never
172    // runs.
173    let exit = ExitGuard(Some(shared.clone()));
174    tokio::spawn(serve(
175        exit,
176        Listener::from(launch.listener),
177        proxy,
178        Limits {
179            total: limit,
180            per_ip,
181        },
182        launch.grace,
183    ));
184    Ok(Started::Serving(addr))
185}
186
187/// Health check rounds until shutdown. The first round runs at once.
188async fn check_health(pools: Arc<Vec<Pool>>, interval: Duration, stop: CancellationToken) {
189    let mut ticker = tokio::time::interval(interval);
190    ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
191    loop {
192        tokio::select! {
193            () = stop.cancelled() => return,
194            _ = ticker.tick() => {
195                futures_util::future::join_all(pools.iter().map(Pool::check)).await;
196            }
197        }
198    }
199}
200
201/// Wait for a connection slot. `None` when shutdown starts first.
202async fn slot(
203    limit: Option<&Arc<Semaphore>>,
204    stop: &CancellationToken,
205) -> Option<Option<OwnedSemaphorePermit>> {
206    let Some(limit) = limit else {
207        return Some(None);
208    };
209    tokio::select! {
210        () = stop.cancelled() => None,
211        permit = limit.clone().acquire_owned() => permit.ok().map(Some),
212    }
213}
214
215/// Open connections per client IP.
216#[derive(Debug)]
217struct PerIp {
218    limit: usize,
219    open: Mutex<HashMap<IpAddr, usize>>,
220}
221
222impl PerIp {
223    fn new(limit: usize) -> Option<Arc<Self>> {
224        (limit > 0).then(|| {
225            Arc::new(Self {
226                limit,
227                open: Mutex::new(HashMap::new()),
228            })
229        })
230    }
231
232    /// A slot for `ip`, or `None` at the limit.
233    fn take(self: &Arc<Self>, ip: IpAddr) -> Option<IpSlot> {
234        let mut open = self.open.lock().unwrap_or_else(PoisonError::into_inner);
235        let count = open.entry(ip).or_insert(0);
236        let admitted = *count < self.limit;
237        if admitted {
238            *count += 1;
239        }
240        drop(open);
241        admitted.then(|| IpSlot(self.clone(), ip))
242    }
243}
244
245/// Gives back one per-IP slot on drop.
246struct IpSlot(Arc<PerIp>, IpAddr);
247
248impl Drop for IpSlot {
249    fn drop(&mut self) {
250        let mut open = self.0.open.lock().unwrap_or_else(PoisonError::into_inner);
251        if let Some(count) = open.get_mut(&self.1) {
252            *count -= 1;
253            if *count == 0 {
254                open.remove(&self.1);
255            }
256        }
257    }
258}
259
260/// Connection limits.
261struct Limits {
262    total: Option<Arc<Semaphore>>,
263    per_ip: Option<Arc<PerIp>>,
264}
265
266/// The per-IP slot for `stream`. `Err` when the client is at its limit.
267fn ip_slot(per_ip: Option<&Arc<PerIp>>, stream: &Stream) -> Result<Option<IpSlot>, ()> {
268    let Some(per_ip) = per_ip else {
269        return Ok(None);
270    };
271    let ip = stream.get_socket_digest().and_then(|digest| {
272        digest
273            .peer_addr()
274            .and_then(|a| a.as_inet())
275            .map(SocketAddr::ip)
276    });
277    ip.map_or(Ok(None), |ip| per_ip.take(ip).map(Some).ok_or(()))
278}
279
280/// Accept until shutdown, then drain.
281async fn serve(
282    mut exit: ExitGuard,
283    listener: Listener,
284    proxy: Arc<HttpProxy<Gateway>>,
285    limits: Limits,
286    grace: Duration,
287) {
288    let Some(shared) = exit.0.clone() else {
289        return;
290    };
291    let (shutdown_tx, shutdown_rx) = watch::channel(false);
292    let mut connections = JoinSet::new();
293    loop {
294        let Some(permit) = slot(limits.total.as_ref(), &shared.stop).await else {
295            break;
296        };
297        tokio::select! {
298            () = shared.stop.cancelled() => break,
299            accepted = listener.accept() => match accepted {
300                Ok(stream) => {
301                    let Ok(ip) = ip_slot(limits.per_ip.as_ref(), &stream) else {
302                        tracing::debug!("pingora proxy: client at max_connections_per_ip");
303                        continue;
304                    };
305                    let active = Active::new(&shared);
306                    let proxy = proxy.clone();
307                    let shutdown = shutdown_rx.clone();
308                    connections.spawn(async move {
309                        serve_connection(proxy, stream, shutdown).await;
310                        drop((active, permit, ip));
311                    });
312                }
313                Err(error) => {
314                    tracing::warn!(%error, "pingora proxy: accept failed");
315                    tokio::select! {
316                        () = shared.stop.cancelled() => {}
317                        () = tokio::time::sleep(ACCEPT_BACKOFF) => {}
318                    }
319                }
320            },
321            Some(_) = connections.join_next(), if !connections.is_empty() => {}
322        }
323    }
324    // Close the listener first: new connections get "refused".
325    drop(listener);
326    drain(&proxy, &shutdown_tx, &mut connections, grace).await;
327    exit.0 = None;
328    shared.finish(LifecycleEvent::Drained);
329}
330
331/// Records `ServerExited` when the serve task ends early, for example when
332/// its runtime stops. Then `shutdown` does not wait forever.
333struct ExitGuard(Option<Arc<Shared>>);
334
335impl Drop for ExitGuard {
336    fn drop(&mut self) {
337        if let Some(shared) = self.0.take() {
338            shared.stop.cancel();
339            shared.finish(LifecycleEvent::ServerExited);
340        }
341    }
342}
343
344/// End keep-alive, wait for open requests up to `grace`, then abort.
345async fn drain(
346    proxy: &HttpProxy<Gateway>,
347    shutdown: &watch::Sender<bool>,
348    connections: &mut JoinSet<()>,
349    grace: Duration,
350) {
351    shutdown.send_replace(true);
352    proxy.http_cleanup().await;
353    let finished = tokio::time::timeout(grace, async {
354        while connections.join_next().await.is_some() {}
355    })
356    .await;
357    if finished.is_err() {
358        tracing::warn!(
359            open = connections.len(),
360            "pingora proxy: the grace period ended; the proxy closes the open connections"
361        );
362        connections.abort_all();
363        while connections.join_next().await.is_some() {}
364    }
365}
366
367/// Serve one client connection, with keep-alive reuse.
368async fn serve_connection(
369    proxy: Arc<HttpProxy<Gateway>>,
370    stream: Stream,
371    shutdown: watch::Receiver<bool>,
372) {
373    let mut next: Option<pingora_core::protocols::Stream> = Some(Box::new(stream));
374    while let Some(stream) = next {
375        next = proxy.process_new(stream, &shutdown).await;
376    }
377}
378
379/// A handle to the proxy. Clone it freely. It is valid before and after
380/// boot.
381#[derive(Debug, Clone)]
382pub struct PingoraHandle {
383    shared: Arc<Shared>,
384}
385
386impl PingoraHandle {
387    pub(crate) const fn new(shared: Arc<Shared>) -> Self {
388        Self { shared }
389    }
390
391    /// The current state.
392    #[must_use]
393    pub fn state(&self) -> Lifecycle {
394        self.shared.lifecycle.get()
395    }
396
397    /// The bound address, after boot.
398    #[must_use]
399    pub fn local_addr(&self) -> Option<SocketAddr> {
400        self.shared.local_addr.get().copied()
401    }
402
403    /// Open client connections.
404    #[must_use]
405    pub fn active_connections(&self) -> usize {
406        self.shared.active_connections()
407    }
408
409    /// Upstream health for each route, in route order.
410    #[must_use]
411    pub fn upstream_health(&self) -> Vec<UpstreamHealth> {
412        self.shared.upstream_health()
413    }
414
415    /// The metric families, as `/actuator/prometheus` shows them.
416    #[must_use]
417    pub fn metric_families(&self) -> Vec<autumn_web::actuator::MetricFamily> {
418        crate::metrics::families(&self.shared)
419    }
420
421    /// Stop the proxy and wait for the drain. You can call it more than one
422    /// time, also before boot. The drain runs in its own task. It continues
423    /// when the caller drops this future.
424    pub async fn shutdown(&self) {
425        match self
426            .shared
427            .lifecycle
428            .apply(LifecycleEvent::ShutdownRequested)
429        {
430            Ok(Lifecycle::Draining) => self.shared.stop.cancel(),
431            Ok(_) => {
432                self.shared.stop.cancel();
433                self.shared.done.send_replace(true);
434            }
435            Err(_) => {}
436        }
437        let mut done = self.shared.done.subscribe();
438        let _ = done.wait_for(|finished| *finished).await;
439    }
440}