-
Notifications
You must be signed in to change notification settings - Fork 79
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
refactor: queue-based block production + better separation of node's mutable state #517
base: main
Are you sure you want to change the base?
Conversation
# Conflicts: # crates/cli/src/cli.rs # crates/cli/src/main.rs # crates/core/src/node/eth.rs # crates/core/src/node/in_memory.rs # crates/core/src/node/in_memory_ext.rs # crates/core/src/node/zks.rs # crates/core/src/system_contracts.rs
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.
Overall I like what I see, but tbh it feels much more complicated and low-level than it intuitively has to be.
Left a few preliminary comments.
let chain_id = tokio::runtime::Handle::current() | ||
.block_on(async { self.node.get_chain_id().await.map_err(RpcError::from) })?; |
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.
Why not make this method async as well?
@@ -19,7 +19,7 @@ struct Bytecode { | |||
|
|||
// Loads a list of bytecodes and addresses from the directory and then inserts them directly | |||
// into the Node's storage. | |||
pub fn override_bytecodes(node: &InMemoryNode, bytecodes_dir: String) -> eyre::Result<()> { | |||
pub async fn override_bytecodes(node: &InMemoryNode, bytecodes_dir: String) -> eyre::Result<()> { | |||
for entry in fs::read_dir(bytecodes_dir)? { |
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 that you made this function async, probably it makes sense to replace calls with non-blocking alternatives.
None => Ok(None), | ||
} | ||
) -> anyhow::Result<Option<api::TransactionReceipt>> { | ||
// TODO: Call fork if not found |
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.
Should we create a task for that?
.inner | ||
.read() | ||
.await | ||
.fork_storage | ||
.inner | ||
.read() | ||
.expect("failed reading fork storage") | ||
.fork | ||
.as_ref() | ||
.and_then(|fork| { |
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.
Nit: here and in other places the composition looks super awkward. A good candidate for the structure being re-thought. Not suggesting to do in this PR obv.
/// A single-instance writer to blockchain state that is only available to [`super::InMemoryNodeInner`]. | ||
pub(super) struct BlockchainWriter { | ||
pub(super) inner: Arc<RwLock<Blockchain>>, | ||
} |
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.
Can't we express BlockchainReader
and BlockchainWriter
as two traits? Actually, probably BlockchainWriter
is not even needed -- you can expose only Box<dyn BlockchainReader>
outside of the crate, and rework Blockchain
as pub(crate) struct Blockchain { inner: Arc<RwLock<Blockchain>> }
.
With that, hopefully, you will be able to expose all the required mutability methods on the structure itself so that you don't have to leak guards in the interface.
use tokio::sync::RwLock; | ||
|
||
impl InMemoryNodeInner { | ||
// TODO: Bake in Arc<RwLock<_>> into the struct itself |
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.
Most of structures already have rwlocks inside, no? This indeed feels a bit clunky
impl Future for NodeExecutor { | ||
type Output = (); | ||
|
||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { |
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.
Maybe it's a stupid question, but why can't we just do a run
method that would do something like this?
It feels a bit overengineered.
while let Some(command) = this.command_receiver.next().await {
...
}
} | ||
|
||
#[derive(Debug)] | ||
pub enum Command { |
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.
Does it have to be public?
} | ||
} | ||
|
||
impl Future for BlockSealer { |
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.
Similarly -- cannot we express this as a simple loop?
What 💻
Closes #501
Apologies for an insane diff once again - one change required another so I ended up refactoring more stuff than originally intended. Vast part of the diff is just moving from
std::sync::RwLock
totokio::sync::RwLock
(was needed due to lifetime shenanigans) and hence addingasync
/await
in a lot of places that did not require it before. I will try to give a brief overview of functional changes below:inner
. Essentially trying to restrict writeable access to as little entrypoints as I could (seeinner
rustdoc for more info)inner/blockchain.rs
that has reader/writer structs.BlockchainWriter
is held exclusively byInMemoryNodeInner
and is inaccessible from outsideinner
module. All API endpoints were refactored to rely onBlockchainReader
for their queries, thus removing the need to lock the entirety ofInMemoryNodeInner
.time
module intoTimeWriter
andTimeReader
. Former is owned byInMemoryNodeInner
and is inaccessible from outside ofinner
module. Latter can still be used to read current time in API endpoints.BlockProducer
was renamed toNodeExecutor
and is the sole place that can seal blocks. Works via mpsc command queue that ensures no contentions between time lock and block production. Note: some more improvements can still be made to relax its lock holding but this at least removes the requirement to hold time lock for the entire block production time. Can also seal multiple blocks "atomically" (term used pretty loosely here but hopefully conveys the meaning).BlockSealer
was adapted accordingly and is now a separate background process that pushes commands toBlockProducer
Why ✋
Hopefully better code quality + less lock contention across the board