@@ -4,7 +4,7 @@ use std::{
44 Arc ,
55 atomic:: { AtomicU64 , Ordering } ,
66 } ,
7- time:: { SystemTime , UNIX_EPOCH } ,
7+ time:: { Duration , SystemTime , UNIX_EPOCH } ,
88} ;
99
1010use anyhow:: { Context , Result } ;
@@ -36,6 +36,7 @@ const DEFAULT_ANALYTICS_MAX_WINDOW_SECS: usize = 7 * 24 * 60 * 60;
3636const ANALYTICS_ROLLUP_THROTTLE_SECS : u64 = 60 ;
3737const ANALYTICS_ROLLUP_LOOKBACK_SECS : i64 = 2 * 60 * 60 ;
3838const ANALYTICS_ROLLUP_LAG_SECS : i64 = 60 ;
39+ const ANALYTICS_ROLLUP_CATCHUP_BUFFER_SECS : u64 = 5 ;
3940const DEFAULT_OVERVIEW_WINDOW_SECS : u64 = 24 * 60 * 60 ;
4041const MAX_OVERVIEW_WINDOW_SECS : u64 = 90 * 24 * 60 * 60 ;
4142const PLAYER_WATCH_DELTA_MAX_MS : u32 = 60_000 ;
@@ -2106,14 +2107,45 @@ fn schedule_analytics_rollup_refresh(state: Arc<AppState>) {
21062107 return ;
21072108 }
21082109
2110+ spawn_analytics_rollup_refresh ( state. clone ( ) , "immediate" , 0 ) ;
2111+ spawn_analytics_rollup_refresh (
2112+ state. clone ( ) ,
2113+ "lag_catchup" ,
2114+ analytics_rollup_lag_catchup_delay_secs ( ) ,
2115+ ) ;
2116+ spawn_analytics_rollup_refresh (
2117+ state,
2118+ "throttle_catchup" ,
2119+ analytics_rollup_throttle_catchup_delay_secs ( ) ,
2120+ ) ;
2121+ }
2122+
2123+ fn spawn_analytics_rollup_refresh (
2124+ state : Arc < AppState > ,
2125+ refresh_kind : & ' static str ,
2126+ delay_secs : u64 ,
2127+ ) {
21092128 tokio:: spawn ( async move {
2129+ if delay_secs > 0 {
2130+ tokio:: time:: sleep ( Duration :: from_secs ( delay_secs) ) . await ;
2131+ }
21102132 if let Err ( error) = refresh_recent_analytics_rollups ( state. clone ( ) ) . await {
21112133 state. metrics . record_analytics_rollup_failure ( ) ;
2112- tracing:: warn!( ?error, "analytics rollup refresh failed" ) ;
2134+ tracing:: warn!( ?error, refresh_kind , "analytics rollup refresh failed" ) ;
21132135 }
21142136 } ) ;
21152137}
21162138
2139+ fn analytics_rollup_lag_catchup_delay_secs ( ) -> u64 {
2140+ u64:: try_from ( ANALYTICS_ROLLUP_LAG_SECS )
2141+ . unwrap_or ( 0 )
2142+ . saturating_add ( ANALYTICS_ROLLUP_CATCHUP_BUFFER_SECS )
2143+ }
2144+
2145+ fn analytics_rollup_throttle_catchup_delay_secs ( ) -> u64 {
2146+ ANALYTICS_ROLLUP_THROTTLE_SECS . saturating_add ( analytics_rollup_lag_catchup_delay_secs ( ) )
2147+ }
2148+
21172149async fn refresh_recent_analytics_rollups (
21182150 state : Arc < AppState > ,
21192151) -> std:: result:: Result < ( ) , AppError > {
@@ -2417,6 +2449,20 @@ mod tests {
24172449 ) ;
24182450 }
24192451
2452+ #[ test]
2453+ fn rollup_catchup_delays_cover_lag_and_throttle_window ( ) {
2454+ assert_eq ! (
2455+ analytics_rollup_lag_catchup_delay_secs( ) ,
2456+ 65 ,
2457+ "first catch-up should run just after the rollup exclusion lag"
2458+ ) ;
2459+ assert_eq ! (
2460+ analytics_rollup_throttle_catchup_delay_secs( ) ,
2461+ 125 ,
2462+ "final catch-up should include events received during the throttle window"
2463+ ) ;
2464+ }
2465+
24202466 #[ test]
24212467 fn clickhouse_rows_use_event_claims_without_postgres_metadata ( ) {
24222468 let ingested_at = DateTime :: parse_from_rfc3339 ( "2026-06-13T12:00:01.000Z" )
0 commit comments