Skip to content

Commit cebab85

Browse files
authored
feat(l1,l2): make write path APIs async (#2336)
**Motivation** Some of our sync APIs can produce starving when running on Tokio due to taking a long time to reach the next `await`-point. Specifically, writing to the DB tends to take a long time, which blocks other tasks, sometimes the whole runtime due to how the scheduler in Tokio works. Thus, we need a way to inform the runtime we're going to be working for a while, and give it control while we wait for stuff. **Description** Take the mutable APIs for the DB and mark them `async`. Then bubble that up to their users. Then make the functions non-blocking by using `spawn_blocking` to run on the blocking thread, releasing the runtime to handle more work. The DB writing APIs had to change to pass-by-value to satisfy the borrow-checker in the blocking task context. I think I can use proper lifetime bounds with a helper crate, if that's preferred. The values were already being discarded after passing to the DB, so passing by value should not be a problem either way. Special considerations: - For some work performed before benchmarks and EF tests which are inherently synchronous I opted for calling with an ad-hoc runtime instance and `block_on`, as that might reduce the changes needed by localizing the async work. If desired, that can be changed up to making a `tokio::main`. The same is true for some setup functions for tests. - For the DBs I had to separate the Tokio import. This is because they need to compile with L2, which means provers' custom compilers, and those don't support the networking functions in the stdlib, which Tokio with full features (as the workspace dep declares) brings them in. - The InMemoryDB was left untouched other than updating the interfaces, given hashmap access should be quick enough. - I need to comment on [this hack](https://github.com/lambdaclass/ethrex/pull/2336/files#diff-264636d3ee6ee67bd6e136b8c98f74152de6a8e2a07f597cfb5f622d4e0d815aR143-R146): `and_then` can't be used on futures and everything became a mess without that little helper. - I'm unsure about whether or not we also want to cover the read APIs, at least for consistency I would think so, but for now I left them out. closes #2402
1 parent b672cd0 commit cebab85

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+1295
-870
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Perf
44

5+
#### 2025-04-01
6+
7+
- Asyncify DB write APIs, as well as its users [#2336](https://github.com/lambdaclass/ethrex/pull/2336)
8+
59
#### 2025-03-30
610

711
- Faster block import, use a slice instead of copy

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ ethrex-prover = { path = "./crates/l2/prover" }
5151
tracing = { version = "0.1", features = ["log"] }
5252
tracing-subscriber = { version = "0.3.0", features = ["env-filter"] }
5353

54+
async-trait = "0.1.88"
5455
ethereum-types = { version = "0.15.1", features = ["serialize"] }
5556
serde = { version = "1.0.203", features = ["derive"] }
5657
serde_json = "1.0.117"

bench/criterion_benchmark.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@ use tracing_subscriber::{filter::Directive, EnvFilter, FmtSubscriber};
1515
fn block_import() {
1616
let data_dir = DEFAULT_DATADIR;
1717
set_datadir(data_dir);
18-
1918
remove_db(data_dir);
2019

2120
let evm_engine = "revm".to_owned().try_into().unwrap();
2221

2322
let network = "../../test_data/genesis-l2-ci.json";
2423

25-
import_blocks(
24+
let rt = tokio::runtime::Runtime::new().unwrap();
25+
rt.block_on(import_blocks(
2626
"../../test_data/l2-1k-erc20.rlp",
2727
data_dir,
2828
network,
2929
evm_engine,
30-
);
30+
));
3131
}
3232

3333
pub fn criterion_benchmark(c: &mut Criterion) {

cmd/ef_tests/blockchain/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ serde_json.workspace = true
1515
bytes.workspace = true
1616
hex.workspace = true
1717
lazy_static.workspace = true
18+
tokio.workspace = true
1819

1920
[dev-dependencies]
2021
datatest-stable = "0.2.9"

cmd/ef_tests/blockchain/test_runner.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ use ethrex_common::types::{
88
use ethrex_rlp::decode::RLPDecode;
99
use ethrex_storage::{EngineType, Store};
1010

11-
pub fn run_ef_test(test_key: &str, test: &TestUnit) {
11+
pub async fn run_ef_test(test_key: &str, test: &TestUnit) {
1212
// check that the decoded genesis block header matches the deserialized one
1313
let genesis_rlp = test.genesis_rlp.clone();
1414
let decoded_block = CoreBlock::decode(&genesis_rlp).unwrap();
1515
let genesis_block_header = CoreBlockHeader::from(test.genesis_block_header.clone());
1616
assert_eq!(decoded_block.header, genesis_block_header);
1717

18-
let store = build_store_for_test(test);
18+
let store = build_store_for_test(test).await;
1919

2020
// Check world_state
2121
check_prestate_against_db(test_key, test, &store);
@@ -33,7 +33,7 @@ pub fn run_ef_test(test_key: &str, test: &TestUnit) {
3333
let hash = block.hash();
3434

3535
// Attempt to add the block as the head of the chain
36-
let chain_result = blockchain.add_block(block);
36+
let chain_result = blockchain.add_block(block).await;
3737
match chain_result {
3838
Err(error) => {
3939
assert!(
@@ -50,7 +50,7 @@ pub fn run_ef_test(test_key: &str, test: &TestUnit) {
5050
test_key,
5151
block_fixture.expect_exception.clone().unwrap()
5252
);
53-
apply_fork_choice(&store, hash, hash, hash).unwrap();
53+
apply_fork_choice(&store, hash, hash, hash).await.unwrap();
5454
}
5555
}
5656
}
@@ -82,12 +82,13 @@ pub fn parse_test_file(path: &Path) -> HashMap<String, TestUnit> {
8282
}
8383

8484
/// Creats a new in-memory store and adds the genesis state
85-
pub fn build_store_for_test(test: &TestUnit) -> Store {
85+
pub async fn build_store_for_test(test: &TestUnit) -> Store {
8686
let store =
8787
Store::new("store.db", EngineType::InMemory).expect("Failed to build DB for testing");
8888
let genesis = test.get_genesis();
8989
store
9090
.add_initial_state(genesis)
91+
.await
9192
.expect("Failed to add genesis state");
9293
store
9394
}

cmd/ef_tests/blockchain/tests/cancun.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use std::path::Path;
1414
// test or set of tests
1515

1616
fn parse_and_execute_until_cancun(path: &Path) -> datatest_stable::Result<()> {
17+
let rt = tokio::runtime::Runtime::new().unwrap();
1718
let tests = parse_test_file(path);
1819

1920
for (test_key, test) in tests {
@@ -22,22 +23,23 @@ fn parse_and_execute_until_cancun(path: &Path) -> datatest_stable::Result<()> {
2223
// them. This produces false positives
2324
continue;
2425
}
25-
run_ef_test(&test_key, &test);
26+
rt.block_on(run_ef_test(&test_key, &test));
2627
}
2728

2829
Ok(())
2930
}
3031

3132
#[allow(dead_code)]
3233
fn parse_and_execute_all(path: &Path) -> datatest_stable::Result<()> {
34+
let rt = tokio::runtime::Runtime::new().unwrap();
3335
let tests = parse_test_file(path);
3436

3537
for (test_key, test) in tests {
3638
if test.network < Network::Merge {
3739
// These tests fall into the not supported forks. This produces false positives
3840
continue;
3941
}
40-
run_ef_test(&test_key, &test);
42+
rt.block_on(run_ef_test(&test_key, &test));
4143
}
4244

4345
Ok(())

cmd/ef_tests/blockchain/tests/prague.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const SKIPPED_TEST: [&str; 2] = [
1313

1414
#[allow(dead_code)]
1515
fn parse_and_execute(path: &Path) -> datatest_stable::Result<()> {
16+
let rt = tokio::runtime::Runtime::new().unwrap();
1617
let tests = parse_test_file(path);
1718

1819
for (test_key, test) in tests {
@@ -21,7 +22,7 @@ fn parse_and_execute(path: &Path) -> datatest_stable::Result<()> {
2122
continue;
2223
}
2324

24-
run_ef_test(&test_key, &test);
25+
rt.block_on(run_ef_test(&test_key, &test));
2526
}
2627
Ok(())
2728
}

cmd/ef_tests/blockchain/tests/shanghai.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use ef_tests_blockchain::{
66
};
77

88
fn parse_and_execute(path: &Path) -> datatest_stable::Result<()> {
9+
let rt = tokio::runtime::Runtime::new().unwrap();
910
let tests = parse_test_file(path);
1011

1112
for (test_key, test) in tests {
@@ -14,7 +15,7 @@ fn parse_and_execute(path: &Path) -> datatest_stable::Result<()> {
1415
continue;
1516
}
1617

17-
run_ef_test(&test_key, &test);
18+
rt.block_on(run_ef_test(&test_key, &test));
1819
}
1920
Ok(())
2021
}

cmd/ef_tests/state/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ revm = { version = "19.0.0", features = [
2929
"optional_no_base_fee",
3030
"optional_block_gas_limit",
3131
], default-features = false }
32+
tokio.workspace = true
3233

3334
[dev-dependencies]
3435
hex = "0.4.3"

0 commit comments

Comments
 (0)