@@ -19,6 +19,7 @@ use near_account_id::AccountId;
1919use near_async:: ActorSystem ;
2020use near_indexer:: Indexer ;
2121use near_mpc_contract_interface:: types:: ProtocolContractState ;
22+ use std:: future:: Future ;
2223use std:: path:: PathBuf ;
2324use std:: sync:: Arc ;
2425#[ cfg( feature = "network-hardship-simulation" ) ]
@@ -110,15 +111,41 @@ pub fn spawn_real_indexer(
110111
111112 let indexer = Indexer :: from_near_node ( near_indexer_config, near_config, & near_node) ;
112113
113- let stream = indexer. streamer ( ) ;
114-
115114 let indexer_state = Arc :: new ( IndexerState :: new (
116115 near_node. view_client ,
117116 near_node. client ,
118117 near_node. rpc_handler ,
119118 mpc_indexer_config. mpc_contract_id . clone ( ) ,
120119 ) ) ;
121120
121+ tracing:: info!( "Indexer waiting for node to finish syncing before streaming blocks." ) ;
122+
123+ // Defer all indexing work until the node has finished syncing. On a
124+ // node that state-syncs from scratch, neard sits at genesis until
125+ // sync completes; starting the streamer then pins its cursor at
126+ // genesis (`LatestSynced`), below the node's block tail, so it can
127+ // never reach the chain tip. (#3623)
128+ //
129+ // The wait is raced against `shutdown_token` so a SIGTERM during the
130+ // (possibly long) initial state sync still tears the thread down
131+ // cleanly instead of blocking until sync finishes.
132+ if !await_sync_or_shutdown (
133+ indexer_state. client . wait_for_full_sync ( ) ,
134+ & shutdown_token,
135+ )
136+ . await
137+ {
138+ tracing:: info!(
139+ "Indexer thread received shutdown signal before sync completed; exiting."
140+ ) ;
141+ let _ = indexer_exit_sender. send ( Ok ( ( ) ) ) ;
142+ return ;
143+ }
144+
145+ // The node is fully synced by this point, so `LatestSynced` resolves
146+ // to the chain tip rather than genesis.
147+ let stream = indexer. streamer ( ) ;
148+
122149 let txn_sender_result = TransactionProcessorHandle :: start_transaction_processor (
123150 my_near_account_id_clone,
124151 account_secret_key. clone ( ) ,
@@ -289,3 +316,54 @@ pub fn spawn_real_indexer(
289316 foreign_chain_policy_reader,
290317 }
291318}
319+
320+ /// Waits for `sync` to resolve, racing it against `shutdown`.
321+ ///
322+ /// Returns `true` if `sync` completed first, or `false` if `shutdown` was
323+ /// requested first. Racing the wait keeps a SIGTERM responsive during a long
324+ /// initial state sync.
325+ async fn await_sync_or_shutdown (
326+ sync : impl Future < Output = ( ) > ,
327+ shutdown : & CancellationToken ,
328+ ) -> bool {
329+ tokio:: select! {
330+ _ = sync => true ,
331+ _ = shutdown. cancelled( ) => false ,
332+ }
333+ }
334+
335+ #[ cfg( test) ]
336+ #[ expect( non_snake_case) ] // tests follow `<system_under_test>__should_<assertion>` convention
337+ mod tests {
338+ use super :: await_sync_or_shutdown;
339+ use std:: future:: pending;
340+ use tokio_util:: sync:: CancellationToken ;
341+
342+ #[ tokio:: test]
343+ async fn await_sync_or_shutdown__should_return_true_when_sync_completes_first ( ) {
344+ // Given
345+ let shutdown = CancellationToken :: new ( ) ;
346+
347+ // When
348+ let synced = await_sync_or_shutdown ( async { } , & shutdown) . await ;
349+
350+ // Then
351+ assert ! ( synced) ;
352+ }
353+
354+ /// The dominant production path: a SIGTERM arrives while the node is still
355+ /// syncing, so the wait must yield to shutdown rather than block on sync.
356+ #[ tokio:: test]
357+ async fn await_sync_or_shutdown__should_return_false_when_shutdown_during_sync ( ) {
358+ // Given
359+ let shutdown = CancellationToken :: new ( ) ;
360+ let shutdown_clone = shutdown. clone ( ) ;
361+ tokio:: spawn ( async move { shutdown_clone. cancel ( ) } ) ;
362+
363+ // When
364+ let synced = await_sync_or_shutdown ( pending :: < ( ) > ( ) , & shutdown) . await ;
365+
366+ // Then
367+ assert ! ( !synced) ;
368+ }
369+ }
0 commit comments