Skip to main content

pingora_core/server/configuration/
mod.rs

1// Copyright 2026 Cloudflare, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Server configurations
16//!
17//! Server configurations define startup settings such as:
18//! * User and group to run as after daemonization
19//! * Number of threads per service
20//! * Error log file path
21
22use clap::Parser;
23use log::{debug, trace};
24use pingora_error::{Error, ErrorType::*, OrErr, Result};
25use serde::{Deserialize, Serialize};
26use std::ffi::OsString;
27use std::fs;
28
29// default maximum upstream retries for retry-able proxy errors
30const DEFAULT_MAX_RETRIES: usize = 16;
31
32/// The configuration file
33///
34/// Pingora configuration files are by default YAML files, but any key value format can potentially
35/// be used.
36///
37/// # Extension
38/// New keys can be added to the configuration files which this configuration object will ignore.
39/// Then, users can parse these key-values to pass to their code to use.
40#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(default)]
42pub struct ServerConf {
43    /// Version
44    pub version: usize,
45    /// Whether to run this process in the background.
46    pub daemon: bool,
47    /// When configured and `daemon` setting is `true`, error log will be written to the given
48    /// file. Otherwise StdErr will be used.
49    pub error_log: Option<String>,
50    /// The pid (process ID) file of this server to be created when running in background
51    pub pid_file: String,
52    /// the path to the upgrade socket
53    ///
54    /// In order to perform zero downtime restart, both the new and old process need to agree on the
55    /// path to this sock in order to coordinate the upgrade.
56    pub upgrade_sock: String,
57    /// If configured, after daemonization, this process will switch to the given user before
58    /// starting to serve traffic.
59    pub user: Option<String>,
60    /// Similar to `user`, the group this process should switch to.
61    pub group: Option<String>,
62    /// How many threads **each** service should get. The threads are not shared across services.
63    pub threads: usize,
64    /// Number of listener tasks to use per fd. This allows for parallel accepts.
65    pub listener_tasks_per_fd: usize,
66    /// Allow work stealing between threads of the same service. Default `true`.
67    pub work_stealing: bool,
68    /// The path to CA file the SSL library should use. If empty, the default trust store location
69    /// defined by the SSL library will be used.
70    pub ca_file: Option<String>,
71    /// The maximum number of unique s2n configs to cache. Creating a new s2n config is an
72    /// expensive operation, so we cache and re-use config objects with identical configurations.
73    /// A value of 0 disables the cache.
74    ///
75    /// WARNING: Disabling the s2n config cache can result in poor performance
76    #[cfg(feature = "s2n")]
77    pub s2n_config_cache_size: Option<usize>,
78    /// Grace period in seconds before starting the final step of the graceful shutdown after signaling shutdown.
79    pub grace_period_seconds: Option<u64>,
80    /// Timeout in seconds of the final step for the graceful shutdown.
81    pub graceful_shutdown_timeout_seconds: Option<u64>,
82    // These options don't belong here as they are specific to certain services
83    /// IPv4 addresses for a client connector to bind to. See
84    /// [`ConnectorOptions`](crate::connectors::ConnectorOptions).
85    /// Note: this is an _unstable_ field that may be renamed or removed in the future.
86    pub client_bind_to_ipv4: Vec<String>,
87    /// IPv6 addresses for a client connector to bind to. See
88    /// [`ConnectorOptions`](crate::connectors::ConnectorOptions).
89    /// Note: this is an _unstable_ field that may be renamed or removed in the future.
90    pub client_bind_to_ipv6: Vec<String>,
91    /// Keepalive pool size for client connections to upstream. See
92    /// [`ConnectorOptions`](crate::connectors::ConnectorOptions).
93    /// Note: this is an _unstable_ field that may be renamed or removed in the future.
94    pub upstream_keepalive_pool_size: usize,
95    /// Number of dedicated thread pools to use for upstream connection establishment.
96    /// See [`ConnectorOptions`](crate::connectors::ConnectorOptions).
97    /// Note: this is an _unstable_ field that may be renamed or removed in the future.
98    pub upstream_connect_offload_threadpools: Option<usize>,
99    /// Number of threads per dedicated upstream connection establishment pool.
100    /// See [`ConnectorOptions`](crate::connectors::ConnectorOptions).
101    /// Note: this is an _unstable_ field that may be renamed or removed in the future.
102    pub upstream_connect_offload_thread_per_pool: Option<usize>,
103    /// When enabled allows TLS keys to be written to a file specified by the SSLKEYLOG
104    /// env variable. This can be used by tools like Wireshark to decrypt upstream traffic
105    /// for debugging purposes.
106    /// Note: this is an _unstable_ field that may be renamed or removed in the future.
107    pub upstream_debug_ssl_keylog: bool,
108    /// The maximum number of retries that will be attempted when an error is
109    /// retry-able (`e.retry() == true`) when proxying to upstream.
110    ///
111    /// This setting is a fail-safe and defaults to 16.
112    pub max_retries: usize,
113    /// Maximum number of retries for upgrade socket connect and accept operations.
114    /// This controls how many times send_fds_to will retry connecting and how many times
115    /// get_fds_from will retry accepting during graceful upgrades.
116    /// The retry interval is 1 second between attempts.
117    /// If not set, defaults to 5 retries.
118    pub upgrade_sock_connect_accept_max_retries: Option<usize>,
119}
120
121impl Default for ServerConf {
122    fn default() -> Self {
123        ServerConf {
124            version: 0,
125            client_bind_to_ipv4: vec![],
126            client_bind_to_ipv6: vec![],
127            ca_file: None,
128            #[cfg(feature = "s2n")]
129            s2n_config_cache_size: None,
130            daemon: false,
131            error_log: None,
132            upstream_debug_ssl_keylog: false,
133            pid_file: "/tmp/pingora.pid".to_string(),
134            upgrade_sock: "/tmp/pingora_upgrade.sock".to_string(),
135            user: None,
136            group: None,
137            threads: 1,
138            listener_tasks_per_fd: 1,
139            work_stealing: true,
140            upstream_keepalive_pool_size: 128,
141            upstream_connect_offload_threadpools: None,
142            upstream_connect_offload_thread_per_pool: None,
143            grace_period_seconds: None,
144            graceful_shutdown_timeout_seconds: None,
145            max_retries: DEFAULT_MAX_RETRIES,
146            upgrade_sock_connect_accept_max_retries: None,
147        }
148    }
149}
150
151/// Command-line options
152///
153/// Call `Opt::parse_args()` to build this object from the process's command line arguments.
154#[derive(Parser, Debug, Default)]
155#[clap(name = "basic", long_about = None)]
156pub struct Opt {
157    /// Whether this server should try to upgrade from a running old server
158    #[clap(
159        short,
160        long,
161        help = "This is the base set of command line arguments for a pingora-based service",
162        long_help = None
163    )]
164    pub upgrade: bool,
165
166    /// Whether this server should run in the background
167    #[clap(short, long)]
168    pub daemon: bool,
169
170    /// Not actually used. This flag is there so that the server is not upset seeing this flag
171    /// passed from `cargo test` sometimes
172    #[clap(long, hide = true)]
173    pub nocapture: bool,
174
175    /// Test the configuration and exit
176    ///
177    /// When this flag is set, calling `server.bootstrap()` will exit the process without errors
178    ///
179    /// This flag is useful for upgrading service where the user wants to make sure the new
180    /// service can start before shutting down the old server process.
181    #[clap(
182        short,
183        long,
184        help = "This flag is useful for upgrading service where the user wants \
185                to make sure the new service can start before shutting down \
186                the old server process.",
187        long_help = None
188    )]
189    pub test: bool,
190
191    /// The path to the configuration file.
192    ///
193    /// See [`ServerConf`] for more details of the configuration file.
194    #[clap(short, long, help = "The path to the configuration file.", long_help = None)]
195    pub conf: Option<String>,
196}
197
198impl ServerConf {
199    // Does not has to be async until we want runtime reload
200    pub fn load_from_yaml<P>(path: P) -> Result<Self>
201    where
202        P: AsRef<std::path::Path> + std::fmt::Display,
203    {
204        let conf_str = fs::read_to_string(&path).or_err_with(ReadError, || {
205            format!("Unable to read conf file from {path}")
206        })?;
207        debug!("Conf file read from {path}");
208        Self::from_yaml(&conf_str)
209    }
210
211    pub fn load_yaml_with_opt_override(opt: &Opt) -> Result<Self> {
212        if let Some(path) = &opt.conf {
213            let mut conf = Self::load_from_yaml(path)?;
214            conf.merge_with_opt(opt);
215            Ok(conf)
216        } else {
217            Error::e_explain(ReadError, "No path specified")
218        }
219    }
220
221    pub fn new() -> Option<Self> {
222        Self::from_yaml("---\nversion: 1").ok()
223    }
224
225    pub fn new_with_opt_override(opt: &Opt) -> Option<Self> {
226        let conf = Self::new();
227        match conf {
228            Some(mut c) => {
229                c.merge_with_opt(opt);
230                Some(c)
231            }
232            None => None,
233        }
234    }
235
236    pub fn from_yaml(conf_str: &str) -> Result<Self> {
237        trace!("Read conf file: {conf_str}");
238        let conf: ServerConf = serde_yaml::from_str(conf_str).or_err_with(ReadError, || {
239            format!("Unable to parse yaml conf {conf_str}")
240        })?;
241
242        trace!("Loaded conf: {conf:?}");
243        conf.validate()
244    }
245
246    pub fn to_yaml(&self) -> String {
247        serde_yaml::to_string(self).unwrap()
248    }
249
250    pub fn validate(self) -> Result<Self> {
251        // TODO: do the validation
252        Ok(self)
253    }
254
255    pub fn merge_with_opt(&mut self, opt: &Opt) {
256        if opt.daemon {
257            self.daemon = true;
258        }
259    }
260}
261
262/// Create an instance of Opt by parsing the current command-line args.
263/// This is equivalent to running `Opt::parse` but does not require the
264/// caller to have included the `clap::Parser`
265impl Opt {
266    pub fn parse_args() -> Self {
267        Opt::parse()
268    }
269
270    pub fn parse_from_args<I, T>(args: I) -> Self
271    where
272        I: IntoIterator<Item = T>,
273        T: Into<OsString> + Clone,
274    {
275        Opt::parse_from(args)
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    fn init_log() {
284        let _ = env_logger::builder().is_test(true).try_init();
285    }
286
287    #[test]
288    fn not_a_test_i_cannot_write_yaml_by_hand() {
289        init_log();
290        let conf = ServerConf {
291            version: 1,
292            client_bind_to_ipv4: vec!["1.2.3.4".to_string(), "5.6.7.8".to_string()],
293            client_bind_to_ipv6: vec![],
294            ca_file: None,
295            #[cfg(feature = "s2n")]
296            s2n_config_cache_size: None,
297            daemon: false,
298            error_log: None,
299            upstream_debug_ssl_keylog: false,
300            pid_file: "".to_string(),
301            upgrade_sock: "".to_string(),
302            user: None,
303            group: None,
304            threads: 1,
305            listener_tasks_per_fd: 1,
306            work_stealing: true,
307            upstream_keepalive_pool_size: 4,
308            upstream_connect_offload_threadpools: None,
309            upstream_connect_offload_thread_per_pool: None,
310            grace_period_seconds: None,
311            graceful_shutdown_timeout_seconds: None,
312            max_retries: 1,
313            upgrade_sock_connect_accept_max_retries: None,
314        };
315        // cargo test -- --nocapture not_a_test_i_cannot_write_yaml_by_hand
316        println!("{}", conf.to_yaml());
317    }
318
319    #[test]
320    fn test_load_file() {
321        init_log();
322        let conf_str = r#"
323---
324version: 1
325client_bind_to_ipv4:
326    - 1.2.3.4
327    - 5.6.7.8
328client_bind_to_ipv6: []
329        "#
330        .to_string();
331        let conf = ServerConf::from_yaml(&conf_str).unwrap();
332        assert_eq!(2, conf.client_bind_to_ipv4.len());
333        assert_eq!(0, conf.client_bind_to_ipv6.len());
334        assert_eq!(1, conf.version);
335    }
336
337    #[test]
338    fn test_default() {
339        init_log();
340        let conf_str = r#"
341---
342version: 1
343        "#
344        .to_string();
345        let conf = ServerConf::from_yaml(&conf_str).unwrap();
346        assert_eq!(0, conf.client_bind_to_ipv4.len());
347        assert_eq!(0, conf.client_bind_to_ipv6.len());
348        assert_eq!(1, conf.version);
349        assert_eq!(DEFAULT_MAX_RETRIES, conf.max_retries);
350        assert_eq!("/tmp/pingora.pid", conf.pid_file);
351    }
352}