Skip to content

Commit bd3d75e

Browse files
authored
fix: indexer getting stuck when syncing from genesis (#3647)
1 parent affdfce commit bd3d75e

2 files changed

Lines changed: 140 additions & 3 deletions

File tree

crates/chain-gateway/src/chain_gateway.rs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::future::Future;
12
use std::path::Path;
23

34
use near_account_id::AccountId;
@@ -153,7 +154,7 @@ fn run_node(
153154
ready_sender: tokio::sync::oneshot::Sender<RunNodeResult>,
154155
near_config: nearcore::NearConfig,
155156
home_dir: &Path,
156-
shutdown_receiver: tokio::sync::oneshot::Receiver<()>,
157+
mut shutdown_receiver: tokio::sync::oneshot::Receiver<()>,
157158
streamer_setup: Option<StreamerSetup>,
158159
) {
159160
let rt = tokio::runtime::Builder::new_multi_thread()
@@ -185,6 +186,24 @@ fn run_node(
185186
let rpc_handler = NearRpcActorHandle::new(near_node.rpc_handler);
186187

187188
let stream = if let Some((indexer, streamer_config)) = indexer_and_params {
189+
// Don't start the streamer until synced, or its `LatestSynced`
190+
// cursor pins at genesis. Raced against shutdown to stay responsive.
191+
tracing::info!(
192+
"chain-gateway waiting for node to finish syncing before streaming blocks."
193+
);
194+
let synced = await_sync_or_shutdown(client.wait_for_full_sync(), async {
195+
let _ = (&mut shutdown_receiver).await;
196+
})
197+
.await;
198+
if !synced {
199+
tracing::info!("shutdown requested before sync completed; exiting startup.");
200+
let _ = ready_sender.send(Err(ChainGatewayError::StartupFailed {
201+
msg: "shutdown requested before node finished syncing".to_string(),
202+
}));
203+
actor_system.stop();
204+
return;
205+
}
206+
188207
let raw_stream: Receiver<StreamerMessage> = indexer.streamer();
189208
match event_subscriber::streamer::start(
190209
streamer_config,
@@ -232,3 +251,48 @@ struct StreamerSetup {
232251
indexer_config: near_indexer::IndexerConfig,
233252
near_config: NearConfig,
234253
}
254+
255+
async fn await_sync_or_shutdown(
256+
sync: impl Future<Output = ()>,
257+
shutdown: impl Future<Output = ()>,
258+
) -> bool {
259+
tokio::select! {
260+
_ = sync => true,
261+
_ = shutdown => false,
262+
}
263+
}
264+
265+
#[cfg(test)]
266+
#[expect(non_snake_case)]
267+
mod tests {
268+
use super::await_sync_or_shutdown;
269+
use std::future::pending;
270+
271+
#[tokio::test]
272+
async fn await_sync_or_shutdown__should_return_true_when_sync_completes_first() {
273+
// Given
274+
let sync = async {};
275+
let shutdown = pending::<()>();
276+
277+
// When
278+
let synced = await_sync_or_shutdown(sync, shutdown).await;
279+
280+
// Then
281+
assert!(synced);
282+
}
283+
284+
/// A shutdown (or abandoned startup) arriving while the node is still
285+
/// syncing must win the race rather than block on sync.
286+
#[tokio::test]
287+
async fn await_sync_or_shutdown__should_return_false_when_shutdown_first() {
288+
// Given
289+
let sync = pending::<()>();
290+
let shutdown = async {};
291+
292+
// When
293+
let synced = await_sync_or_shutdown(sync, shutdown).await;
294+
295+
// Then
296+
assert!(!synced);
297+
}
298+
}

crates/node/src/indexer/real.rs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use near_account_id::AccountId;
1919
use near_async::ActorSystem;
2020
use near_indexer::Indexer;
2121
use near_mpc_contract_interface::types::ProtocolContractState;
22+
use std::future::Future;
2223
use std::path::PathBuf;
2324
use 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,49 @@ pub fn spawn_real_indexer(
289316
foreign_chain_policy_reader,
290317
}
291318
}
319+
320+
async fn await_sync_or_shutdown(
321+
sync: impl Future<Output = ()>,
322+
shutdown: &CancellationToken,
323+
) -> bool {
324+
tokio::select! {
325+
_ = sync => true,
326+
_ = shutdown.cancelled() => false,
327+
}
328+
}
329+
330+
#[cfg(test)]
331+
#[expect(non_snake_case)]
332+
mod tests {
333+
use super::await_sync_or_shutdown;
334+
use std::future::pending;
335+
use tokio_util::sync::CancellationToken;
336+
337+
#[tokio::test]
338+
async fn await_sync_or_shutdown__should_return_true_when_sync_completes_first() {
339+
// Given
340+
let shutdown = CancellationToken::new();
341+
342+
// When
343+
let synced = await_sync_or_shutdown(async {}, &shutdown).await;
344+
345+
// Then
346+
assert!(synced);
347+
}
348+
349+
/// The dominant production path: a SIGTERM arrives while the node is still
350+
/// syncing, so the wait must yield to shutdown rather than block on sync.
351+
#[tokio::test]
352+
async fn await_sync_or_shutdown__should_return_false_when_shutdown_during_sync() {
353+
// Given
354+
let shutdown = CancellationToken::new();
355+
let shutdown_clone = shutdown.clone();
356+
tokio::spawn(async move { shutdown_clone.cancel() });
357+
358+
// When
359+
let synced = await_sync_or_shutdown(pending::<()>(), &shutdown).await;
360+
361+
// Then
362+
assert!(!synced);
363+
}
364+
}

0 commit comments

Comments
 (0)