1use 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
34const DEFAULT_MAX_RETRIES: usize = 16;
36const MAX_RUNTIME_METRICS_POLL_TIME_HISTOGRAM_BUCKETS: usize = 1024;
37
38#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(default)]
48pub struct ServerConf {
49 pub version: usize,
51 pub daemon: bool,
53 pub error_log: Option<String>,
56 pub pid_file: String,
58 pub upgrade_sock: String,
63 pub user: Option<String>,
66 pub group: Option<String>,
68 pub working_directory: Option<PathBuf>,
73 pub threads: usize,
75 pub listener_tasks_per_fd: usize,
77 pub work_stealing: bool,
79 pub runtime_enable_alt_timer: bool,
84 pub ca_file: Option<String>,
87 #[cfg(feature = "s2n")]
93 pub s2n_config_cache_size: Option<usize>,
94 pub grace_period_seconds: Option<u64>,
96 pub graceful_shutdown_timeout_seconds: Option<u64>,
98 pub client_bind_to_ipv4: Vec<String>,
103 pub client_bind_to_ipv6: Vec<String>,
107 pub upstream_keepalive_pool_size: usize,
111 pub upstream_connect_offload_threadpools: Option<usize>,
115 pub upstream_connect_offload_thread_per_pool: Option<usize>,
119 pub downstream_tls_offload_threadpools: Option<usize>,
123 pub downstream_tls_offload_thread_per_pool: Option<usize>,
127 pub upstream_debug_ssl_keylog: bool,
132 pub max_retries: usize,
137 pub upgrade_sock_connect_accept_max_retries: Option<usize>,
143 pub max_blocking_threads: Option<usize>,
148 pub blocking_threads_ttl_seconds: Option<u64>,
152 pub fast_timeout_to_tokio_threshold_seconds: Option<u64>,
159 pub runtime_metrics_poll_time_histogram: bool,
165 pub runtime_metrics_poll_time_histogram_scale: Option<RuntimeMetricsPollTimeHistogramScale>,
169 pub runtime_metrics_poll_time_histogram_resolution_micros: Option<u64>,
173 pub runtime_metrics_poll_time_histogram_buckets: Option<usize>,
178 pub daemon_wait_for_ready: bool,
191 pub daemon_ready_timeout_seconds: Option<NonZeroU64>,
199 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#[derive(Parser, Debug, Default)]
269#[clap(name = "basic", long_about = None)]
270pub struct Opt {
271 #[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 #[clap(short, long)]
282 pub daemon: bool,
283
284 #[clap(long, hide = true)]
287 pub nocapture: bool,
288
289 #[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 #[clap(short, long, help = "The path to the configuration file.", long_help = None)]
309 pub conf: Option<String>,
310}
311
312impl ServerConf {
313 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 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 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 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
440impl 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 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}