pub const MIN_HANDLER_TIMEOUT_MS: u64 = 100;
pub const MIN_RETRY_AFTER_SECS: u64 = 1;
pub const MAX_RETRY_AFTER_SECS: u64 = 30;
pub fn default_max_handlers() -> usize {
let cores = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
(2 * cores).clamp(8, 256)
}
pub const DEFAULT_HANDLER_TIMEOUT_MS: u64 = 30_000;
pub const DEFAULT_RETRY_AFTER_SECS: u64 = 5;
pub fn validate_max_handlers(value: usize) -> Result<usize, String> {
if value == 0 {
return Err("http max_handlers must be >= 1".to_string());
}
Ok(value)
}
pub fn validate_handler_timeout_ms(value: u64) -> Result<u64, String> {
if value < MIN_HANDLER_TIMEOUT_MS {
return Err(format!(
"http handler_timeout_ms must be >= {MIN_HANDLER_TIMEOUT_MS}"
));
}
Ok(value)
}
pub fn validate_retry_after_secs(value: u64) -> Result<u64, String> {
if !(MIN_RETRY_AFTER_SECS..=MAX_RETRY_AFTER_SECS).contains(&value) {
return Err(format!(
"http retry_after_secs must be in [{MIN_RETRY_AFTER_SECS}, {MAX_RETRY_AFTER_SECS}]"
));
}
Ok(value)
}
#[derive(Debug, Default, Clone)]
pub struct HttpLimitsCliInput {
pub max_handlers_flag: Option<usize>,
pub max_handlers_env: Option<usize>,
pub handler_timeout_ms_flag: Option<u64>,
pub handler_timeout_ms_env: Option<u64>,
pub retry_after_secs_flag: Option<u64>,
pub retry_after_secs_env: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HttpLimitsResolved {
pub max_handlers: usize,
pub handler_timeout_ms: u64,
pub retry_after_secs: u64,
}
impl HttpLimitsResolved {
pub fn builtin_defaults() -> Self {
Self {
max_handlers: default_max_handlers(),
handler_timeout_ms: DEFAULT_HANDLER_TIMEOUT_MS,
retry_after_secs: DEFAULT_RETRY_AFTER_SECS,
}
}
}
pub fn resolve_http_limits<F>(input: &HttpLimitsCliInput, config_lookup: F) -> HttpLimitsResolved
where
F: Fn(&str) -> Option<String>,
{
let defaults = HttpLimitsResolved::builtin_defaults();
let max_handlers = input
.max_handlers_flag
.or_else(|| {
config_lookup("red.http.max_handlers")
.and_then(|raw| raw.parse::<usize>().ok())
.and_then(|v| validate_max_handlers(v).ok())
})
.or(input.max_handlers_env)
.unwrap_or(defaults.max_handlers);
let handler_timeout_ms = input
.handler_timeout_ms_flag
.or_else(|| {
config_lookup("red.http.handler_timeout_ms")
.and_then(|raw| raw.parse::<u64>().ok())
.and_then(|v| validate_handler_timeout_ms(v).ok())
})
.or(input.handler_timeout_ms_env)
.unwrap_or(defaults.handler_timeout_ms);
let retry_after_secs = input
.retry_after_secs_flag
.or_else(|| {
config_lookup("red.http.retry_after_secs")
.and_then(|raw| raw.parse::<u64>().ok())
.and_then(|v| validate_retry_after_secs(v).ok())
})
.or(input.retry_after_secs_env)
.unwrap_or(defaults.retry_after_secs);
HttpLimitsResolved {
max_handlers,
handler_timeout_ms,
retry_after_secs,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn no_config() -> impl Fn(&str) -> Option<String> {
|_| None
}
fn map_lookup(map: HashMap<&'static str, &'static str>) -> impl Fn(&str) -> Option<String> {
move |key| map.get(key).map(|v| v.to_string())
}
#[test]
fn defaults_when_nothing_set() {
let resolved = resolve_http_limits(&HttpLimitsCliInput::default(), no_config());
assert_eq!(resolved, HttpLimitsResolved::builtin_defaults());
}
#[test]
fn flag_wins_over_env_and_default() {
let input = HttpLimitsCliInput {
max_handlers_flag: Some(16),
max_handlers_env: Some(99),
handler_timeout_ms_flag: Some(5_000),
handler_timeout_ms_env: Some(7_000),
retry_after_secs_flag: Some(3),
retry_after_secs_env: Some(7),
..Default::default()
};
let resolved = resolve_http_limits(&input, no_config());
assert_eq!(resolved.max_handlers, 16);
assert_eq!(resolved.handler_timeout_ms, 5_000);
assert_eq!(resolved.retry_after_secs, 3);
}
#[test]
fn flag_wins_over_red_config() {
let input = HttpLimitsCliInput {
max_handlers_flag: Some(16),
handler_timeout_ms_flag: Some(5_000),
retry_after_secs_flag: Some(3),
..Default::default()
};
let lookup = map_lookup(HashMap::from([
("red.http.max_handlers", "64"),
("red.http.handler_timeout_ms", "9000"),
("red.http.retry_after_secs", "9"),
]));
let resolved = resolve_http_limits(&input, lookup);
assert_eq!(resolved.max_handlers, 16);
assert_eq!(resolved.handler_timeout_ms, 5_000);
assert_eq!(resolved.retry_after_secs, 3);
}
#[test]
fn red_config_wins_over_env() {
let input = HttpLimitsCliInput {
max_handlers_env: Some(99),
handler_timeout_ms_env: Some(7_000),
retry_after_secs_env: Some(7),
..Default::default()
};
let lookup = map_lookup(HashMap::from([
("red.http.max_handlers", "64"),
("red.http.handler_timeout_ms", "9000"),
("red.http.retry_after_secs", "9"),
]));
let resolved = resolve_http_limits(&input, lookup);
assert_eq!(resolved.max_handlers, 64);
assert_eq!(resolved.handler_timeout_ms, 9_000);
assert_eq!(resolved.retry_after_secs, 9);
}
#[test]
fn env_wins_over_default() {
let input = HttpLimitsCliInput {
max_handlers_env: Some(11),
handler_timeout_ms_env: Some(1_500),
retry_after_secs_env: Some(2),
..Default::default()
};
let resolved = resolve_http_limits(&input, no_config());
assert_eq!(resolved.max_handlers, 11);
assert_eq!(resolved.handler_timeout_ms, 1_500);
assert_eq!(resolved.retry_after_secs, 2);
}
#[test]
fn invalid_red_config_is_ignored_in_favor_of_lower_layers() {
let input = HttpLimitsCliInput {
max_handlers_env: Some(11),
..Default::default()
};
let lookup = map_lookup(HashMap::from([
("red.http.max_handlers", "0"), ("red.http.handler_timeout_ms", "5"), ("red.http.retry_after_secs", "9999"), ]));
let resolved = resolve_http_limits(&input, lookup);
assert_eq!(resolved.max_handlers, 11);
assert_eq!(
resolved.handler_timeout_ms,
DEFAULT_HANDLER_TIMEOUT_MS
);
assert_eq!(resolved.retry_after_secs, DEFAULT_RETRY_AFTER_SECS);
}
#[test]
fn validators_reject_zero_equivalent_values() {
assert!(validate_max_handlers(0).is_err());
assert!(validate_max_handlers(1).is_ok());
assert!(validate_handler_timeout_ms(0).is_err());
assert!(validate_handler_timeout_ms(MIN_HANDLER_TIMEOUT_MS - 1).is_err());
assert!(validate_handler_timeout_ms(MIN_HANDLER_TIMEOUT_MS).is_ok());
assert!(validate_retry_after_secs(0).is_err());
assert!(validate_retry_after_secs(MIN_RETRY_AFTER_SECS).is_ok());
assert!(validate_retry_after_secs(MAX_RETRY_AFTER_SECS).is_ok());
assert!(validate_retry_after_secs(MAX_RETRY_AFTER_SECS + 1).is_err());
}
#[test]
fn default_max_handlers_in_bounds() {
let cap = default_max_handlers();
assert!((8..=256).contains(&cap));
}
}