@@ -29,7 +29,7 @@ use crate::{
2929 timeseries_db:: timeseries_db_client:: TimeseriesDbClient ,
3030} ;
3131use anyhow:: { Context , Result } ;
32- use chrono:: { Duration , Utc } ;
32+ use chrono:: { DateTime , Duration , Utc } ;
3333use moka:: sync:: Cache ;
3434
3535use std:: sync:: Arc ;
@@ -90,6 +90,17 @@ const TIMESERIES_DB_QUERY_STEP: StdDuration = StdDuration::from_secs(60); // 1 m
9090pub const CALCULATE_UTILIZATION_LOCK_PREFIX : & str = "util_lock" ;
9191const ACTIVE_FUNCTION_SET_NAME : & str = "RecentlyInvokedFunctions" ;
9292
93+ /// Return the previous complete TimeseriesDb step.
94+ ///
95+ /// The current step may contain only a subset of the latest scrape cycle.
96+ fn settled_timeseries_end_time ( now : DateTime < Utc > ) -> DateTime < Utc > {
97+ let step_seconds = TIMESERIES_DB_QUERY_STEP . as_secs ( ) as i64 ;
98+ let current_boundary = now. timestamp ( ) . div_euclid ( step_seconds) * step_seconds;
99+ let settled_timestamp = current_boundary. saturating_sub ( step_seconds) ;
100+
101+ DateTime :: from_timestamp ( settled_timestamp, 0 ) . unwrap_or ( now)
102+ }
103+
93104fn scaling_lock_name ( bucket_index : usize ) -> String {
94105 format ! (
95106 "{}_{}_{}" ,
@@ -131,10 +142,10 @@ async fn get_function_utilization_history(
131142 env : & str ,
132143 metric_source : MetricSource ,
133144 ignore_env : bool ,
145+ end_time : DateTime < Utc > ,
134146 lookback : StdDuration ,
135147 utilization_window_seconds : u64 ,
136148) -> Result < Vec < ( i64 , String ) > > {
137- let end_time = Utc :: now ( ) ;
138149 let start_time = end_time - Duration :: from_std ( lookback) ?;
139150 let step = TIMESERIES_DB_QUERY_STEP ;
140151
@@ -268,6 +279,7 @@ async fn get_byoc_instance_count(
268279 function_version_id : & Uuid ,
269280 env : & str ,
270281 ignore_env : bool ,
282+ end_time : DateTime < Utc > ,
271283) -> Result < Option < usize > > {
272284 let env_suffix = if ignore_env {
273285 String :: new ( )
@@ -283,14 +295,7 @@ async fn get_byoc_instance_count(
283295 env = env_suffix
284296 ) ;
285297
286- // Align end to the previous fully-settled step boundary (one step back from now).
287- // Reading the bleeding edge can pick up a partial scrape cycle and report a wrong count.
288- const STEP_SECS : i64 = 60 ;
289- let now_secs = Utc :: now ( ) . timestamp ( ) ;
290- let end_secs = ( now_secs / STEP_SECS ) * STEP_SECS - STEP_SECS ;
291- let end_time = chrono:: DateTime :: from_timestamp ( end_secs, 0 ) . unwrap_or_else ( Utc :: now) ;
292- let start_time = end_time - chrono:: Duration :: seconds ( STEP_SECS ) ;
293- let step = std:: time:: Duration :: from_secs ( STEP_SECS as u64 ) ;
298+ let start_time = end_time - Duration :: from_std ( TIMESERIES_DB_QUERY_STEP ) ?;
294299
295300 tracing:: info!(
296301 "BYOC instance count query for {}:{}: {}" ,
@@ -300,7 +305,7 @@ async fn get_byoc_instance_count(
300305 ) ;
301306
302307 let response = timeseries_db_client
303- . query_range ( & query, start_time, end_time, step )
308+ . query_range ( & query, start_time, end_time, TIMESERIES_DB_QUERY_STEP )
304309 . await ?;
305310
306311 tracing:: info!(
@@ -636,6 +641,12 @@ async fn gather_scaling_inputs(
636641 }
637642 }
638643
644+ // Control-plane instance count and utilization must describe the same settled snapshot.
645+ // Otherwise a running-instance transition can combine an old count with new utilization
646+ // and produce contradictory targets.
647+ let control_plane_query_end_time = ( metric_source == MetricSource :: ControlPlane )
648+ . then ( || settled_timeseries_end_time ( Utc :: now ( ) ) ) ;
649+
639650 match metric_source {
640651 MetricSource :: WorkerThreads => { }
641652 MetricSource :: LlmGateway => {
@@ -657,6 +668,7 @@ async fn gather_scaling_inputs(
657668 function_version_id,
658669 env,
659670 ignore_env,
671+ control_plane_query_end_time. expect ( "control-plane query end time must be present" ) ,
660672 )
661673 . await
662674 . unwrap_or_else ( |error| {
@@ -694,6 +706,7 @@ async fn gather_scaling_inputs(
694706 env,
695707 metric_source,
696708 ignore_env,
709+ control_plane_query_end_time. unwrap_or_else ( Utc :: now) ,
697710 scaling_settings. lookback ,
698711 scaling_settings. utilization_window_seconds ,
699712 )
@@ -1156,6 +1169,17 @@ mod tests {
11561169 assert_eq ! ( scaling_lock_name( 7 ) , "util_lock_7_RecentlyInvokedFunctions" ) ;
11571170 }
11581171
1172+ #[ test]
1173+ fn settled_timeseries_end_time_uses_previous_complete_step ( ) {
1174+ let now = DateTime :: from_timestamp ( 1_700_000_123 , 456_000_000 )
1175+ . expect ( "valid timestamp" ) ;
1176+
1177+ assert_eq ! (
1178+ settled_timeseries_end_time( now) ,
1179+ DateTime :: from_timestamp( 1_700_000_040 , 0 ) . expect( "valid timestamp" ) ,
1180+ ) ;
1181+ }
1182+
11591183 #[ test]
11601184 fn utilization_data_age_never_goes_negative ( ) {
11611185 assert_eq ! (
@@ -1325,6 +1349,7 @@ mod tests {
13251349 "stg" ,
13261350 MetricSource :: ControlPlane ,
13271351 true ,
1352+ Utc :: now ( ) ,
13281353 StdDuration :: from_secs ( 5 * 60 ) ,
13291354 60 ,
13301355 )
@@ -1367,6 +1392,7 @@ mod tests {
13671392 configured_env,
13681393 MetricSource :: ControlPlane ,
13691394 false ,
1395+ Utc :: now ( ) ,
13701396 StdDuration :: from_secs ( 5 * 60 ) ,
13711397 60 ,
13721398 )
@@ -1521,6 +1547,7 @@ mod tests {
15211547 "prod" ,
15221548 MetricSource :: LlmGateway ,
15231549 false ,
1550+ Utc :: now( ) ,
15241551 StdDuration :: from_secs( 5 * 60 ) ,
15251552 70 ,
15261553 )
@@ -1638,13 +1665,13 @@ mod tests {
16381665 let client = ts_client ( server. url ( ) ) ;
16391666
16401667 assert_eq ! (
1641- get_byoc_instance_count( & client, & fid, & fvid, "prd" , false )
1668+ get_byoc_instance_count( & client, & fid, & fvid, "prd" , false , Utc :: now ( ) )
16421669 . await
16431670 . expect( "prod cp instance count" ) ,
16441671 Some ( 3 )
16451672 ) ;
16461673 assert_eq ! (
1647- get_byoc_instance_count( & client, & fid, & fvid, "stg" , false )
1674+ get_byoc_instance_count( & client, & fid, & fvid, "stg" , false , Utc :: now ( ) )
16481675 . await
16491676 . expect( "stage cp instance count" ) ,
16501677 Some ( 3 )
@@ -1675,6 +1702,7 @@ mod tests {
16751702 & fvid,
16761703 "stg" ,
16771704 true ,
1705+ Utc :: now( ) ,
16781706 )
16791707 . await
16801708 . expect( "missing cp instance count" ) ,
@@ -1703,6 +1731,7 @@ mod tests {
17031731 & fvid,
17041732 "stg" ,
17051733 true ,
1734+ Utc :: now( ) ,
17061735 )
17071736 . await
17081737 . expect( "zero cp instance count" ) ,
0 commit comments