Skip to content

Commit d050a62

Browse files
authored
fix(function-autoscaler): align control-plane metric timestamps (#1919)
Signed-off-by: Bora Oztekin <boztekin@nvidia.com>
1 parent faf2fa0 commit d050a62

1 file changed

Lines changed: 42 additions & 13 deletions

File tree

  • src/control-plane-services/function-autoscaler/crates/server/src/work

‎src/control-plane-services/function-autoscaler/crates/server/src/work/mod.rs‎

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use crate::{
2929
timeseries_db::timeseries_db_client::TimeseriesDbClient,
3030
};
3131
use anyhow::{Context, Result};
32-
use chrono::{Duration, Utc};
32+
use chrono::{DateTime, Duration, Utc};
3333
use moka::sync::Cache;
3434

3535
use std::sync::Arc;
@@ -90,6 +90,17 @@ const TIMESERIES_DB_QUERY_STEP: StdDuration = StdDuration::from_secs(60); // 1 m
9090
pub const CALCULATE_UTILIZATION_LOCK_PREFIX: &str = "util_lock";
9191
const 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+
93104
fn 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

Comments
 (0)