use clap::Parser;
use log::{debug, trace};
use pingora_error::{Error, ErrorType::*, OrErr, Result};
pub use pingora_runtime::RuntimeMetricsPollTimeHistogramScale;
use pingora_runtime::{RuntimeMetricsOpts, RuntimeOpts};
use serde::{Deserialize, Serialize};
use std::ffi::OsString;
use std::fs;
use std::num::NonZeroU64;
use std::path::PathBuf;
use std::time::Duration;
const DEFAULT_MAX_RETRIES: usize = 16;
const MAX_RUNTIME_METRICS_POLL_TIME_HISTOGRAM_BUCKETS: usize = 1024;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct ServerConf {
pub version: usize,
pub daemon: bool,
pub error_log: Option<String>,
pub pid_file: String,
pub upgrade_sock: String,
pub user: Option<String>,
pub group: Option<String>,
pub working_directory: Option<PathBuf>,
pub threads: usize,
pub listener_tasks_per_fd: usize,
pub work_stealing: bool,
pub runtime_enable_alt_timer: bool,
pub ca_file: Option<String>,
#[cfg(feature = "s2n")]
pub s2n_config_cache_size: Option<usize>,
pub grace_period_seconds: Option<u64>,
pub graceful_shutdown_timeout_seconds: Option<u64>,
pub client_bind_to_ipv4: Vec<String>,
pub client_bind_to_ipv6: Vec<String>,
pub upstream_keepalive_pool_size: usize,
pub upstream_connect_offload_threadpools: Option<usize>,
pub upstream_connect_offload_thread_per_pool: Option<usize>,
pub downstream_tls_offload_threadpools: Option<usize>,
pub downstream_tls_offload_thread_per_pool: Option<usize>,
pub upstream_debug_ssl_keylog: bool,
pub max_retries: usize,
pub upgrade_sock_connect_accept_max_retries: Option<usize>,
pub max_blocking_threads: Option<usize>,
pub blocking_threads_ttl_seconds: Option<u64>,
pub fast_timeout_to_tokio_threshold_seconds: Option<u64>,
pub runtime_metrics_poll_time_histogram: bool,
pub runtime_metrics_poll_time_histogram_scale: Option<RuntimeMetricsPollTimeHistogramScale>,
pub runtime_metrics_poll_time_histogram_resolution_micros: Option<u64>,
pub runtime_metrics_poll_time_histogram_buckets: Option<usize>,
pub daemon_wait_for_ready: bool,
pub daemon_ready_timeout_seconds: Option<NonZeroU64>,
pub daemon_notify_timeout_seconds: Option<NonZeroU64>,
}
impl Default for ServerConf {
fn default() -> Self {
ServerConf {
version: 0,
client_bind_to_ipv4: vec![],
client_bind_to_ipv6: vec![],
ca_file: None,
#[cfg(feature = "s2n")]
s2n_config_cache_size: None,
daemon: false,
error_log: None,
upstream_debug_ssl_keylog: false,
pid_file: "/tmp/pingora.pid".to_string(),
upgrade_sock: "/tmp/pingora_upgrade.sock".to_string(),
user: None,
group: None,
working_directory: None,
threads: 1,
listener_tasks_per_fd: 1,
work_stealing: true,
runtime_enable_alt_timer: false,
upstream_keepalive_pool_size: 128,
upstream_connect_offload_threadpools: None,
upstream_connect_offload_thread_per_pool: None,
downstream_tls_offload_threadpools: None,
downstream_tls_offload_thread_per_pool: None,
grace_period_seconds: None,
graceful_shutdown_timeout_seconds: None,
max_retries: DEFAULT_MAX_RETRIES,
upgrade_sock_connect_accept_max_retries: None,
max_blocking_threads: None,
blocking_threads_ttl_seconds: None,
fast_timeout_to_tokio_threshold_seconds: Some(
pingora_timeout::fast_timeout::DEFAULT_FAST_TIMEOUT_TO_TOKIO_THRESHOLD.as_secs(),
),
runtime_metrics_poll_time_histogram: false,
runtime_metrics_poll_time_histogram_scale: None,
runtime_metrics_poll_time_histogram_resolution_micros: None,
runtime_metrics_poll_time_histogram_buckets: None,
daemon_ready_timeout_seconds: None,
daemon_wait_for_ready: false,
daemon_notify_timeout_seconds: None,
}
}
}
#[derive(Parser, Debug, Default)]
#[clap(name = "basic", long_about = None)]
pub struct Opt {
#[clap(
short,
long,
help = "This is the base set of command line arguments for a pingora-based service",
long_help = None
)]
pub upgrade: bool,
#[clap(short, long)]
pub daemon: bool,
#[clap(long, hide = true)]
pub nocapture: bool,
#[clap(
short,
long,
help = "This flag is useful for upgrading service where the user wants \
to make sure the new service can start before shutting down \
the old server process.",
long_help = None
)]
pub test: bool,
#[clap(short, long, help = "The path to the configuration file.", long_help = None)]
pub conf: Option<String>,
}
impl ServerConf {
pub fn load_from_yaml<P>(path: P) -> Result<Self>
where
P: AsRef<std::path::Path> + std::fmt::Display,
{
let conf_str = fs::read_to_string(&path).or_err_with(ReadError, || {
format!("Unable to read conf file from {path}")
})?;
debug!("Conf file read from {path}");
Self::from_yaml(&conf_str)
}
pub fn load_yaml_with_opt_override(opt: &Opt) -> Result<Self> {
if let Some(path) = &opt.conf {
let mut conf = Self::load_from_yaml(path)?;
conf.merge_with_opt(opt);
Ok(conf)
} else {
Error::e_explain(ReadError, "No path specified")
}
}
pub fn new() -> Option<Self> {
Self::from_yaml("---\nversion: 1").ok()
}
pub fn new_with_opt_override(opt: &Opt) -> Option<Self> {
let conf = Self::new();
match conf {
Some(mut c) => {
c.merge_with_opt(opt);
Some(c)
}
None => None,
}
}
pub fn from_yaml(conf_str: &str) -> Result<Self> {
trace!("Read conf file: {conf_str}");
let conf: ServerConf = serde_yaml::from_str(conf_str).or_err_with(ReadError, || {
format!("Unable to parse yaml conf {conf_str}")
})?;
trace!("Loaded conf: {conf:?}");
conf.validate()
}
pub fn to_yaml(&self) -> String {
serde_yaml::to_string(self).unwrap()
}
pub fn validate(self) -> Result<Self> {
if self.max_blocking_threads == Some(0) {
return Error::e_explain(ReadError, "max_blocking_threads must be greater than zero");
}
if self.runtime_metrics_poll_time_histogram_resolution_micros == Some(0) {
return Error::e_explain(
ReadError,
"runtime_metrics_poll_time_histogram_resolution_micros must be greater than zero",
);
}
if self.runtime_metrics_poll_time_histogram_buckets == Some(0) {
return Error::e_explain(
ReadError,
"runtime_metrics_poll_time_histogram_buckets must be greater than zero",
);
}
if self
.runtime_metrics_poll_time_histogram_buckets
.is_some_and(|buckets| buckets > MAX_RUNTIME_METRICS_POLL_TIME_HISTOGRAM_BUCKETS)
{
return Error::e_explain(
ReadError,
format!(
"runtime_metrics_poll_time_histogram_buckets must be at most {MAX_RUNTIME_METRICS_POLL_TIME_HISTOGRAM_BUCKETS}"
),
);
}
Ok(self)
}
pub fn upstream_connect_offload_threadpool(&self) -> Option<(usize, usize)> {
self.upstream_connect_offload_threadpools
.zip(self.upstream_connect_offload_thread_per_pool)
.filter(|(pools, threads)| *pools > 0 && *threads > 0)
}
pub fn downstream_tls_offload_threadpool(&self) -> Option<(usize, usize)> {
self.downstream_tls_offload_threadpools
.zip(self.downstream_tls_offload_thread_per_pool)
.filter(|(pools, threads)| *pools > 0 && *threads > 0)
}
pub fn runtime_opts(&self) -> RuntimeOpts {
RuntimeOpts {
metrics: RuntimeMetricsOpts {
poll_time_histogram: self.runtime_metrics_poll_time_histogram,
poll_time_histogram_scale: self.runtime_metrics_poll_time_histogram_scale,
poll_time_histogram_resolution: self
.runtime_metrics_poll_time_histogram_resolution_micros
.map(Duration::from_micros),
poll_time_histogram_buckets: self.runtime_metrics_poll_time_histogram_buckets,
},
enable_alt_timer: self.runtime_enable_alt_timer,
#[cfg(feature = "dial9")]
dial9: None,
}
}
pub fn merge_with_opt(&mut self, opt: &Opt) {
if opt.daemon {
self.daemon = true;
}
}
}
impl Opt {
pub fn parse_args() -> Self {
Opt::parse()
}
pub fn parse_from_args<I, T>(args: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
Opt::parse_from(args)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn init_log() {
let _ = env_logger::builder().is_test(true).try_init();
}
#[test]
fn not_a_test_i_cannot_write_yaml_by_hand() {
init_log();
let conf = ServerConf {
version: 1,
client_bind_to_ipv4: vec!["1.2.3.4".to_string(), "5.6.7.8".to_string()],
client_bind_to_ipv6: vec![],
ca_file: None,
#[cfg(feature = "s2n")]
s2n_config_cache_size: None,
daemon: false,
error_log: None,
upstream_debug_ssl_keylog: false,
pid_file: "".to_string(),
upgrade_sock: "".to_string(),
user: None,
group: None,
working_directory: None,
threads: 1,
listener_tasks_per_fd: 1,
work_stealing: true,
runtime_enable_alt_timer: false,
upstream_keepalive_pool_size: 4,
upstream_connect_offload_threadpools: None,
upstream_connect_offload_thread_per_pool: None,
downstream_tls_offload_threadpools: None,
downstream_tls_offload_thread_per_pool: None,
grace_period_seconds: None,
graceful_shutdown_timeout_seconds: None,
max_retries: 1,
upgrade_sock_connect_accept_max_retries: None,
max_blocking_threads: None,
blocking_threads_ttl_seconds: None,
fast_timeout_to_tokio_threshold_seconds: Some(
pingora_timeout::fast_timeout::DEFAULT_FAST_TIMEOUT_TO_TOKIO_THRESHOLD.as_secs(),
),
runtime_metrics_poll_time_histogram: false,
runtime_metrics_poll_time_histogram_scale: None,
runtime_metrics_poll_time_histogram_resolution_micros: None,
runtime_metrics_poll_time_histogram_buckets: None,
daemon_ready_timeout_seconds: None,
daemon_wait_for_ready: false,
daemon_notify_timeout_seconds: None,
};
println!("{}", conf.to_yaml());
}
#[test]
fn test_load_file() {
init_log();
let conf_str = r#"
---
version: 1
client_bind_to_ipv4:
- 1.2.3.4
- 5.6.7.8
client_bind_to_ipv6: []
"#
.to_string();
let conf = ServerConf::from_yaml(&conf_str).unwrap();
assert_eq!(2, conf.client_bind_to_ipv4.len());
assert_eq!(0, conf.client_bind_to_ipv6.len());
assert_eq!(1, conf.version);
}
#[test]
fn test_default() {
init_log();
let conf_str = r#"
---
version: 1
"#
.to_string();
let conf = ServerConf::from_yaml(&conf_str).unwrap();
assert_eq!(0, conf.client_bind_to_ipv4.len());
assert_eq!(0, conf.client_bind_to_ipv6.len());
assert_eq!(1, conf.version);
assert_eq!(DEFAULT_MAX_RETRIES, conf.max_retries);
assert_eq!("/tmp/pingora.pid", conf.pid_file);
assert_eq!(
Some(pingora_timeout::fast_timeout::DEFAULT_FAST_TIMEOUT_TO_TOKIO_THRESHOLD.as_secs()),
conf.fast_timeout_to_tokio_threshold_seconds
);
}
#[test]
fn test_runtime_enable_alt_timer_config() {
init_log();
let conf_str = r#"
---
version: 1
runtime_enable_alt_timer: true
"#;
let conf = ServerConf::from_yaml(conf_str).unwrap();
assert!(conf.runtime_enable_alt_timer);
}
#[test]
fn test_offload_threadpool_config() {
init_log();
let conf_str = r#"
---
version: 1
upstream_connect_offload_threadpools: 2
upstream_connect_offload_thread_per_pool: 3
downstream_tls_offload_threadpools: 4
downstream_tls_offload_thread_per_pool: 5
"#;
let conf = ServerConf::from_yaml(conf_str).unwrap();
assert_eq!(Some((2, 3)), conf.upstream_connect_offload_threadpool());
assert_eq!(Some((4, 5)), conf.downstream_tls_offload_threadpool());
}
#[test]
fn test_offload_threadpool_config_zero_disables() {
init_log();
let conf_str = r#"
---
version: 1
upstream_connect_offload_threadpools: 2
upstream_connect_offload_thread_per_pool: 0
downstream_tls_offload_threadpools: 0
downstream_tls_offload_thread_per_pool: 5
"#;
let conf = ServerConf::from_yaml(conf_str).unwrap();
assert_eq!(None, conf.upstream_connect_offload_threadpool());
assert_eq!(None, conf.downstream_tls_offload_threadpool());
}
#[test]
fn test_runtime_opts_from_config() {
init_log();
let conf_str = r#"
---
version: 1
runtime_enable_alt_timer: true
runtime_metrics_poll_time_histogram: true
runtime_metrics_poll_time_histogram_scale: log
runtime_metrics_poll_time_histogram_resolution_micros: 20
runtime_metrics_poll_time_histogram_buckets: 16
"#;
let conf = ServerConf::from_yaml(conf_str).unwrap();
let opts = conf.runtime_opts();
assert!(opts.enable_alt_timer);
assert!(opts.metrics.poll_time_histogram);
assert_eq!(
Some(RuntimeMetricsPollTimeHistogramScale::Log),
opts.metrics.poll_time_histogram_scale
);
assert_eq!(
Some(Duration::from_micros(20)),
opts.metrics.poll_time_histogram_resolution
);
assert_eq!(Some(16), opts.metrics.poll_time_histogram_buckets);
#[cfg(feature = "dial9")]
assert!(opts.dial9.is_none());
}
#[test]
fn test_working_directory_deserializes_from_yaml_string() {
init_log();
let conf_str = r#"
---
version: 1
daemon: true
working_directory: /var/lib/pingora
"#;
let conf = ServerConf::from_yaml(conf_str).unwrap();
assert_eq!(
conf.working_directory.as_deref(),
Some(std::path::Path::new("/var/lib/pingora"))
);
let yaml = serde_yaml::to_value(&conf).unwrap();
assert_eq!(
yaml.get("working_directory"),
Some(&serde_yaml::Value::String("/var/lib/pingora".to_string()))
);
}
#[test]
fn test_zero_max_blocking_threads_is_rejected() {
init_log();
let conf_str = r#"
---
version: 1
max_blocking_threads: 0
"#;
let result = ServerConf::from_yaml(conf_str);
assert!(
result.is_err(),
"max_blocking_threads: 0 should fail validation"
);
}
#[test]
fn test_valid_max_blocking_threads() {
init_log();
let conf_str = r#"
---
version: 1
max_blocking_threads: 64
blocking_threads_ttl_seconds: 30
"#;
let conf = ServerConf::from_yaml(conf_str).unwrap();
assert_eq!(Some(64), conf.max_blocking_threads);
assert_eq!(Some(30), conf.blocking_threads_ttl_seconds);
}
#[test]
fn test_fast_timeout_to_tokio_threshold_config() {
init_log();
let conf_str = r#"
---
version: 1
fast_timeout_to_tokio_threshold_seconds: 120
"#;
let conf = ServerConf::from_yaml(conf_str).unwrap();
assert_eq!(Some(120), conf.fast_timeout_to_tokio_threshold_seconds);
}
#[test]
fn test_fast_timeout_to_tokio_threshold_can_be_disabled() {
init_log();
let conf_str = r#"
---
version: 1
fast_timeout_to_tokio_threshold_seconds:
"#;
let conf = ServerConf::from_yaml(conf_str).unwrap();
assert_eq!(None, conf.fast_timeout_to_tokio_threshold_seconds);
}
#[test]
fn test_runtime_poll_time_histogram_config() {
init_log();
let conf_str = r#"
---
version: 1
runtime_metrics_poll_time_histogram: true
runtime_metrics_poll_time_histogram_scale: log
runtime_metrics_poll_time_histogram_resolution_micros: 20
runtime_metrics_poll_time_histogram_buckets: 16
"#;
let conf = ServerConf::from_yaml(conf_str).unwrap();
assert!(conf.runtime_metrics_poll_time_histogram);
assert_eq!(
Some(RuntimeMetricsPollTimeHistogramScale::Log),
conf.runtime_metrics_poll_time_histogram_scale
);
assert_eq!(
Some(20),
conf.runtime_metrics_poll_time_histogram_resolution_micros
);
assert_eq!(Some(16), conf.runtime_metrics_poll_time_histogram_buckets);
}
#[test]
fn test_runtime_poll_time_histogram_bucket_limit() {
init_log();
let conf_str = format!(
r#"
---
version: 1
runtime_metrics_poll_time_histogram: true
runtime_metrics_poll_time_histogram_buckets: {}
"#,
MAX_RUNTIME_METRICS_POLL_TIME_HISTOGRAM_BUCKETS + 1
);
let result = ServerConf::from_yaml(&conf_str);
assert!(
result.is_err(),
"excessive runtime_metrics_poll_time_histogram_buckets should fail validation"
);
}
}