diff --git a/crates/op-rbuilder/src/builder/service.rs b/crates/op-rbuilder/src/builder/service.rs index 9d243edf..48f75a45 100644 --- a/crates/op-rbuilder/src/builder/service.rs +++ b/crates/op-rbuilder/src/builder/service.rs @@ -16,6 +16,7 @@ use crate::{ traits::{NodeBounds, PoolBounds}, }; use eyre::WrapErr as _; +use futures::FutureExt; use reth_node_api::NodeTypes; use reth_node_builder::{BuilderContext, components::PayloadServiceBuilder}; use reth_optimism_evm::OpEvmConfig; @@ -121,11 +122,22 @@ impl FlashblocksServiceBuilder { let mut pending_txs = pool.pending_transactions_listener_for(TransactionListenerKind::All); let pool_change_epoch = pool_change_epoch.clone(); + let listener_metrics = metrics.clone(); ctx.task_executor().spawn_task(async move { - // Every new pending tx may improve current candidate - while pending_txs.recv().await.is_some() { - pool_change_epoch.fetch_add(1, Ordering::Relaxed); - } + let listen = async { + // Every new pending tx may improve current candidate + while pending_txs.recv().await.is_some() { + let epoch = pool_change_epoch.fetch_add(1, Ordering::Relaxed) + 1; + listener_metrics.pool_change_epoch.set(epoch as f64); + } + }; + let _ = std::panic::AssertUnwindSafe(listen).catch_unwind().await; + listener_metrics.pool_epoch_listener_exit.increment(1); + warn!( + target: "payload_builder", + "Pool epoch listener exited (channel closed or task panicked); \ + continuous candidate gating is now frozen and will degrade to idle-backoff" + ); }); } diff --git a/crates/op-rbuilder/src/metrics.rs b/crates/op-rbuilder/src/metrics.rs index 1aa9f251..5546839d 100644 --- a/crates/op-rbuilder/src/metrics.rs +++ b/crates/op-rbuilder/src/metrics.rs @@ -198,6 +198,14 @@ pub struct OpRBuilderMetrics { /// candidate loop: best-candidate fees minus first-candidate fees. /// Recorded once per flashblock interval; 0 if no improvement. pub continuous_fee_improvement: Histogram, + /// Current value of the continuous-build pool-change epoch counter. + /// A flatlined gauge on a dashboard means the pool listener died, not + /// that the pool is idle. + pub pool_change_epoch: Gauge, + /// Pool-change epoch listener task exited (pending-tx channel closed or + /// panicked); continuous candidate gating is frozen and degrades to its + /// idle-backoff ceiling until restart. + pub pool_epoch_listener_exit: Counter, /// Builds completed but not published due to resolved gate pub flashblock_publish_suppressed_total: Counter, /// TODO: Use labels for these