throttlecrab_server/
config.rs

1//! Server configuration and CLI argument parsing
2//!
3//! This module handles all server configuration through a flexible system that supports:
4//! - Command-line arguments
5//! - Environment variables (with THROTTLECRAB_ prefix)
6//! - Configuration file (future enhancement)
7//!
8//! # Configuration Priority
9//!
10//! The configuration system follows this precedence order:
11//! 1. CLI arguments (highest priority)
12//! 2. Environment variables
13//! 3. Default values (lowest priority)
14//!
15//! # Example Usage
16//!
17//! ```bash
18//! # Using CLI arguments
19//! throttlecrab-server --http --http-port 9090
20//!
21//! # Using environment variables
22//! export THROTTLECRAB_HTTP=true
23//! export THROTTLECRAB_HTTP_PORT=8080
24//! export THROTTLECRAB_STORE=adaptive
25//! throttlecrab-server
26//!
27//! # Mixed (CLI overrides env)
28//! export THROTTLECRAB_HTTP_PORT=8080
29//! throttlecrab-server --http --http-port 9090  # Uses port 9090
30//! ```
31
32use anyhow::{Result, anyhow};
33use clap::Parser;
34use serde::Deserialize;
35
36/// Main configuration structure for the server
37///
38/// This structure is built from CLI arguments and environment variables,
39/// and contains all settings needed to run the server.
40#[derive(Debug, Clone, Deserialize)]
41pub struct Config {
42    /// Transport layer configuration
43    pub transports: TransportConfig,
44    /// Rate limiter store configuration
45    pub store: StoreConfig,
46    /// Channel buffer size for actor communication
47    pub buffer_size: usize,
48    /// Maximum number of denied keys to track in metrics
49    pub max_denied_keys: u32,
50    /// Logging level (error, warn, info, debug, trace)
51    pub log_level: String,
52}
53
54/// Transport layer configuration
55///
56/// At least one transport must be enabled for the server to function.
57/// Multiple transports can be enabled simultaneously.
58#[derive(Debug, Clone, Deserialize)]
59pub struct TransportConfig {
60    /// HTTP/JSON transport configuration
61    pub http: Option<HttpConfig>,
62    /// gRPC transport configuration
63    pub grpc: Option<GrpcConfig>,
64    /// Redis protocol transport configuration
65    pub redis: Option<RedisConfig>,
66}
67
68/// HTTP transport configuration
69#[derive(Debug, Clone, Deserialize)]
70pub struct HttpConfig {
71    /// Host address to bind to (e.g., "0.0.0.0")
72    pub host: String,
73    /// Port number to listen on
74    pub port: u16,
75}
76
77/// gRPC transport configuration
78#[derive(Debug, Clone, Deserialize)]
79pub struct GrpcConfig {
80    /// Host address to bind to (e.g., "0.0.0.0")
81    pub host: String,
82    /// Port number to listen on
83    pub port: u16,
84}
85
86/// Redis transport configuration
87#[derive(Debug, Clone, Deserialize)]
88pub struct RedisConfig {
89    /// Host address to bind to (e.g., "0.0.0.0")
90    pub host: String,
91    /// Port number to listen on
92    pub port: u16,
93}
94
95/// Rate limiter store configuration
96///
97/// Different store types have different performance characteristics:
98/// - **Periodic**: Cleanups at fixed intervals, predictable memory usage
99/// - **Probabilistic**: Random cleanups, lower overhead but less predictable
100/// - **Adaptive**: Adjusts cleanup frequency based on load
101#[derive(Debug, Clone, Deserialize)]
102pub struct StoreConfig {
103    /// Type of store to use
104    pub store_type: StoreType,
105    /// Initial capacity of the store
106    pub capacity: usize,
107    // Store-specific parameters
108    /// Cleanup interval for periodic store (seconds)
109    pub cleanup_interval: u64,
110    /// Cleanup probability for probabilistic store (1 in N)
111    pub cleanup_probability: u64,
112    /// Minimum cleanup interval for adaptive store (seconds)
113    pub min_interval: u64,
114    /// Maximum cleanup interval for adaptive store (seconds)
115    pub max_interval: u64,
116    /// Maximum operations before cleanup for adaptive store
117    pub max_operations: usize,
118}
119
120/// Available store types for the rate limiter
121///
122/// Each store type offers different trade-offs:
123/// - **Periodic**: Best for consistent workloads
124/// - **Probabilistic**: Best for unpredictable workloads
125/// - **Adaptive**: Best for variable workloads
126#[derive(Debug, Clone, Copy, Deserialize, PartialEq)]
127#[serde(rename_all = "lowercase")]
128pub enum StoreType {
129    /// Fixed interval cleanup
130    Periodic,
131    /// Random cleanup based on probability
132    Probabilistic,
133    /// Dynamic cleanup interval based on load
134    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/// Command-line arguments for the server
154///
155/// All arguments can also be set via environment variables with the
156/// THROTTLECRAB_ prefix. CLI arguments take precedence over environment variables.
157///
158/// # Examples
159///
160/// Basic usage with HTTP protocol:
161/// ```bash
162/// throttlecrab-server --http
163/// ```
164///
165/// Multiple transports with custom ports:
166/// ```bash
167/// throttlecrab-server --http --http-port 8080 --grpc --grpc-port 50051
168/// ```
169///
170/// Using adaptive store with debug logging:
171/// ```bash
172/// throttlecrab-server --http --store adaptive --log-level debug
173/// ```
174#[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    // HTTP Transport
183    #[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    // gRPC Transport
203    #[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    // Redis Transport
223    #[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    // Store Configuration
247    #[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    // Store-specific options
265    #[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    // General options
307    #[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    // Utility options
334    #[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    /// Build configuration from environment variables and CLI arguments
344    ///
345    /// This method:
346    /// 1. Parses CLI arguments (with env var fallback via clap)
347    /// 2. Handles special flags like --list-env-vars
348    /// 3. Builds the configuration structure
349    /// 4. Validates the configuration
350    ///
351    /// # Errors
352    ///
353    /// Returns an error if:
354    /// - No transport is specified
355    /// - Invalid configuration values are provided
356    pub fn from_env_and_args() -> Result<Self> {
357        // Clap automatically handles environment variables with the precedence:
358        // 1. CLI arguments (highest priority)
359        // 2. Environment variables
360        // 3. Default values (lowest priority)
361        let args = Args::parse();
362
363        // Handle --list-env-vars
364        if args.list_env_vars {
365            Self::print_env_vars();
366            std::process::exit(0);
367        }
368
369        // Build config from parsed args (which already include env vars)
370        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        // Configure transports based on parsed args
391        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        // Validate configuration
413        config.validate()?;
414
415        Ok(config)
416    }
417
418    /// Check if at least one transport is configured
419    ///
420    /// The server requires at least one transport to be functional.
421    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    /// Validate the configuration
428    ///
429    /// Currently checks that at least one transport is enabled.
430    /// Additional validation can be added here in the future.
431    ///
432    /// # Errors
433    ///
434    /// Returns an error if the configuration is invalid.
435    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        // Additional validation could be added here in the future
451        // e.g., validate port ranges, check for conflicting options, etc.
452
453        Ok(())
454    }
455
456    /// Print all available environment variables and their descriptions
457    ///
458    /// This is called when the --list-env-vars flag is used.
459    /// It provides a comprehensive reference for all environment variables
460    /// that can be used to configure the server.
461    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}