@@ -24,7 +24,7 @@ pub const USAGE: &str = concat!(
2424 " [--appendfsync always|everysec|no]\n " ,
2525 " [--client-timeout SECONDS] [--maxclients N]\n " ,
2626 " [--maxmemory BYTES] [--maxmemory-policy POLICY]\n " ,
27- " [--maxmemory-samples N]\n " ,
27+ " [--maxmemory-samples N] [--io-threads N] \n " ,
2828 " [--loglevel off|error|warn|info|debug|trace]"
2929) ;
3030
@@ -60,6 +60,9 @@ pub struct CliArgs {
6060 max_memory_policy : Option < EvictionPolicy > ,
6161 /// How many random candidates each eviction round considers.
6262 max_memory_samples : Option < usize > ,
63+ /// Number of tokio worker threads; `0` (unset) asks tokio for the
64+ /// default (usually one per logical CPU).
65+ io_threads : Option < usize > ,
6366}
6467
6568/// Raw, un-merged values taken verbatim from the command line.
@@ -81,6 +84,7 @@ struct RawFlags {
8184 max_memory : Option < u64 > ,
8285 max_memory_policy : Option < EvictionPolicy > ,
8386 max_memory_samples : Option < usize > ,
87+ io_threads : Option < usize > ,
8488 /// Whether AOF was explicitly enabled via the config file's `appendonly yes`.
8589 /// CLI `--aof-path` implies enabled; this field only carries the file's
8690 /// intent so that a later merge step can decide.
@@ -92,7 +96,7 @@ impl CliArgs {
9296 pub fn parse < I : IntoIterator < Item = String > > ( args : I ) -> Result < Invocation , String > {
9397 let raw = match scan_argv ( args) ? {
9498 ScanOutcome :: Help => return Ok ( Invocation :: Help ) ,
95- ScanOutcome :: Flags ( f) => f,
99+ ScanOutcome :: Flags ( f) => * f,
96100 } ;
97101
98102 let file_cfg = if let Some ( path) = raw. config_path . as_deref ( ) {
@@ -127,6 +131,12 @@ impl CliArgs {
127131 self . loglevel . as_deref ( )
128132 }
129133
134+ /// Returns the explicit tokio worker thread count. `None` means
135+ /// "let tokio pick", which is what most deployments want.
136+ pub fn io_threads ( & self ) -> Option < usize > {
137+ self . io_threads
138+ }
139+
130140 /// Returns the resolved eviction configuration. Defaults (unlimited
131141 /// memory, `noeviction`, 5 samples) apply when neither CLI nor config
132142 /// file specifies a value.
@@ -142,7 +152,7 @@ impl CliArgs {
142152
143153enum ScanOutcome {
144154 Help ,
145- Flags ( RawFlags ) ,
155+ Flags ( Box < RawFlags > ) ,
146156}
147157
148158/// First pass: walk `argv` and record every flag we recognise without
@@ -210,12 +220,18 @@ fn scan_argv<I: IntoIterator<Item = String>>(args: I) -> Result<ScanOutcome, Str
210220 format ! ( "invalid --maxmemory-samples: '{value}' is not a non-negative integer" )
211221 } ) ?) ;
212222 }
223+ "--io-threads" => {
224+ let value = take_value ( & mut iter, "--io-threads" ) ?;
225+ raw. io_threads = Some ( value. parse ( ) . map_err ( |_| {
226+ format ! ( "invalid --io-threads: '{value}' is not a non-negative integer" )
227+ } ) ?) ;
228+ }
213229 "-h" | "--help" => return Ok ( ScanOutcome :: Help ) ,
214230 other => return Err ( format ! ( "unrecognised argument: '{other}'" ) ) ,
215231 }
216232 }
217233
218- Ok ( ScanOutcome :: Flags ( raw) )
234+ Ok ( ScanOutcome :: Flags ( Box :: new ( raw) ) )
219235}
220236
221237/// Merges `raw` flags with an optional [`FileConfig`], applying defaults for
@@ -278,6 +294,7 @@ fn merge(raw: RawFlags, file: Option<&FileConfig>) -> Result<CliArgs, String> {
278294 let max_memory_samples = raw
279295 . max_memory_samples
280296 . or_else ( || file. and_then ( |f| f. max_memory_samples ) ) ;
297+ let io_threads = raw. io_threads . or_else ( || file. and_then ( |f| f. io_threads ) ) ;
281298
282299 Ok ( CliArgs {
283300 addr,
@@ -289,6 +306,7 @@ fn merge(raw: RawFlags, file: Option<&FileConfig>) -> Result<CliArgs, String> {
289306 max_memory,
290307 max_memory_policy,
291308 max_memory_samples,
309+ io_threads,
292310 } )
293311}
294312
@@ -649,4 +667,35 @@ mod tests {
649667 assert_eq ! ( cfg. policy, EvictionPolicy :: AllKeysLru ) ;
650668 assert_eq ! ( cfg. samples, 3 ) ;
651669 }
670+
671+ #[ test]
672+ fn io_threads_defaults_to_none ( ) {
673+ assert ! ( parse_run( & [ ] ) . io_threads( ) . is_none( ) ) ;
674+ }
675+
676+ #[ test]
677+ fn io_threads_flag_is_parsed ( ) {
678+ assert_eq ! ( parse_run( & [ "--io-threads" , "4" ] ) . io_threads( ) , Some ( 4 ) ) ;
679+ assert_eq ! ( parse_run( & [ "--io-threads" , "0" ] ) . io_threads( ) , Some ( 0 ) ) ;
680+ }
681+
682+ #[ test]
683+ fn io_threads_rejects_non_integer ( ) {
684+ let err = parse ( & [ "--io-threads" , "lots" ] ) . unwrap_err ( ) ;
685+ assert ! ( err. contains( "--io-threads" ) ) ;
686+ }
687+
688+ #[ test]
689+ fn io_threads_from_config_file ( ) {
690+ let conf = TempConf :: new ( "io-threads" , "io-threads 8\n " ) ;
691+ let args = parse_run ( & [ "--config" , conf. path . to_str ( ) . unwrap ( ) ] ) ;
692+ assert_eq ! ( args. io_threads( ) , Some ( 8 ) ) ;
693+ }
694+
695+ #[ test]
696+ fn cli_io_threads_overrides_config_file ( ) {
697+ let conf = TempConf :: new ( "io-threads-override" , "io-threads 8\n " ) ;
698+ let args = parse_run ( & [ "--config" , conf. path . to_str ( ) . unwrap ( ) , "--io-threads" , "2" ] ) ;
699+ assert_eq ! ( args. io_threads( ) , Some ( 2 ) ) ;
700+ }
652701}
0 commit comments