Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(engine): hooks #4582

Merged
merged 31 commits into from
Sep 18, 2023
Merged

feat(engine): hooks #4582

merged 31 commits into from
Sep 18, 2023

Conversation

shekhirin
Copy link
Collaborator

@shekhirin shekhirin commented Sep 13, 2023

This PR introduces the engine hooks (name is debatable, we can also call them triggers or actions). They are used to extend the functionality of consensus engine with custom logic provided by the caller (in src/node/mod.rs for now, and in CLI builder later).

The first instance of such functionality is the pruner, that needs to be run on every iteration of the main engine loop. Next will be snapshot hook (#4588).


Only the way pruner is called from the engine has changed in this PR, and no changes in engine or pruner behavior are expected.

@codecov
Copy link

codecov bot commented Sep 13, 2023

Codecov Report

Merging #4582 (d5f2101) into main (b5905c4) will decrease coverage by 0.08%.
The diff coverage is 73.07%.

Impacted file tree graph

Files Changed Coverage Δ
bin/reth/src/node/mod.rs 26.11% <0.00%> (-0.08%) ⬇️
crates/consensus/beacon/src/engine/error.rs 33.33% <ø> (ø)
crates/consensus/beacon/src/engine/metrics.rs 100.00% <ø> (ø)
crates/interfaces/src/sync.rs 40.00% <0.00%> (ø)
crates/consensus/beacon/src/engine/hooks/mod.rs 65.21% <65.21%> (ø)
...es/consensus/beacon/src/engine/hooks/controller.rs 74.24% <74.24%> (ø)
crates/consensus/beacon/src/engine/hooks/prune.rs 86.25% <80.00%> (ø)
crates/consensus/beacon/src/engine/mod.rs 74.42% <80.64%> (-0.18%) ⬇️
crates/consensus/beacon/src/engine/test_utils.rs 68.21% <100.00%> (+0.31%) ⬆️

... and 11 files with indirect coverage changes

Flag Coverage Δ
integration-tests 16.93% <0.00%> (-0.04%) ⬇️
unit-tests 63.17% <73.07%> (-0.07%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
reth binary 31.94% <0.00%> (-0.02%) ⬇️
blockchain tree 83.59% <ø> (ø)
pipeline 88.54% <ø> (ø)
storage (db) 73.47% <ø> (ø)
trie 94.70% <ø> (-0.04%) ⬇️
txpool 49.44% <ø> (-0.49%) ⬇️
networking 77.17% <ø> (-0.05%) ⬇️
rpc 57.31% <ø> (-0.01%) ⬇️
consensus 62.58% <76.43%> (+0.16%) ⬆️
revm 19.66% <ø> (ø)
payload builder 9.06% <ø> (ø)
primitives 86.43% <0.00%> (-0.04%) ⬇️

@shekhirin shekhirin marked this pull request as ready for review September 13, 2023 18:29
@rkrasiuk rkrasiuk added C-enhancement New feature or request A-consensus Related to the consensus engine labels Sep 14, 2023
Copy link
Collaborator

@mattsse mattsse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the direction,
we should think about the trait a bit more

Comment on lines 38 to 39
/// Returns an [action][`HookAction`], if any, according to the passed [event][`HookEvent`].
fn on_event(&mut self, event: HookEvent) -> Result<Option<HookAction>, HookError>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this I don't understand after reading the event docs.

why do we feed the outcome of poll back into an event callback, this looks unnecessary because the hook already knows this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we act on Started/Finished here:

if !finished {
self.running_hook_with_db_write = Some((hook_idx, hook));
} else {
self.hooks[hook_idx] = Some(hook);
}
if started && hook.dependencies().db_write {
self.running_hook_with_db_write = Some((hook_idx, hook));
} else {
self.hooks[hook_idx] = Some(hook);
}

also, there might be no action required (as will be for snapshots, for example), but the hook still need to signal as about its status.

We can combine it into one HookEvent::Finished(HookAction) though, and it will be returned on hook polling.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I just made the Hook::poll return both event and an optional action, so we could:

  1. Act on event and set/unset the running_hook_with_db_write
  2. Pass the action further down

Comment on lines 41 to 42
/// Returns [dependencies][`HookDependencies`] for running this hook.
fn dependencies(&self) -> HookDependencies;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this implies the dependencies are required for the duration of the hook which might not be the case.

I think we can solve this a bit differently, for example with multiple poll functions:

like poll_start -> Poll<Deps>,
and poll_hook()

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not following...

this implies the dependencies are required for the duration of the hook which might not be the case.

for DB write access, this is the case. Not sure what other dependencies we might have in the future though, so you might be right, but should we be ready for this now?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simplified according to #4582 (comment)

crates/consensus/beacon/src/engine/hooks/mod.rs Outdated Show resolved Hide resolved
Comment on lines 14 to 18
hooks: Vec<Option<Box<dyn Hook>>>,
/// Next hook index to poll.
next_hook_idx: usize,
/// Currently running hook with DB write access, if any.
running_hook_with_db_write: Option<(usize, Box<dyn Hook>)>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this just a VecDeque?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, fixed

Comment on lines 1688 to 1692
match self.blockchain.restore_canonical_hashes() {
Ok(()) => {}
Err(error) => {
error!(target: "consensus::engine", ?error, "Error restoring blockchain tree state");
return Err(error.into())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
match self.blockchain.restore_canonical_hashes() {
Ok(()) => {}
Err(error) => {
error!(target: "consensus::engine", ?error, "Error restoring blockchain tree state");
return Err(error.into())
if let Err(error) = self.blockchain.restore_canonical_hashes() {
error!(target: "consensus::engine", ?error, "Error restoring blockchain tree state");
return Err(error.into())

Comment on lines 110 to 115
/// Dependencies that [hook][`Hook`] require for execution.
pub struct HookDependencies {
/// Hook needs DB write access. If `true`, then only one hook with DB write access can be run
/// at a time.
pub db_write: bool,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shall we keep it simple for now and get rid of this generalization? dependencies naming is a bit confusing and db write access feels important enough to be extracted to fn db_access_level() -> HookDbAccess on the Hook trait

enum HookDbAccess {
    RO,
    RW
}

or smth similar

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, that would be enough for prune and snapshot hooks, and we can add more functionality on top later

Copy link
Collaborator Author

@shekhirin shekhirin Sep 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would also be nice to somehow restrict the database provider that custom user-provided hook has access to according to this level

Comment on lines 1800 to 1810
if is_pending && !this.forkchoice_state_tracker.is_latest_invalid() {
if let Poll::Ready(result) = this.hooks.poll_next_hook(
cx,
HookArguments { tip_block_number: this.blockchain.canonical_tip().number },
HookDependencies { db_write: this.sync.is_pipeline_active() },
) {
if let Err(err) = this.on_hook_action(result?) {
return Poll::Ready(Err(err))
}
}
}
Copy link
Collaborator

@joshieDo joshieDo Sep 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we are only polling one hook (maybe two if there was a previous write poll) per engine poll.

Is that intended? why not more? we don't want to potentially block the FCU handle?

Copy link
Collaborator

@joshieDo joshieDo Sep 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

random thought: is it possible to run them in parallel here (as long as they're read-only)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't want to potentially block the FCU handle?

yes, that was the main reason, so we would return the control asap and process any new incoming messages, because they're higher priority than hooks

Comment on lines 88 to 94
pub enum HookAction {
/// Notify about a [SyncState] update.
UpdateSyncState(SyncState),
/// Read the last relevant canonical hashes from the database and update the block indices of
/// the blockchain tree.
RestoreCanonicalHashes,
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given these variants... is the goal to move the engine fcu handling to an hook as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm yeah, that's a good idea, I think we can do that with both engine messages handling and pipeline. This would also breakdown the huge engine/mod.rs file and separate the engine polling, engine messages and pipeline control logics. @mattsse wdyt?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to hide the actual FCU inside a dyn Hook,
the entire purpose of the engine is to handle fcu and payload, this should be baked in directly


/// Hook that will be run during the main loop of
/// [consensus engine][`crate::engine::BeaconConsensusEngine`].
pub trait Hook: Send + Sync + 'static {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanna start bikeshedding the name,

how about EngineHook?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i like that


/// Arguments passed to the [hook polling function][`Hook::poll`].
#[derive(Copy, Clone, Debug)]
pub struct HookArguments {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think Arguments is fitting here,
how about EngineContext?

Copy link
Collaborator

@mattsse mattsse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link
Member

@rkrasiuk rkrasiuk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@shekhirin shekhirin added this pull request to the merge queue Sep 18, 2023
Merged via the queue into main with commit 11f5f3f Sep 18, 2023
22 checks passed
@shekhirin shekhirin deleted the alexey/engine-hooks branch September 18, 2023 18:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-consensus Related to the consensus engine C-enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants