2323import org .apache .druid .indexing .seekablestream .supervisor .SeekableStreamSupervisor ;
2424import org .apache .druid .indexing .seekablestream .supervisor .SeekableStreamSupervisorIOConfig ;
2525import org .apache .druid .java .util .emitter .service .ServiceEmitter ;
26+ import org .apache .druid .java .util .emitter .service .ServiceEventBuilder ;
27+ import org .apache .druid .java .util .emitter .service .ServiceMetricEvent ;
2628import org .joda .time .Duration ;
2729import org .junit .Assert ;
2830import org .junit .Before ;
2931import org .junit .Test ;
32+ import org .mockito .ArgumentCaptor ;
3033import org .mockito .Mockito ;
3134
35+ import java .util .List ;
3236
3337import static org .mockito .ArgumentMatchers .any ;
3438import static org .mockito .Mockito .doReturn ;
3539import static org .mockito .Mockito .mock ;
3640import static org .mockito .Mockito .spy ;
41+ import static org .mockito .Mockito .verify ;
3742import static org .mockito .Mockito .when ;
3843
3944public class CostBasedAutoScalerMockTest
@@ -59,7 +64,7 @@ public void setUp()
5964 mockIoConfig = Mockito .mock (SeekableStreamSupervisorIOConfig .class );
6065
6166 when (mockSpec .getId ()).thenReturn (SUPERVISOR_ID );
62- when (mockSpec .getDataSources ()).thenReturn (java . util . List .of ("test-datasource" ));
67+ when (mockSpec .getDataSources ()).thenReturn (List .of ("test-datasource" ));
6368 when (mockSpec .isSuspended ()).thenReturn (false );
6469 when (mockSupervisor .getIoConfig ()).thenReturn (mockIoConfig );
6570 when (mockIoConfig .getStream ()).thenReturn (STREAM_NAME );
@@ -232,6 +237,60 @@ public void testScaleUpFromMinimumTasks()
232237 );
233238 }
234239
240+ @ Test
241+ public void testReturnsTaskCountMinWhenConfiguredTaskCountIsBelowMin ()
242+ {
243+ CostBasedAutoScalerConfig boundedConfig = CostBasedAutoScalerConfig .builder ()
244+ .taskCountMax (100 )
245+ .taskCountMin (50 )
246+ .enableTaskAutoScaler (true )
247+ .build ();
248+ CostBasedAutoScaler autoScaler = spy (new CostBasedAutoScaler (mockSupervisor , boundedConfig , mockSpec , mockEmitter ));
249+
250+ final int configuredTaskCount = 1 ;
251+ final int taskCountMin = 50 ;
252+
253+ // Mock computeOptimalTaskCount to return a value different from the boundary,
254+ // so the assertion proves the boundary clamping path was taken.
255+ doReturn (taskCountMin - 1 ).when (autoScaler ).computeOptimalTaskCount (any ());
256+ setupMocksForMetricsCollection (autoScaler , configuredTaskCount , 1000.0 , 0.2 );
257+
258+ final int result = autoScaler .computeTaskCountForScaleAction ();
259+
260+ Assert .assertEquals (
261+ "Should scale to taskCountMin when the configured task count is below the minimum boundary" ,
262+ taskCountMin ,
263+ result
264+ );
265+ }
266+
267+ @ Test
268+ public void testReturnsTaskCountMaxWhenConfiguredTaskCountIsAboveMax ()
269+ {
270+ CostBasedAutoScalerConfig boundedConfig = CostBasedAutoScalerConfig .builder ()
271+ .taskCountMax (50 )
272+ .taskCountMin (1 )
273+ .enableTaskAutoScaler (true )
274+ .build ();
275+ CostBasedAutoScaler autoScaler = spy (new CostBasedAutoScaler (mockSupervisor , boundedConfig , mockSpec , mockEmitter ));
276+
277+ final int configuredTaskCount = 100 ;
278+ final int taskCountMax = 50 ;
279+
280+ // Mock computeOptimalTaskCount to return a value different from the boundary,
281+ // so the assertion proves the boundary clamping path was taken.
282+ doReturn (taskCountMax + 1 ).when (autoScaler ).computeOptimalTaskCount (any ());
283+ setupMocksForMetricsCollection (autoScaler , configuredTaskCount , 10.0 , 0.8 );
284+
285+ final int result = autoScaler .computeTaskCountForScaleAction ();
286+
287+ Assert .assertEquals (
288+ "Should scale to taskCountMax when the configured task count is above the maximum boundary" ,
289+ taskCountMax ,
290+ result
291+ );
292+ }
293+
235294 @ Test
236295 public void testScaleUpToMaximumTasks ()
237296 {
@@ -357,6 +416,89 @@ public void testScaleDownAllowedDuringRolloverWhenScaleDownOnRolloverOnlyEnabled
357416 );
358417 }
359418
419+ @ Test
420+ public void testEmitsMaxTaskCountSkipReasonWhenCurrentIsAtMax ()
421+ {
422+ CostBasedAutoScalerConfig boundedConfig = CostBasedAutoScalerConfig .builder ()
423+ .taskCountMax (10 )
424+ .taskCountMin (1 )
425+ .enableTaskAutoScaler (true )
426+ .build ();
427+ CostBasedAutoScaler autoScaler = spy (new CostBasedAutoScaler (mockSupervisor , boundedConfig , mockSpec , mockEmitter ));
428+
429+ final int currentTaskCount = 10 ; // already at max
430+ doReturn (-1 ).when (autoScaler ).computeOptimalTaskCount (any ());
431+ setupMocksForMetricsCollection (autoScaler , currentTaskCount , 100.0 , 0.5 );
432+
433+ Assert .assertEquals (-1 , autoScaler .computeTaskCountForScaleAction ());
434+
435+ @ SuppressWarnings ("unchecked" )
436+ ArgumentCaptor <ServiceEventBuilder <ServiceMetricEvent >> captor = ArgumentCaptor .forClass (ServiceEventBuilder .class );
437+ verify (mockEmitter ).emit (captor .capture ());
438+ Assert .assertEquals (
439+ "Should emit 'Already at max task count' skip reason when current task count is at maximum" ,
440+ "Already at max task count" ,
441+ ((ServiceMetricEvent .Builder ) captor .getValue ())
442+ .getDimension (SeekableStreamSupervisor .AUTOSCALER_SKIP_REASON_DIMENSION )
443+ );
444+ }
445+
446+ @ Test
447+ public void testEmitsMinTaskCountSkipReasonWhenCurrentIsAtMin ()
448+ {
449+ CostBasedAutoScalerConfig boundedConfig = CostBasedAutoScalerConfig .builder ()
450+ .taskCountMax (100 )
451+ .taskCountMin (10 )
452+ .enableTaskAutoScaler (true )
453+ .build ();
454+ CostBasedAutoScaler autoScaler = spy (new CostBasedAutoScaler (mockSupervisor , boundedConfig , mockSpec , mockEmitter ));
455+
456+ final int currentTaskCount = 10 ; // already at min
457+ doReturn (-1 ).when (autoScaler ).computeOptimalTaskCount (any ());
458+ setupMocksForMetricsCollection (autoScaler , currentTaskCount , 100.0 , 0.5 );
459+
460+ Assert .assertEquals (-1 , autoScaler .computeTaskCountForScaleAction ());
461+
462+ @ SuppressWarnings ("unchecked" )
463+ ArgumentCaptor <ServiceEventBuilder <ServiceMetricEvent >> captor = ArgumentCaptor .forClass (ServiceEventBuilder .class );
464+ verify (mockEmitter ).emit (captor .capture ());
465+ Assert .assertEquals (
466+ "Should emit 'Already at min task count' skip reason when current task count is at minimum" ,
467+ "Already at min task count" ,
468+ ((ServiceMetricEvent .Builder ) captor .getValue ())
469+ .getDimension (SeekableStreamSupervisor .AUTOSCALER_SKIP_REASON_DIMENSION )
470+ );
471+ }
472+
473+ @ Test
474+ public void testMaxSkipReasonTakesPriorityWhenMinEqualsMax ()
475+ {
476+ // When min == max, current is simultaneously at both bounds.
477+ // The comment in the production code states that signaling max has higher priority.
478+ CostBasedAutoScalerConfig boundedConfig = CostBasedAutoScalerConfig .builder ()
479+ .taskCountMax (5 )
480+ .taskCountMin (5 )
481+ .enableTaskAutoScaler (true )
482+ .build ();
483+ CostBasedAutoScaler autoScaler = spy (new CostBasedAutoScaler (mockSupervisor , boundedConfig , mockSpec , mockEmitter ));
484+
485+ final int currentTaskCount = 5 ; // at both min and max
486+ doReturn (-1 ).when (autoScaler ).computeOptimalTaskCount (any ());
487+ setupMocksForMetricsCollection (autoScaler , currentTaskCount , 100.0 , 0.5 );
488+
489+ Assert .assertEquals (-1 , autoScaler .computeTaskCountForScaleAction ());
490+
491+ @ SuppressWarnings ("unchecked" )
492+ ArgumentCaptor <ServiceEventBuilder <ServiceMetricEvent >> captor = ArgumentCaptor .forClass (ServiceEventBuilder .class );
493+ verify (mockEmitter ).emit (captor .capture ());
494+ Assert .assertEquals (
495+ "Max skip reason should take priority over min skip reason when min equals max" ,
496+ "Already at max task count" ,
497+ ((ServiceMetricEvent .Builder ) captor .getValue ())
498+ .getDimension (SeekableStreamSupervisor .AUTOSCALER_SKIP_REASON_DIMENSION )
499+ );
500+ }
501+
360502 private void setupMocksForMetricsCollection (
361503 CostBasedAutoScaler autoScaler ,
362504 int taskCount ,
@@ -377,22 +519,7 @@ private void setupMocksForMetricsCollection(
377519 SeekableStreamSupervisorIOConfig ioConfig = mock (SeekableStreamSupervisorIOConfig .class );
378520 doReturn (ioConfig ).when (mockSupervisor ).getIoConfig ();
379521 doReturn (taskCount ).when (ioConfig ).getTaskCount ();
522+ doReturn (STREAM_NAME ).when (ioConfig ).getStream ();
380523 }
381524
382- private CostMetrics createMetrics (
383- double avgPartitionLag ,
384- int currentTaskCount ,
385- int partitionCount ,
386- double pollIdleRatio
387- )
388- {
389- return new CostMetrics (
390- avgPartitionLag ,
391- currentTaskCount ,
392- partitionCount ,
393- pollIdleRatio ,
394- TASK_DURATION_SECONDS ,
395- AVG_PROCESSING_RATE
396- );
397- }
398525}
0 commit comments