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}