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}