1pub const MIN_HANDLER_TIMEOUT_MS: u64 = 100;
20pub const MIN_RETRY_AFTER_SECS: u64 = 1;
24pub const MAX_RETRY_AFTER_SECS: u64 = 30;
25
26pub fn default_max_handlers() -> usize {
29 let cores = std::thread::available_parallelism()
30 .map(|n| n.get())
31 .unwrap_or(1);
32 (2 * cores).clamp(8, 256)
33}
34
35pub const DEFAULT_HANDLER_TIMEOUT_MS: u64 = 30_000;
36pub const DEFAULT_RETRY_AFTER_SECS: u64 = 5;
37
38pub const DEFAULT_MAX_INFLIGHT_PER_PRINCIPAL: usize = 64;
47
48pub fn validate_max_handlers(value: usize) -> Result<usize, String> {
51 if value == 0 {
52 return Err("http max_handlers must be >= 1".to_string());
53 }
54 Ok(value)
55}
56
57pub fn validate_max_inflight_per_principal(value: usize) -> Result<usize, String> {
63 Ok(value)
64}
65
66pub fn validate_handler_timeout_ms(value: u64) -> Result<u64, String> {
67 if value < MIN_HANDLER_TIMEOUT_MS {
68 return Err(format!(
69 "http handler_timeout_ms must be >= {MIN_HANDLER_TIMEOUT_MS}"
70 ));
71 }
72 Ok(value)
73}
74
75pub fn validate_retry_after_secs(value: u64) -> Result<u64, String> {
76 if !(MIN_RETRY_AFTER_SECS..=MAX_RETRY_AFTER_SECS).contains(&value) {
77 return Err(format!(
78 "http retry_after_secs must be in [{MIN_RETRY_AFTER_SECS}, {MAX_RETRY_AFTER_SECS}]"
79 ));
80 }
81 Ok(value)
82}
83
84#[derive(Debug, Default, Clone)]
89pub struct HttpLimitsCliInput {
90 pub max_handlers_flag: Option<usize>,
91 pub max_handlers_env: Option<usize>,
92 pub handler_timeout_ms_flag: Option<u64>,
93 pub handler_timeout_ms_env: Option<u64>,
94 pub retry_after_secs_flag: Option<u64>,
95 pub retry_after_secs_env: Option<u64>,
96 pub max_inflight_per_principal_flag: Option<usize>,
97 pub max_inflight_per_principal_env: Option<usize>,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub struct HttpLimitsResolved {
104 pub max_handlers: usize,
105 pub handler_timeout_ms: u64,
106 pub retry_after_secs: u64,
107 pub max_inflight_per_principal: usize,
109}
110
111impl HttpLimitsResolved {
112 pub fn builtin_defaults() -> Self {
113 Self {
114 max_handlers: default_max_handlers(),
115 handler_timeout_ms: DEFAULT_HANDLER_TIMEOUT_MS,
116 retry_after_secs: DEFAULT_RETRY_AFTER_SECS,
117 max_inflight_per_principal: DEFAULT_MAX_INFLIGHT_PER_PRINCIPAL,
118 }
119 }
120}
121
122pub fn resolve_http_limits<F>(input: &HttpLimitsCliInput, config_lookup: F) -> HttpLimitsResolved
129where
130 F: Fn(&str) -> Option<String>,
131{
132 let defaults = HttpLimitsResolved::builtin_defaults();
133
134 let max_handlers = input
135 .max_handlers_flag
136 .or_else(|| {
137 config_lookup("red.http.max_handlers")
138 .and_then(|raw| raw.parse::<usize>().ok())
139 .and_then(|v| validate_max_handlers(v).ok())
140 })
141 .or(input.max_handlers_env)
142 .unwrap_or(defaults.max_handlers);
143
144 let handler_timeout_ms = input
145 .handler_timeout_ms_flag
146 .or_else(|| {
147 config_lookup("red.http.handler_timeout_ms")
148 .and_then(|raw| raw.parse::<u64>().ok())
149 .and_then(|v| validate_handler_timeout_ms(v).ok())
150 })
151 .or(input.handler_timeout_ms_env)
152 .unwrap_or(defaults.handler_timeout_ms);
153
154 let retry_after_secs = input
155 .retry_after_secs_flag
156 .or_else(|| {
157 config_lookup("red.http.retry_after_secs")
158 .and_then(|raw| raw.parse::<u64>().ok())
159 .and_then(|v| validate_retry_after_secs(v).ok())
160 })
161 .or(input.retry_after_secs_env)
162 .unwrap_or(defaults.retry_after_secs);
163
164 let max_inflight_per_principal = input
165 .max_inflight_per_principal_flag
166 .or_else(|| {
167 config_lookup("red.http.max_inflight_per_principal")
168 .and_then(|raw| raw.parse::<usize>().ok())
169 .and_then(|v| validate_max_inflight_per_principal(v).ok())
170 })
171 .or(input.max_inflight_per_principal_env)
172 .unwrap_or(defaults.max_inflight_per_principal);
173
174 HttpLimitsResolved {
175 max_handlers,
176 handler_timeout_ms,
177 retry_after_secs,
178 max_inflight_per_principal,
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use std::collections::HashMap;
186
187 fn no_config() -> impl Fn(&str) -> Option<String> {
188 |_| None
189 }
190
191 fn map_lookup(map: HashMap<&'static str, &'static str>) -> impl Fn(&str) -> Option<String> {
192 move |key| map.get(key).map(|v| v.to_string())
193 }
194
195 #[test]
196 fn defaults_when_nothing_set() {
197 let resolved = resolve_http_limits(&HttpLimitsCliInput::default(), no_config());
198 assert_eq!(resolved, HttpLimitsResolved::builtin_defaults());
199 }
200
201 #[test]
202 fn flag_wins_over_env_and_default() {
203 let input = HttpLimitsCliInput {
204 max_handlers_flag: Some(16),
205 max_handlers_env: Some(99),
206 handler_timeout_ms_flag: Some(5_000),
207 handler_timeout_ms_env: Some(7_000),
208 retry_after_secs_flag: Some(3),
209 retry_after_secs_env: Some(7),
210 ..Default::default()
211 };
212 let resolved = resolve_http_limits(&input, no_config());
213 assert_eq!(resolved.max_handlers, 16);
214 assert_eq!(resolved.handler_timeout_ms, 5_000);
215 assert_eq!(resolved.retry_after_secs, 3);
216 }
217
218 #[test]
219 fn flag_wins_over_red_config() {
220 let input = HttpLimitsCliInput {
221 max_handlers_flag: Some(16),
222 handler_timeout_ms_flag: Some(5_000),
223 retry_after_secs_flag: Some(3),
224 ..Default::default()
225 };
226 let lookup = map_lookup(HashMap::from([
227 ("red.http.max_handlers", "64"),
228 ("red.http.handler_timeout_ms", "9000"),
229 ("red.http.retry_after_secs", "9"),
230 ]));
231 let resolved = resolve_http_limits(&input, lookup);
232 assert_eq!(resolved.max_handlers, 16);
233 assert_eq!(resolved.handler_timeout_ms, 5_000);
234 assert_eq!(resolved.retry_after_secs, 3);
235 }
236
237 #[test]
238 fn red_config_wins_over_env() {
239 let input = HttpLimitsCliInput {
240 max_handlers_env: Some(99),
241 handler_timeout_ms_env: Some(7_000),
242 retry_after_secs_env: Some(7),
243 ..Default::default()
244 };
245 let lookup = map_lookup(HashMap::from([
246 ("red.http.max_handlers", "64"),
247 ("red.http.handler_timeout_ms", "9000"),
248 ("red.http.retry_after_secs", "9"),
249 ]));
250 let resolved = resolve_http_limits(&input, lookup);
251 assert_eq!(resolved.max_handlers, 64);
252 assert_eq!(resolved.handler_timeout_ms, 9_000);
253 assert_eq!(resolved.retry_after_secs, 9);
254 }
255
256 #[test]
257 fn env_wins_over_default() {
258 let input = HttpLimitsCliInput {
259 max_handlers_env: Some(11),
260 handler_timeout_ms_env: Some(1_500),
261 retry_after_secs_env: Some(2),
262 ..Default::default()
263 };
264 let resolved = resolve_http_limits(&input, no_config());
265 assert_eq!(resolved.max_handlers, 11);
266 assert_eq!(resolved.handler_timeout_ms, 1_500);
267 assert_eq!(resolved.retry_after_secs, 2);
268 }
269
270 #[test]
271 fn invalid_red_config_is_ignored_in_favor_of_lower_layers() {
272 let input = HttpLimitsCliInput {
275 max_handlers_env: Some(11),
276 ..Default::default()
277 };
278 let lookup = map_lookup(HashMap::from([
279 ("red.http.max_handlers", "0"), ("red.http.handler_timeout_ms", "5"), ("red.http.retry_after_secs", "9999"), ]));
283 let resolved = resolve_http_limits(&input, lookup);
284 assert_eq!(resolved.max_handlers, 11);
286 assert_eq!(resolved.handler_timeout_ms, DEFAULT_HANDLER_TIMEOUT_MS);
288 assert_eq!(resolved.retry_after_secs, DEFAULT_RETRY_AFTER_SECS);
290 }
291
292 #[test]
293 fn validators_reject_zero_equivalent_values() {
294 assert!(validate_max_handlers(0).is_err());
295 assert!(validate_max_handlers(1).is_ok());
296
297 assert!(validate_handler_timeout_ms(0).is_err());
298 assert!(validate_handler_timeout_ms(MIN_HANDLER_TIMEOUT_MS - 1).is_err());
299 assert!(validate_handler_timeout_ms(MIN_HANDLER_TIMEOUT_MS).is_ok());
300
301 assert!(validate_retry_after_secs(0).is_err());
302 assert!(validate_retry_after_secs(MIN_RETRY_AFTER_SECS).is_ok());
303 assert!(validate_retry_after_secs(MAX_RETRY_AFTER_SECS).is_ok());
304 assert!(validate_retry_after_secs(MAX_RETRY_AFTER_SECS + 1).is_err());
305 }
306
307 #[test]
308 fn default_max_handlers_in_bounds() {
309 let cap = default_max_handlers();
310 assert!((8..=256).contains(&cap));
311 }
312
313 #[test]
314 fn max_inflight_per_principal_follows_precedence_chain() {
315 let resolved = resolve_http_limits(&HttpLimitsCliInput::default(), no_config());
317 assert_eq!(
318 resolved.max_inflight_per_principal,
319 DEFAULT_MAX_INFLIGHT_PER_PRINCIPAL
320 );
321
322 let input = HttpLimitsCliInput {
324 max_inflight_per_principal_env: Some(17),
325 ..Default::default()
326 };
327 assert_eq!(
328 resolve_http_limits(&input, no_config()).max_inflight_per_principal,
329 17
330 );
331
332 let input = HttpLimitsCliInput {
334 max_inflight_per_principal_env: Some(17),
335 ..Default::default()
336 };
337 let lookup = map_lookup(HashMap::from([(
338 "red.http.max_inflight_per_principal",
339 "9",
340 )]));
341 assert_eq!(
342 resolve_http_limits(&input, lookup).max_inflight_per_principal,
343 9
344 );
345
346 let input = HttpLimitsCliInput {
348 max_inflight_per_principal_flag: Some(3),
349 max_inflight_per_principal_env: Some(17),
350 ..Default::default()
351 };
352 let lookup = map_lookup(HashMap::from([(
353 "red.http.max_inflight_per_principal",
354 "9",
355 )]));
356 assert_eq!(
357 resolve_http_limits(&input, lookup).max_inflight_per_principal,
358 3
359 );
360 }
361
362 #[test]
363 fn max_inflight_per_principal_zero_disables_and_is_honored() {
364 let lookup = map_lookup(HashMap::from([(
367 "red.http.max_inflight_per_principal",
368 "0",
369 )]));
370 assert_eq!(
371 resolve_http_limits(&HttpLimitsCliInput::default(), lookup).max_inflight_per_principal,
372 0
373 );
374 assert!(validate_max_inflight_per_principal(0).is_ok());
375 }
376}