Skip to content

Commit 1cb4ae0

Browse files
committed
refactor: extract post-seal side-effects into PostSealHook trait
Move WS publication, p2p forwarding, engine propagation, and per-flashblock metrics out of the building loop and behind a PostSealHook trait. Hooks are constructed once in OpPayloadBuilder::new() and dispatched after each sealed candidate (fallback + every flashblock). build_next_flashblock no longer performs side-effects: it returns a FlashblockBuildResult struct and the async loop handles dispatch. The side-effect call sites in the fallback path and the FB iteration are collapsed to a single dispatch_post_seal call each. p2p and engine forwarders are unified as ChannelHook(name, sender) since they only differ in the failure log label.
1 parent 55edd74 commit 1cb4ae0

7 files changed

Lines changed: 292 additions & 100 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use crate::builder::hooks::post_seal::{PostSealHook, SealedCandidate, SealedCtx};
2+
use reth_optimism_node::OpBuiltPayload;
3+
use tokio::sync::mpsc;
4+
use tracing::warn;
5+
6+
/// Forwards each sealed candidate over a named mpsc channel.
7+
#[derive(Debug)]
8+
pub(in crate::builder) struct ChannelHook {
9+
name: &'static str,
10+
sender: mpsc::Sender<OpBuiltPayload>,
11+
}
12+
13+
impl ChannelHook {
14+
pub(in crate::builder) fn new(
15+
name: &'static str,
16+
sender: mpsc::Sender<OpBuiltPayload>,
17+
) -> Self {
18+
Self { name, sender }
19+
}
20+
}
21+
22+
impl PostSealHook for ChannelHook {
23+
fn on_sealed(&self, candidate: &SealedCandidate, ctx: &SealedCtx) {
24+
if let Err(e) = self.sender.try_send(candidate.payload.clone()) {
25+
warn!(
26+
target: "payload_builder",
27+
channel = self.name,
28+
error = %e,
29+
flashblock_index = ctx.flashblock_index,
30+
"Failed to forward sealed payload over channel"
31+
);
32+
}
33+
}
34+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use crate::{
2+
builder::hooks::post_seal::{PostSealHook, SealedCandidate, SealedCtx},
3+
metrics::OpRBuilderMetrics,
4+
};
5+
use std::sync::Arc;
6+
7+
/// Records per-flashblock metrics that aren't tied to publication:
8+
/// build duration and transaction-count histogram.
9+
#[derive(Debug)]
10+
pub(in crate::builder) struct MetricsHook {
11+
metrics: Arc<OpRBuilderMetrics>,
12+
}
13+
14+
impl MetricsHook {
15+
pub(in crate::builder) fn new(metrics: Arc<OpRBuilderMetrics>) -> Self {
16+
Self { metrics }
17+
}
18+
}
19+
20+
impl PostSealHook for MetricsHook {
21+
fn on_sealed(&self, _candidate: &SealedCandidate, ctx: &SealedCtx) {
22+
if let Some(duration) = ctx.flashblock_build_duration {
23+
self.metrics.flashblock_build_duration.record(duration);
24+
}
25+
self.metrics
26+
.flashblock_num_tx_histogram
27+
.record(ctx.executed_tx_count as f64);
28+
}
29+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//! Lifecycle hooks dispatched from the building loop.
2+
//!
3+
//! [`PostSealHook`] fires after each sealed candidate (fallback or
4+
//! flashblock) and isolates downstream side-effects — WS publication, p2p
5+
//! broadcast, engine propagation, metrics — from the building loop itself.
6+
//! Concrete implementations live alongside this module.
7+
8+
mod channel;
9+
mod metrics;
10+
mod post_seal;
11+
mod ws;
12+
13+
pub(super) use channel::ChannelHook;
14+
pub(super) use metrics::MetricsHook;
15+
pub(super) use post_seal::{PostSealHook, SealedCandidate, SealedCtx};
16+
pub(super) use ws::WsHook;
17+
18+
/// Dispatch a sealed candidate to every hook in `hooks`.
19+
///
20+
/// Hook impls are expected to be cheap; we run them sequentially in the
21+
/// caller's context (sync, including from within `spawn_blocking`).
22+
pub(super) fn dispatch_post_seal(
23+
hooks: &[Box<dyn PostSealHook>],
24+
candidate: &SealedCandidate,
25+
ctx: &SealedCtx,
26+
) {
27+
for h in hooks {
28+
h.on_sealed(candidate, ctx);
29+
}
30+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use core::time::Duration;
2+
use op_alloy_rpc_types_engine::OpFlashblockPayload;
3+
use reth_optimism_node::OpBuiltPayload;
4+
use reth_payload_builder::PayloadId;
5+
6+
/// A flashblock candidate that has been sealed and is ready for publication
7+
/// and downstream propagation.
8+
///
9+
/// `payload` is the full built payload for engine/p2p delivery; `fb_payload`
10+
/// is the slim, serialisable view streamed to flashblocks subscribers.
11+
#[derive(Debug, Clone)]
12+
pub(in crate::builder) struct SealedCandidate {
13+
pub payload: OpBuiltPayload,
14+
pub fb_payload: OpFlashblockPayload,
15+
}
16+
17+
/// Context describing the slot a sealed candidate belongs to.
18+
///
19+
/// The fields are intentionally limited to data downstream hooks need.
20+
#[derive(Debug, Clone)]
21+
pub(in crate::builder) struct SealedCtx {
22+
pub payload_id: PayloadId,
23+
pub block_number: u64,
24+
pub flashblock_index: u64,
25+
/// True when the FCU specified `no_tx_pool`.
26+
pub no_tx_pool: bool,
27+
pub executed_tx_count: usize,
28+
/// Slot start timestamp from the payload attributes.
29+
pub slot_timestamp_secs: u64,
30+
pub block_time: Duration,
31+
/// Wall-clock time spent building this flashblock.
32+
/// `None` for the fallback candidate.
33+
pub flashblock_build_duration: Option<Duration>,
34+
pub enable_tx_tracking_debug_logs: bool,
35+
}
36+
37+
/// Hook invoked after a flashblock or fallback candidate has been sealed.
38+
///
39+
/// Implementations should be cheap and non-blocking: dispatch happens on the
40+
/// builder's hot path. Errors are intentionally swallowed at the dispatch site;
41+
/// hooks that want to surface failures should do so via metrics or logs.
42+
pub(in crate::builder) trait PostSealHook:
43+
Send + Sync + std::fmt::Debug
44+
{
45+
fn on_sealed(&self, candidate: &SealedCandidate, ctx: &SealedCtx);
46+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
use crate::{
2+
builder::{
3+
hooks::post_seal::{PostSealHook, SealedCandidate, SealedCtx},
4+
timing::compute_slot_offset_ms,
5+
wspub::WebSocketPublisher,
6+
},
7+
metrics::{OpRBuilderMetrics, record_flashblock_publish_timing},
8+
};
9+
use std::sync::Arc;
10+
use tracing::{debug, warn};
11+
12+
/// Publishes the flashblock payload to WebSocket subscribers, record metrics.
13+
///
14+
/// Suppressed when `SealedCtx::no_tx_pool` is true
15+
pub(in crate::builder) struct WsHook {
16+
ws_pub: Arc<WebSocketPublisher>,
17+
metrics: Arc<OpRBuilderMetrics>,
18+
}
19+
20+
impl std::fmt::Debug for WsHook {
21+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22+
f.debug_struct("WsHook").finish_non_exhaustive()
23+
}
24+
}
25+
26+
impl WsHook {
27+
pub(in crate::builder) fn new(
28+
ws_pub: Arc<WebSocketPublisher>,
29+
metrics: Arc<OpRBuilderMetrics>,
30+
) -> Self {
31+
Self { ws_pub, metrics }
32+
}
33+
}
34+
35+
impl PostSealHook for WsHook {
36+
fn on_sealed(&self, candidate: &SealedCandidate, ctx: &SealedCtx) {
37+
if ctx.no_tx_pool {
38+
return;
39+
}
40+
41+
let byte_size = match self.ws_pub.publish(&candidate.fb_payload) {
42+
Ok(size) => size,
43+
Err(e) => {
44+
warn!(
45+
target: "payload_builder",
46+
error = %e,
47+
flashblock_index = ctx.flashblock_index,
48+
"Failed to publish flashblock via websocket"
49+
);
50+
return;
51+
}
52+
};
53+
54+
let slot_offset_ms = compute_slot_offset_ms(ctx.slot_timestamp_secs, ctx.block_time);
55+
record_flashblock_publish_timing(candidate.fb_payload.index, slot_offset_ms);
56+
self.metrics
57+
.flashblock_byte_size_histogram
58+
.record(byte_size as f64);
59+
60+
if ctx.enable_tx_tracking_debug_logs {
61+
debug!(
62+
target: "tx_trace",
63+
payload_id = %ctx.payload_id,
64+
block_number = ctx.block_number,
65+
flashblock_index = candidate.fb_payload.index,
66+
byte_size,
67+
total_txs = ctx.executed_tx_count,
68+
slot_offset_ms,
69+
stage = "fb_published"
70+
);
71+
}
72+
}
73+
}

crates/op-rbuilder/src/builder/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ mod config;
1717
mod context;
1818
mod flashblocks_builder_tx;
1919
mod generator;
20+
mod hooks;
2021
mod p2p;
2122
mod payload;
2223
mod payload_handler;

0 commit comments

Comments
 (0)