@@ -10,21 +10,60 @@ use near_indexer::near_primitives::transaction::SignedTransaction;
1010use std:: future:: Future ;
1111use std:: time:: Duration ;
1212
13+ /// Snapshot of the node's sync progress relative to its peers.
14+ pub ( crate ) struct SyncStatus {
15+ /// Whether the node currently reports itself as syncing.
16+ pub syncing : bool ,
17+ /// Height of the node's local head.
18+ pub head_height : u64 ,
19+ /// Highest block height advertised by a connected peer, if any.
20+ pub max_peer_height : Option < u64 > ,
21+ }
22+
23+ /// How far behind the highest peer the local head may be while still counting
24+ /// as caught up. Absorbs peers advancing a block or two between polls; the
25+ /// `syncing` flag carries the steady state.
26+ const SYNC_HEIGHT_TOLERANCE : u64 = 5 ;
27+
28+ impl SyncStatus {
29+ /// Whether the node is fully synced to the network head.
30+ ///
31+ /// The `syncing` flag alone is insufficient: on a freshly state-syncing
32+ /// node (or one returning from long downtime) it reads `false` during the
33+ /// startup window before the node has learned it is behind, which would pin
34+ /// the streamer's `LatestSynced` cursor at a stale head it can never reach.
35+ /// We also require the head to be within [`SYNC_HEIGHT_TOLERANCE`] of the
36+ /// highest connected peer. While no peer reports a height we cannot confirm
37+ /// we are caught up, so we keep waiting.
38+ fn is_caught_up ( & self ) -> bool {
39+ if self . syncing {
40+ return false ;
41+ }
42+ match self . max_peer_height {
43+ None => false ,
44+ Some ( peer_height) => {
45+ peer_height. saturating_sub ( self . head_height ) <= SYNC_HEIGHT_TOLERANCE
46+ }
47+ }
48+ }
49+ }
50+
1351/// Low-level trait for checking indexer sync status.
1452pub ( crate ) trait IsSyncing : Send + Sync + ' static {
1553 type Error : std:: error:: Error + Send + Sync + ' static ;
16- /// Returns whether the node is currently syncing .
17- fn is_syncing ( & self ) -> impl Future < Output = Result < bool , Self :: Error > > + Send ;
54+ /// Returns the node's current sync progress relative to its peers .
55+ fn sync_status ( & self ) -> impl Future < Output = Result < SyncStatus , Self :: Error > > + Send ;
1856
1957 const INTERVAL : Duration = Duration :: from_millis ( 500 ) ;
20- /// Polls [`is_syncing`](Self::is_syncing) until the node is fully synced.
58+ /// Polls [`sync_status`](Self::sync_status) until the node has caught up to
59+ /// the network head.
2160 fn wait_for_full_sync ( & self ) -> impl Future < Output = ( ) > + Send {
2261 async {
2362 let mut attempt = 0u32 ;
2463 loop {
25- match self . is_syncing ( ) . await {
26- Ok ( false ) => return ,
27- Ok ( true ) => {
64+ match self . sync_status ( ) . await {
65+ Ok ( status ) if status . is_caught_up ( ) => return ,
66+ Ok ( _ ) => {
2867 if attempt. is_multiple_of ( 120 ) {
2968 tracing:: info!( "has been syncing for: {} seconds" , attempt / 2 ) ;
3069 }
@@ -65,3 +104,132 @@ pub(crate) trait SubmitSignedTransaction: Send + Sync + 'static {
65104 transaction : SignedTransaction ,
66105 ) -> impl Future < Output = Result < ( ) , Self :: Error > > + Send ;
67106}
107+
108+ #[ cfg( test) ]
109+ #[ expect( non_snake_case) ]
110+ mod tests {
111+ use super :: { SYNC_HEIGHT_TOLERANCE , SyncStatus } ;
112+
113+ #[ test]
114+ fn is_caught_up__should_be_false_while_node_reports_syncing ( ) {
115+ // Given
116+ let status = SyncStatus {
117+ syncing : true ,
118+ head_height : 257_000_000 ,
119+ max_peer_height : Some ( 257_000_000 ) ,
120+ } ;
121+
122+ // When
123+ let caught_up = status. is_caught_up ( ) ;
124+
125+ // Then
126+ assert ! ( !caught_up) ;
127+ }
128+
129+ /// The #3623 wedge: at fresh boot the node sits at genesis with `syncing`
130+ /// transiently `false` before it has learned a peer is far ahead.
131+ #[ test]
132+ fn is_caught_up__should_be_false_at_genesis_before_sync_starts ( ) {
133+ // Given
134+ let status = SyncStatus {
135+ syncing : false ,
136+ head_height : 42_376_888 ,
137+ max_peer_height : Some ( 257_409_058 ) ,
138+ } ;
139+
140+ // When
141+ let caught_up = status. is_caught_up ( ) ;
142+
143+ // Then
144+ assert ! ( !caught_up) ;
145+ }
146+
147+ /// Same wedge after long downtime: the stale head is far above genesis but
148+ /// still far below the peers.
149+ #[ test]
150+ fn is_caught_up__should_be_false_with_stale_head_far_above_genesis ( ) {
151+ // Given
152+ let status = SyncStatus {
153+ syncing : false ,
154+ head_height : 200_000_000 ,
155+ max_peer_height : Some ( 257_409_058 ) ,
156+ } ;
157+
158+ // When
159+ let caught_up = status. is_caught_up ( ) ;
160+
161+ // Then
162+ assert ! ( !caught_up) ;
163+ }
164+
165+ /// Until a peer advertises a height we cannot confirm we are caught up.
166+ #[ test]
167+ fn is_caught_up__should_be_false_when_no_peer_height_known ( ) {
168+ // Given
169+ let status = SyncStatus {
170+ syncing : false ,
171+ head_height : 257_409_058 ,
172+ max_peer_height : None ,
173+ } ;
174+
175+ // When
176+ let caught_up = status. is_caught_up ( ) ;
177+
178+ // Then
179+ assert ! ( !caught_up) ;
180+ }
181+
182+ #[ test]
183+ fn is_caught_up__should_be_true_when_head_reaches_peer_height ( ) {
184+ // Given
185+ let peer_head = 257_409_058 ;
186+ let status = SyncStatus {
187+ syncing : false ,
188+ head_height : peer_head,
189+ max_peer_height : Some ( peer_head) ,
190+ } ;
191+
192+ // When
193+ let caught_up = status. is_caught_up ( ) ;
194+
195+ // Then
196+ assert ! ( caught_up) ;
197+ }
198+
199+ /// Peers may advance a few blocks between polls; being within the tolerance
200+ /// still counts as caught up.
201+ #[ test]
202+ fn is_caught_up__should_be_true_within_tolerance_of_peer_height ( ) {
203+ // Given
204+ let peer_head = 257_409_058 ;
205+ let status = SyncStatus {
206+ syncing : false ,
207+ head_height : peer_head - SYNC_HEIGHT_TOLERANCE ,
208+ max_peer_height : Some ( peer_head) ,
209+ } ;
210+
211+ // When
212+ let caught_up = status. is_caught_up ( ) ;
213+
214+ // Then
215+ assert ! ( caught_up) ;
216+ }
217+
218+ /// A node slightly ahead of the peers it currently sees is caught up.
219+ #[ test]
220+ fn is_caught_up__should_be_true_when_head_above_peer_height ( ) {
221+ // Given
222+ let peer_head = 257_409_058 ;
223+ let status = SyncStatus {
224+ syncing : false ,
225+ head_height : peer_head + 10 ,
226+ max_peer_height : Some ( peer_head) ,
227+ } ;
228+
229+ // When
230+ let caught_up = status. is_caught_up ( ) ;
231+
232+ // Then
233+ assert ! ( caught_up) ;
234+ }
235+ }
0 commit comments