-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
feat(engine): hooks #4582
Conversation
Codecov Report
... and 11 files with indirect coverage changes
Flags with carried forward coverage won't be shown. Click here to find out more.
|
There was a problem hiding this 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
/// Returns an [action][`HookAction`], if any, according to the passed [event][`HookEvent`]. | ||
fn on_event(&mut self, event: HookEvent) -> Result<Option<HookAction>, HookError>; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
reth/crates/consensus/beacon/src/engine/hooks/controller.rs
Lines 64 to 68 in 2a6fd5c
if !finished { | |
self.running_hook_with_db_write = Some((hook_idx, hook)); | |
} else { | |
self.hooks[hook_idx] = Some(hook); | |
} |
reth/crates/consensus/beacon/src/engine/hooks/controller.rs
Lines 123 to 127 in 2a6fd5c
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.
There was a problem hiding this comment.
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:
- Act on event and set/unset the
running_hook_with_db_write
- Pass the action further down
/// Returns [dependencies][`HookDependencies`] for running this hook. | ||
fn dependencies(&self) -> HookDependencies; |
There was a problem hiding this comment.
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()
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
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>)>, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks, fixed
86fad45
to
f86f4da
Compare
match self.blockchain.restore_canonical_hashes() { | ||
Ok(()) => {} | ||
Err(error) => { | ||
error!(target: "consensus::engine", ?error, "Error restoring blockchain tree state"); | ||
return Err(error.into()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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()) |
/// 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, | ||
} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
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)) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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
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, | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
e552f18
to
ba037ee
Compare
ba037ee
to
3b69c3b
Compare
|
||
/// Hook that will be run during the main loop of | ||
/// [consensus engine][`crate::engine::BeaconConsensusEngine`]. | ||
pub trait Hook: Send + Sync + 'static { |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
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.