|
pub async fn start( |
|
view_client: IndexerViewClientFetcher, |
|
client: IndexerClientFetcher, |
|
shard_tracker: ShardTracker, |
|
indexer_config: IndexerConfig, |
|
store_config: near_store::StoreConfig, |
|
blocks_sink: mpsc::Sender<StreamerMessage>, |
|
clock: Clock, |
|
) { |
|
tracing::info!(target: INDEXER, "starting streamer"); |
|
let indexer_db_path = |
|
near_store::NodeStorage::opener(&indexer_config.home_dir, &store_config, None, None) |
|
.path() |
|
.join("indexer"); |
|
|
|
let db = match DB::open_default(indexer_db_path) { |
|
Ok(db) => db, |
|
Err(err) => panic!("Unable to open indexer db: {:?}", err), |
|
}; |
|
|
|
let mut last_synced_block_height: Option<BlockHeight> = None; |
|
// Consecutive failed attempts to build a streamer message; reset on success. |
|
// In `WaitForFullSync` mode it is also reset while the node is syncing (see |
|
// `MAX_BUILD_STREAMER_MESSAGE_ATTEMPTS`). |
|
let mut build_streamer_message_attempts: u32 = 0; |
|
|
|
'main: loop { |
|
clock.sleep(INTERVAL).await; |
|
match indexer_config.await_for_node_synced { |
|
AwaitForNodeSyncedEnum::WaitForFullSync => { |
|
let status = client.fetch_status().await; |
|
let Ok(status) = status else { |
|
tracing::error!(target: INDEXER, ?status, "failed to fetch node status, retrying"); |
|
continue; |
|
}; |
|
if status.sync_info.syncing { |
|
tracing::debug!(target: INDEXER, ?status, "the node is syncing, waiting"); |
|
continue; |
|
} |
|
} |
|
AwaitForNodeSyncedEnum::StreamWhileSyncing => {} |
|
}; |
|
|
|
tracing::debug!(target: INDEXER, "starting streaming the next block range"); |
|
let block = view_client.fetch_latest_block(indexer_config.finality.clone()).await; |
|
let Ok(block) = block else { |
|
tracing::error!(target: INDEXER, ?block, "failed to fetch latest block, retrying"); |
|
continue; |
|
}; |
|
|
|
let latest_block_height = block.header.height; |
|
let start_syncing_block_height = get_start_syncing_block_height( |
|
&db, |
|
&indexer_config, |
|
last_synced_block_height, |
|
latest_block_height, |
|
); |
|
|
|
tracing::debug!( |
|
target: INDEXER, |
|
%start_syncing_block_height, |
|
%latest_block_height, |
|
"streaming is about to start", |
|
); |
|
metrics::START_BLOCK_HEIGHT.set(start_syncing_block_height as i64); |
|
metrics::LATEST_BLOCK_HEIGHT.set(latest_block_height as i64); |
|
for block_height in start_syncing_block_height..=latest_block_height { |
|
metrics::CURRENT_BLOCK_HEIGHT.set(block_height as i64); |
|
|
|
let block = match view_client.fetch_block_by_height(block_height).await { |
|
Ok(Some(block)) => block, |
|
Ok(None) => { |
|
tracing::debug!(target: INDEXER, ?block_height, "skip height - missing block"); |
|
continue; |
|
} |
|
Err(err) => { |
|
tracing::error!(target: INDEXER, ?block_height, ?err, "skip height - failed to fetch block"); |
|
continue; |
|
} |
|
}; |
|
|
|
let streamer_message = |
|
Box::pin(build_streamer_message(&view_client, block, &shard_tracker)).await; |
|
let streamer_message = match streamer_message { |
|
Ok(streamer_message) => { |
|
build_streamer_message_attempts = 0; |
|
streamer_message |
|
} |
|
Err(err) => { |
|
// When waiting for full sync, a build failure while the node |
|
// is not yet ready is expected (e.g. epoch data not available |
|
// after a restart, see #15867): retry the same height forever |
|
// without counting it against the budget. When streaming while |
|
// syncing the node is ~always "syncing", so that gate would |
|
// make the budget unreachable; there we count every failure so |
|
// a genuinely stuck height eventually surfaces. |
|
let transient_while_syncing = matches!( |
|
indexer_config.await_for_node_synced, |
|
AwaitForNodeSyncedEnum::WaitForFullSync |
|
) && !node_is_ready(&client).await; |
|
if transient_while_syncing { |
|
build_streamer_message_attempts = 0; |
|
tracing::warn!(target: INDEXER, ?block_height, ?err, "failed to build streamer message while the node is syncing, retrying the same height"); |
|
} else { |
|
build_streamer_message_attempts += 1; |
|
tracing::error!(target: INDEXER, ?block_height, ?err, attempts = build_streamer_message_attempts, "failed to build streamer message, retrying the same height"); |
|
assert!( |
|
build_streamer_message_attempts < MAX_BUILD_STREAMER_MESSAGE_ATTEMPTS, |
|
"failed to build streamer message at height {block_height} after {MAX_BUILD_STREAMER_MESSAGE_ATTEMPTS} attempts: {err:?}" |
|
); |
|
} |
|
// Retry the same height on the next outer iteration instead of |
|
// advancing `last_synced_block_height`. |
|
break; |
|
} |
|
}; |
|
|
|
tracing::debug!(target: INDEXER, ?block_height, "sending streamer message to the listener"); |
|
let send_result = blocks_sink.send(streamer_message).await; |
|
if send_result.is_err() { |
|
tracing::error!( |
|
target: INDEXER, |
|
?block_height, |
|
?send_result, |
|
"unable to send streamer message to listener, listener doesn't listen, terminating", |
|
); |
|
break 'main; |
|
}; |
|
|
|
metrics::NUM_STREAMER_MESSAGES_SENT.inc(); |
|
db.put(b"last_synced_block_height", &block_height.to_string()).unwrap(); |
|
last_synced_block_height = Some(block_height); |
|
} |
|
} |
|
} |
|
|
|
fn get_start_syncing_block_height( |
|
db: &rocksdb::DB, |
|
indexer_config: &IndexerConfig, |
|
last_synced_block_height: Option<u64>, |
|
latest_block_height: u64, |
|
) -> u64 { |
|
// If last synced is set, start from the next height |
|
if let Some(last_synced_block_height) = last_synced_block_height { |
|
return last_synced_block_height + 1; |
Background
The MPC team recently observed an issue with the nearcore indexer. While the view client had a correct view of the chain and could serve finalized contract state, the indexer streamer was way behind the tip of the chain, close to genesis (c.f. mpc-#3623).
The MPC node spins up the nearcore indexer with
SyncModeEnum::LatestSyncedandawait_for_node_synced: StreamWhileSyncingin the following order:status.sync_info.syncing == falsehttps://github.com/near/mpc/blob/5b2e62bf5b2c6d7763d6e167ce5e7b713ed86e53/crates/node/src/indexer/real.rs#L127-L170
https://github.com/near/mpc/blob/5b2e62bf5b2c6d7763d6e167ce5e7b713ed86e53/crates/node/src/indexer.rs#L424-L449
The issue is that the client may report
status.sync_info.syncing = falsedespite not having caught up with the tip of the chain. This then leads to the indexer stream attempting to stream every message from genesis.nearcore/chain/indexer/src/streamer/mod.rs
Lines 259 to 403 in 3cae67a
For the MPC node, this has the following effects:
Why the indexer reports
status.sync_info.syncing = falseafter startupstatus.sync_info.syncing = falsegets set innearcore/chain/client/src/client_actor.rs
Line 905 in 3cae67a
SyncStatus::is_syncing(&self)returnsfalseifself == SyncStatus::NoSync:nearcore/chain/client-primitives/src/types.rs
Lines 168 to 173 in 3cae67a
sync_statusis updated inClientActor::run_sync_stepand is set toNoSyncwhen the client has no eligible peers:nearcore/chain/client/src/client_actor.rs
Lines 1817 to 1844 in 3cae67a
The client reports
SyncRequirement::NoPeerswhen it has no eligible peers (i.e. no peer whose block height is known):nearcore/chain/client/src/client_actor.rs
Line 1695 in 3cae67a
It seems like this is possible in two scenarios:
ConnectedPeerState.block_info) isNoneat handshake and only set once that peer sends its firstBlockmessage, sohighest_height_peersis empty despite the connected peers — causingsyncing_info()to returnNoPeers:Noneon connect:nearcore/chain/network/src/peer_manager/network_state/mod.rs
Line 498 in 3cae67a
PeerMessage::Block:nearcore/chain/network/src/peer_manager/network_state/mod.rs
Line 1157 in 3cae67a
Scenario 2 is the one that affects the MPC node: it briefly reports
syncing = falsewhile still at genesis, the streamer starts, and itsLatestSyncedcursor pins at genesis.Proposed Fix
The MPC node does not run a validator; it is a follower that always has to sync from peers. A follower that has not yet confirmed any peer's height has no way of knowing it is caught up, so it should keep reporting
syncing = truerather than declaring itself synced.Concretely:
SyncRequirement::NoPeersshould only result inNoSyncwhen the node is a validator (which may legitimately bootstrap a fresh or one-node network). For a non-validator node,NoPeersshould instead keep the node inAwaitingPeers(syncing = true) until it has confirmed a peer's height. This preserves the intended single-node-validator behavior (scenario 1) while fixing scenario 2.edit:
So, this proposed fix isn't super robust. It only resolves the issue for non-validator nodes. A better approach might be to have a boolean to indicate if the node is supposed to be the only validator in the network.