1use anyhow::{Result, anyhow};
33use clap::Parser;
34use serde::Deserialize;
35
36#[derive(Debug, Clone, Deserialize)]
41pub struct Config {
42 pub transports: TransportConfig,
44 pub store: StoreConfig,
46 pub buffer_size: usize,
48 pub max_denied_keys: u32,
50 pub log_level: String,
52}
53
54#[derive(Debug, Clone, Deserialize)]
59pub struct TransportConfig {
60 pub http: Option<HttpConfig>,
62 pub grpc: Option<GrpcConfig>,
64 pub redis: Option<RedisConfig>,
66}
67
68#[derive(Debug, Clone, Deserialize)]
70pub struct HttpConfig {
71 pub host: String,
73 pub port: u16,
75}
76
77#[derive(Debug, Clone, Deserialize)]
79pub struct GrpcConfig {
80 pub host: String,
82 pub port: u16,
84}
85
86#[derive(Debug, Clone, Deserialize)]
88pub struct RedisConfig {
89 pub host: String,
91 pub port: u16,
93}
94
95#[derive(Debug, Clone, Deserialize)]
102pub struct StoreConfig {
103 pub store_type: StoreType,
105 pub capacity: usize,
107 pub cleanup_interval: u64,
110 pub cleanup_probability: u64,
112 pub min_interval: u64,
114 pub max_interval: u64,
116 pub max_operations: usize,
118}
119
120#[derive(Debug, Clone, Copy, Deserialize, PartialEq)]
127#[serde(rename_all = "lowercase")]
128pub enum StoreType {
129 Periodic,
131 Probabilistic,
133 Adaptive,
135}
136
137impl std::str::FromStr for StoreType {
138 type Err = anyhow::Error;
139
140 fn from_str(s: &str) -> Result<Self> {
141 match s.to_lowercase().as_str() {
142 "periodic" => Ok(StoreType::Periodic),
143 "probabilistic" => Ok(StoreType::Probabilistic),
144 "adaptive" => Ok(StoreType::Adaptive),
145 _ => Err(anyhow!(
146 "Invalid store type: {}. Valid options are: periodic, probabilistic, adaptive",
147 s
148 )),
149 }
150 }
151}
152
153#[derive(Parser, Debug)]
175#[command(
176 name = "throttlecrab-server",
177 version = env!("CARGO_PKG_VERSION"),
178 about = "High-performance rate limiting server",
179 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."
180)]
181pub struct Args {
182 #[arg(long, help = "Enable HTTP transport", env = "THROTTLECRAB_HTTP")]
184 pub http: bool,
185 #[arg(
186 long,
187 value_name = "HOST",
188 help = "HTTP host",
189 default_value = "127.0.0.1",
190 env = "THROTTLECRAB_HTTP_HOST"
191 )]
192 pub http_host: String,
193 #[arg(
194 long,
195 value_name = "PORT",
196 help = "HTTP port",
197 default_value_t = 8080,
198 env = "THROTTLECRAB_HTTP_PORT"
199 )]
200 pub http_port: u16,
201
202 #[arg(long, help = "Enable gRPC transport", env = "THROTTLECRAB_GRPC")]
204 pub grpc: bool,
205 #[arg(
206 long,
207 value_name = "HOST",
208 help = "gRPC host",
209 default_value = "127.0.0.1",
210 env = "THROTTLECRAB_GRPC_HOST"
211 )]
212 pub grpc_host: String,
213 #[arg(
214 long,
215 value_name = "PORT",
216 help = "gRPC port",
217 default_value_t = 8070,
218 env = "THROTTLECRAB_GRPC_PORT"
219 )]
220 pub grpc_port: u16,
221
222 #[arg(
224 long,
225 help = "Enable Redis protocol transport",
226 env = "THROTTLECRAB_REDIS"
227 )]
228 pub redis: bool,
229 #[arg(
230 long,
231 value_name = "HOST",
232 help = "Redis host",
233 default_value = "127.0.0.1",
234 env = "THROTTLECRAB_REDIS_HOST"
235 )]
236 pub redis_host: String,
237 #[arg(
238 long,
239 value_name = "PORT",
240 help = "Redis port",
241 default_value_t = 6379,
242 env = "THROTTLECRAB_REDIS_PORT"
243 )]
244 pub redis_port: u16,
245
246 #[arg(
248 long,
249 value_name = "TYPE",
250 help = "Store type: periodic, probabilistic, adaptive",
251 default_value = "periodic",
252 env = "THROTTLECRAB_STORE"
253 )]
254 pub store: StoreType,
255 #[arg(
256 long,
257 value_name = "SIZE",
258 help = "Initial store capacity",
259 default_value_t = 100_000,
260 env = "THROTTLECRAB_STORE_CAPACITY"
261 )]
262 pub store_capacity: usize,
263
264 #[arg(
266 long,
267 value_name = "SECS",
268 help = "Cleanup interval for periodic store (seconds)",
269 default_value_t = 300,
270 env = "THROTTLECRAB_STORE_CLEANUP_INTERVAL"
271 )]
272 pub store_cleanup_interval: u64,
273 #[arg(
274 long,
275 value_name = "N",
276 help = "Cleanup probability for probabilistic store (1 in N)",
277 default_value_t = 10_000,
278 env = "THROTTLECRAB_STORE_CLEANUP_PROBABILITY"
279 )]
280 pub store_cleanup_probability: u64,
281 #[arg(
282 long,
283 value_name = "SECS",
284 help = "Minimum cleanup interval for adaptive store (seconds)",
285 default_value_t = 5,
286 env = "THROTTLECRAB_STORE_MIN_INTERVAL"
287 )]
288 pub store_min_interval: u64,
289 #[arg(
290 long,
291 value_name = "SECS",
292 help = "Maximum cleanup interval for adaptive store (seconds)",
293 default_value_t = 300,
294 env = "THROTTLECRAB_STORE_MAX_INTERVAL"
295 )]
296 pub store_max_interval: u64,
297 #[arg(
298 long,
299 value_name = "N",
300 help = "Maximum operations before cleanup for adaptive store",
301 default_value_t = 1_000_000,
302 env = "THROTTLECRAB_STORE_MAX_OPERATIONS"
303 )]
304 pub store_max_operations: usize,
305
306 #[arg(
308 long,
309 value_name = "SIZE",
310 help = "Channel buffer size",
311 default_value_t = 100_000,
312 env = "THROTTLECRAB_BUFFER_SIZE"
313 )]
314 pub buffer_size: usize,
315 #[arg(
316 long,
317 value_name = "COUNT",
318 help = "Maximum number of denied keys to track in metrics (0 to disable, max: 10000)",
319 default_value_t = 100,
320 env = "THROTTLECRAB_MAX_DENIED_KEYS",
321 value_parser = clap::value_parser!(u32).range(0..=10000)
322 )]
323 pub max_denied_keys: u32,
324 #[arg(
325 long,
326 value_name = "LEVEL",
327 help = "Log level: error, warn, info, debug, trace",
328 default_value = "info",
329 env = "THROTTLECRAB_LOG_LEVEL"
330 )]
331 pub log_level: String,
332
333 #[arg(
335 long,
336 help = "List all environment variables and exit",
337 action = clap::ArgAction::SetTrue
338 )]
339 pub list_env_vars: bool,
340}
341
342impl Config {
343 pub fn from_env_and_args() -> Result<Self> {
357 let args = Args::parse();
362
363 if args.list_env_vars {
365 Self::print_env_vars();
366 std::process::exit(0);
367 }
368
369 let mut config = Config {
371 transports: TransportConfig {
372 http: None,
373 grpc: None,
374 redis: None,
375 },
376 store: StoreConfig {
377 store_type: args.store,
378 capacity: args.store_capacity,
379 cleanup_interval: args.store_cleanup_interval,
380 cleanup_probability: args.store_cleanup_probability,
381 min_interval: args.store_min_interval,
382 max_interval: args.store_max_interval,
383 max_operations: args.store_max_operations,
384 },
385 buffer_size: args.buffer_size,
386 max_denied_keys: args.max_denied_keys,
387 log_level: args.log_level,
388 };
389
390 if args.http {
392 config.transports.http = Some(HttpConfig {
393 host: args.http_host,
394 port: args.http_port,
395 });
396 }
397
398 if args.grpc {
399 config.transports.grpc = Some(GrpcConfig {
400 host: args.grpc_host,
401 port: args.grpc_port,
402 });
403 }
404
405 if args.redis {
406 config.transports.redis = Some(RedisConfig {
407 host: args.redis_host,
408 port: args.redis_port,
409 });
410 }
411
412 config.validate()?;
414
415 Ok(config)
416 }
417
418 pub fn has_any_transport(&self) -> bool {
422 self.transports.http.is_some()
423 || self.transports.grpc.is_some()
424 || self.transports.redis.is_some()
425 }
426
427 fn validate(&self) -> Result<()> {
436 if !self.has_any_transport() {
437 return Err(anyhow!(
438 "At least one transport must be specified.\n\n\
439 Available transports:\n \
440 --http Enable HTTP transport\n \
441 --grpc Enable gRPC transport\n \
442 --redis Enable Redis protocol transport\n \
443 Example:\n \
444 throttlecrab-server --http --http-port 7070\n \
445 throttlecrab-server --http --grpc --redis\n\n\
446 For more information, try '--help'"
447 ));
448 }
449
450 Ok(())
454 }
455
456 fn print_env_vars() {
462 println!("ThrottleCrab Environment Variables");
463 println!("==================================");
464 println!();
465 println!("All environment variables use the THROTTLECRAB_ prefix.");
466 println!("CLI arguments take precedence over environment variables.");
467 println!();
468
469 println!("Transport Configuration:");
470 println!(" THROTTLECRAB_HTTP=true|false Enable HTTP transport");
471 println!(" THROTTLECRAB_HTTP_HOST=<host> HTTP host [default: 127.0.0.1]");
472 println!(" THROTTLECRAB_HTTP_PORT=<port> HTTP port [default: 8080]");
473 println!();
474 println!(" THROTTLECRAB_GRPC=true|false Enable gRPC transport");
475 println!(" THROTTLECRAB_GRPC_HOST=<host> gRPC host [default: 127.0.0.1]");
476 println!(" THROTTLECRAB_GRPC_PORT=<port> gRPC port [default: 8070]");
477 println!();
478 println!(" THROTTLECRAB_REDIS=true|false Enable Redis protocol transport");
479 println!(" THROTTLECRAB_REDIS_HOST=<host> Redis host [default: 127.0.0.1]");
480 println!(" THROTTLECRAB_REDIS_PORT=<port> Redis port [default: 6379]");
481 println!();
482
483 println!("Store Configuration:");
484 println!(
485 " THROTTLECRAB_STORE=<type> Store type: periodic, probabilistic, adaptive [default: periodic]"
486 );
487 println!(
488 " THROTTLECRAB_STORE_CAPACITY=<size> Initial store capacity [default: 100000]"
489 );
490 println!();
491 println!(" For periodic store:");
492 println!(
493 " THROTTLECRAB_STORE_CLEANUP_INTERVAL=<secs> Cleanup interval in seconds [default: 300]"
494 );
495 println!();
496 println!(" For probabilistic store:");
497 println!(
498 " THROTTLECRAB_STORE_CLEANUP_PROBABILITY=<n> Cleanup probability (1 in N) [default: 10000]"
499 );
500 println!();
501 println!(" For adaptive store:");
502 println!(
503 " THROTTLECRAB_STORE_MIN_INTERVAL=<secs> Minimum cleanup interval [default: 5]"
504 );
505 println!(
506 " THROTTLECRAB_STORE_MAX_INTERVAL=<secs> Maximum cleanup interval [default: 300]"
507 );
508 println!(
509 " THROTTLECRAB_STORE_MAX_OPERATIONS=<n> Max operations before cleanup [default: 1000000]"
510 );
511 println!();
512
513 println!("General Configuration:");
514 println!(" THROTTLECRAB_BUFFER_SIZE=<size> Channel buffer size [default: 100000]");
515 println!(
516 " THROTTLECRAB_MAX_DENIED_KEYS=<count> Maximum denied keys to track (0=disabled, max: 10000) [default: 100]"
517 );
518 println!(
519 " THROTTLECRAB_LOG_LEVEL=<level> Log level: error, warn, info, debug, trace [default: info]"
520 );
521 println!();
522
523 println!("Examples:");
524 println!(" # Enable HTTP transport on port 8080");
525 println!(" export THROTTLECRAB_HTTP=true");
526 println!(" export THROTTLECRAB_HTTP_PORT=8080");
527 println!();
528 println!(" # Use adaptive store with custom settings");
529 println!(" export THROTTLECRAB_STORE=adaptive");
530 println!(" export THROTTLECRAB_STORE_MIN_INTERVAL=10");
531 println!(" export THROTTLECRAB_STORE_MAX_INTERVAL=600");
532 println!();
533 println!(" # Run server (CLI args override env vars)");
534 println!(" throttlecrab-server --http-port 9090 # Will use port 9090, not 8080");
535 }
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541 use std::str::FromStr;
542
543 #[test]
544 fn test_store_type_from_str() {
545 assert_eq!(
546 StoreType::from_str("periodic").unwrap(),
547 StoreType::Periodic
548 );
549 assert_eq!(
550 StoreType::from_str("PERIODIC").unwrap(),
551 StoreType::Periodic
552 );
553 assert_eq!(
554 StoreType::from_str("probabilistic").unwrap(),
555 StoreType::Probabilistic
556 );
557 assert_eq!(
558 StoreType::from_str("adaptive").unwrap(),
559 StoreType::Adaptive
560 );
561 assert!(StoreType::from_str("invalid").is_err());
562 }
563
564 #[test]
565 fn test_config_validation_no_transport() {
566 let config = Config {
567 transports: TransportConfig {
568 http: None,
569 grpc: None,
570 redis: None,
571 },
572 store: StoreConfig {
573 store_type: StoreType::Periodic,
574 capacity: 100_000,
575 cleanup_interval: 300,
576 cleanup_probability: 10_000,
577 min_interval: 5,
578 max_interval: 300,
579 max_operations: 1_000_000,
580 },
581 buffer_size: 100_000,
582 max_denied_keys: 100,
583 log_level: "info".to_string(),
584 };
585
586 assert!(config.validate().is_err());
587 assert!(!config.has_any_transport());
588 }
589
590 #[test]
591 fn test_config_validation_with_transport() {
592 let config = Config {
593 transports: TransportConfig {
594 http: Some(HttpConfig {
595 host: "127.0.0.1".to_string(),
596 port: 8080,
597 }),
598 grpc: None,
599 redis: None,
600 },
601 store: StoreConfig {
602 store_type: StoreType::Periodic,
603 capacity: 100_000,
604 cleanup_interval: 300,
605 cleanup_probability: 10_000,
606 min_interval: 5,
607 max_interval: 300,
608 max_operations: 1_000_000,
609 },
610 buffer_size: 100_000,
611 max_denied_keys: 100,
612 log_level: "info".to_string(),
613 };
614
615 assert!(config.validate().is_ok());
616 assert!(config.has_any_transport());
617 }
618
619 #[test]
620 fn test_config_multiple_transports() {
621 let config = Config {
622 transports: TransportConfig {
623 http: Some(HttpConfig {
624 host: "0.0.0.0".to_string(),
625 port: 8080,
626 }),
627 grpc: Some(GrpcConfig {
628 host: "0.0.0.0".to_string(),
629 port: 50051,
630 }),
631 redis: None,
632 },
633 store: StoreConfig {
634 store_type: StoreType::Adaptive,
635 capacity: 200_000,
636 cleanup_interval: 300,
637 cleanup_probability: 10_000,
638 min_interval: 10,
639 max_interval: 600,
640 max_operations: 2_000_000,
641 },
642 buffer_size: 50_000,
643 max_denied_keys: 100,
644 log_level: "debug".to_string(),
645 };
646
647 assert!(config.validate().is_ok());
648 assert!(config.has_any_transport());
649 }
650}