@@ -21,7 +21,7 @@ use near_async::{
2121use near_client:: { RpcHandlerActor , Status , ViewClientActor , client_actor:: ClientActor } ;
2222use near_indexer:: near_primitives:: transaction:: SignedTransaction ;
2323use near_indexer_primitives:: {
24- types:: { BlockReference , Finality } ,
24+ types:: { BlockHeight , BlockReference , Finality } ,
2525 views:: { BlockView , QueryRequest , QueryResponseKind } ,
2626} ;
2727use near_mpc_contract_interface:: method_names:: {
@@ -421,28 +421,79 @@ struct IndexerClient {
421421
422422const INTERVAL : Duration = Duration :: from_millis ( 500 ) ;
423423
424+ /// Consecutive non-syncing polls with head progress before the node counts as caught up.
425+ const REQUIRED_STABLE_POLLS : u32 = 4 ;
426+
424427impl IndexerClient {
428+ /// Polls sync status, yielding `(syncing, head_height)`, or `None` on a
429+ /// failed request.
430+ async fn sync_info ( & self ) -> Option < ( bool , BlockHeight ) > {
431+ let status_request = Status {
432+ is_health_check : false ,
433+ detailed : false ,
434+ } ;
435+ let Ok ( Ok ( status) ) = self
436+ . client
437+ . send_async (
438+ near_o11y:: span_wrapped_msg:: SpanWrappedMessageExt :: span_wrap ( status_request) ,
439+ )
440+ . await
441+ else {
442+ return None ;
443+ } ;
444+ Some ( (
445+ status. sync_info . syncing ,
446+ status. sync_info . latest_block_height ,
447+ ) )
448+ }
449+
450+ /// Returns once neard clears its `syncing` flag.
425451 async fn wait_for_full_sync ( & self ) {
426452 loop {
427453 tokio:: time:: sleep ( INTERVAL ) . await ;
454+ if matches ! ( self . sync_info( ) . await , Some ( ( false , _) ) ) {
455+ return ;
456+ }
457+ }
458+ }
428459
429- let status_request = Status {
430- is_health_check : false ,
431- detailed : false ,
432- } ;
433- let status_response = self
434- . client
435- . send_async (
436- near_o11y:: span_wrapped_msg:: SpanWrappedMessageExt :: span_wrap ( status_request) ,
437- )
438- . await ;
460+ async fn ensure_head_follows_tip ( & self ) {
461+ let mut progress = SyncProgress :: default ( ) ;
462+ loop {
463+ tokio:: time:: sleep ( INTERVAL ) . await ;
464+ if let Some ( ( syncing, head_height) ) = self . sync_info ( ) . await
465+ && progress. observe ( syncing, head_height)
466+ {
467+ return ;
468+ }
469+ }
470+ }
471+ }
439472
440- let Ok ( Ok ( status) ) = status_response else {
441- continue ;
442- } ;
473+ /// Reports caught-up only after [`REQUIRED_STABLE_POLLS`] consecutive
474+ /// non-syncing polls over which the head advances.
475+ #[ derive( Default ) ]
476+ struct SyncProgress {
477+ run_start_head : Option < BlockHeight > ,
478+ run_polls : u32 ,
479+ }
443480
444- if !status. sync_info . syncing {
445- return ;
481+ impl SyncProgress {
482+ fn observe ( & mut self , syncing : bool , head_height : BlockHeight ) -> bool {
483+ if syncing {
484+ self . run_start_head = None ;
485+ self . run_polls = 0 ;
486+ return false ;
487+ }
488+ match self . run_start_head {
489+ None => {
490+ self . run_start_head = Some ( head_height) ;
491+ self . run_polls = 1 ;
492+ false
493+ }
494+ Some ( start_head) => {
495+ self . run_polls += 1 ;
496+ self . run_polls >= REQUIRED_STABLE_POLLS && head_height > start_head
446497 }
447498 }
448499 }
@@ -503,3 +554,109 @@ pub struct IndexerAPI<TransactionSender, ForeignChainPolicyReader> {
503554
504555 pub foreign_chain_policy_reader : ForeignChainPolicyReader ,
505556}
557+
558+ #[ cfg( test) ]
559+ #[ expect( non_snake_case) ]
560+ mod tests {
561+ use super :: { BlockHeight , REQUIRED_STABLE_POLLS , SyncProgress } ;
562+
563+ fn first_caught_up_poll ( samples : & [ ( bool , BlockHeight ) ] ) -> Option < usize > {
564+ let mut progress = SyncProgress :: default ( ) ;
565+ samples
566+ . iter ( )
567+ . position ( |& ( syncing, head) | progress. observe ( syncing, head) )
568+ }
569+
570+ #[ test]
571+ fn observe__should_never_report_caught_up_while_syncing ( ) {
572+ // Given
573+ let samples: Vec < _ > = ( 0 ..10 ) . map ( |i| ( true , 42_000_000 + i) ) . collect ( ) ;
574+
575+ // When
576+ let caught_up_at = first_caught_up_poll ( & samples) ;
577+
578+ // Then
579+ assert_eq ! ( caught_up_at, None ) ;
580+ }
581+
582+ #[ test]
583+ fn observe__should_never_report_caught_up_with_static_head_at_genesis ( ) {
584+ // Given
585+ let genesis = 42_376_888 ;
586+ let samples: Vec < _ > = ( 0 ..10 ) . map ( |_| ( false , genesis) ) . collect ( ) ;
587+
588+ // When
589+ let caught_up_at = first_caught_up_poll ( & samples) ;
590+
591+ // Then
592+ assert_eq ! ( caught_up_at, None ) ;
593+ }
594+
595+ #[ test]
596+ fn observe__should_not_report_caught_up_before_required_polls ( ) {
597+ // Given
598+ let samples: Vec < _ > = ( 0 ..REQUIRED_STABLE_POLLS - 1 )
599+ . map ( |i| ( false , 257_000_000 + u64:: from ( i) ) )
600+ . collect ( ) ;
601+
602+ // When
603+ let caught_up_at = first_caught_up_poll ( & samples) ;
604+
605+ // Then
606+ assert_eq ! ( caught_up_at, None ) ;
607+ }
608+
609+ #[ test]
610+ fn observe__should_report_caught_up_after_sustained_progress ( ) {
611+ // Given
612+ let samples: Vec < _ > = ( 0 ..REQUIRED_STABLE_POLLS )
613+ . map ( |i| ( false , 257_000_000 + u64:: from ( i) ) )
614+ . collect ( ) ;
615+
616+ // When
617+ let caught_up_at = first_caught_up_poll ( & samples) ;
618+
619+ // Then
620+ let expected = usize:: try_from ( REQUIRED_STABLE_POLLS ) . unwrap ( ) - 1 ;
621+ assert_eq ! ( caught_up_at, Some ( expected) ) ;
622+ }
623+
624+ #[ test]
625+ fn observe__should_wait_for_head_to_advance_past_run_start ( ) {
626+ // Given
627+ let head = 257_000_000 ;
628+ let mut samples: Vec < _ > = ( 0 ..REQUIRED_STABLE_POLLS + 2 )
629+ . map ( |_| ( false , head) )
630+ . collect ( ) ;
631+ samples. push ( ( false , head + 1 ) ) ;
632+
633+ // When
634+ let caught_up_at = first_caught_up_poll ( & samples) ;
635+
636+ // Then
637+ assert_eq ! ( caught_up_at, Some ( samples. len( ) - 1 ) ) ;
638+ }
639+
640+ #[ test]
641+ fn observe__should_reset_run_when_syncing_resumes ( ) {
642+ // Given
643+ let pre = [
644+ ( false , 42_000_000 ) ,
645+ ( false , 42_000_001 ) ,
646+ ( false , 42_000_002 ) ,
647+ ] ;
648+ let resync = [ ( true , 100_000_000 ) ] ;
649+ let post: Vec < _ > = ( 0 ..REQUIRED_STABLE_POLLS )
650+ . map ( |i| ( false , 257_000_000 + u64:: from ( i) ) )
651+ . collect ( ) ;
652+ let samples: Vec < _ > = pre. iter ( ) . chain ( & resync) . chain ( & post) . copied ( ) . collect ( ) ;
653+
654+ // When
655+ let caught_up_at = first_caught_up_poll ( & samples) ;
656+
657+ // Then
658+ let expected =
659+ pre. len ( ) + resync. len ( ) + usize:: try_from ( REQUIRED_STABLE_POLLS ) . unwrap ( ) - 1 ;
660+ assert_eq ! ( caught_up_at, Some ( expected) ) ;
661+ }
662+ }
0 commit comments