reth_engine_tree/
backfill.rs1use futures::FutureExt;
11use reth_provider::providers::ProviderNodeTypes;
12use reth_stages_api::{ControlFlow, Pipeline, PipelineError, PipelineTarget, PipelineWithResult};
13use reth_tasks::Runtime;
14use std::task::{ready, Context, Poll};
15use tokio::sync::oneshot;
16use tracing::trace;
17
18#[derive(Debug, PartialEq, Eq, Default)]
20pub enum BackfillSyncState {
21 #[default]
24 Idle,
25 Pending,
27 PendingRevalidation,
29 Active,
31}
32
33impl BackfillSyncState {
34 pub const fn is_idle(&self) -> bool {
36 matches!(self, Self::Idle)
37 }
38
39 pub const fn is_pending(&self) -> bool {
41 matches!(self, Self::Pending)
42 }
43
44 pub const fn is_pending_revalidation(&self) -> bool {
46 matches!(self, Self::PendingRevalidation)
47 }
48
49 pub const fn is_active(&self) -> bool {
51 matches!(self, Self::Active)
52 }
53}
54
55pub trait BackfillSync: Send {
57 fn on_action(&mut self, action: BackfillAction);
59
60 fn poll(&mut self, cx: &mut Context<'_>) -> Poll<BackfillEvent>;
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum BackfillAction {
67 Start(PipelineTarget),
69}
70
71#[derive(Debug)]
73pub enum BackfillEvent {
74 Started(PipelineTarget),
76 Finished(Result<ControlFlow, PipelineError>),
80 TaskDropped(String),
83}
84
85#[derive(Debug)]
87pub struct PipelineSync<N: ProviderNodeTypes> {
88 pipeline_task_spawner: Runtime,
90 pipeline_state: PipelineState<N>,
93 pending_pipeline_target: Option<PipelineTarget>,
95}
96
97impl<N: ProviderNodeTypes> PipelineSync<N> {
98 pub fn new(pipeline: Pipeline<N>, pipeline_task_spawner: Runtime) -> Self {
100 Self {
101 pipeline_task_spawner,
102 pipeline_state: PipelineState::Idle(Some(Box::new(pipeline))),
103 pending_pipeline_target: None,
104 }
105 }
106
107 #[expect(dead_code)]
109 const fn is_pipeline_sync_pending(&self) -> bool {
110 self.pending_pipeline_target.is_some() && self.pipeline_state.is_idle()
111 }
112
113 const fn is_pipeline_idle(&self) -> bool {
115 self.pipeline_state.is_idle()
116 }
117
118 const fn is_pipeline_active(&self) -> bool {
120 !self.is_pipeline_idle()
121 }
122
123 fn set_pipeline_sync_target(&mut self, target: PipelineTarget) {
127 if target.sync_target().is_some_and(|target| target.is_zero()) {
128 trace!(
129 target: "consensus::engine::sync",
130 "Pipeline target cannot be zero hash."
131 );
132 return
134 }
135 self.pending_pipeline_target = Some(target);
136 }
137
138 fn try_spawn_pipeline(&mut self) -> Option<BackfillEvent> {
141 match &mut self.pipeline_state {
142 PipelineState::Idle(pipeline) => {
143 let target = self.pending_pipeline_target.take()?;
144 let (tx, rx) = oneshot::channel();
145
146 let pipeline = pipeline.take().expect("exists");
147 self.pipeline_task_spawner.spawn_critical_blocking_task(
148 "pipeline task",
149 async move {
150 let result = pipeline.run_as_fut(Some(target)).await;
151 let _ = tx.send(result);
152 },
153 );
154 self.pipeline_state = PipelineState::Running(rx);
155
156 Some(BackfillEvent::Started(target))
157 }
158 PipelineState::Running(_) => None,
159 }
160 }
161
162 fn poll_pipeline(&mut self, cx: &mut Context<'_>) -> Poll<BackfillEvent> {
166 let res = match self.pipeline_state {
167 PipelineState::Idle(_) => return Poll::Pending,
168 PipelineState::Running(ref mut fut) => {
169 ready!(fut.poll_unpin(cx))
170 }
171 };
172 let ev = match res {
173 Ok((pipeline, result)) => {
174 self.pipeline_state = PipelineState::Idle(Some(Box::new(pipeline)));
175 BackfillEvent::Finished(result)
176 }
177 Err(why) => {
178 BackfillEvent::TaskDropped(why.to_string())
180 }
181 };
182 Poll::Ready(ev)
183 }
184}
185
186impl<N: ProviderNodeTypes> BackfillSync for PipelineSync<N> {
187 fn on_action(&mut self, event: BackfillAction) {
188 match event {
189 BackfillAction::Start(target) => self.set_pipeline_sync_target(target),
190 }
191 }
192
193 fn poll(&mut self, cx: &mut Context<'_>) -> Poll<BackfillEvent> {
194 if let Some(event) = self.try_spawn_pipeline() {
196 return Poll::Ready(event)
197 }
198
199 if self.is_pipeline_active() {
201 if let Poll::Ready(event) = self.poll_pipeline(cx) {
203 return Poll::Ready(event)
204 }
205 }
206
207 Poll::Pending
208 }
209}
210
211#[derive(Debug)]
221enum PipelineState<N: ProviderNodeTypes> {
222 Idle(Option<Box<Pipeline<N>>>),
224 Running(oneshot::Receiver<PipelineWithResult<N>>),
226}
227
228impl<N: ProviderNodeTypes> PipelineState<N> {
229 const fn is_idle(&self) -> bool {
231 matches!(self, Self::Idle(_))
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use crate::test_utils::{insert_headers_into_client, TestPipelineBuilder};
239 use alloy_consensus::Header;
240 use alloy_eips::eip1559::ETHEREUM_BLOCK_GAS_LIMIT_30M;
241 use alloy_primitives::{BlockNumber, B256};
242 use assert_matches::assert_matches;
243 use futures::poll;
244 use reth_chainspec::{ChainSpecBuilder, MAINNET};
245 use reth_network_p2p::test_utils::TestFullBlockClient;
246 use reth_primitives_traits::SealedHeader;
247 use reth_provider::test_utils::MockNodeTypesWithDB;
248 use reth_stages::ExecOutput;
249 use reth_stages_api::StageCheckpoint;
250 use reth_tasks::Runtime;
251 use std::{collections::VecDeque, future::poll_fn, sync::Arc};
252
253 struct TestHarness {
254 pipeline_sync: PipelineSync<MockNodeTypesWithDB>,
255 tip: B256,
256 }
257
258 impl TestHarness {
259 fn new(total_blocks: usize, pipeline_done_after: u64) -> Self {
260 let chain_spec = Arc::new(
261 ChainSpecBuilder::default()
262 .chain(MAINNET.chain)
263 .genesis(MAINNET.genesis.clone())
264 .paris_activated()
265 .build(),
266 );
267
268 let pipeline = TestPipelineBuilder::new()
270 .with_pipeline_exec_outputs(VecDeque::from([Ok(ExecOutput {
271 checkpoint: StageCheckpoint::new(BlockNumber::from(pipeline_done_after)),
272 done: true,
273 })]))
274 .build(chain_spec);
275
276 let pipeline_sync = PipelineSync::new(pipeline, Runtime::test());
277 let client = TestFullBlockClient::default();
278 let header = Header {
279 base_fee_per_gas: Some(7),
280 gas_limit: ETHEREUM_BLOCK_GAS_LIMIT_30M,
281 ..Default::default()
282 };
283 let header = SealedHeader::seal_slow(header);
284 insert_headers_into_client(&client, header, 0..total_blocks);
285
286 let tip = client.highest_block().expect("there should be blocks here").hash();
287
288 Self { pipeline_sync, tip }
289 }
290 }
291
292 #[tokio::test]
293 async fn pipeline_started_and_finished() {
294 const TOTAL_BLOCKS: usize = 10;
295 const PIPELINE_DONE_AFTER: u64 = 5;
296 let TestHarness { mut pipeline_sync, tip } =
297 TestHarness::new(TOTAL_BLOCKS, PIPELINE_DONE_AFTER);
298
299 let sync_future = poll_fn(|cx| pipeline_sync.poll(cx));
300 let next_event = poll!(sync_future);
301
302 assert_matches!(next_event, Poll::Pending);
304
305 pipeline_sync.on_action(BackfillAction::Start(PipelineTarget::Sync(tip)));
306
307 let sync_future = poll_fn(|cx| pipeline_sync.poll(cx));
308 let next_event = poll!(sync_future);
309
310 assert_matches!(next_event, Poll::Ready(BackfillEvent::Started(target)) => {
312 assert_eq!(target.sync_target().unwrap(), tip);
313 });
314
315 let sync_future = poll_fn(|cx| pipeline_sync.poll(cx));
317 let next_ready = sync_future.await;
318 assert_matches!(next_ready, BackfillEvent::Finished(result) => {
319 assert_matches!(result, Ok(control_flow) => assert_eq!(control_flow, ControlFlow::Continue { block_number: PIPELINE_DONE_AFTER }));
320 });
321 }
322}