Avoid duplicate committee cache loads (#3574)

## Issue Addressed

NA

## Proposed Changes

I have observed scenarios on Goerli where Lighthouse was receiving attestations which reference the same, un-cached shuffling on multiple threads at the same time. Lighthouse was then loading the same state from database and determining the shuffling on multiple threads at the same time. This is unnecessary load on the disk and RAM.

This PR modifies the shuffling cache so that each entry can be either:

- A committee
- A promise for a committee (i.e., a `crossbeam_channel::Receiver`)

Now, in the scenario where we have thread A and thread B simultaneously requesting the same un-cached shuffling, we will have the following:

1. Thread A will take the write-lock on the shuffling cache, find that there's no cached committee and then create a "promise" (a `crossbeam_channel::Sender`) for a committee before dropping the write-lock.
1. Thread B will then be allowed to take the write-lock for the shuffling cache and find the promise created by thread A. It will block the current thread waiting for thread A to fulfill that promise.
1. Thread A will load the state from disk, obtain the shuffling, send it down the channel, insert the entry into the cache and then continue to verify the attestation.
1. Thread B will then receive the shuffling from the receiver, be un-blocked and then continue to verify the attestation.

In the case where thread A fails to generate the shuffling and drops the sender, the next time that specific shuffling is requested we will detect that the channel is disconnected and return a `None` entry for that shuffling. This will cause the shuffling to be re-calculated.

## Additional Info

NA
This commit is contained in:
Paul Hauner
2022-09-16 08:54:03 +00:00
parent 7d3948c8fe
commit 2cd3e3a768
9 changed files with 381 additions and 22 deletions

View File

@@ -1498,6 +1498,26 @@ impl<T: EthSpec> BeaconState<T> {
}
}
/// Returns the cache for some `RelativeEpoch`, replacing the existing cache with an
/// un-initialized cache. Returns an error if the existing cache has not been initialized.
pub fn take_committee_cache(
&mut self,
relative_epoch: RelativeEpoch,
) -> Result<CommitteeCache, Error> {
let i = Self::committee_cache_index(relative_epoch);
let current_epoch = self.current_epoch();
let cache = self
.committee_caches_mut()
.get_mut(i)
.ok_or(Error::CommitteeCachesOutOfBounds(i))?;
if cache.is_initialized_at(relative_epoch.into_epoch(current_epoch)) {
Ok(mem::take(cache))
} else {
Err(Error::CommitteeCacheUninitialized(Some(relative_epoch)))
}
}
/// Drops the cache, leaving it in an uninitialized state.
pub fn drop_committee_cache(&mut self, relative_epoch: RelativeEpoch) -> Result<(), Error> {
*self.committee_cache_at_index_mut(Self::committee_cache_index(relative_epoch))? =