Skip to main content

reddb_server/server/
http_limits.rs

1//! Resolution of the three HTTP handler-pool knobs (issue #574 slice 5).
2//!
3//! The values are configurable through the standard precedence chain
4//! used elsewhere in the boot path:
5//!
6//!   flag > red_config > env > built-in default
7//!
8//! Built-in defaults reproduce the hard-coded values from slices 1+2:
9//!   - max_handlers      = (2 * num_cpus).clamp(8, 256)
10//!   - handler_timeout   = 30_000 ms
11//!   - retry_after_secs  = 5
12//!
13//! Each knob is validated at parse time and at resolution time so a
14//! stale red_config value cannot corrupt the running server.
15
16/// Lower bound for `handler_timeout_ms`. Anything below this is so
17/// short the deadline trips on healthy requests; we reject the value.
18pub const MIN_HANDLER_TIMEOUT_MS: u64 = 100;
19/// Inclusive bounds for `retry_after_secs`. Below 1s means clients
20/// hammer the server; above 30s means a transient overload looks like
21/// a permanent outage to load balancers.
22pub const MIN_RETRY_AFTER_SECS: u64 = 1;
23pub const MAX_RETRY_AFTER_SECS: u64 = 30;
24
25/// Built-in default for `max_handlers`. Matches
26/// `HttpConnectionLimiter::with_default_cap`.
27pub fn default_max_handlers() -> usize {
28    let cores = std::thread::available_parallelism()
29        .map(|n| n.get())
30        .unwrap_or(1);
31    (2 * cores).clamp(8, 256)
32}
33
34pub const DEFAULT_HANDLER_TIMEOUT_MS: u64 = 30_000;
35pub const DEFAULT_RETRY_AFTER_SECS: u64 = 5;
36
37/// Validate a `max_handlers` candidate from any source. Returns the
38/// value unchanged on success.
39pub fn validate_max_handlers(value: usize) -> Result<usize, String> {
40    if value == 0 {
41        return Err("http max_handlers must be >= 1".to_string());
42    }
43    Ok(value)
44}
45
46pub fn validate_handler_timeout_ms(value: u64) -> Result<u64, String> {
47    if value < MIN_HANDLER_TIMEOUT_MS {
48        return Err(format!(
49            "http handler_timeout_ms must be >= {MIN_HANDLER_TIMEOUT_MS}"
50        ));
51    }
52    Ok(value)
53}
54
55pub fn validate_retry_after_secs(value: u64) -> Result<u64, String> {
56    if !(MIN_RETRY_AFTER_SECS..=MAX_RETRY_AFTER_SECS).contains(&value) {
57        return Err(format!(
58            "http retry_after_secs must be in [{MIN_RETRY_AFTER_SECS}, {MAX_RETRY_AFTER_SECS}]"
59        ));
60    }
61    Ok(value)
62}
63
64/// CLI-layer input. Each pair holds the already-validated value coming
65/// from a flag and from an env var, respectively. The resolver applies
66/// the `flag > red_config > env > default` precedence using these
67/// inputs plus a config-store lookup.
68#[derive(Debug, Default, Clone)]
69pub struct HttpLimitsCliInput {
70    pub max_handlers_flag: Option<usize>,
71    pub max_handlers_env: Option<usize>,
72    pub handler_timeout_ms_flag: Option<u64>,
73    pub handler_timeout_ms_env: Option<u64>,
74    pub retry_after_secs_flag: Option<u64>,
75    pub retry_after_secs_env: Option<u64>,
76}
77
78/// Resolved values after applying the full precedence chain. Stamped
79/// into both the `RedDBServer` and the startup log line.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct HttpLimitsResolved {
82    pub max_handlers: usize,
83    pub handler_timeout_ms: u64,
84    pub retry_after_secs: u64,
85}
86
87impl HttpLimitsResolved {
88    pub fn builtin_defaults() -> Self {
89        Self {
90            max_handlers: default_max_handlers(),
91            handler_timeout_ms: DEFAULT_HANDLER_TIMEOUT_MS,
92            retry_after_secs: DEFAULT_RETRY_AFTER_SECS,
93        }
94    }
95}
96
97/// Apply the `flag > red_config > env > default` chain.
98///
99/// `config_lookup` is a closure so this function is independent of the
100/// runtime/config-store type — keeps the resolver pure and testable.
101/// Each lookup returns the raw text value stored under the given key,
102/// matching how `set_config_tree` persists scalars.
103pub fn resolve_http_limits<F>(input: &HttpLimitsCliInput, config_lookup: F) -> HttpLimitsResolved
104where
105    F: Fn(&str) -> Option<String>,
106{
107    let defaults = HttpLimitsResolved::builtin_defaults();
108
109    let max_handlers = input
110        .max_handlers_flag
111        .or_else(|| {
112            config_lookup("red.http.max_handlers")
113                .and_then(|raw| raw.parse::<usize>().ok())
114                .and_then(|v| validate_max_handlers(v).ok())
115        })
116        .or(input.max_handlers_env)
117        .unwrap_or(defaults.max_handlers);
118
119    let handler_timeout_ms = input
120        .handler_timeout_ms_flag
121        .or_else(|| {
122            config_lookup("red.http.handler_timeout_ms")
123                .and_then(|raw| raw.parse::<u64>().ok())
124                .and_then(|v| validate_handler_timeout_ms(v).ok())
125        })
126        .or(input.handler_timeout_ms_env)
127        .unwrap_or(defaults.handler_timeout_ms);
128
129    let retry_after_secs = input
130        .retry_after_secs_flag
131        .or_else(|| {
132            config_lookup("red.http.retry_after_secs")
133                .and_then(|raw| raw.parse::<u64>().ok())
134                .and_then(|v| validate_retry_after_secs(v).ok())
135        })
136        .or(input.retry_after_secs_env)
137        .unwrap_or(defaults.retry_after_secs);
138
139    HttpLimitsResolved {
140        max_handlers,
141        handler_timeout_ms,
142        retry_after_secs,
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use std::collections::HashMap;
150
151    fn no_config() -> impl Fn(&str) -> Option<String> {
152        |_| None
153    }
154
155    fn map_lookup(map: HashMap<&'static str, &'static str>) -> impl Fn(&str) -> Option<String> {
156        move |key| map.get(key).map(|v| v.to_string())
157    }
158
159    #[test]
160    fn defaults_when_nothing_set() {
161        let resolved = resolve_http_limits(&HttpLimitsCliInput::default(), no_config());
162        assert_eq!(resolved, HttpLimitsResolved::builtin_defaults());
163    }
164
165    #[test]
166    fn flag_wins_over_env_and_default() {
167        let input = HttpLimitsCliInput {
168            max_handlers_flag: Some(16),
169            max_handlers_env: Some(99),
170            handler_timeout_ms_flag: Some(5_000),
171            handler_timeout_ms_env: Some(7_000),
172            retry_after_secs_flag: Some(3),
173            retry_after_secs_env: Some(7),
174            ..Default::default()
175        };
176        let resolved = resolve_http_limits(&input, no_config());
177        assert_eq!(resolved.max_handlers, 16);
178        assert_eq!(resolved.handler_timeout_ms, 5_000);
179        assert_eq!(resolved.retry_after_secs, 3);
180    }
181
182    #[test]
183    fn flag_wins_over_red_config() {
184        let input = HttpLimitsCliInput {
185            max_handlers_flag: Some(16),
186            handler_timeout_ms_flag: Some(5_000),
187            retry_after_secs_flag: Some(3),
188            ..Default::default()
189        };
190        let lookup = map_lookup(HashMap::from([
191            ("red.http.max_handlers", "64"),
192            ("red.http.handler_timeout_ms", "9000"),
193            ("red.http.retry_after_secs", "9"),
194        ]));
195        let resolved = resolve_http_limits(&input, lookup);
196        assert_eq!(resolved.max_handlers, 16);
197        assert_eq!(resolved.handler_timeout_ms, 5_000);
198        assert_eq!(resolved.retry_after_secs, 3);
199    }
200
201    #[test]
202    fn red_config_wins_over_env() {
203        let input = HttpLimitsCliInput {
204            max_handlers_env: Some(99),
205            handler_timeout_ms_env: Some(7_000),
206            retry_after_secs_env: Some(7),
207            ..Default::default()
208        };
209        let lookup = map_lookup(HashMap::from([
210            ("red.http.max_handlers", "64"),
211            ("red.http.handler_timeout_ms", "9000"),
212            ("red.http.retry_after_secs", "9"),
213        ]));
214        let resolved = resolve_http_limits(&input, lookup);
215        assert_eq!(resolved.max_handlers, 64);
216        assert_eq!(resolved.handler_timeout_ms, 9_000);
217        assert_eq!(resolved.retry_after_secs, 9);
218    }
219
220    #[test]
221    fn env_wins_over_default() {
222        let input = HttpLimitsCliInput {
223            max_handlers_env: Some(11),
224            handler_timeout_ms_env: Some(1_500),
225            retry_after_secs_env: Some(2),
226            ..Default::default()
227        };
228        let resolved = resolve_http_limits(&input, no_config());
229        assert_eq!(resolved.max_handlers, 11);
230        assert_eq!(resolved.handler_timeout_ms, 1_500);
231        assert_eq!(resolved.retry_after_secs, 2);
232    }
233
234    #[test]
235    fn invalid_red_config_is_ignored_in_favor_of_lower_layers() {
236        // Garbage in red_config — must not break boot. Env value wins;
237        // if env is absent, default wins.
238        let input = HttpLimitsCliInput {
239            max_handlers_env: Some(11),
240            ..Default::default()
241        };
242        let lookup = map_lookup(HashMap::from([
243            ("red.http.max_handlers", "0"), // rejected by validate
244            ("red.http.handler_timeout_ms", "5"), // rejected by validate
245            ("red.http.retry_after_secs", "9999"), // rejected by validate
246        ]));
247        let resolved = resolve_http_limits(&input, lookup);
248        // max_handlers: red_config invalid -> env (11)
249        assert_eq!(resolved.max_handlers, 11);
250        // handler_timeout_ms: red_config invalid, no env -> default
251        assert_eq!(
252            resolved.handler_timeout_ms,
253            DEFAULT_HANDLER_TIMEOUT_MS
254        );
255        // retry_after_secs: red_config invalid, no env -> default
256        assert_eq!(resolved.retry_after_secs, DEFAULT_RETRY_AFTER_SECS);
257    }
258
259    #[test]
260    fn validators_reject_zero_equivalent_values() {
261        assert!(validate_max_handlers(0).is_err());
262        assert!(validate_max_handlers(1).is_ok());
263
264        assert!(validate_handler_timeout_ms(0).is_err());
265        assert!(validate_handler_timeout_ms(MIN_HANDLER_TIMEOUT_MS - 1).is_err());
266        assert!(validate_handler_timeout_ms(MIN_HANDLER_TIMEOUT_MS).is_ok());
267
268        assert!(validate_retry_after_secs(0).is_err());
269        assert!(validate_retry_after_secs(MIN_RETRY_AFTER_SECS).is_ok());
270        assert!(validate_retry_after_secs(MAX_RETRY_AFTER_SECS).is_ok());
271        assert!(validate_retry_after_secs(MAX_RETRY_AFTER_SECS + 1).is_err());
272    }
273
274    #[test]
275    fn default_max_handlers_in_bounds() {
276        let cap = default_max_handlers();
277        assert!((8..=256).contains(&cap));
278    }
279}