feat(indexer): add Stalled status and chain breaker snapshot

test `RISC0_DEV_MODE=1 RISC0_SKIP_BUILD=1 cargo test -p indexer_core --lib status`
This commit is contained in:
erhant 2026-06-26 15:07:45 +03:00
parent fc3692813a
commit 4cc0d17d95
2 changed files with 43 additions and 0 deletions

View File

@ -62,9 +62,11 @@ impl IndexerCore {
pub fn status(&self) -> IndexerStatus {
let sync = IndexerSyncStatus::clone(&self.status.load());
let indexed_block_id = self.store.get_last_block_id().ok().flatten();
let chain_breaker = self.store.get_chain_breaker().ok().flatten();
IndexerStatus {
sync,
indexed_block_id,
chain_breaker,
}
}

View File

@ -1,5 +1,7 @@
use serde::Serialize;
use crate::chain_breaker::ChainBreaker;
/// Coarse lifecycle state of the indexer's ingestion loop, so a client can tell
/// "still catching up" apart from "something went wrong".
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
@ -13,6 +15,9 @@ pub enum IndexerSyncState {
CaughtUp,
/// The last cycle failed (e.g. the L1 node is unreachable). See `last_error`.
Error,
/// Parked on a chain breaker: the validated tip is frozen awaiting a valid
/// continuation. See `last_error` and the snapshot's `chain_breaker`.
Stalled,
}
/// Live ingestion status owned by the ingest loop: the coarse `state` plus the
@ -56,6 +61,15 @@ impl IndexerSyncStatus {
last_error: Some(reason),
}
}
/// Parked on a chain breaker; `reason` mirrors the breaker's error message.
/// The full breaker is attached to the [`IndexerStatus`] snapshot.
pub(crate) const fn stalled(reason: String) -> Self {
Self {
state: IndexerSyncState::Stalled,
last_error: Some(reason),
}
}
}
/// Full status snapshot returned to callers (FFI/RPC): the live [`IndexerSyncStatus`]
@ -69,6 +83,7 @@ pub struct IndexerStatus {
#[serde(flatten)]
pub sync: IndexerSyncStatus,
pub indexed_block_id: Option<u64>,
pub chain_breaker: Option<ChainBreaker>,
}
#[cfg(test)]
@ -80,6 +95,7 @@ mod tests {
let status = IndexerStatus {
sync: IndexerSyncStatus::error("boom".to_owned()),
indexed_block_id: Some(7),
chain_breaker: None,
};
let value = serde_json::to_value(&status).expect("serialize");
assert_eq!(
@ -88,6 +104,7 @@ mod tests {
"state": "error",
"lastError": "boom",
"indexedBlockId": 7,
"chainBreaker": null,
})
);
}
@ -100,4 +117,28 @@ mod tests {
serde_json::json!({ "state": "caught_up", "lastError": null })
);
}
#[test]
fn stalled_status_serializes_with_breaker() {
use crate::{chain_breaker::ChainBreaker, ingest_error::BlockIngestError};
let status = IndexerStatus {
sync: IndexerSyncStatus::stalled("broken chain link".to_owned()),
indexed_block_id: Some(41),
chain_breaker: Some(ChainBreaker {
block_id: Some(42),
block_hash: None,
prev_block_hash: None,
l1_slot: serde_json::Value::Null,
error: BlockIngestError::StateTransition("boom".to_owned()),
first_seen: None,
orphans_since: 2,
}),
};
let value = serde_json::to_value(&status).expect("serialize");
assert_eq!(value["state"], serde_json::json!("stalled"));
assert_eq!(value["lastError"], serde_json::json!("broken chain link"));
assert_eq!(value["indexedBlockId"], serde_json::json!(41));
assert_eq!(value["chainBreaker"]["orphansSince"], serde_json::json!(2));
}
}