throttlecrab_server/
config.rs

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    // Store-specific parameters
43    pub cleanup_interval: u64,    // For periodic store (seconds)
44    pub cleanup_probability: u64, // For probabilistic store (1 in N)
45    pub min_interval: u64,        // For adaptive store (seconds)
46    pub max_interval: u64,        // For adaptive store (seconds)
47    pub max_operations: usize,    // For adaptive store
48}
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    // HTTP Transport
82    #[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    // gRPC Transport
102    #[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    // Native Transport
122    #[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    // Store Configuration
142    #[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    // Store-specific options
160    #[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    // General options
202    #[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    // Utility options
220    #[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        // Clap automatically handles environment variables with the precedence:
231        // 1. CLI arguments (highest priority)
232        // 2. Environment variables
233        // 3. Default values (lowest priority)
234        let args = Args::parse();
235
236        // Handle --list-env-vars
237        if args.list_env_vars {
238            Self::print_env_vars();
239            std::process::exit(0);
240        }
241
242        // Build config from parsed args (which already include env vars)
243        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        // Configure transports based on parsed args
263        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        // Validate configuration
285        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        // Additional validation could be added here in the future
312        // e.g., validate port ranges, check for conflicting options, etc.
313
314        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}