reddb_server/server/
http_limits.rs1pub const MIN_HANDLER_TIMEOUT_MS: u64 = 100;
19pub const MIN_RETRY_AFTER_SECS: u64 = 1;
23pub const MAX_RETRY_AFTER_SECS: u64 = 30;
24
25pub 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
37pub 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#[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#[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
97pub 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 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"), ("red.http.handler_timeout_ms", "5"), ("red.http.retry_after_secs", "9999"), ]));
247 let resolved = resolve_http_limits(&input, lookup);
248 assert_eq!(resolved.max_handlers, 11);
250 assert_eq!(
252 resolved.handler_timeout_ms,
253 DEFAULT_HANDLER_TIMEOUT_MS
254 );
255 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}