-
Notifications
You must be signed in to change notification settings - Fork 14
feat: introduce versioned LRU cache for contract_account_loc_cache
#161
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
base: main
Are you sure you want to change the base?
Conversation
CHAIN-1726 introduce CacheManager
be able to centrally manage the |
🟡 Heimdall Review Status
|
d89de7e to
c78c825
Compare
contract_account_loc_cache
c78c825 to
2120dc8
Compare
contract_account_loc_cachedfb94d5 to
bba089a
Compare
contract_account_loc_cache
src/cache.rs
Outdated
|
|
||
| // Find smallest snapshot_id for this key | ||
| let smallest = if let Some(versions) = self.entries.get(&tail_key) { | ||
| versions.iter().map(|e| e.snapshot_id).min() |
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.
This can just get the first element, assuming that these are sorted by snapshot_id
src/cache.rs
Outdated
| let tail_idx = self.tail.unwrap(); | ||
| let tail_key = self.lru[tail_idx].key.clone(); | ||
|
|
||
| // Find smallest snapshot_id for this key |
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.
The rationale for removing the item with the minimum snapshot_id is that this ensures that there are no version gaps in the data for a specific key/path (temporal coherence). This is what allows us to safely look up values by snapshot, and be able to fall back to not using the cache if there is definitely no match.
This invariant must hold through all permutations of get and set. For a given key/path, this means that we can only append new versions to the right side and pop old versions from the left.
- A writer transaction will always have the latest db version and append to the right.
- A reader transaction may be at the latest or an older db version (e.g. in a long-running RPC query). When inserting an entry into the cache, it would either already be cached (from a reader or writer), not be cached due to the key not being accessed since the db was opened, or not be cached as all versions were evicted from the LRU. Only the last case here is risky, as it may not actually be the latest version of the specific key/value pair. We can solve this by tracking the monotonically-increasing
max_evicted_versionfor the cache, and forbidding insertion of any entry with a version less than this.
5e1eab0 to
8247421
Compare
8247421 to
3e64c53
Compare
src/cache.rs
Outdated
| if let Some(min_id) = self.min_snapshot_id { | ||
| if let Some(versions) = self.entries.get_mut(key) { | ||
| if let Some(idx) = versions.iter().position(|e| e.snapshot_id >= min_id) { | ||
| versions.drain(0..idx); |
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.
We actually need to keep at least one version with snapshot_id <= min_id in order to ensure that we can still query values at min_id
src/cache.rs
Outdated
| if let Some(min_id) = self.min_snapshot_id { | ||
| if let Some(versions) = self.entries.get_mut(key) { | ||
| if let Some(idx) = versions.iter().position(|e| e.snapshot_id >= min_id) { | ||
| versions.drain(0..idx); |
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.
This could end up dropping the head or tail of the LRU, which would currently cause the cache to get into a bad state. We also need to count any entries removed, and adjust the size accordingly.
My general suggestion is to call a shared remove method on any entries we're evicting
| unsafe impl Send for Entry {} | ||
| unsafe impl Send for VersionedLru {} |
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.
not sure if there's a cleaner way to do this
Overview
Introduces a shared cache to benefit Readers whenever caching an account to speed up reading the contract state, as it'd use the same state consistently.
This is done by introducing a versioned LRU cache, which is a doubly-linked-list of entries and a mapping of keys to entries. The two main operations for this versioned LRU cache are:
get(key, target_snapshot_id) => value, which finds the entry matching the key (via the HashMap) with the largest snapshot_id <= target_snapshot_id, and moves the entry to the front.set(key, snapshot_id, value)creates a new entry, puts it at the front of the linked list, and inserts into the HashMap. If the cache is full, it then finds the tail entry, removes it from its HashMap, and then pops it off the list.The LRU always evicts the smallest snapshot_id. It also proactively purges entries that are marked as
None(aka obsolete) by setting amin_snapshot_id.The versioned LRU cache attempts to unify the
contract_account_loc_cacheand not have it at a per-transaction but instead managing it at theStorageEnginelevel.Tests