Skip to main content

praxis_core/server/
pingora.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2024 Praxis Contributors
3
4//! Pingora-specific server factory and lifecycle management.
5
6use pingora_core::server::{RunArgs, 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    /// Start all registered services with a shutdown signal.
50    ///
51    /// Unlike [`run`], this method returns when the [`RunArgs`]
52    /// shutdown signal fires, allowing test harnesses to stop
53    /// the server cleanly.
54    ///
55    /// [`run`]: Self::run
56    /// [`RunArgs`]: pingora_core::server::RunArgs
57    pub fn run_with_args(self, args: RunArgs) {
58        self.server.run(args);
59    }
60}
61
62// -----------------------------------------------------------------------------
63// Server Factory
64// -----------------------------------------------------------------------------
65
66/// Build a new Pingora server.
67///
68/// ```no_run
69/// use praxis_core::server::RuntimeOptions;
70///
71/// let server = praxis_core::server::build_http_server(30, &RuntimeOptions::default());
72/// // praxis_protocol::http::pingora::handler::load_http_handler(&mut server, &listener, pipeline);
73/// // server.run_forever();
74/// ```
75pub fn build_http_server(shutdown_timeout_secs: u64, runtime: &RuntimeOptions) -> Server {
76    let threads = resolve_thread_count(runtime.threads);
77    let conf = build_server_conf(shutdown_timeout_secs, threads, runtime);
78
79    let mut server = Server::new_with_opt_and_conf(None, conf);
80    server.bootstrap();
81
82    info!(
83        shutdown_timeout_secs, threads,
84        work_stealing = runtime.work_stealing,
85        upstream_ca_file = ?runtime.upstream_ca_file,
86        upstream_keepalive_pool_size = ?runtime.upstream_keepalive_pool_size,
87        "server configured"
88    );
89
90    server
91}
92
93/// Build a [`ServerConf`] from runtime options.
94fn build_server_conf(shutdown_timeout_secs: u64, threads: usize, runtime: &RuntimeOptions) -> ServerConf {
95    let mut conf = ServerConf {
96        grace_period_seconds: Some(shutdown_timeout_secs),
97        graceful_shutdown_timeout_seconds: Some(shutdown_timeout_secs),
98        threads,
99        work_stealing: runtime.work_stealing,
100        ..ServerConf::default()
101    };
102
103    if let Some(pool_size) = runtime.upstream_keepalive_pool_size {
104        conf.upstream_keepalive_pool_size = pool_size;
105    }
106
107    apply_upstream_ca(&mut conf, runtime);
108    warn_unsupported_global_queue_interval(runtime);
109
110    conf
111}
112
113/// Apply the upstream CA file to the server config, if configured.
114fn apply_upstream_ca(conf: &mut ServerConf, runtime: &RuntimeOptions) {
115    if let Some(ca_file) = &runtime.upstream_ca_file {
116        info!(ca_file, "setting global upstream CA file (replaces system trust store)");
117        conf.ca_file = Some(ca_file.clone());
118    }
119}
120
121/// Warn if `global_queue_interval` is configured but unsupported.
122fn warn_unsupported_global_queue_interval(runtime: &RuntimeOptions) {
123    if runtime.global_queue_interval.is_some() {
124        tracing::warn!(
125            interval = ?runtime.global_queue_interval,
126            "global_queue_interval is configured but not yet supported by Pingora's ServerConf"
127        );
128    }
129}
130
131// -----------------------------------------------------------------------------
132// Utility Functions
133// -----------------------------------------------------------------------------
134
135/// Resolve the number of worker threads: auto-detect if zero.
136fn resolve_thread_count(configured: usize) -> usize {
137    if configured == 0 {
138        std::thread::available_parallelism().map_or(1, std::num::NonZero::get)
139    } else {
140        configured
141    }
142}
143
144// -----------------------------------------------------------------------------
145// Tests
146// -----------------------------------------------------------------------------
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn build_http_server_returns_bootstrapped_server() {
154        let server = build_http_server(30, &RuntimeOptions::default());
155        assert_eq!(
156            server.configuration.grace_period_seconds,
157            Some(30),
158            "grace period should match shutdown timeout"
159        );
160    }
161
162    #[test]
163    fn build_http_server_with_explicit_threads() {
164        let runtime = RuntimeOptions {
165            threads: 4,
166            work_stealing: false,
167            ..RuntimeOptions::default()
168        };
169
170        let server = build_http_server(10, &runtime);
171        assert_eq!(
172            server.configuration.threads, 4,
173            "thread count should match configured value"
174        );
175        assert!(!server.configuration.work_stealing, "work stealing should be disabled");
176    }
177}