@@ -190,6 +190,9 @@ const MAX_SCHEDULE_DELAY: Duration = Duration::from_millis(
190190 ( 1 << ( 6 * 6 ) ) - 1 ,
191191) ;
192192
193+ /// Warn when a scheduled function starts more than this long after it was due.
194+ const SCHEDULED_FUNCTION_DELAY_WARNING_THRESHOLD : Duration = Duration :: from_millis ( 30 ) ;
195+
193196#[ derive( thiserror:: Error , Debug ) ]
194197pub enum ScheduleError {
195198 #[ error( "Unable to schedule with long delay at {0:?}" ) ]
@@ -447,8 +450,9 @@ struct Reschedule {
447450enum ScheduledProcedureStep {
448451 Done ( CallScheduledFunctionResult , bool ) ,
449452 Procedure {
450- params : CallProcedureParams ,
453+ params : Box < CallProcedureParams > ,
451454 reschedule : Option < Reschedule > ,
455+ delay : Option < ( Arc < str > , Duration ) > ,
452456 } ,
453457}
454458
@@ -465,9 +469,17 @@ pub(super) async fn call_scheduled_procedure(
465469 // even though it has been already moved during `delete_scheduled_function_row` call.
466470 match next_step {
467471 ScheduledProcedureStep :: Done ( result, trapped) => ( result, trapped) ,
468- ScheduledProcedureStep :: Procedure { params, reschedule } => {
472+ ScheduledProcedureStep :: Procedure {
473+ params,
474+ reschedule,
475+ delay,
476+ } => {
477+ if let Some ( ( function_name, delay) ) = delay. as_ref ( ) {
478+ record_scheduled_function_delay ( module_info, function_name, * delay) ;
479+ }
480+
469481 // Execute the procedure. See above for commentary on `catch_unwind()`.
470- let result = panic:: AssertUnwindSafe ( inst_common. call_procedure ( params, inst) )
482+ let result = panic:: AssertUnwindSafe ( inst_common. call_procedure ( * params, inst) )
471483 . catch_unwind ( )
472484 . await ;
473485
@@ -504,6 +516,7 @@ fn prepare_scheduled_procedure_call(
504516 inst : & mut impl WasmInstance ,
505517) -> ScheduledProcedureStep {
506518 let ScheduledFunctionParams ( item) = params;
519+ let delay = scheduled_function_delay_context_for_item ( & item) ;
507520 let id = scheduled_item_id ( & item) ;
508521 let db = & * * module_info. relational_db ( ) ;
509522 let tx = db. begin_mut_tx ( IsolationLevel :: Serializable , Workload :: Internal ) ;
@@ -530,7 +543,13 @@ fn prepare_scheduled_procedure_call(
530543 let reschedule = id. and_then ( |id| {
531544 delete_scheduled_function_row ( module_info, db, id, Some ( tx) , ( timestamp, instant) , inst_common, inst)
532545 } ) ;
533- ScheduledProcedureStep :: Procedure { params, reschedule }
546+ let delay =
547+ delay. map ( |( function_name, requested_at) | ( function_name, scheduled_function_delay ( timestamp, requested_at) ) ) ;
548+ ScheduledProcedureStep :: Procedure {
549+ params : Box :: new ( params) ,
550+ reschedule,
551+ delay,
552+ }
534553}
535554
536555fn call_scheduled_reducer_until_done (
@@ -540,6 +559,7 @@ fn call_scheduled_reducer_until_done(
540559 inst : & mut impl WasmInstance ,
541560) -> ( CallScheduledFunctionResult , bool ) {
542561 let ScheduledFunctionParams ( item) = params;
562+ let delay = scheduled_function_delay_context_for_item ( & item) ;
543563 let id = scheduled_item_id ( & item) ;
544564 let db = & * * module_info. relational_db ( ) ;
545565 let tx = db. begin_mut_tx ( IsolationLevel :: Serializable , Workload :: Internal ) ;
@@ -561,6 +581,10 @@ fn call_scheduled_reducer_until_done(
561581 }
562582 } ;
563583
584+ if let Some ( ( function_name, requested_at) ) = delay {
585+ let delay = scheduled_function_delay ( timestamp, requested_at) ;
586+ record_scheduled_function_delay ( module_info, & function_name, delay) ;
587+ }
564588 call_scheduled_reducer_with_tx ( module_info, db, id, tx, ( timestamp, instant) , params, inst_common, inst)
565589}
566590
@@ -571,6 +595,35 @@ fn scheduled_item_id(item: &QueueItem) -> Option<ScheduledFunctionId> {
571595 }
572596}
573597
598+ fn scheduled_function_delay_context_for_item ( item : & QueueItem ) -> Option < ( Arc < str > , Timestamp ) > {
599+ match item {
600+ QueueItem :: Id { function_name, at, .. } => Some ( ( function_name. clone ( ) , * at) ) ,
601+ QueueItem :: VolatileNonatomicImmediate { .. } => None ,
602+ }
603+ }
604+
605+ fn record_scheduled_function_delay ( module_info : & ModuleInfo , function_name : & str , delay : Duration ) {
606+ module_info
607+ . metrics
608+ . observe_scheduled_function_delay ( function_name, delay) ;
609+
610+ if delay <= SCHEDULED_FUNCTION_DELAY_WARNING_THRESHOLD {
611+ return ;
612+ }
613+
614+ log:: warn!(
615+ "scheduled function `{}` for database {} is delayed by {:.3}s, exceeding the {:.3}s threshold" ,
616+ function_name,
617+ module_info. database_identity,
618+ delay. as_secs_f64( ) ,
619+ SCHEDULED_FUNCTION_DELAY_WARNING_THRESHOLD . as_secs_f64( ) ,
620+ ) ;
621+ }
622+
623+ fn scheduled_function_delay ( actual : Timestamp , requested : Timestamp ) -> Duration {
624+ actual. duration_since ( requested) . unwrap_or ( Duration :: ZERO )
625+ }
626+
574627#[ allow( clippy:: too_many_arguments) ]
575628fn call_scheduled_reducer_with_tx (
576629 module_info : & ModuleInfo ,
@@ -755,7 +808,9 @@ fn call_params_for_queued_item<T>(
755808 ) -> anyhow:: Result < ( Timestamp , Instant , T ) > ,
756809) -> anyhow:: Result < Option < ( Timestamp , Instant , T ) > > {
757810 Ok ( Some ( match item {
758- QueueItem :: Id { id, function_name, at } => {
811+ QueueItem :: Id {
812+ id, function_name, at, ..
813+ } => {
759814 let Some ( schedule_row) = get_schedule_row_mut ( tx, db, id) ? else {
760815 // If the row is not found, it means the schedule is cancelled by the user.
761816 return Ok ( None ) ;
0 commit comments