Skip to main content

praxis_core/server/
pingora.rs

1// SPDX-License-Identifier: LGPL-3.0-only
2// Copyright (c) 2024 Shane Utt
3
4//! Pingora-specific server factory and lifecycle management.
5
6use pingora_core::server::{Server, configuration::ServerConf};
7use tracing::info;
8
9use super::RuntimeOptions;
10
11// -----------------------------------------------------------------------------
12// PingoraServerRuntime
13// -----------------------------------------------------------------------------
14
15/// Wraps the Pingora server lifecycle. Protocols register
16/// services onto the runtime, then `run()` starts all services.
17pub struct PingoraServerRuntime {
18    /// The underlying Pingora server instance.
19    server: Server,
20}
21
22impl std::fmt::Debug for PingoraServerRuntime {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        f.debug_struct("PingoraServerRuntime")
25            .field("threads", &self.server.configuration.threads)
26            .finish_non_exhaustive()
27    }
28}
29
30impl PingoraServerRuntime {
31    /// Create a new server runtime from config.
32    #[must_use]
33    pub fn new(config: &crate::config::Config) -> Self {
34        let opts = RuntimeOptions::from(&config.runtime);
35        let server = build_http_server(config.shutdown_timeout_secs, &opts);
36        Self { server }
37    }
38
39    /// Access the inner Pingora server for service registration.
40    pub fn server_mut(&mut self) -> &mut Server {
41        &mut self.server
42    }
43
44    /// Start all registered services. Blocks forever.
45    pub fn run(self) -> ! {
46        self.server.run_forever()
47    }
48}
49
50// -----------------------------------------------------------------------------
51// Server Factory
52// -----------------------------------------------------------------------------
53
54/// Build a new Pingora server.
55///
56/// ```no_run
57/// use praxis_core::server::RuntimeOptions;
58///
59/// let server = praxis_core::server::build_http_server(30, &RuntimeOptions::default());
60/// // praxis_protocol::http::pingora::handler::load_http_handler(&mut server, &listener, pipeline);
61/// // server.run_forever();
62/// ```
63pub fn build_http_server(shutdown_timeout_secs: u64, runtime: &RuntimeOptions) -> Server {
64    let threads = resolve_thread_count(runtime.threads);
65    let conf = build_server_conf(shutdown_timeout_secs, threads, runtime);
66
67    let mut server = Server::new_with_opt_and_conf(None, conf);
68    server.bootstrap();
69
70    info!(
71        shutdown_timeout_secs, threads,
72        work_stealing = runtime.work_stealing,
73        upstream_ca_file = ?runtime.upstream_ca_file,
74        upstream_keepalive_pool_size = ?runtime.upstream_keepalive_pool_size,
75        "server configured"
76    );
77
78    server
79}
80
81/// Build a [`ServerConf`] from runtime options.
82fn build_server_conf(shutdown_timeout_secs: u64, threads: usize, runtime: &RuntimeOptions) -> ServerConf {
83    let mut conf = ServerConf {
84        grace_period_seconds: Some(shutdown_timeout_secs),
85        graceful_shutdown_timeout_seconds: Some(shutdown_timeout_secs),
86        threads,
87        work_stealing: runtime.work_stealing,
88        ..ServerConf::default()
89    };
90
91    if let Some(pool_size) = runtime.upstream_keepalive_pool_size {
92        conf.upstream_keepalive_pool_size = pool_size;
93    }
94
95    if let Some(ref ca_file) = runtime.upstream_ca_file {
96        info!(ca_file, "setting global upstream CA file (replaces system trust store)");
97        conf.ca_file = Some(ca_file.clone());
98    }
99
100    if runtime.global_queue_interval.is_some() {
101        tracing::warn!(
102            interval = ?runtime.global_queue_interval,
103            "global_queue_interval is configured but not yet supported by Pingora's ServerConf"
104        );
105    }
106
107    conf
108}
109
110// -----------------------------------------------------------------------------
111// Utility Functions
112// -----------------------------------------------------------------------------
113
114/// Resolve the number of worker threads: auto-detect if zero.
115fn resolve_thread_count(configured: usize) -> usize {
116    if configured == 0 {
117        std::thread::available_parallelism()
118            .map(std::num::NonZero::get)
119            .unwrap_or(1)
120    } else {
121        configured
122    }
123}
124
125// -----------------------------------------------------------------------------
126// Tests
127// -----------------------------------------------------------------------------
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn build_http_server_returns_bootstrapped_server() {
135        let server = build_http_server(30, &RuntimeOptions::default());
136        assert_eq!(
137            server.configuration.grace_period_seconds,
138            Some(30),
139            "grace period should match shutdown timeout"
140        );
141    }
142
143    #[test]
144    fn build_http_server_with_explicit_threads() {
145        let runtime = RuntimeOptions {
146            threads: 4,
147            work_stealing: false,
148            ..RuntimeOptions::default()
149        };
150
151        let server = build_http_server(10, &runtime);
152        assert_eq!(
153            server.configuration.threads, 4,
154            "thread count should match configured value"
155        );
156        assert!(!server.configuration.work_stealing, "work stealing should be disabled");
157    }
158}