Another check to prevent duplicate block imports (#8050)

Attempt to address performance issues caused by importing the same block multiple times.


  - Check fork choice "after" obtaining the fork choice write lock in `BeaconChain::import_block`. We actually use an upgradable read lock, but this is semantically equivalent (the upgradable read has the advantage of not excluding regular reads).

The hope is that this change has several benefits:

1. By preventing duplicate block imports we save time repeating work inside `import_block` that is unnecessary, e.g. writing the state to disk. Although the store itself now takes some measures to avoid re-writing diffs, it is even better if we avoid a disk write entirely.
2. By returning `DuplicateFullyImported`, we reduce some duplicated work downstream. E.g. if multiple threads importing columns trigger `import_block`, now only _one_ of them will get a notification of the block import completing successfully, and only this one will run `recompute_head`. This should help avoid a situation where multiple beacon processor workers are consumed by threads blocking on the `recompute_head_lock`. However, a similar block-fest is still possible with the upgradable fork choice lock (a large number of threads can be blocked waiting for the first thread to complete block import).


Co-Authored-By: Michael Sproul <michael@sigmaprime.io>
This commit is contained in:
Michael Sproul
2025-09-16 14:10:42 +10:00
committed by GitHub
parent b8178515cd
commit f04d5ecddd
4 changed files with 38 additions and 6 deletions

View File

@@ -3889,9 +3889,16 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.map_err(BeaconChainError::from)?;
}
// Take an upgradable read lock on fork choice so we can check if this block has already
// been imported. We don't want to repeat work importing a block that is already imported.
let fork_choice_reader = self.canonical_head.fork_choice_upgradable_read_lock();
if fork_choice_reader.contains_block(&block_root) {
return Err(BlockError::DuplicateFullyImported(block_root));
}
// Take an exclusive write-lock on fork choice. It's very important to prevent deadlocks by
// avoiding taking other locks whilst holding this lock.
let mut fork_choice = self.canonical_head.fork_choice_write_lock();
let mut fork_choice = parking_lot::RwLockUpgradableReadGuard::upgrade(fork_choice_reader);
// Do not import a block that doesn't descend from the finalized root.
let signed_block =

View File

@@ -48,7 +48,7 @@ use fork_choice::{
};
use itertools::process_results;
use logging::crit;
use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard};
use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockUpgradableReadGuard, RwLockWriteGuard};
use slot_clock::SlotClock;
use state_processing::AllCaches;
use std::sync::Arc;
@@ -79,6 +79,10 @@ impl<T> CanonicalHeadRwLock<T> {
self.0.read()
}
fn upgradable_read(&self) -> RwLockUpgradableReadGuard<'_, T> {
self.0.upgradable_read()
}
fn write(&self) -> RwLockWriteGuard<'_, T> {
self.0.write()
}
@@ -389,6 +393,14 @@ impl<T: BeaconChainTypes> CanonicalHead<T> {
self.fork_choice.read()
}
/// Access an upgradable read-lock for fork choice.
pub fn fork_choice_upgradable_read_lock(
&self,
) -> RwLockUpgradableReadGuard<'_, BeaconForkChoice<T>> {
let _timer = metrics::start_timer(&metrics::FORK_CHOICE_UPGRADABLE_READ_LOCK_AQUIRE_TIMES);
self.fork_choice.upgradable_read()
}
/// Access a write-lock for fork choice.
pub fn fork_choice_write_lock(&self) -> RwLockWriteGuard<'_, BeaconForkChoice<T>> {
let _timer = metrics::start_timer(&metrics::FORK_CHOICE_WRITE_LOCK_AQUIRE_TIMES);

View File

@@ -578,6 +578,14 @@ pub static FORK_CHOICE_READ_LOCK_AQUIRE_TIMES: LazyLock<Result<Histogram>> = Laz
exponential_buckets(1e-4, 4.0, 7),
)
});
pub static FORK_CHOICE_UPGRADABLE_READ_LOCK_AQUIRE_TIMES: LazyLock<Result<Histogram>> =
LazyLock::new(|| {
try_create_histogram_with_buckets(
"beacon_fork_choice_upgradable_read_lock_aquire_seconds",
"Time taken to aquire the fork-choice upgradable read lock",
exponential_buckets(1e-4, 4.0, 7),
)
});
pub static FORK_CHOICE_WRITE_LOCK_AQUIRE_TIMES: LazyLock<Result<Histogram>> = LazyLock::new(|| {
try_create_histogram_with_buckets(
"beacon_fork_choice_write_lock_aquire_seconds",