1use anyhow::{Result, anyhow};
2use clap::Parser;
3use serde::Deserialize;
4
5#[derive(Debug, Clone, Deserialize)]
6pub struct Config {
7 pub transports: TransportConfig,
8 pub store: StoreConfig,
9 pub buffer_size: usize,
10 pub log_level: String,
11}
12
13#[derive(Debug, Clone, Deserialize)]
14pub struct TransportConfig {
15 pub http: Option<HttpConfig>,
16 pub grpc: Option<GrpcConfig>,
17 pub native: Option<NativeConfig>,
18}
19
20#[derive(Debug, Clone, Deserialize)]
21pub struct HttpConfig {
22 pub host: String,
23 pub port: u16,
24}
25
26#[derive(Debug, Clone, Deserialize)]
27pub struct GrpcConfig {
28 pub host: String,
29 pub port: u16,
30}
31
32#[derive(Debug, Clone, Deserialize)]
33pub struct NativeConfig {
34 pub host: String,
35 pub port: u16,
36}
37
38#[derive(Debug, Clone, Deserialize)]
39pub struct StoreConfig {
40 pub store_type: StoreType,
41 pub capacity: usize,
42 pub cleanup_interval: u64, pub cleanup_probability: u64, pub min_interval: u64, pub max_interval: u64, pub max_operations: usize, }
49
50#[derive(Debug, Clone, Copy, Deserialize, PartialEq)]
51#[serde(rename_all = "lowercase")]
52pub enum StoreType {
53 Periodic,
54 Probabilistic,
55 Adaptive,
56}
57
58impl std::str::FromStr for StoreType {
59 type Err = anyhow::Error;
60
61 fn from_str(s: &str) -> Result<Self> {
62 match s.to_lowercase().as_str() {
63 "periodic" => Ok(StoreType::Periodic),
64 "probabilistic" => Ok(StoreType::Probabilistic),
65 "adaptive" => Ok(StoreType::Adaptive),
66 _ => Err(anyhow!(
67 "Invalid store type: {}. Valid options are: periodic, probabilistic, adaptive",
68 s
69 )),
70 }
71 }
72}
73
74#[derive(Parser, Debug)]
75#[command(
76 name = "throttlecrab-server",
77 about = "High-performance rate limiting server",
78 long_about = "A high-performance rate limiting server with multiple protocol support.\n\nAt least one transport must be specified.\n\nEnvironment variables with THROTTLECRAB_ prefix are supported. CLI arguments take precedence over environment variables."
79)]
80pub struct Args {
81 #[arg(long, help = "Enable HTTP transport", env = "THROTTLECRAB_HTTP")]
83 pub http: bool,
84 #[arg(
85 long,
86 value_name = "HOST",
87 help = "HTTP host",
88 default_value = "127.0.0.1",
89 env = "THROTTLECRAB_HTTP_HOST"
90 )]
91 pub http_host: String,
92 #[arg(
93 long,
94 value_name = "PORT",
95 help = "HTTP port",
96 default_value_t = 8080,
97 env = "THROTTLECRAB_HTTP_PORT"
98 )]
99 pub http_port: u16,
100
101 #[arg(long, help = "Enable gRPC transport", env = "THROTTLECRAB_GRPC")]
103 pub grpc: bool,
104 #[arg(
105 long,
106 value_name = "HOST",
107 help = "gRPC host",
108 default_value = "127.0.0.1",
109 env = "THROTTLECRAB_GRPC_HOST"
110 )]
111 pub grpc_host: String,
112 #[arg(
113 long,
114 value_name = "PORT",
115 help = "gRPC port",
116 default_value_t = 8070,
117 env = "THROTTLECRAB_GRPC_PORT"
118 )]
119 pub grpc_port: u16,
120
121 #[arg(long, help = "Enable Native transport", env = "THROTTLECRAB_NATIVE")]
123 pub native: bool,
124 #[arg(
125 long,
126 value_name = "HOST",
127 help = "Native host",
128 default_value = "127.0.0.1",
129 env = "THROTTLECRAB_NATIVE_HOST"
130 )]
131 pub native_host: String,
132 #[arg(
133 long,
134 value_name = "PORT",
135 help = "Native port",
136 default_value_t = 8072,
137 env = "THROTTLECRAB_NATIVE_PORT"
138 )]
139 pub native_port: u16,
140
141 #[arg(
143 long,
144 value_name = "TYPE",
145 help = "Store type: periodic, probabilistic, adaptive",
146 default_value = "periodic",
147 env = "THROTTLECRAB_STORE"
148 )]
149 pub store: StoreType,
150 #[arg(
151 long,
152 value_name = "SIZE",
153 help = "Initial store capacity",
154 default_value_t = 100_000,
155 env = "THROTTLECRAB_STORE_CAPACITY"
156 )]
157 pub store_capacity: usize,
158
159 #[arg(
161 long,
162 value_name = "SECS",
163 help = "Cleanup interval for periodic store (seconds)",
164 default_value_t = 300,
165 env = "THROTTLECRAB_STORE_CLEANUP_INTERVAL"
166 )]
167 pub store_cleanup_interval: u64,
168 #[arg(
169 long,
170 value_name = "N",
171 help = "Cleanup probability for probabilistic store (1 in N)",
172 default_value_t = 10_000,
173 env = "THROTTLECRAB_STORE_CLEANUP_PROBABILITY"
174 )]
175 pub store_cleanup_probability: u64,
176 #[arg(
177 long,
178 value_name = "SECS",
179 help = "Minimum cleanup interval for adaptive store (seconds)",
180 default_value_t = 5,
181 env = "THROTTLECRAB_STORE_MIN_INTERVAL"
182 )]
183 pub store_min_interval: u64,
184 #[arg(
185 long,
186 value_name = "SECS",
187 help = "Maximum cleanup interval for adaptive store (seconds)",
188 default_value_t = 300,
189 env = "THROTTLECRAB_STORE_MAX_INTERVAL"
190 )]
191 pub store_max_interval: u64,
192 #[arg(
193 long,
194 value_name = "N",
195 help = "Maximum operations before cleanup for adaptive store",
196 default_value_t = 1_000_000,
197 env = "THROTTLECRAB_STORE_MAX_OPERATIONS"
198 )]
199 pub store_max_operations: usize,
200
201 #[arg(
203 long,
204 value_name = "SIZE",
205 help = "Channel buffer size",
206 default_value_t = 100_000,
207 env = "THROTTLECRAB_BUFFER_SIZE"
208 )]
209 pub buffer_size: usize,
210 #[arg(
211 long,
212 value_name = "LEVEL",
213 help = "Log level: error, warn, info, debug, trace",
214 default_value = "info",
215 env = "THROTTLECRAB_LOG_LEVEL"
216 )]
217 pub log_level: String,
218
219 #[arg(
221 long,
222 help = "List all environment variables and exit",
223 action = clap::ArgAction::SetTrue
224 )]
225 pub list_env_vars: bool,
226}
227
228impl Config {
229 pub fn from_env_and_args() -> Result<Self> {
230 let args = Args::parse();
235
236 if args.list_env_vars {
238 Self::print_env_vars();
239 std::process::exit(0);
240 }
241
242 let mut config = Config {
244 transports: TransportConfig {
245 http: None,
246 grpc: None,
247 native: None,
248 },
249 store: StoreConfig {
250 store_type: args.store,
251 capacity: args.store_capacity,
252 cleanup_interval: args.store_cleanup_interval,
253 cleanup_probability: args.store_cleanup_probability,
254 min_interval: args.store_min_interval,
255 max_interval: args.store_max_interval,
256 max_operations: args.store_max_operations,
257 },
258 buffer_size: args.buffer_size,
259 log_level: args.log_level,
260 };
261
262 if args.http {
264 config.transports.http = Some(HttpConfig {
265 host: args.http_host,
266 port: args.http_port,
267 });
268 }
269
270 if args.grpc {
271 config.transports.grpc = Some(GrpcConfig {
272 host: args.grpc_host,
273 port: args.grpc_port,
274 });
275 }
276
277 if args.native {
278 config.transports.native = Some(NativeConfig {
279 host: args.native_host,
280 port: args.native_port,
281 });
282 }
283
284 config.validate()?;
286
287 Ok(config)
288 }
289
290 pub fn has_any_transport(&self) -> bool {
291 self.transports.http.is_some()
292 || self.transports.grpc.is_some()
293 || self.transports.native.is_some()
294 }
295
296 fn validate(&self) -> Result<()> {
297 if !self.has_any_transport() {
298 return Err(anyhow!(
299 "At least one transport must be specified.\n\n\
300 Available transports:\n \
301 --http Enable HTTP transport\n \
302 --grpc Enable gRPC transport\n \
303 --native Enable Native transport\n\n\
304 Example:\n \
305 throttlecrab-server --http --http-port 7070\n \
306 throttlecrab-server --grpc --native\n\n\
307 For more information, try '--help'"
308 ));
309 }
310
311 Ok(())
315 }
316
317 fn print_env_vars() {
318 println!("ThrottleCrab Environment Variables");
319 println!("==================================");
320 println!();
321 println!("All environment variables use the THROTTLECRAB_ prefix.");
322 println!("CLI arguments take precedence over environment variables.");
323 println!();
324
325 println!("Transport Configuration:");
326 println!(" THROTTLECRAB_HTTP=true|false Enable HTTP transport");
327 println!(" THROTTLECRAB_HTTP_HOST=<host> HTTP host [default: 127.0.0.1]");
328 println!(" THROTTLECRAB_HTTP_PORT=<port> HTTP port [default: 8080]");
329 println!();
330 println!(" THROTTLECRAB_GRPC=true|false Enable gRPC transport");
331 println!(" THROTTLECRAB_GRPC_HOST=<host> gRPC host [default: 127.0.0.1]");
332 println!(" THROTTLECRAB_GRPC_PORT=<port> gRPC port [default: 8070]");
333 println!();
334 println!(" THROTTLECRAB_NATIVE=true|false Enable Native transport");
335 println!(" THROTTLECRAB_NATIVE_HOST=<host> Native host [default: 127.0.0.1]");
336 println!(" THROTTLECRAB_NATIVE_PORT=<port> Native port [default: 8072]");
337 println!();
338
339 println!("Store Configuration:");
340 println!(
341 " THROTTLECRAB_STORE=<type> Store type: periodic, probabilistic, adaptive [default: periodic]"
342 );
343 println!(
344 " THROTTLECRAB_STORE_CAPACITY=<size> Initial store capacity [default: 100000]"
345 );
346 println!();
347 println!(" For periodic store:");
348 println!(
349 " THROTTLECRAB_STORE_CLEANUP_INTERVAL=<secs> Cleanup interval in seconds [default: 300]"
350 );
351 println!();
352 println!(" For probabilistic store:");
353 println!(
354 " THROTTLECRAB_STORE_CLEANUP_PROBABILITY=<n> Cleanup probability (1 in N) [default: 10000]"
355 );
356 println!();
357 println!(" For adaptive store:");
358 println!(
359 " THROTTLECRAB_STORE_MIN_INTERVAL=<secs> Minimum cleanup interval [default: 5]"
360 );
361 println!(
362 " THROTTLECRAB_STORE_MAX_INTERVAL=<secs> Maximum cleanup interval [default: 300]"
363 );
364 println!(
365 " THROTTLECRAB_STORE_MAX_OPERATIONS=<n> Max operations before cleanup [default: 1000000]"
366 );
367 println!();
368
369 println!("General Configuration:");
370 println!(" THROTTLECRAB_BUFFER_SIZE=<size> Channel buffer size [default: 100000]");
371 println!(
372 " THROTTLECRAB_LOG_LEVEL=<level> Log level: error, warn, info, debug, trace [default: info]"
373 );
374 println!();
375
376 println!("Examples:");
377 println!(" # Enable HTTP transport on port 8080");
378 println!(" export THROTTLECRAB_HTTP=true");
379 println!(" export THROTTLECRAB_HTTP_PORT=8080");
380 println!();
381 println!(" # Use adaptive store with custom settings");
382 println!(" export THROTTLECRAB_STORE=adaptive");
383 println!(" export THROTTLECRAB_STORE_MIN_INTERVAL=10");
384 println!(" export THROTTLECRAB_STORE_MAX_INTERVAL=600");
385 println!();
386 println!(" # Run server (CLI args override env vars)");
387 println!(" throttlecrab-server --http-port 9090 # Will use port 9090, not 8080");
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394 use std::str::FromStr;
395
396 #[test]
397 fn test_store_type_from_str() {
398 assert_eq!(
399 StoreType::from_str("periodic").unwrap(),
400 StoreType::Periodic
401 );
402 assert_eq!(
403 StoreType::from_str("PERIODIC").unwrap(),
404 StoreType::Periodic
405 );
406 assert_eq!(
407 StoreType::from_str("probabilistic").unwrap(),
408 StoreType::Probabilistic
409 );
410 assert_eq!(
411 StoreType::from_str("adaptive").unwrap(),
412 StoreType::Adaptive
413 );
414 assert!(StoreType::from_str("invalid").is_err());
415 }
416
417 #[test]
418 fn test_config_validation_no_transport() {
419 let config = Config {
420 transports: TransportConfig {
421 http: None,
422 grpc: None,
423 native: None,
424 },
425 store: StoreConfig {
426 store_type: StoreType::Periodic,
427 capacity: 100_000,
428 cleanup_interval: 300,
429 cleanup_probability: 10_000,
430 min_interval: 5,
431 max_interval: 300,
432 max_operations: 1_000_000,
433 },
434 buffer_size: 100_000,
435 log_level: "info".to_string(),
436 };
437
438 assert!(config.validate().is_err());
439 assert!(!config.has_any_transport());
440 }
441
442 #[test]
443 fn test_config_validation_with_transport() {
444 let config = Config {
445 transports: TransportConfig {
446 http: Some(HttpConfig {
447 host: "127.0.0.1".to_string(),
448 port: 8080,
449 }),
450 grpc: None,
451 native: None,
452 },
453 store: StoreConfig {
454 store_type: StoreType::Periodic,
455 capacity: 100_000,
456 cleanup_interval: 300,
457 cleanup_probability: 10_000,
458 min_interval: 5,
459 max_interval: 300,
460 max_operations: 1_000_000,
461 },
462 buffer_size: 100_000,
463 log_level: "info".to_string(),
464 };
465
466 assert!(config.validate().is_ok());
467 assert!(config.has_any_transport());
468 }
469
470 #[test]
471 fn test_config_multiple_transports() {
472 let config = Config {
473 transports: TransportConfig {
474 http: Some(HttpConfig {
475 host: "0.0.0.0".to_string(),
476 port: 8080,
477 }),
478 grpc: Some(GrpcConfig {
479 host: "0.0.0.0".to_string(),
480 port: 50051,
481 }),
482 native: None,
483 },
484 store: StoreConfig {
485 store_type: StoreType::Adaptive,
486 capacity: 200_000,
487 cleanup_interval: 300,
488 cleanup_probability: 10_000,
489 min_interval: 10,
490 max_interval: 600,
491 max_operations: 2_000_000,
492 },
493 buffer_size: 50_000,
494 log_level: "debug".to_string(),
495 };
496
497 assert!(config.validate().is_ok());
498 assert!(config.has_any_transport());
499 }
500}