Skip to content

Commit 701e5ec

Browse files
mattssejoshieDo
andauthored
chore: add engine terminate (paradigmxyz#20420)
Co-authored-by: joshieDo <93316087+joshieDo@users.noreply.github.com>
1 parent 8e00e81 commit 701e5ec

8 files changed

Lines changed: 275 additions & 26 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/engine/tree/src/chain.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,10 +219,19 @@ pub enum HandlerEvent<T> {
219219
}
220220

221221
/// Internal events issued by the [`ChainOrchestrator`].
222-
#[derive(Clone, Debug)]
222+
#[derive(Debug)]
223223
pub enum FromOrchestrator {
224224
/// Invoked when backfill sync finished
225225
BackfillSyncFinished(ControlFlow),
226226
/// Invoked when backfill sync started
227227
BackfillSyncStarted,
228+
/// Gracefully terminate the engine service.
229+
///
230+
/// When this variant is received, the engine will persist all remaining in-memory blocks
231+
/// to disk before shutting down. Once persistence is complete, a signal is sent through
232+
/// the oneshot channel to notify the caller.
233+
Terminate {
234+
/// Channel to signal termination completion.
235+
tx: tokio::sync::oneshot::Sender<()>,
236+
},
228237
}

crates/engine/tree/src/tree/mod.rs

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use revm::state::EvmState;
3939
use state::TreeState;
4040
use std::{
4141
fmt::Debug,
42+
ops,
4243
sync::{
4344
mpsc::{Receiver, RecvError, RecvTimeoutError, Sender},
4445
Arc,
@@ -426,9 +427,13 @@ where
426427
match self.try_recv_engine_message() {
427428
Ok(Some(msg)) => {
428429
debug!(target: "engine::tree", %msg, "received new engine message");
429-
if let Err(fatal) = self.on_engine_message(msg) {
430-
error!(target: "engine::tree", %fatal, "insert block fatal error");
431-
return
430+
match self.on_engine_message(msg) {
431+
Ok(ops::ControlFlow::Break(())) => return,
432+
Ok(ops::ControlFlow::Continue(())) => {}
433+
Err(fatal) => {
434+
error!(target: "engine::tree", %fatal, "insert block fatal error");
435+
return
436+
}
432437
}
433438
}
434439
Ok(None) => {
@@ -1315,14 +1320,50 @@ where
13151320
if let Some(new_tip_num) = self.find_disk_reorg()? {
13161321
self.remove_blocks(new_tip_num)
13171322
} else if self.should_persist() {
1318-
let blocks_to_persist = self.get_canonical_blocks_to_persist()?;
1323+
let blocks_to_persist =
1324+
self.get_canonical_blocks_to_persist(PersistTarget::Threshold)?;
13191325
self.persist_blocks(blocks_to_persist);
13201326
}
13211327
}
13221328

13231329
Ok(())
13241330
}
13251331

1332+
/// Finishes termination by persisting all remaining blocks and signaling completion.
1333+
///
1334+
/// This blocks until all persistence is complete. Always signals completion,
1335+
/// even if an error occurs.
1336+
fn finish_termination(
1337+
&mut self,
1338+
pending_termination: oneshot::Sender<()>,
1339+
) -> Result<(), AdvancePersistenceError> {
1340+
trace!(target: "engine::tree", "finishing termination, persisting remaining blocks");
1341+
let result = self.persist_until_complete();
1342+
let _ = pending_termination.send(());
1343+
result
1344+
}
1345+
1346+
/// Persists all remaining blocks until none are left.
1347+
fn persist_until_complete(&mut self) -> Result<(), AdvancePersistenceError> {
1348+
loop {
1349+
// Wait for any in-progress persistence to complete (blocking)
1350+
if let Some((rx, start_time, _action)) = self.persistence_state.rx.take() {
1351+
let result = rx.blocking_recv().map_err(|_| TryRecvError::Closed)?;
1352+
self.on_persistence_complete(result, start_time)?;
1353+
}
1354+
1355+
let blocks_to_persist = self.get_canonical_blocks_to_persist(PersistTarget::Head)?;
1356+
1357+
if blocks_to_persist.is_empty() {
1358+
debug!(target: "engine::tree", "persistence complete, signaling termination");
1359+
return Ok(())
1360+
}
1361+
1362+
debug!(target: "engine::tree", count = blocks_to_persist.len(), "persisting remaining blocks before shutdown");
1363+
self.persist_blocks(blocks_to_persist);
1364+
}
1365+
}
1366+
13261367
/// Handles a completed persistence task.
13271368
fn on_persistence_complete(
13281369
&mut self,
@@ -1348,10 +1389,12 @@ where
13481389
}
13491390

13501391
/// Handles a message from the engine.
1392+
///
1393+
/// Returns `ControlFlow::Break(())` if the engine should terminate.
13511394
fn on_engine_message(
13521395
&mut self,
13531396
msg: FromEngine<EngineApiRequest<T, N>, N::Block>,
1354-
) -> Result<(), InsertBlockFatalError> {
1397+
) -> Result<ops::ControlFlow<()>, InsertBlockFatalError> {
13551398
match msg {
13561399
FromEngine::Event(event) => match event {
13571400
FromOrchestrator::BackfillSyncStarted => {
@@ -1361,14 +1404,21 @@ where
13611404
FromOrchestrator::BackfillSyncFinished(ctrl) => {
13621405
self.on_backfill_sync_finished(ctrl)?;
13631406
}
1407+
FromOrchestrator::Terminate { tx } => {
1408+
debug!(target: "engine::tree", "received terminate request");
1409+
if let Err(err) = self.finish_termination(tx) {
1410+
error!(target: "engine::tree", %err, "Termination failed");
1411+
}
1412+
return Ok(ops::ControlFlow::Break(()))
1413+
}
13641414
},
13651415
FromEngine::Request(request) => {
13661416
match request {
13671417
EngineApiRequest::InsertExecutedBlock(block) => {
13681418
let block_num_hash = block.recovered_block().num_hash();
13691419
if block_num_hash.number <= self.state.tree_state.canonical_block_number() {
13701420
// outdated block that can be skipped
1371-
return Ok(())
1421+
return Ok(ops::ControlFlow::Continue(()))
13721422
}
13731423

13741424
debug!(target: "engine::tree", block=?block_num_hash, "inserting already executed block");
@@ -1476,7 +1526,7 @@ where
14761526
}
14771527
}
14781528
}
1479-
Ok(())
1529+
Ok(ops::ControlFlow::Continue(()))
14801530
}
14811531

14821532
/// Invoked if the backfill sync has finished to target.
@@ -1710,10 +1760,10 @@ where
17101760
}
17111761

17121762
/// Returns a batch of consecutive canonical blocks to persist in the range
1713-
/// `(last_persisted_number .. canonical_head - threshold]`. The expected
1714-
/// order is oldest -> newest.
1763+
/// `(last_persisted_number .. target]`. The expected order is oldest -> newest.
17151764
fn get_canonical_blocks_to_persist(
17161765
&self,
1766+
target: PersistTarget,
17171767
) -> Result<Vec<ExecutedBlock<N>>, AdvancePersistenceError> {
17181768
// We will calculate the state root using the database, so we need to be sure there are no
17191769
// changes
@@ -1724,9 +1774,12 @@ where
17241774
let last_persisted_number = self.persistence_state.last_persisted_block.number;
17251775
let canonical_head_number = self.state.tree_state.canonical_block_number();
17261776

1727-
// Persist only up to block buffer target
1728-
let target_number =
1729-
canonical_head_number.saturating_sub(self.config.memory_block_buffer_target());
1777+
let target_number = match target {
1778+
PersistTarget::Head => canonical_head_number,
1779+
PersistTarget::Threshold => {
1780+
canonical_head_number.saturating_sub(self.config.memory_block_buffer_target())
1781+
}
1782+
};
17301783

17311784
debug!(
17321785
target: "engine::tree",
@@ -2869,3 +2922,12 @@ pub enum InsertPayloadOk {
28692922
/// The payload was valid and inserted into the tree.
28702923
Inserted(BlockStatus),
28712924
}
2925+
2926+
/// Target for block persistence.
2927+
#[derive(Debug, Clone, Copy)]
2928+
enum PersistTarget {
2929+
/// Persist up to `canonical_head - memory_block_buffer_target`.
2930+
Threshold,
2931+
/// Persist all blocks up to and including the canonical head.
2932+
Head,
2933+
}

crates/engine/tree/src/tree/tests.rs

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::{
44
tree::{
55
payload_validator::{BasicEngineValidator, TreeCtx, ValidationOutcome},
66
persistence_state::CurrentPersistenceAction,
7-
TreeConfig,
7+
PersistTarget, TreeConfig,
88
},
99
};
1010

@@ -285,7 +285,8 @@ impl TestHarness {
285285
let fcu_state = self.fcu_state(block_hash);
286286

287287
let (tx, rx) = oneshot::channel();
288-
self.tree
288+
let _ = self
289+
.tree
289290
.on_engine_message(FromEngine::Request(
290291
BeaconEngineMessage::ForkchoiceUpdated {
291292
state: fcu_state,
@@ -498,7 +499,7 @@ fn test_tree_persist_block_batch() {
498499

499500
// process the message
500501
let msg = test_harness.tree.try_recv_engine_message().unwrap().unwrap();
501-
test_harness.tree.on_engine_message(msg).unwrap();
502+
let _ = test_harness.tree.on_engine_message(msg).unwrap();
502503

503504
// we now should receive the other batch
504505
let msg = test_harness.tree.try_recv_engine_message().unwrap().unwrap();
@@ -577,7 +578,7 @@ async fn test_engine_request_during_backfill() {
577578
.with_backfill_state(BackfillSyncState::Active);
578579

579580
let (tx, rx) = oneshot::channel();
580-
test_harness
581+
let _ = test_harness
581582
.tree
582583
.on_engine_message(FromEngine::Request(
583584
BeaconEngineMessage::ForkchoiceUpdated {
@@ -658,7 +659,7 @@ async fn test_holesky_payload() {
658659
TestHarness::new(HOLESKY.clone()).with_backfill_state(BackfillSyncState::Active);
659660

660661
let (tx, rx) = oneshot::channel();
661-
test_harness
662+
let _ = test_harness
662663
.tree
663664
.on_engine_message(FromEngine::Request(
664665
BeaconEngineMessage::NewPayload {
@@ -883,7 +884,8 @@ async fn test_get_canonical_blocks_to_persist() {
883884
.with_persistence_threshold(persistence_threshold)
884885
.with_memory_block_buffer_target(memory_block_buffer_target);
885886

886-
let blocks_to_persist = test_harness.tree.get_canonical_blocks_to_persist().unwrap();
887+
let blocks_to_persist =
888+
test_harness.tree.get_canonical_blocks_to_persist(PersistTarget::Threshold).unwrap();
887889

888890
let expected_blocks_to_persist_length: usize =
889891
(canonical_head_number - memory_block_buffer_target - last_persisted_block_number)
@@ -902,7 +904,8 @@ async fn test_get_canonical_blocks_to_persist() {
902904

903905
assert!(test_harness.tree.state.tree_state.sealed_header_by_hash(&fork_block_hash).is_some());
904906

905-
let blocks_to_persist = test_harness.tree.get_canonical_blocks_to_persist().unwrap();
907+
let blocks_to_persist =
908+
test_harness.tree.get_canonical_blocks_to_persist(PersistTarget::Threshold).unwrap();
906909
assert_eq!(blocks_to_persist.len(), expected_blocks_to_persist_length);
907910

908911
// check that the fork block is not included in the blocks to persist
@@ -981,7 +984,7 @@ async fn test_engine_tree_live_sync_transition_required_blocks_requested() {
981984
let backfill_tip_block = main_chain[(backfill_finished_block_number - 1) as usize].clone();
982985
// add block to mock provider to enable persistence clean up.
983986
test_harness.provider.add_block(backfill_tip_block.hash(), backfill_tip_block.into_block());
984-
test_harness.tree.on_engine_message(FromEngine::Event(backfill_finished)).unwrap();
987+
let _ = test_harness.tree.on_engine_message(FromEngine::Event(backfill_finished)).unwrap();
985988

986989
let event = test_harness.from_tree_rx.recv().await.unwrap();
987990
match event {
@@ -991,7 +994,7 @@ async fn test_engine_tree_live_sync_transition_required_blocks_requested() {
991994
_ => panic!("Unexpected event: {event:#?}"),
992995
}
993996

994-
test_harness
997+
let _ = test_harness
995998
.tree
996999
.on_engine_message(FromEngine::DownloadedBlocks(vec![main_chain
9971000
.last()
@@ -1047,7 +1050,7 @@ async fn test_fcu_with_canonical_ancestor_updates_latest_block() {
10471050

10481051
// Send FCU to the canonical ancestor
10491052
let (tx, rx) = oneshot::channel();
1050-
test_harness
1053+
let _ = test_harness
10511054
.tree
10521055
.on_engine_message(FromEngine::Request(
10531056
BeaconEngineMessage::ForkchoiceUpdated {
@@ -1943,4 +1946,53 @@ mod forkchoice_updated_tests {
19431946
.unwrap();
19441947
assert!(result.is_some(), "OpStack should handle canonical head");
19451948
}
1949+
1950+
/// Test that engine termination persists all blocks and signals completion.
1951+
#[test]
1952+
fn test_engine_termination_with_everything_persisted() {
1953+
let chain_spec = MAINNET.clone();
1954+
let mut test_block_builder = TestBlockBuilder::eth().with_chain_spec((*chain_spec).clone());
1955+
1956+
// Create 10 blocks to persist
1957+
let blocks: Vec<_> = test_block_builder.get_executed_blocks(1..11).collect();
1958+
let canonical_tip = blocks.last().unwrap().recovered_block().number;
1959+
let test_harness = TestHarness::new(chain_spec).with_blocks(blocks);
1960+
1961+
// Create termination channel
1962+
let (terminate_tx, mut terminate_rx) = oneshot::channel();
1963+
1964+
let to_tree_tx = test_harness.to_tree_tx.clone();
1965+
let action_rx = test_harness.action_rx;
1966+
1967+
// Spawn tree in background thread
1968+
std::thread::Builder::new()
1969+
.name("Engine Task".to_string())
1970+
.spawn(|| test_harness.tree.run())
1971+
.unwrap();
1972+
1973+
// Send terminate request
1974+
to_tree_tx
1975+
.send(FromEngine::Event(FromOrchestrator::Terminate { tx: terminate_tx }))
1976+
.unwrap();
1977+
1978+
// Handle persistence actions until termination completes
1979+
let mut last_persisted_number = 0;
1980+
loop {
1981+
if terminate_rx.try_recv().is_ok() {
1982+
break;
1983+
}
1984+
1985+
if let Ok(PersistenceAction::SaveBlocks(saved_blocks, sender)) =
1986+
action_rx.recv_timeout(std::time::Duration::from_millis(100))
1987+
{
1988+
if let Some(last) = saved_blocks.last() {
1989+
last_persisted_number = last.recovered_block().number;
1990+
}
1991+
sender.send(saved_blocks.last().map(|b| b.recovered_block().num_hash())).unwrap();
1992+
}
1993+
}
1994+
1995+
// Ensure we persisted right to the tip
1996+
assert_eq!(last_persisted_number, canonical_tip);
1997+
}
19461998
}

0 commit comments

Comments
 (0)