1313// limitations under the License.
1414
1515use std:: num:: NonZeroUsize ;
16+ use std:: ops:: RangeInclusive ;
1617
1718use crate :: model:: { ScalingMode , ShardStats } ;
1819
@@ -71,8 +72,10 @@ impl ScalingArbiter {
7172 pub ( crate ) fn should_scale (
7273 & self ,
7374 shard_stats : ShardStats ,
74- min_shards : NonZeroUsize ,
75+ num_shards_range : RangeInclusive < NonZeroUsize > ,
7576 ) -> Option < ScalingMode > {
77+ let min_shards = * num_shards_range. start ( ) ;
78+ let max_shards = * num_shards_range. end ( ) ;
7679 // If ingest is idle, there is nothing to do. Idle shards are automatically closed by
7780 // ingesters (see `quickwit_ingest::ingest_v2::idle::CloseIdleShardsTask`).
7881 if shard_stats. num_open_shards == 0 || shard_stats. avg_long_term_ingestion_rate == 0.0 {
@@ -85,15 +88,19 @@ impl ScalingArbiter {
8588 }
8689 // Scale up based on the short term metric value while making sure that
8790 // the long term value doesn't get near the scale down threshold.
91+ // We skip the scale-up block entirely when already at the cap so that
92+ // execution can fall through to the scale-down check below (relevant
93+ // when max_shards was lowered after shards were already opened).
8894 if shard_stats. avg_short_term_ingestion_rate
8995 >= self . scale_up_shards_short_term_threshold_mib_per_sec
96+ && shard_stats. num_open_shards < max_shards. get ( )
9097 {
9198 let new_calculated_num_shards = usize:: min (
9299 self . long_term_scale_up_threshold_max_shards ( shard_stats) ,
93100 self . scale_up_factor_target_shards ( shard_stats) ,
94101 ) ;
95-
96- let target_num_shards = usize :: max ( min_shards. get ( ) , new_calculated_num_shards ) ;
102+ let target_num_shards =
103+ new_calculated_num_shards . clamp ( min_shards. get ( ) , max_shards . get ( ) ) ;
97104
98105 if target_num_shards > shard_stats. num_open_shards {
99106 let num_shards_to_open = target_num_shards - shard_stats. num_open_shards ;
@@ -133,7 +140,7 @@ mod tests {
133140 avg_short_term_ingestion_rate: 0.0 ,
134141 avg_long_term_ingestion_rate: 0.0 ,
135142 } ,
136- NonZeroUsize :: MIN
143+ NonZeroUsize :: MIN ..= NonZeroUsize :: MAX
137144 ) ,
138145 None ,
139146 ) ;
@@ -145,7 +152,7 @@ mod tests {
145152 avg_short_term_ingestion_rate: 5.0 ,
146153 avg_long_term_ingestion_rate: 6.0 ,
147154 } ,
148- NonZeroUsize :: MIN
155+ NonZeroUsize :: MIN ..= NonZeroUsize :: MAX
149156 ) ,
150157 None
151158 ) ;
@@ -157,7 +164,7 @@ mod tests {
157164 avg_short_term_ingestion_rate: 8.1 ,
158165 avg_long_term_ingestion_rate: 8.1 ,
159166 } ,
160- NonZeroUsize :: MIN
167+ NonZeroUsize :: MIN ..= NonZeroUsize :: MAX
161168 ) ,
162169 Some ( ScalingMode :: Up ( 1 ) )
163170 ) ;
@@ -169,7 +176,7 @@ mod tests {
169176 avg_short_term_ingestion_rate: 8.1 ,
170177 avg_long_term_ingestion_rate: 8.1 ,
171178 } ,
172- NonZeroUsize :: MIN
179+ NonZeroUsize :: MIN ..= NonZeroUsize :: MAX
173180 ) ,
174181 Some ( ScalingMode :: Up ( 1 ) )
175182 ) ;
@@ -181,7 +188,7 @@ mod tests {
181188 avg_short_term_ingestion_rate: 3.0 ,
182189 avg_long_term_ingestion_rate: 1.5 ,
183190 } ,
184- NonZeroUsize :: MIN
191+ NonZeroUsize :: MIN ..= NonZeroUsize :: MAX
185192 ) ,
186193 Some ( ScalingMode :: Down )
187194 ) ;
@@ -193,7 +200,7 @@ mod tests {
193200 avg_short_term_ingestion_rate: 3.0 ,
194201 avg_long_term_ingestion_rate: 1.5 ,
195202 } ,
196- NonZeroUsize :: MIN
203+ NonZeroUsize :: MIN ..= NonZeroUsize :: MAX
197204 ) ,
198205 None ,
199206 ) ;
@@ -205,7 +212,7 @@ mod tests {
205212 avg_short_term_ingestion_rate: 8.0 ,
206213 avg_long_term_ingestion_rate: 3.0 ,
207214 } ,
208- NonZeroUsize :: MIN
215+ NonZeroUsize :: MIN ..= NonZeroUsize :: MAX
209216 ) ,
210217 None ,
211218 ) ;
@@ -224,7 +231,7 @@ mod tests {
224231 avg_short_term_ingestion_rate: 0.0 ,
225232 avg_long_term_ingestion_rate: 0.0 ,
226233 } ,
227- NonZeroUsize :: MIN
234+ NonZeroUsize :: MIN ..= NonZeroUsize :: MAX
228235 ) ,
229236 None ,
230237 ) ;
@@ -236,7 +243,7 @@ mod tests {
236243 avg_short_term_ingestion_rate: 5.0 ,
237244 avg_long_term_ingestion_rate: 6.0 ,
238245 } ,
239- NonZeroUsize :: MIN
246+ NonZeroUsize :: MIN ..= NonZeroUsize :: MAX
240247 ) ,
241248 None
242249 ) ;
@@ -248,7 +255,7 @@ mod tests {
248255 avg_short_term_ingestion_rate: 8.1 ,
249256 avg_long_term_ingestion_rate: 8.1 ,
250257 } ,
251- NonZeroUsize :: MIN
258+ NonZeroUsize :: MIN ..= NonZeroUsize :: MAX
252259 ) ,
253260 Some ( ScalingMode :: Up ( 1 ) )
254261 ) ;
@@ -260,7 +267,7 @@ mod tests {
260267 avg_short_term_ingestion_rate: 8.1 ,
261268 avg_long_term_ingestion_rate: 8.1 ,
262269 } ,
263- NonZeroUsize :: MIN
270+ NonZeroUsize :: MIN ..= NonZeroUsize :: MAX
264271 ) ,
265272 Some ( ScalingMode :: Up ( 2 ) )
266273 ) ;
@@ -272,7 +279,7 @@ mod tests {
272279 avg_short_term_ingestion_rate: 3.0 ,
273280 avg_long_term_ingestion_rate: 1.5 ,
274281 } ,
275- NonZeroUsize :: MIN
282+ NonZeroUsize :: MIN ..= NonZeroUsize :: MAX
276283 ) ,
277284 Some ( ScalingMode :: Down )
278285 ) ;
@@ -284,7 +291,7 @@ mod tests {
284291 avg_short_term_ingestion_rate: 3.0 ,
285292 avg_long_term_ingestion_rate: 1.5 ,
286293 } ,
287- NonZeroUsize :: MIN
294+ NonZeroUsize :: MIN ..= NonZeroUsize :: MAX
288295 ) ,
289296 None ,
290297 ) ;
@@ -296,7 +303,7 @@ mod tests {
296303 avg_short_term_ingestion_rate: 8.0 ,
297304 avg_long_term_ingestion_rate: 3.1 ,
298305 } ,
299- NonZeroUsize :: MIN
306+ NonZeroUsize :: MIN ..= NonZeroUsize :: MAX
300307 ) ,
301308 None ,
302309 ) ;
@@ -309,7 +316,7 @@ mod tests {
309316 avg_short_term_ingestion_rate: 8.1 ,
310317 avg_long_term_ingestion_rate: 5. ,
311318 } ,
312- NonZeroUsize :: MIN
319+ NonZeroUsize :: MIN ..= NonZeroUsize :: MAX
313320 ) ,
314321 Some ( ScalingMode :: Up ( 1 ) ) ,
315322 ) ;
@@ -408,8 +415,8 @@ mod tests {
408415 avg_short_term_ingestion_rate : 0.0 ,
409416 avg_long_term_ingestion_rate : 0.0 ,
410417 } ;
411- let min_shards = NonZeroUsize :: MIN ;
412- let scaling_mode = scaling_arbiter. should_scale ( shard_stats, min_shards ) ;
418+ let scaling_mode =
419+ scaling_arbiter. should_scale ( shard_stats, NonZeroUsize :: MIN ..= NonZeroUsize :: MAX ) ;
413420 assert ! ( scaling_mode. is_none( ) ) ;
414421
415422 let shard_stats = ShardStats {
@@ -419,7 +426,8 @@ mod tests {
419426 avg_long_term_ingestion_rate : 0.0 ,
420427 } ;
421428 let min_shards = NonZeroUsize :: new ( 2 ) . unwrap ( ) ;
422- let scaling_mode = scaling_arbiter. should_scale ( shard_stats, min_shards) ;
429+ let scaling_mode =
430+ scaling_arbiter. should_scale ( shard_stats, min_shards..=NonZeroUsize :: MAX ) ;
423431 assert ! ( scaling_mode. is_none( ) ) ;
424432 }
425433
@@ -436,8 +444,52 @@ mod tests {
436444 } ;
437445 let min_shards = NonZeroUsize :: new ( 5 ) . unwrap ( ) ;
438446 let scaling_mode = scaling_arbiter
439- . should_scale ( shard_stats, min_shards)
447+ . should_scale ( shard_stats, min_shards..= NonZeroUsize :: MAX )
440448 . unwrap ( ) ;
441449 assert_eq ! ( scaling_mode, ScalingMode :: Up ( 4 ) ) ;
442450 }
451+
452+ #[ test]
453+ fn test_scaling_arbiter_max_shards ( ) {
454+ let scaling_arbiter =
455+ ScalingArbiter :: with_max_shard_ingestion_throughput_mib_per_sec ( 10.0 , 2.0 ) ;
456+
457+ // Already at max — scale-up should be suppressed even when load is high.
458+ let shard_stats = ShardStats {
459+ num_open_shards : 3 ,
460+ num_closed_shards : 0 ,
461+ avg_short_term_ingestion_rate : 9.0 ,
462+ avg_long_term_ingestion_rate : 9.0 ,
463+ } ;
464+ let max_shards = NonZeroUsize :: new ( 3 ) . unwrap ( ) ;
465+ assert_eq ! (
466+ scaling_arbiter. should_scale( shard_stats, NonZeroUsize :: MIN ..=max_shards) ,
467+ None ,
468+ ) ;
469+
470+ // Below max — scale-up is allowed but capped at max.
471+ let shard_stats = ShardStats {
472+ num_open_shards : 2 ,
473+ num_closed_shards : 0 ,
474+ avg_short_term_ingestion_rate : 9.0 ,
475+ avg_long_term_ingestion_rate : 9.0 ,
476+ } ;
477+ assert_eq ! (
478+ scaling_arbiter. should_scale( shard_stats, NonZeroUsize :: MIN ..=max_shards) ,
479+ Some ( ScalingMode :: Up ( 1 ) ) ,
480+ ) ;
481+
482+ // Above max (e.g. max_shards was lowered after shards were opened) with low load —
483+ // scale-down should still trigger because execution falls through the scale-up block.
484+ let shard_stats = ShardStats {
485+ num_open_shards : 4 ,
486+ num_closed_shards : 0 ,
487+ avg_short_term_ingestion_rate : 9.0 ,
488+ avg_long_term_ingestion_rate : 1.0 ,
489+ } ;
490+ assert_eq ! (
491+ scaling_arbiter. should_scale( shard_stats, NonZeroUsize :: MIN ..=max_shards) ,
492+ Some ( ScalingMode :: Down ) ,
493+ ) ;
494+ }
443495}
0 commit comments