fix: kvcache evict workflow

This commit is contained in:
2026-04-14 15:46:36 +08:00
parent 663ca9c5b9
commit eaf574cd4e
4 changed files with 257 additions and 59 deletions

View File

@@ -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)]);
}
}