Skip to content
This repository was archived by the owner on Jan 12, 2026. It is now read-only.

Commit 61ab24e

Browse files
meyer9claude
andauthored
Implement TaskGuard RAII pattern and fix Block-STM livelock (#7)
This commit includes two major improvements to the Block-STM parallel execution engine: 1. **TaskGuard RAII Pattern**: Refactored `num_active_tasks` tracking to use a RAII guard pattern. - TaskGuard automatically increments counter on creation and decrements on drop - Eliminates manual increment/decrement tracking that was error-prone - Guards are created only when tasks are successfully acquired - Guards can be reused when finish_execution/finish_validation return follow-up tasks 2. **Fixed MVHashMap Livelock**: Resolved infinite retry loop in parallel execution. - Root cause: When converting writes to estimates during validation abort, old write entries weren't removed from the MVHashMap - This caused subsequent reads to get Estimate values instead of actual Write values - Transactions would infinitely retry when `add_dependency()` returned false - Fix: Remove old write entries before updating `last_written_locations` in `convert_writes_to_estimates()` Additional changes: - Removed debug logging added during troubleshooting - Updated all scheduler methods to return `Option<(Task, TaskGuard)>` tuples - Updated executor to destructure and pass guards through the task lifecycle 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 3c6d943 commit 61ab24e

5 files changed

Lines changed: 287 additions & 177 deletions

File tree

crates/op-rbuilder/src/block_stm/executor.rs

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use revm::{
1818
state::{Account, EvmState},
1919
};
2020
use tokio_util::sync::CancellationToken;
21-
use tracing::{Span, debug, warn};
21+
use tracing::{Span, warn};
2222

2323
use crate::{
2424
block_stm::{
@@ -106,9 +106,9 @@ impl<
106106
ExecuteTxFn: (Fn(
107107
&Recovered<op_alloy_consensus::OpTxEnvelope>,
108108
&mut State<LazyDatabaseWrapper<VersionedDatabase<'_, DB>>>,
109-
&HashSet<EvmStateKey>, // NEW: conflicting keys
110-
Option<&TxExecutionResult>, // NEW: previous execution result
111-
u64, // NEW: tx_da_size
109+
&HashSet<EvmStateKey>,
110+
Option<&TxExecutionResult>,
111+
u64,
112112
) -> Result<
113113
ResultAndState<OpHaltReason>,
114114
EVMError<VersionedDbError, OpTransactionError>,
@@ -154,19 +154,25 @@ impl<
154154
if !prev_read_set.is_empty() {
155155
use crate::block_stm::types::ReadResult;
156156

157-
prev_read_set.iter()
157+
let res = prev_read_set
158+
.iter()
158159
.filter_map(|(key, expected_version)| {
159160
// Check if this key's current version matches expected
160161
let current_read = self.mv_hashmap.read(key, txn_idx);
161162
match (expected_version, &current_read) {
162163
(Some(expected_ver), ReadResult::Value { version, .. })
163-
if version != expected_ver => Some(key.clone()),
164+
if version != expected_ver =>
165+
{
166+
Some(key.clone())
167+
}
164168
(None, ReadResult::Value { .. }) => Some(key.clone()),
165169
(Some(_), ReadResult::NotFound) => Some(key.clone()),
166170
_ => None,
167171
}
168172
})
169-
.collect()
173+
.collect();
174+
175+
res
170176
} else {
171177
HashSet::new()
172178
}
@@ -176,16 +182,21 @@ impl<
176182

177183
// Get previous execution result if this is a re-execution
178184
// We need to hold the lock guard to keep the reference alive
179-
let results_guard = self.execution_results.lock().unwrap();
185+
let results_guard = self.execution_results.lock().unwrap()[txn_idx as usize].clone();
180186
let previous_result = if incarnation > 0 {
181-
results_guard[txn_idx as usize].as_ref()
187+
results_guard.as_ref()
182188
} else {
183189
None
184190
};
185191

186192
// Execute transaction with versioned state, conflicting keys, previous result, and tx_da_size
187-
let result = execute_tx(&tx, &mut tx_state, &conflicting_keys, previous_result, tx_da_size);
188-
drop(results_guard); // Drop the lock before processing results
193+
let result = execute_tx(
194+
&tx,
195+
&mut tx_state,
196+
&conflicting_keys,
197+
previous_result,
198+
tx_da_size,
199+
);
189200

190201
match result {
191202
Ok(result) => {
@@ -221,7 +232,6 @@ impl<
221232

222233
// Add writes only for values that actually changed
223234
for (addr, account) in state.iter() {
224-
debug!("Account touched: {}", addr);
225235
if account.is_touched() {
226236
// Get original values from captured reads (if available)
227237
let original_balance = captured_reads.get(&EvmStateKey::Balance(*addr));
@@ -381,9 +391,9 @@ impl<
381391
ExecuteTxFn: (Fn(
382392
&Recovered<op_alloy_consensus::OpTxEnvelope>,
383393
&mut State<LazyDatabaseWrapper<VersionedDatabase<'_, DB>>>,
384-
&HashSet<EvmStateKey>, // NEW: conflicting keys
385-
Option<&TxExecutionResult>, // NEW: previous execution result
386-
u64, // NEW: tx_da_size
394+
&HashSet<EvmStateKey>,
395+
Option<&TxExecutionResult>,
396+
u64,
387397
) -> Result<
388398
ResultAndState<OpHaltReason>,
389399
EVMError<VersionedDbError, OpTransactionError>,
@@ -415,15 +425,14 @@ impl<
415425
);
416426
s.spawn(move || {
417427
let _worker_guard = worker_span.entered();
418-
debug!("Spawning worker thread {}", worker_id);
419428

420429
let mut task = None;
421430

422431
while !this.scheduler.done() {
423432
let is_cancelled = cancellation_token.is_cancelled();
424433

425434
// Finish validation tasks only if cancelled.
426-
if is_cancelled && !matches!(task, Some(Task::Validate { .. })) {
435+
if is_cancelled && !matches!(task, Some((Task::Validate { .. }, _))) {
427436
// first, see if we can get a validation task:
428437
task = this.scheduler.next_validation_task();
429438

@@ -433,7 +442,7 @@ impl<
433442
}
434443
}
435444

436-
task = if let Some(Task::Execute { version }) = task {
445+
task = if let Some((Task::Execute { version }, guard)) = task {
437446
let Version {
438447
txn_idx,
439448
incarnation,
@@ -465,7 +474,7 @@ impl<
465474

466475
let next_task = this
467476
.scheduler
468-
.finish_execution(txn_idx, incarnation, wrote_new_path);
477+
.finish_execution(txn_idx, incarnation, wrote_new_path, guard);
469478
debug!(
470479
worker_id = worker_id,
471480
txn_idx = txn_idx,
@@ -477,13 +486,13 @@ impl<
477486
} else {
478487
task
479488
};
480-
task = if let Some(Task::Validate {
489+
task = if let Some((Task::Validate {
481490
version:
482491
Version {
483492
txn_idx,
484493
incarnation,
485494
},
486-
}) = task
495+
}, guard)) = task
487496
{
488497
let tx_validate_span = tracing::info_span!(
489498
parent: Span::current(),
@@ -527,7 +536,7 @@ impl<
527536
this.mv_hashmap.convert_writes_to_estimates(txn_idx);
528537
}
529538

530-
let next_task = this.scheduler.finish_validation(txn_idx, aborted);
539+
let next_task = this.scheduler.finish_validation(txn_idx, aborted, guard);
531540
debug!(
532541
worker_id = worker_id,
533542
txn_idx = txn_idx,

crates/op-rbuilder/src/block_stm/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub mod types;
2828
// Re-export commonly used types
2929
pub use db_adapter::{SharedCodeCache, VersionedDatabase, VersionedDbError};
3030
pub use mv_hashmap::{MVHashMap, ValidationResult};
31-
pub use scheduler::Scheduler;
31+
pub use scheduler::{Scheduler, TaskGuard};
3232
pub use types::{
3333
EvmStateKey, EvmStateValue, ExecutionStatus, Incarnation, ReadResult, Task, TxnIndex, Version,
3434
};

crates/op-rbuilder/src/block_stm/mv_hashmap.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ impl MVHashMap {
104104
new_locations: HashSet<EvmStateKey>,
105105
) -> bool {
106106
let mut last_written_locations = self.last_written_locations[txn_idx as usize].write();
107+
for location in new_locations.iter() {
108+
if let Some(version_map) = self.data.get_mut(location) {
109+
version_map.write().remove(&txn_idx);
110+
}
111+
}
107112
let unwritten_locations = new_locations.difference(&last_written_locations).count();
108113
if unwritten_locations == 0 {
109114
return false;

0 commit comments

Comments
 (0)