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};
25pub use pingora_runtime::RuntimeMetricsPollTimeHistogramScale;
26use pingora_runtime::{RuntimeMetricsOpts, RuntimeOpts};
27use serde::{Deserialize, Serialize};
28use std::ffi::OsString;
29use std::fs;
30use std::num::NonZeroU64;
31use std::path::PathBuf;
32use std::time::Duration;
33
34// default maximum upstream retries for retry-able proxy errors
35const DEFAULT_MAX_RETRIES: usize = 16;
36const MAX_RUNTIME_METRICS_POLL_TIME_HISTOGRAM_BUCKETS: usize = 1024;
37
38/// The configuration file
39///
40/// Pingora configuration files are by default YAML files, but any key value format can potentially
41/// be used.
42///
43/// # Extension
44/// New keys can be added to the configuration files which this configuration object will ignore.
45/// Then, users can parse these key-values to pass to their code to use.
46#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(default)]
48pub struct ServerConf {
49    /// Version
50    pub version: usize,
51    /// Whether to run this process in the background.
52    pub daemon: bool,
53    /// When configured and `daemon` setting is `true`, error log will be written to the given
54    /// file. Otherwise StdErr will be used.
55    pub error_log: Option<String>,
56    /// The pid (process ID) file of this server to be created when running in background
57    pub pid_file: String,
58    /// the path to the upgrade socket
59    ///
60    /// In order to perform zero downtime restart, both the new and old process need to agree on the
61    /// path to this sock in order to coordinate the upgrade.
62    pub upgrade_sock: String,
63    /// If configured, after daemonization, this process will switch to the given user before
64    /// starting to serve traffic.
65    pub user: Option<String>,
66    /// Similar to `user`, the group this process should switch to.
67    pub group: Option<String>,
68    /// Working directory for the daemonized process.
69    ///
70    /// Only applied when `daemon` is `true`; set this to start the daemon from a known cwd.
71    // TODO: other OS path options should likely be `PathBuf` as well.
72    pub working_directory: Option<PathBuf>,
73    /// How many threads **each** service should get. The threads are not shared across services.
74    pub threads: usize,
75    /// Number of listener tasks to use per fd. This allows for parallel accepts.
76    pub listener_tasks_per_fd: usize,
77    /// Allow work stealing between threads of the same service. Default `true`.
78    pub work_stealing: bool,
79    /// Enable Tokio's experimental alternative timer on work-stealing service runtimes.
80    ///
81    /// Requires building with `--cfg tokio_unstable`. Ignored when
82    /// [`Self::work_stealing`] is disabled.
83    pub runtime_enable_alt_timer: bool,
84    /// The path to CA file the SSL library should use. If empty, the default trust store location
85    /// defined by the SSL library will be used.
86    pub ca_file: Option<String>,
87    /// The maximum number of unique s2n configs to cache. Creating a new s2n config is an
88    /// expensive operation, so we cache and re-use config objects with identical configurations.
89    /// A value of 0 disables the cache.
90    ///
91    /// WARNING: Disabling the s2n config cache can result in poor performance
92    #[cfg(feature = "s2n")]
93    pub s2n_config_cache_size: Option<usize>,
94    /// Grace period in seconds before starting the final step of the graceful shutdown after signaling shutdown.
95    pub grace_period_seconds: Option<u64>,
96    /// Timeout in seconds of the final step for the graceful shutdown.
97    pub graceful_shutdown_timeout_seconds: Option<u64>,
98    // These options don't belong here as they are specific to certain services
99    /// IPv4 addresses for a client connector to bind to. See
100    /// [`ConnectorOptions`](crate::connectors::ConnectorOptions).
101    /// Note: this is an _unstable_ field that may be renamed or removed in the future.
102    pub client_bind_to_ipv4: Vec<String>,
103    /// IPv6 addresses for a client connector to bind to. See
104    /// [`ConnectorOptions`](crate::connectors::ConnectorOptions).
105    /// Note: this is an _unstable_ field that may be renamed or removed in the future.
106    pub client_bind_to_ipv6: Vec<String>,
107    /// Keepalive pool size for client connections to upstream. See
108    /// [`ConnectorOptions`](crate::connectors::ConnectorOptions).
109    /// Note: this is an _unstable_ field that may be renamed or removed in the future.
110    pub upstream_keepalive_pool_size: usize,
111    /// Number of dedicated thread pools to use for upstream connection establishment.
112    /// See [`ConnectorOptions`](crate::connectors::ConnectorOptions).
113    /// Note: this is an _unstable_ field that may be renamed or removed in the future.
114    pub upstream_connect_offload_threadpools: Option<usize>,
115    /// Number of threads per dedicated upstream connection establishment pool.
116    /// See [`ConnectorOptions`](crate::connectors::ConnectorOptions).
117    /// Note: this is an _unstable_ field that may be renamed or removed in the future.
118    pub upstream_connect_offload_thread_per_pool: Option<usize>,
119    /// Number of dedicated thread pools to use for downstream TLS handshakes.
120    /// See [`TlsSettings::set_offload_threadpool_from_server_conf`](crate::listeners::tls::TlsSettings::set_offload_threadpool_from_server_conf).
121    /// Note: this is an _unstable_ field that may be renamed or removed in the future.
122    pub downstream_tls_offload_threadpools: Option<usize>,
123    /// Number of threads per dedicated downstream TLS handshake pool.
124    /// See [`TlsSettings::set_offload_threadpool_from_server_conf`](crate::listeners::tls::TlsSettings::set_offload_threadpool_from_server_conf).
125    /// Note: this is an _unstable_ field that may be renamed or removed in the future.
126    pub downstream_tls_offload_thread_per_pool: Option<usize>,
127    /// When enabled allows TLS keys to be written to a file specified by the SSLKEYLOG
128    /// env variable. This can be used by tools like Wireshark to decrypt upstream traffic
129    /// for debugging purposes.
130    /// Note: this is an _unstable_ field that may be renamed or removed in the future.
131    pub upstream_debug_ssl_keylog: bool,
132    /// The maximum number of retries that will be attempted when an error is
133    /// retry-able (`e.retry() == true`) when proxying to upstream.
134    ///
135    /// This setting is a fail-safe and defaults to 16.
136    pub max_retries: usize,
137    /// Maximum number of retries for upgrade socket connect and accept operations.
138    /// This controls how many times send_fds_to will retry connecting and how many times
139    /// get_fds_from will retry accepting during graceful upgrades.
140    /// The retry interval is 1 second between attempts.
141    /// If not set, defaults to 5 retries.
142    pub upgrade_sock_connect_accept_max_retries: Option<usize>,
143    /// The maximum number of threads in each runtime's blocking thread pool.
144    ///
145    /// The blocking pool handles [`tokio::task::spawn_blocking`] tasks.
146    /// When not set, the tokio default (512) is used.
147    pub max_blocking_threads: Option<usize>,
148    /// How long, in seconds, idle blocking threads are kept alive before being shut down.
149    ///
150    /// When not set, the tokio default (10 seconds) is used.
151    pub blocking_threads_ttl_seconds: Option<u64>,
152    /// Timeout durations greater than this threshold use Tokio's native timeout instead of
153    /// Pingora's fast timeout.
154    ///
155    /// This avoids retaining long-duration cancelled timers in Pingora's shared timer map until
156    /// their original deadline. When not set, defaults to 900 seconds (15 minutes). Set to `null`
157    /// to disable the Tokio fallback.
158    pub fast_timeout_to_tokio_threshold_seconds: Option<u64>,
159    /// Enable Tokio's poll-time histogram on runtimes created by this server.
160    ///
161    /// This adds two timestamp reads to every task poll, so it should be
162    /// enabled deliberately when investigating runtime latency. Requires
163    /// building with `--cfg tokio_unstable`.
164    pub runtime_metrics_poll_time_histogram: bool,
165    /// Bucket scale for Tokio's poll-time histogram.
166    ///
167    /// Ignored unless [`Self::runtime_metrics_poll_time_histogram`] is enabled.
168    pub runtime_metrics_poll_time_histogram_scale: Option<RuntimeMetricsPollTimeHistogramScale>,
169    /// Width of the first Tokio poll-time histogram bucket in microseconds.
170    ///
171    /// Ignored unless [`Self::runtime_metrics_poll_time_histogram`] is enabled.
172    pub runtime_metrics_poll_time_histogram_resolution_micros: Option<u64>,
173    /// Number of Tokio poll-time histogram buckets.
174    ///
175    /// Ignored unless [`Self::runtime_metrics_poll_time_histogram`] is enabled. Memory usage
176    /// scales with runtimes × workers × buckets, so values above 1024 are rejected.
177    pub runtime_metrics_poll_time_histogram_buckets: Option<usize>,
178    /// When `daemon` is `true`, controls whether the parent process of the daemon fork waits for
179    /// the child to signal readiness before exiting.
180    ///
181    /// When `false` (default), the parent exits immediately after the daemon fork, matching the
182    /// traditional daemonization behavior. Systemd will consider the service started as soon as
183    /// the parent exits, which may be before the child has finished bootstrapping.
184    ///
185    /// When `true`, the parent waits (up to [`Self::daemon_ready_timeout_seconds`]) for the child
186    /// to send `SIGUSR1` after bootstrap completes. This causes systemd to delay any subsequent
187    /// steps (such as sending `SIGQUIT` to the old process) until the new instance is fully ready
188    /// to serve traffic. If the child does not signal in time, the parent exits with a non-zero
189    /// exit code, causing systemd to abort the reload.
190    pub daemon_wait_for_ready: bool,
191    /// Timeout in seconds for the parent process to wait for the child to signal readiness during
192    /// daemonization when [`Self::daemon_wait_for_ready`] is `true`.
193    ///
194    /// If the child does not send `SIGUSR1` within this timeout, the parent exits with a non-zero
195    /// exit code.
196    ///
197    /// Defaults to 600 seconds (10 minutes).
198    pub daemon_ready_timeout_seconds: Option<NonZeroU64>,
199    /// How long the child process will keep retrying `SIGUSR1` to the parent when the signal
200    /// fails with a permission error (`EPERM`) during daemonization.
201    ///
202    /// After the daemon fork, the parent always drops its credentials to the configured user and
203    /// group (see [`Self::user`], [`Self::group`]). Because the privilege drop happens after the
204    /// fork, there is a small window where the child may attempt to signal the parent before the
205    /// parent has finished changing its credentials. During this window the kernel will reject the
206    /// signal with `EPERM` because the child and parent are running as different users. The child
207    /// retries every 100 ms until this timeout elapses.
208    ///
209    /// In practice this window is very small, so the default of 60 seconds is far more than
210    /// enough to account for it.
211    ///
212    /// Only retries on `EPERM`; any other error (e.g. `ESRCH` — parent no longer exists) is
213    /// treated as fatal and logged without retrying.
214    ///
215    /// Defaults to 60 seconds.
216    pub daemon_notify_timeout_seconds: Option<NonZeroU64>,
217}
218
219impl Default for ServerConf {
220    fn default() -> Self {
221        ServerConf {
222            version: 0,
223            client_bind_to_ipv4: vec![],
224            client_bind_to_ipv6: vec![],
225            ca_file: None,
226            #[cfg(feature = "s2n")]
227            s2n_config_cache_size: None,
228            daemon: false,
229            error_log: None,
230            upstream_debug_ssl_keylog: false,
231            pid_file: "/tmp/pingora.pid".to_string(),
232            upgrade_sock: "/tmp/pingora_upgrade.sock".to_string(),
233            user: None,
234            group: None,
235            working_directory: None,
236            threads: 1,
237            listener_tasks_per_fd: 1,
238            work_stealing: true,
239            runtime_enable_alt_timer: false,
240            upstream_keepalive_pool_size: 128,
241            upstream_connect_offload_threadpools: None,
242            upstream_connect_offload_thread_per_pool: None,
243            downstream_tls_offload_threadpools: None,
244            downstream_tls_offload_thread_per_pool: None,
245            grace_period_seconds: None,
246            graceful_shutdown_timeout_seconds: None,
247            max_retries: DEFAULT_MAX_RETRIES,
248            upgrade_sock_connect_accept_max_retries: None,
249            max_blocking_threads: None,
250            blocking_threads_ttl_seconds: None,
251            fast_timeout_to_tokio_threshold_seconds: Some(
252                pingora_timeout::fast_timeout::DEFAULT_FAST_TIMEOUT_TO_TOKIO_THRESHOLD.as_secs(),
253            ),
254            runtime_metrics_poll_time_histogram: false,
255            runtime_metrics_poll_time_histogram_scale: None,
256            runtime_metrics_poll_time_histogram_resolution_micros: None,
257            runtime_metrics_poll_time_histogram_buckets: None,
258            daemon_ready_timeout_seconds: None,
259            daemon_wait_for_ready: false,
260            daemon_notify_timeout_seconds: None,
261        }
262    }
263}
264
265/// Command-line options
266///
267/// Call `Opt::parse_args()` to build this object from the process's command line arguments.
268#[derive(Parser, Debug, Default)]
269#[clap(name = "basic", long_about = None)]
270pub struct Opt {
271    /// Whether this server should try to upgrade from a running old server
272    #[clap(
273        short,
274        long,
275        help = "This is the base set of command line arguments for a pingora-based service",
276        long_help = None
277    )]
278    pub upgrade: bool,
279
280    /// Whether this server should run in the background
281    #[clap(short, long)]
282    pub daemon: bool,
283
284    /// Not actually used. This flag is there so that the server is not upset seeing this flag
285    /// passed from `cargo test` sometimes
286    #[clap(long, hide = true)]
287    pub nocapture: bool,
288
289    /// Test the configuration and exit
290    ///
291    /// When this flag is set, calling `server.bootstrap()` will exit the process without errors
292    ///
293    /// This flag is useful for upgrading service where the user wants to make sure the new
294    /// service can start before shutting down the old server process.
295    #[clap(
296        short,
297        long,
298        help = "This flag is useful for upgrading service where the user wants \
299                to make sure the new service can start before shutting down \
300                the old server process.",
301        long_help = None
302    )]
303    pub test: bool,
304
305    /// The path to the configuration file.
306    ///
307    /// See [`ServerConf`] for more details of the configuration file.
308    #[clap(short, long, help = "The path to the configuration file.", long_help = None)]
309    pub conf: Option<String>,
310}
311
312impl ServerConf {
313    // Does not has to be async until we want runtime reload
314    pub fn load_from_yaml<P>(path: P) -> Result<Self>
315    where
316        P: AsRef<std::path::Path> + std::fmt::Display,
317    {
318        let conf_str = fs::read_to_string(&path).or_err_with(ReadError, || {
319            format!("Unable to read conf file from {path}")
320        })?;
321        debug!("Conf file read from {path}");
322        Self::from_yaml(&conf_str)
323    }
324
325    pub fn load_yaml_with_opt_override(opt: &Opt) -> Result<Self> {
326        if let Some(path) = &opt.conf {
327            let mut conf = Self::load_from_yaml(path)?;
328            conf.merge_with_opt(opt);
329            Ok(conf)
330        } else {
331            Error::e_explain(ReadError, "No path specified")
332        }
333    }
334
335    pub fn new() -> Option<Self> {
336        Self::from_yaml("---\nversion: 1").ok()
337    }
338
339    pub fn new_with_opt_override(opt: &Opt) -> Option<Self> {
340        let conf = Self::new();
341        match conf {
342            Some(mut c) => {
343                c.merge_with_opt(opt);
344                Some(c)
345            }
346            None => None,
347        }
348    }
349
350    pub fn from_yaml(conf_str: &str) -> Result<Self> {
351        trace!("Read conf file: {conf_str}");
352        let conf: ServerConf = serde_yaml::from_str(conf_str).or_err_with(ReadError, || {
353            format!("Unable to parse yaml conf {conf_str}")
354        })?;
355
356        trace!("Loaded conf: {conf:?}");
357        conf.validate()
358    }
359
360    pub fn to_yaml(&self) -> String {
361        serde_yaml::to_string(self).unwrap()
362    }
363
364    pub fn validate(self) -> Result<Self> {
365        if self.max_blocking_threads == Some(0) {
366            return Error::e_explain(ReadError, "max_blocking_threads must be greater than zero");
367        }
368        if self.runtime_metrics_poll_time_histogram_resolution_micros == Some(0) {
369            return Error::e_explain(
370                ReadError,
371                "runtime_metrics_poll_time_histogram_resolution_micros must be greater than zero",
372            );
373        }
374        if self.runtime_metrics_poll_time_histogram_buckets == Some(0) {
375            return Error::e_explain(
376                ReadError,
377                "runtime_metrics_poll_time_histogram_buckets must be greater than zero",
378            );
379        }
380        if self
381            .runtime_metrics_poll_time_histogram_buckets
382            .is_some_and(|buckets| buckets > MAX_RUNTIME_METRICS_POLL_TIME_HISTOGRAM_BUCKETS)
383        {
384            return Error::e_explain(
385                ReadError,
386                format!(
387                    "runtime_metrics_poll_time_histogram_buckets must be at most {MAX_RUNTIME_METRICS_POLL_TIME_HISTOGRAM_BUCKETS}"
388                ),
389            );
390        }
391        Ok(self)
392    }
393
394    /// Return the upstream connection offload setting from this configuration.
395    ///
396    /// Both `upstream_connect_offload_threadpools` and
397    /// `upstream_connect_offload_thread_per_pool` must be set and greater than
398    /// zero. Otherwise, upstream connection offload remains disabled.
399    pub fn upstream_connect_offload_threadpool(&self) -> Option<(usize, usize)> {
400        self.upstream_connect_offload_threadpools
401            .zip(self.upstream_connect_offload_thread_per_pool)
402            .filter(|(pools, threads)| *pools > 0 && *threads > 0)
403    }
404
405    /// Return the downstream TLS handshake offload setting from this configuration.
406    ///
407    /// Both `downstream_tls_offload_threadpools` and
408    /// `downstream_tls_offload_thread_per_pool` must be set and greater than
409    /// zero. Otherwise, downstream TLS handshake offload remains disabled.
410    pub fn downstream_tls_offload_threadpool(&self) -> Option<(usize, usize)> {
411        self.downstream_tls_offload_threadpools
412            .zip(self.downstream_tls_offload_thread_per_pool)
413            .filter(|(pools, threads)| *pools > 0 && *threads > 0)
414    }
415
416    /// Build the default runtime options derived from this server configuration.
417    pub fn runtime_opts(&self) -> RuntimeOpts {
418        RuntimeOpts {
419            metrics: RuntimeMetricsOpts {
420                poll_time_histogram: self.runtime_metrics_poll_time_histogram,
421                poll_time_histogram_scale: self.runtime_metrics_poll_time_histogram_scale,
422                poll_time_histogram_resolution: self
423                    .runtime_metrics_poll_time_histogram_resolution_micros
424                    .map(Duration::from_micros),
425                poll_time_histogram_buckets: self.runtime_metrics_poll_time_histogram_buckets,
426            },
427            enable_alt_timer: self.runtime_enable_alt_timer,
428            #[cfg(feature = "dial9")]
429            dial9: None,
430        }
431    }
432
433    pub fn merge_with_opt(&mut self, opt: &Opt) {
434        if opt.daemon {
435            self.daemon = true;
436        }
437    }
438}
439
440/// Create an instance of Opt by parsing the current command-line args.
441/// This is equivalent to running `Opt::parse` but does not require the
442/// caller to have included the `clap::Parser`
443impl Opt {
444    pub fn parse_args() -> Self {
445        Opt::parse()
446    }
447
448    pub fn parse_from_args<I, T>(args: I) -> Self
449    where
450        I: IntoIterator<Item = T>,
451        T: Into<OsString> + Clone,
452    {
453        Opt::parse_from(args)
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460
461    fn init_log() {
462        let _ = env_logger::builder().is_test(true).try_init();
463    }
464
465    #[test]
466    fn not_a_test_i_cannot_write_yaml_by_hand() {
467        init_log();
468        let conf = ServerConf {
469            version: 1,
470            client_bind_to_ipv4: vec!["1.2.3.4".to_string(), "5.6.7.8".to_string()],
471            client_bind_to_ipv6: vec![],
472            ca_file: None,
473            #[cfg(feature = "s2n")]
474            s2n_config_cache_size: None,
475            daemon: false,
476            error_log: None,
477            upstream_debug_ssl_keylog: false,
478            pid_file: "".to_string(),
479            upgrade_sock: "".to_string(),
480            user: None,
481            group: None,
482            working_directory: None,
483            threads: 1,
484            listener_tasks_per_fd: 1,
485            work_stealing: true,
486            runtime_enable_alt_timer: false,
487            upstream_keepalive_pool_size: 4,
488            upstream_connect_offload_threadpools: None,
489            upstream_connect_offload_thread_per_pool: None,
490            downstream_tls_offload_threadpools: None,
491            downstream_tls_offload_thread_per_pool: None,
492            grace_period_seconds: None,
493            graceful_shutdown_timeout_seconds: None,
494            max_retries: 1,
495            upgrade_sock_connect_accept_max_retries: None,
496            max_blocking_threads: None,
497            blocking_threads_ttl_seconds: None,
498            fast_timeout_to_tokio_threshold_seconds: Some(
499                pingora_timeout::fast_timeout::DEFAULT_FAST_TIMEOUT_TO_TOKIO_THRESHOLD.as_secs(),
500            ),
501            runtime_metrics_poll_time_histogram: false,
502            runtime_metrics_poll_time_histogram_scale: None,
503            runtime_metrics_poll_time_histogram_resolution_micros: None,
504            runtime_metrics_poll_time_histogram_buckets: None,
505            daemon_ready_timeout_seconds: None,
506            daemon_wait_for_ready: false,
507            daemon_notify_timeout_seconds: None,
508        };
509        // cargo test -- --nocapture not_a_test_i_cannot_write_yaml_by_hand
510        println!("{}", conf.to_yaml());
511    }
512
513    #[test]
514    fn test_load_file() {
515        init_log();
516        let conf_str = r#"
517---
518version: 1
519client_bind_to_ipv4:
520    - 1.2.3.4
521    - 5.6.7.8
522client_bind_to_ipv6: []
523        "#
524        .to_string();
525        let conf = ServerConf::from_yaml(&conf_str).unwrap();
526        assert_eq!(2, conf.client_bind_to_ipv4.len());
527        assert_eq!(0, conf.client_bind_to_ipv6.len());
528        assert_eq!(1, conf.version);
529    }
530
531    #[test]
532    fn test_default() {
533        init_log();
534        let conf_str = r#"
535---
536version: 1
537        "#
538        .to_string();
539        let conf = ServerConf::from_yaml(&conf_str).unwrap();
540        assert_eq!(0, conf.client_bind_to_ipv4.len());
541        assert_eq!(0, conf.client_bind_to_ipv6.len());
542        assert_eq!(1, conf.version);
543        assert_eq!(DEFAULT_MAX_RETRIES, conf.max_retries);
544        assert_eq!("/tmp/pingora.pid", conf.pid_file);
545        assert_eq!(
546            Some(pingora_timeout::fast_timeout::DEFAULT_FAST_TIMEOUT_TO_TOKIO_THRESHOLD.as_secs()),
547            conf.fast_timeout_to_tokio_threshold_seconds
548        );
549    }
550
551    #[test]
552    fn test_runtime_enable_alt_timer_config() {
553        init_log();
554        let conf_str = r#"
555---
556version: 1
557runtime_enable_alt_timer: true
558        "#;
559
560        let conf = ServerConf::from_yaml(conf_str).unwrap();
561        assert!(conf.runtime_enable_alt_timer);
562    }
563
564    #[test]
565    fn test_offload_threadpool_config() {
566        init_log();
567        let conf_str = r#"
568---
569version: 1
570upstream_connect_offload_threadpools: 2
571upstream_connect_offload_thread_per_pool: 3
572downstream_tls_offload_threadpools: 4
573downstream_tls_offload_thread_per_pool: 5
574        "#;
575
576        let conf = ServerConf::from_yaml(conf_str).unwrap();
577        assert_eq!(Some((2, 3)), conf.upstream_connect_offload_threadpool());
578        assert_eq!(Some((4, 5)), conf.downstream_tls_offload_threadpool());
579    }
580
581    #[test]
582    fn test_offload_threadpool_config_zero_disables() {
583        init_log();
584        let conf_str = r#"
585---
586version: 1
587upstream_connect_offload_threadpools: 2
588upstream_connect_offload_thread_per_pool: 0
589downstream_tls_offload_threadpools: 0
590downstream_tls_offload_thread_per_pool: 5
591        "#;
592
593        let conf = ServerConf::from_yaml(conf_str).unwrap();
594        assert_eq!(None, conf.upstream_connect_offload_threadpool());
595        assert_eq!(None, conf.downstream_tls_offload_threadpool());
596    }
597
598    #[test]
599    fn test_runtime_opts_from_config() {
600        init_log();
601        let conf_str = r#"
602---
603version: 1
604runtime_enable_alt_timer: true
605runtime_metrics_poll_time_histogram: true
606runtime_metrics_poll_time_histogram_scale: log
607runtime_metrics_poll_time_histogram_resolution_micros: 20
608runtime_metrics_poll_time_histogram_buckets: 16
609        "#;
610
611        let conf = ServerConf::from_yaml(conf_str).unwrap();
612        let opts = conf.runtime_opts();
613        assert!(opts.enable_alt_timer);
614        assert!(opts.metrics.poll_time_histogram);
615        assert_eq!(
616            Some(RuntimeMetricsPollTimeHistogramScale::Log),
617            opts.metrics.poll_time_histogram_scale
618        );
619        assert_eq!(
620            Some(Duration::from_micros(20)),
621            opts.metrics.poll_time_histogram_resolution
622        );
623        assert_eq!(Some(16), opts.metrics.poll_time_histogram_buckets);
624        #[cfg(feature = "dial9")]
625        assert!(opts.dial9.is_none());
626    }
627
628    #[test]
629    fn test_working_directory_deserializes_from_yaml_string() {
630        init_log();
631        let conf_str = r#"
632---
633version: 1
634daemon: true
635working_directory: /var/lib/pingora
636        "#;
637
638        let conf = ServerConf::from_yaml(conf_str).unwrap();
639        assert_eq!(
640            conf.working_directory.as_deref(),
641            Some(std::path::Path::new("/var/lib/pingora"))
642        );
643
644        let yaml = serde_yaml::to_value(&conf).unwrap();
645        assert_eq!(
646            yaml.get("working_directory"),
647            Some(&serde_yaml::Value::String("/var/lib/pingora".to_string()))
648        );
649    }
650
651    #[test]
652    fn test_zero_max_blocking_threads_is_rejected() {
653        init_log();
654        let conf_str = r#"
655---
656version: 1
657max_blocking_threads: 0
658        "#;
659        let result = ServerConf::from_yaml(conf_str);
660        assert!(
661            result.is_err(),
662            "max_blocking_threads: 0 should fail validation"
663        );
664    }
665
666    #[test]
667    fn test_valid_max_blocking_threads() {
668        init_log();
669        let conf_str = r#"
670---
671version: 1
672max_blocking_threads: 64
673blocking_threads_ttl_seconds: 30
674        "#;
675        let conf = ServerConf::from_yaml(conf_str).unwrap();
676        assert_eq!(Some(64), conf.max_blocking_threads);
677        assert_eq!(Some(30), conf.blocking_threads_ttl_seconds);
678    }
679
680    #[test]
681    fn test_fast_timeout_to_tokio_threshold_config() {
682        init_log();
683        let conf_str = r#"
684---
685version: 1
686fast_timeout_to_tokio_threshold_seconds: 120
687        "#;
688        let conf = ServerConf::from_yaml(conf_str).unwrap();
689        assert_eq!(Some(120), conf.fast_timeout_to_tokio_threshold_seconds);
690    }
691
692    #[test]
693    fn test_fast_timeout_to_tokio_threshold_can_be_disabled() {
694        init_log();
695        let conf_str = r#"
696---
697version: 1
698fast_timeout_to_tokio_threshold_seconds:
699        "#;
700        let conf = ServerConf::from_yaml(conf_str).unwrap();
701        assert_eq!(None, conf.fast_timeout_to_tokio_threshold_seconds);
702    }
703
704    #[test]
705    fn test_runtime_poll_time_histogram_config() {
706        init_log();
707        let conf_str = r#"
708---
709version: 1
710runtime_metrics_poll_time_histogram: true
711runtime_metrics_poll_time_histogram_scale: log
712runtime_metrics_poll_time_histogram_resolution_micros: 20
713runtime_metrics_poll_time_histogram_buckets: 16
714        "#;
715
716        let conf = ServerConf::from_yaml(conf_str).unwrap();
717        assert!(conf.runtime_metrics_poll_time_histogram);
718        assert_eq!(
719            Some(RuntimeMetricsPollTimeHistogramScale::Log),
720            conf.runtime_metrics_poll_time_histogram_scale
721        );
722        assert_eq!(
723            Some(20),
724            conf.runtime_metrics_poll_time_histogram_resolution_micros
725        );
726        assert_eq!(Some(16), conf.runtime_metrics_poll_time_histogram_buckets);
727    }
728
729    #[test]
730    fn test_runtime_poll_time_histogram_bucket_limit() {
731        init_log();
732        let conf_str = format!(
733            r#"
734---
735version: 1
736runtime_metrics_poll_time_histogram: true
737runtime_metrics_poll_time_histogram_buckets: {}
738        "#,
739            MAX_RUNTIME_METRICS_POLL_TIME_HISTOGRAM_BUCKETS + 1
740        );
741
742        let result = ServerConf::from_yaml(&conf_str);
743        assert!(
744            result.is_err(),
745            "excessive runtime_metrics_poll_time_histogram_buckets should fail validation"
746        );
747    }
748}