From 4cc0d17d955375ff1b2d83f5897c4669cc605ad7 Mon Sep 17 00:00:00 2001 From: erhant Date: Fri, 26 Jun 2026 15:07:45 +0300 Subject: [PATCH] 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` --- lez/indexer/core/src/lib.rs | 2 ++ lez/indexer/core/src/status.rs | 41 ++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/lez/indexer/core/src/lib.rs b/lez/indexer/core/src/lib.rs index a48712fd..b2cf077a 100644 --- a/lez/indexer/core/src/lib.rs +++ b/lez/indexer/core/src/lib.rs @@ -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, } } diff --git a/lez/indexer/core/src/status.rs b/lez/indexer/core/src/status.rs index 1193e124..954cfa50 100644 --- a/lez/indexer/core/src/status.rs +++ b/lez/indexer/core/src/status.rs @@ -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, + pub chain_breaker: Option, } #[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)); + } }