fix: kvcache evict workflow
This commit is contained in:
@@ -10,6 +10,12 @@
|
||||
|
||||
use ahash::AHashMap;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum L1Change {
|
||||
Added(u64),
|
||||
Removed(u64),
|
||||
}
|
||||
|
||||
/// Doubly-linked-list-backed LRU keyed by block hash.
|
||||
#[derive(Debug)]
|
||||
pub struct LruBlocks {
|
||||
@@ -56,6 +62,16 @@ impl LruBlocks {
|
||||
self.map.contains_key(&key)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, key: u64) -> bool {
|
||||
if let Some(idx) = self.map.remove(&key) {
|
||||
self.detach(idx);
|
||||
self.free.push(idx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Touch (move to MRU) if present. Returns whether the key was present.
|
||||
pub fn touch(&mut self, key: u64) -> bool {
|
||||
if let Some(&idx) = self.map.get(&key) {
|
||||
@@ -70,33 +86,47 @@ impl LruBlocks {
|
||||
/// existing block just touches it.
|
||||
pub fn insert_blocks(&mut self, hashes: &[u64], evicted_out: &mut Vec<u64>) {
|
||||
for &h in hashes {
|
||||
if self.touch(h) {
|
||||
continue;
|
||||
if let Some(evicted) = self.insert_block(h) {
|
||||
evicted_out.push(evicted);
|
||||
}
|
||||
// need to make room?
|
||||
if self.map.len() == self.capacity {
|
||||
if let Some(tail_idx) = self.tail {
|
||||
let tail_key = self.nodes[tail_idx].key;
|
||||
self.detach(tail_idx);
|
||||
self.map.remove(&tail_key);
|
||||
self.free.push(tail_idx);
|
||||
evicted_out.push(tail_key);
|
||||
}
|
||||
}
|
||||
// allocate node
|
||||
let idx = if let Some(i) = self.free.pop() {
|
||||
self.nodes[i] = Node { key: h, prev: None, next: None };
|
||||
i
|
||||
} else {
|
||||
let i = self.nodes.len();
|
||||
self.nodes.push(Node { key: h, prev: None, next: None });
|
||||
i
|
||||
};
|
||||
self.map.insert(h, idx);
|
||||
self.attach_to_head(idx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_block(&mut self, key: u64) -> Option<u64> {
|
||||
if self.touch(key) {
|
||||
return None;
|
||||
}
|
||||
let mut evicted = None;
|
||||
if self.map.len() == self.capacity {
|
||||
if let Some(tail_idx) = self.tail {
|
||||
let tail_key = self.nodes[tail_idx].key;
|
||||
self.detach(tail_idx);
|
||||
self.map.remove(&tail_key);
|
||||
self.free.push(tail_idx);
|
||||
evicted = Some(tail_key);
|
||||
}
|
||||
}
|
||||
let idx = if let Some(i) = self.free.pop() {
|
||||
self.nodes[i] = Node {
|
||||
key,
|
||||
prev: None,
|
||||
next: None,
|
||||
};
|
||||
i
|
||||
} else {
|
||||
let i = self.nodes.len();
|
||||
self.nodes.push(Node {
|
||||
key,
|
||||
prev: None,
|
||||
next: None,
|
||||
});
|
||||
i
|
||||
};
|
||||
self.map.insert(key, idx);
|
||||
self.attach_to_head(idx);
|
||||
evicted
|
||||
}
|
||||
|
||||
/// Longest leading prefix of `hashes` present; touches the matched blocks.
|
||||
pub fn longest_prefix(&mut self, hashes: &[u64]) -> usize {
|
||||
let mut n = 0usize;
|
||||
@@ -178,6 +208,68 @@ impl TwoTierCache {
|
||||
l1: LruBlocks::new(l1_cap),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_blocks_into_l0(&mut self, hashes: &[u64]) -> Vec<L1Change> {
|
||||
let mut changes = Vec::new();
|
||||
for &h in hashes {
|
||||
self.insert_block_into_l0(h, &mut changes);
|
||||
}
|
||||
changes
|
||||
}
|
||||
|
||||
pub fn promote_l1_blocks_to_l0(&mut self, hashes: &[u64]) -> Vec<L1Change> {
|
||||
let mut changes = Vec::new();
|
||||
for &h in hashes {
|
||||
if self.l1.remove(h) {
|
||||
changes.push(L1Change::Removed(h));
|
||||
}
|
||||
self.insert_block_into_l0(h, &mut changes);
|
||||
}
|
||||
changes
|
||||
}
|
||||
|
||||
pub fn fetch_remote_blocks_to_l0(&mut self, hashes: &[u64]) -> Vec<L1Change> {
|
||||
let mut changes = Vec::new();
|
||||
for &h in hashes {
|
||||
self.stage_remote_block_in_l1(h, &mut changes);
|
||||
let removed = self.l1.remove(h);
|
||||
debug_assert!(removed, "staged remote block must be present in l1");
|
||||
self.insert_block_into_l0(h, &mut changes);
|
||||
}
|
||||
changes
|
||||
}
|
||||
|
||||
fn insert_block_into_l0(&mut self, hash: u64, changes: &mut Vec<L1Change>) {
|
||||
if self.l0.touch(hash) {
|
||||
return;
|
||||
}
|
||||
if self.l1.remove(hash) {
|
||||
changes.push(L1Change::Removed(hash));
|
||||
}
|
||||
if let Some(evicted_l0) = self.l0.insert_block(hash) {
|
||||
self.demote_into_l1(evicted_l0, changes);
|
||||
}
|
||||
}
|
||||
|
||||
fn stage_remote_block_in_l1(&mut self, hash: u64, changes: &mut Vec<L1Change>) {
|
||||
if self.l0.contains(hash) || self.l1.contains(hash) {
|
||||
return;
|
||||
}
|
||||
if let Some(evicted_l1) = self.l1.insert_block(hash) {
|
||||
changes.push(L1Change::Removed(evicted_l1));
|
||||
}
|
||||
}
|
||||
|
||||
fn demote_into_l1(&mut self, hash: u64, changes: &mut Vec<L1Change>) {
|
||||
debug_assert!(!self.l0.contains(hash));
|
||||
if self.l1.touch(hash) {
|
||||
return;
|
||||
}
|
||||
if let Some(evicted_l1) = self.l1.insert_block(hash) {
|
||||
changes.push(L1Change::Removed(evicted_l1));
|
||||
}
|
||||
changes.push(L1Change::Added(hash));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -223,4 +315,61 @@ mod tests {
|
||||
c.insert_blocks(&[4], &mut ev);
|
||||
assert_eq!(ev, vec![2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_tier_cache_demotes_l0_evictions_into_l1() {
|
||||
let mut c = TwoTierCache::new(2, 2);
|
||||
|
||||
assert!(c.insert_blocks_into_l0(&[1, 2]).is_empty());
|
||||
let changes = c.insert_blocks_into_l0(&[3]);
|
||||
|
||||
assert!(c.l0.contains(2));
|
||||
assert!(c.l0.contains(3));
|
||||
assert!(!c.l0.contains(1));
|
||||
assert!(c.l1.contains(1));
|
||||
assert_eq!(changes, vec![L1Change::Added(1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn promoting_l1_blocks_to_l0_keeps_tiers_exclusive() {
|
||||
let mut c = TwoTierCache::new(2, 2);
|
||||
c.insert_blocks_into_l0(&[1, 2, 3]);
|
||||
|
||||
let changes = c.promote_l1_blocks_to_l0(&[1]);
|
||||
|
||||
assert!(c.l0.contains(1));
|
||||
assert!(c.l0.contains(3));
|
||||
assert!(!c.l0.contains(2));
|
||||
assert!(!c.l1.contains(1));
|
||||
assert!(c.l1.contains(2));
|
||||
assert_eq!(changes, vec![L1Change::Removed(1), L1Change::Added(2)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reinserting_block_into_l0_removes_duplicate_from_l1() {
|
||||
let mut c = TwoTierCache::new(2, 2);
|
||||
c.insert_blocks_into_l0(&[1, 2, 3]);
|
||||
|
||||
let changes = c.insert_blocks_into_l0(&[1]);
|
||||
|
||||
assert!(c.l0.contains(1));
|
||||
assert!(c.l0.contains(3));
|
||||
assert!(!c.l1.contains(1));
|
||||
assert!(c.l1.contains(2));
|
||||
assert_eq!(changes, vec![L1Change::Removed(1), L1Change::Added(2)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_fetch_uses_l1_capacity_before_promoting_to_l0() {
|
||||
let mut c = TwoTierCache::new(2, 1);
|
||||
c.insert_blocks_into_l0(&[1, 2, 3]);
|
||||
|
||||
let changes = c.fetch_remote_blocks_to_l0(&[4]);
|
||||
|
||||
assert!(c.l0.contains(3));
|
||||
assert!(c.l0.contains(4));
|
||||
assert!(!c.l1.contains(1));
|
||||
assert!(c.l1.contains(2));
|
||||
assert_eq!(changes, vec![L1Change::Removed(1), L1Change::Added(2)]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user