pingora_core/server/configuration/
mod.rs

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