@@ -3785,6 +3785,176 @@ def test_handle_metrics_on_dead_serve_actor(self, mock_deployment_state_manager)
37853785 dsm .update ()
37863786 check_counts (ds1 , total = 2 , by_state = [(ReplicaState .STOPPING , 2 , None )])
37873787
3788+ def test_autoscaling_timestamps (self , mock_deployment_state_manager ):
3789+ """Test that last_scale_up_time and last_scale_down_time are properly tracked.
3790+
3791+ This test verifies that:
3792+ 1. Timestamps are None initially
3793+ 2. last_scale_up_time is set after a scale-up event
3794+ 3. last_scale_down_time is set after a scale-down event
3795+ 4. Timestamps are available in AutoscalingContext
3796+ """
3797+ # Create deployment state manager
3798+ create_dsm , timer , _ , asm = mock_deployment_state_manager
3799+ dsm : DeploymentStateManager = create_dsm ()
3800+ asm : AutoscalingStateManager = asm
3801+
3802+ # Deploy deployment with autoscaling
3803+ info , _ = deployment_info (
3804+ autoscaling_config = {
3805+ "target_ongoing_requests" : 1 ,
3806+ "min_replicas" : 1 ,
3807+ "max_replicas" : 5 ,
3808+ "initial_replicas" : 2 ,
3809+ "upscale_delay_s" : 0 ,
3810+ "downscale_delay_s" : 0 ,
3811+ "metrics_interval_s" : 100 ,
3812+ }
3813+ )
3814+ dsm .deploy (TEST_DEPLOYMENT_ID , info )
3815+ ds : DeploymentState = dsm ._deployment_states [TEST_DEPLOYMENT_ID ]
3816+
3817+ # Make replicas ready
3818+ dsm .update ()
3819+ for replica in ds ._replicas .get ():
3820+ replica ._actor .set_ready ()
3821+ dsm .update ()
3822+
3823+ # Get autoscaling state
3824+ app_state = asm ._app_autoscaling_states [TEST_DEPLOYMENT_ID .app_name ]
3825+ dep_autoscaling_state = app_state ._deployment_autoscaling_states [
3826+ TEST_DEPLOYMENT_ID
3827+ ]
3828+
3829+ # Initially, timestamps should be None
3830+ ctx = dep_autoscaling_state .get_autoscaling_context (2 )
3831+ assert ctx .last_scale_up_time is None
3832+ assert ctx .last_scale_down_time is None
3833+
3834+ # Trigger scale-up by setting high request metrics
3835+ replicas = ds ._replicas .get ()
3836+ req_per_replica = 5 # High load to trigger scale-up
3837+
3838+ if RAY_SERVE_COLLECT_AUTOSCALING_METRICS_ON_HANDLE :
3839+ handle_metric_report = HandleMetricReport (
3840+ deployment_id = TEST_DEPLOYMENT_ID ,
3841+ handle_id = "test_handle" ,
3842+ actor_id = "test_actor" ,
3843+ handle_source = DeploymentHandleSource .UNKNOWN ,
3844+ queued_requests = [TimeStampedValue (timer .time () - 0.1 , 0 )],
3845+ aggregated_queued_requests = 0 ,
3846+ aggregated_metrics = {
3847+ RUNNING_REQUESTS_KEY : {
3848+ replica ._actor .replica_id : req_per_replica
3849+ for replica in replicas
3850+ }
3851+ },
3852+ metrics = {
3853+ RUNNING_REQUESTS_KEY : {
3854+ replica ._actor .replica_id : [
3855+ TimeStampedValue (timer .time () - 0.1 , req_per_replica )
3856+ ]
3857+ for replica in replicas
3858+ }
3859+ },
3860+ timestamp = timer .time (),
3861+ )
3862+ asm .record_request_metrics_for_handle (handle_metric_report )
3863+ else :
3864+ for replica in replicas :
3865+ replica_metric_report = ReplicaMetricReport (
3866+ replica_id = replica ._actor .replica_id ,
3867+ aggregated_metrics = {RUNNING_REQUESTS_KEY : req_per_replica },
3868+ metrics = {
3869+ RUNNING_REQUESTS_KEY : [
3870+ TimeStampedValue (timer .time () - 0.1 , req_per_replica )
3871+ ]
3872+ },
3873+ timestamp = timer .time (),
3874+ )
3875+ asm .record_request_metrics_for_replica (replica_metric_report )
3876+
3877+ # Record time before scale-up
3878+ time_before_scale_up = timer .time ()
3879+
3880+ # Trigger autoscaling decision
3881+ self .scale (dsm , asm , [TEST_DEPLOYMENT_ID ])
3882+
3883+ # After scale-up, last_scale_up_time should be set and greater than the time before
3884+ ctx_after_scale_up = dep_autoscaling_state .get_autoscaling_context (5 )
3885+ assert ctx_after_scale_up .last_scale_up_time is not None
3886+ assert ctx_after_scale_up .last_scale_up_time >= time_before_scale_up
3887+ assert ctx_after_scale_up .last_scale_down_time is None
3888+
3889+ scale_up_time = ctx_after_scale_up .last_scale_up_time
3890+
3891+ # Advance timer to simulate time passing
3892+ timer .advance (10 )
3893+
3894+ # Set replicas ready
3895+ dsm .update ()
3896+ for replica in ds ._replicas .get ():
3897+ replica ._actor .set_ready ()
3898+ dsm .update ()
3899+
3900+ # Now trigger scale-down by setting low request metrics
3901+ replicas = ds ._replicas .get ()
3902+ req_per_replica = 0 # No load to trigger scale-down
3903+
3904+ if RAY_SERVE_COLLECT_AUTOSCALING_METRICS_ON_HANDLE :
3905+ handle_metric_report = HandleMetricReport (
3906+ deployment_id = TEST_DEPLOYMENT_ID ,
3907+ handle_id = "test_handle" ,
3908+ actor_id = "test_actor" ,
3909+ handle_source = DeploymentHandleSource .UNKNOWN ,
3910+ queued_requests = [TimeStampedValue (timer .time () - 0.1 , 0 )],
3911+ aggregated_queued_requests = 0 ,
3912+ aggregated_metrics = {
3913+ RUNNING_REQUESTS_KEY : {
3914+ replica ._actor .replica_id : req_per_replica
3915+ for replica in replicas
3916+ }
3917+ },
3918+ metrics = {
3919+ RUNNING_REQUESTS_KEY : {
3920+ replica ._actor .replica_id : [
3921+ TimeStampedValue (timer .time () - 0.1 , req_per_replica )
3922+ ]
3923+ for replica in replicas
3924+ }
3925+ },
3926+ timestamp = timer .time (),
3927+ )
3928+ asm .record_request_metrics_for_handle (handle_metric_report )
3929+ else :
3930+ for replica in replicas :
3931+ replica_metric_report = ReplicaMetricReport (
3932+ replica_id = replica ._actor .replica_id ,
3933+ aggregated_metrics = {RUNNING_REQUESTS_KEY : req_per_replica },
3934+ metrics = {
3935+ RUNNING_REQUESTS_KEY : [
3936+ TimeStampedValue (timer .time () - 0.1 , req_per_replica )
3937+ ]
3938+ },
3939+ timestamp = timer .time (),
3940+ )
3941+ asm .record_request_metrics_for_replica (replica_metric_report )
3942+
3943+ # Record time before scale-down
3944+ time_before_scale_down = timer .time ()
3945+
3946+ # Trigger autoscaling decision for scale-down
3947+ self .scale (dsm , asm , [TEST_DEPLOYMENT_ID ])
3948+
3949+ # After scale-down, last_scale_down_time should be set and greater than the time before
3950+ ctx_after_scale_down = dep_autoscaling_state .get_autoscaling_context (1 )
3951+ assert (
3952+ ctx_after_scale_down .last_scale_up_time == scale_up_time
3953+ ) # Should remain unchanged
3954+ assert ctx_after_scale_down .last_scale_down_time is not None
3955+ assert ctx_after_scale_down .last_scale_down_time >= time_before_scale_down
3956+ assert ctx_after_scale_down .last_scale_down_time > scale_up_time
3957+
37883958
37893959class TestTargetCapacity :
37903960 """
0 commit comments