use std::{
collections::{HashMap, HashSet},
sync::{
Arc,
atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
},
};
use moka::notification::RemovalCause;
use parking_lot::Mutex;
type CacheKey = (u64, u64, u64);
type CacheValue = Arc<Vec<u8>>;
const FO_SHARDS: usize = 16;
const LRU_SEGMENTS: usize = 64;
const _: () = assert!(FO_SHARDS.is_power_of_two());
type FoShard = Mutex<HashMap<(u64, u64), HashSet<u64>>>;
struct FileOffsetsIndex {
shards: [FoShard; FO_SHARDS],
}
impl FileOffsetsIndex {
fn new() -> Self {
Self {
shards: std::array::from_fn(|_| Mutex::new(HashMap::new())),
}
}
#[inline]
fn shard_for(member: u64, file_number: u64) -> usize {
let h = (member ^ file_number.rotate_left(17)).wrapping_mul(0x9E37_79B9_7F4A_7C15);
(h >> 60) as usize & (FO_SHARDS - 1)
}
fn add(&self, member: u64, file_number: u64, block_offset: u64) {
self.shards[Self::shard_for(member, file_number)]
.lock()
.entry((member, file_number))
.or_default()
.insert(block_offset);
}
fn remove(&self, member: u64, file_number: u64, block_offset: u64) {
let mut map = self.shards[Self::shard_for(member, file_number)].lock();
if let Some(set) = map.get_mut(&(member, file_number)) {
set.remove(&block_offset);
if set.is_empty() {
map.remove(&(member, file_number));
}
}
}
fn take_file(&self, member: u64, file_number: u64) -> Option<HashSet<u64>> {
self.shards[Self::shard_for(member, file_number)]
.lock()
.remove(&(member, file_number))
}
fn take_member(&self, member: u64) -> Vec<(u64, HashSet<u64>)> {
let mut out = Vec::new();
for shard in &self.shards {
let mut map = shard.lock();
let files: Vec<u64> = map
.keys()
.filter(|&&(m, _)| m == member)
.map(|&(_, f)| f)
.collect();
for f in files {
if let Some(set) = map.remove(&(member, f)) {
out.push((f, set));
}
}
}
out
}
}
pub struct BlockCachePool {
inner: moka::sync::SegmentedCache<CacheKey, CacheValue>,
index: Arc<FileOffsetsIndex>,
next_member: AtomicU64,
disabled: bool,
}
impl BlockCachePool {
pub fn new(capacity_bytes: u64) -> Self {
let index = Arc::new(FileOffsetsIndex::new());
let listener_index = index.clone();
let inner = moka::sync::SegmentedCache::builder(LRU_SEGMENTS)
.max_capacity(capacity_bytes)
.weigher(|_key: &CacheKey, value: &CacheValue| -> u32 {
value.len().min(u32::MAX as usize) as u32
})
.eviction_listener(
move |key: Arc<CacheKey>, _value: CacheValue, _cause: RemovalCause| {
let (member, file_number, block_offset) = *key;
listener_index.remove(member, file_number, block_offset);
},
)
.build();
Self {
inner,
index,
next_member: AtomicU64::new(0),
disabled: capacity_bytes == 0,
}
}
pub fn attach(self: &Arc<Self>) -> BlockCache {
BlockCache {
pool: self.clone(),
member: self.next_member.fetch_add(1, Ordering::Relaxed),
detached: AtomicBool::new(false),
pinned: Mutex::new(HashMap::new()),
pinned_count: AtomicUsize::new(0),
pinned_bytes: AtomicU64::new(0),
}
}
pub fn entry_count(&self) -> u64 {
self.inner.entry_count()
}
}
pub struct BlockCache {
pool: Arc<BlockCachePool>,
member: u64,
detached: AtomicBool,
pinned: Mutex<HashMap<(u64, u64), CacheValue>>,
pinned_count: AtomicUsize,
pinned_bytes: AtomicU64,
}
impl BlockCache {
pub fn new(capacity_bytes: u64) -> Self {
Arc::new(BlockCachePool::new(capacity_bytes)).attach()
}
pub fn get(&self, file_number: u64, block_offset: u64) -> Option<Arc<Vec<u8>>> {
if self.pool.disabled || self.detached.load(Ordering::Relaxed) {
return None;
}
if self.pinned_count.load(Ordering::Relaxed) != 0
&& let Some(v) = self.pinned.lock().get(&(file_number, block_offset))
{
return Some(v.clone());
}
self.pool
.inner
.get(&(self.member, file_number, block_offset))
}
pub fn insert(&self, file_number: u64, block_offset: u64, data: Vec<u8>) -> Arc<Vec<u8>> {
let arc = Arc::new(data);
if self.pool.disabled || self.detached.load(Ordering::Relaxed) {
return arc;
}
self.pool
.inner
.insert((self.member, file_number, block_offset), arc.clone());
self.pool.index.add(self.member, file_number, block_offset);
arc
}
pub fn insert_pinned(
&self,
file_number: u64,
block_offset: u64,
data: Vec<u8>,
) -> Arc<Vec<u8>> {
let arc = Arc::new(data);
if self.pool.disabled {
return arc;
}
{
let mut pinned = self.pinned.lock();
if self.detached.load(Ordering::Relaxed) {
return arc;
}
match pinned.get(&(file_number, block_offset)) {
Some(old) => {
let (old_len, new_len) = (old.len() as u64, arc.len() as u64);
if new_len >= old_len {
self.pinned_bytes
.fetch_add(new_len - old_len, Ordering::Relaxed);
} else {
self.pinned_bytes
.fetch_sub(old_len - new_len, Ordering::Relaxed);
}
}
None => {
self.pinned_count.fetch_add(1, Ordering::Relaxed);
self.pinned_bytes
.fetch_add(arc.len() as u64, Ordering::Relaxed);
}
}
pinned.insert((file_number, block_offset), arc.clone());
}
arc
}
pub fn unpin_file(&self, file_number: u64) {
if self.detached.load(Ordering::Relaxed) {
return;
}
let mut pinned = self.pinned.lock();
let mut removed = 0usize;
let mut removed_bytes = 0u64;
pinned.retain(|&(f, _), v| {
if f == file_number {
removed += 1;
removed_bytes += v.len() as u64;
false
} else {
true
}
});
if removed != 0 {
self.pinned_count.fetch_sub(removed, Ordering::Relaxed);
self.pinned_bytes
.fetch_sub(removed_bytes, Ordering::Relaxed);
}
}
pub fn invalidate_file(&self, file_number: u64) {
if self.detached.load(Ordering::Relaxed) {
return;
}
self.unpin_file(file_number);
if let Some(offsets) = self.pool.index.take_file(self.member, file_number) {
for offset in offsets {
self.pool
.inner
.invalidate(&(self.member, file_number, offset));
}
self.pool.inner.run_pending_tasks();
}
}
pub fn detach(&self) {
if self.detached.swap(true, Ordering::SeqCst) {
return;
}
{
let mut pinned = self.pinned.lock();
let removed = pinned.len();
pinned.clear();
if removed != 0 {
self.pinned_count.fetch_sub(removed, Ordering::Relaxed);
}
}
self.pinned_bytes.store(0, Ordering::Relaxed);
let files = self.pool.index.take_member(self.member);
let mut any = false;
for (file_number, offsets) in files {
for offset in offsets {
self.pool
.inner
.invalidate(&(self.member, file_number, offset));
any = true;
}
}
if any {
self.pool.inner.run_pending_tasks();
}
}
pub fn pinned_bytes(&self) -> u64 {
self.pinned_bytes.load(Ordering::Relaxed)
}
pub fn entry_count(&self) -> u64 {
if self.detached.load(Ordering::Relaxed) {
return 0;
}
self.pool.entry_count() + self.pinned.lock().len() as u64
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_block_cache_basic() {
let cache = BlockCache::new(1024 * 1024);
cache.insert(1, 0, vec![1, 2, 3]);
cache.insert(1, 4096, vec![4, 5, 6]);
cache.insert(2, 0, vec![7, 8, 9]);
assert_eq!(*cache.get(1, 0).unwrap(), vec![1, 2, 3]);
assert_eq!(*cache.get(1, 4096).unwrap(), vec![4, 5, 6]);
assert_eq!(*cache.get(2, 0).unwrap(), vec![7, 8, 9]);
assert!(cache.get(3, 0).is_none());
}
#[test]
fn test_block_cache_pinned_fast_path() {
let cache = BlockCache::new(1024 * 1024);
assert_eq!(cache.pinned_count.load(Ordering::Relaxed), 0);
assert!(cache.get(1, 0).is_none());
cache.insert_pinned(1, 0, vec![9, 9, 9]);
assert_eq!(cache.pinned_count.load(Ordering::Relaxed), 1);
assert_eq!(*cache.get(1, 0).unwrap(), vec![9, 9, 9]);
cache.insert_pinned(1, 0, vec![9, 9, 9]);
assert_eq!(cache.pinned_count.load(Ordering::Relaxed), 1);
cache.insert(2, 0, vec![1, 2, 3]);
assert_eq!(cache.pinned_count.load(Ordering::Relaxed), 1);
assert_eq!(*cache.get(2, 0).unwrap(), vec![1, 2, 3]);
cache.unpin_file(1);
assert_eq!(cache.pinned_count.load(Ordering::Relaxed), 0);
assert!(cache.get(1, 0).is_none());
}
#[test]
fn test_block_cache_unpin_file_multiple_pinned_entries() {
let cache = BlockCache::new(1024 * 1024);
cache.insert_pinned(1, 0, vec![1]);
cache.insert_pinned(1, 4096, vec![2]);
cache.insert_pinned(2, 0, vec![3]);
assert_eq!(cache.pinned_count.load(Ordering::Relaxed), 3);
cache.unpin_file(1);
assert_eq!(cache.pinned_count.load(Ordering::Relaxed), 1);
assert!(cache.get(1, 0).is_none());
assert!(cache.get(1, 4096).is_none());
assert_eq!(*cache.get(2, 0).unwrap(), vec![3]);
}
#[test]
fn test_pool_members_never_collide() {
let pool = Arc::new(BlockCachePool::new(1024 * 1024));
let a = pool.attach();
let b = pool.attach();
a.insert(5, 0, vec![0xAA]);
b.insert(5, 0, vec![0xBB]);
assert_eq!(*a.get(5, 0).unwrap(), vec![0xAA]);
assert_eq!(*b.get(5, 0).unwrap(), vec![0xBB]);
a.insert_pinned(6, 0, vec![0xA1]);
b.insert_pinned(6, 0, vec![0xB1]);
assert_eq!(*a.get(6, 0).unwrap(), vec![0xA1]);
assert_eq!(*b.get(6, 0).unwrap(), vec![0xB1]);
a.invalidate_file(5);
assert!(a.get(5, 0).is_none());
assert_eq!(*b.get(5, 0).unwrap(), vec![0xBB]);
a.unpin_file(6);
assert!(a.get(6, 0).is_none());
assert_eq!(*b.get(6, 0).unwrap(), vec![0xB1]);
}
#[test]
fn test_pool_detach_sweeps_only_the_member() {
let pool = Arc::new(BlockCachePool::new(1024 * 1024));
let a = pool.attach();
let b = pool.attach();
a.insert(1, 0, vec![1]);
a.insert(2, 0, vec![2]);
a.insert_pinned(3, 0, vec![3]);
b.insert(1, 0, vec![9]);
b.insert_pinned(3, 0, vec![8]);
a.detach();
assert!(a.get(1, 0).is_none());
assert!(a.get(2, 0).is_none());
assert!(a.get(3, 0).is_none());
assert_eq!(a.pinned_bytes(), 0);
assert_eq!(a.entry_count(), 0);
assert_eq!(*b.get(1, 0).unwrap(), vec![9]);
assert_eq!(*b.get(3, 0).unwrap(), vec![8]);
assert_eq!(b.pinned_count.load(Ordering::Relaxed), 1);
let arc = a.insert(4, 0, vec![4]);
assert_eq!(*arc, vec![4]); assert!(a.get(4, 0).is_none());
let arc = a.insert_pinned(5, 0, vec![5]);
assert_eq!(*arc, vec![5]);
assert!(a.get(5, 0).is_none());
assert_eq!(a.pinned_count.load(Ordering::Relaxed), 0);
a.detach();
assert_eq!(*b.get(1, 0).unwrap(), vec![9]);
}
#[test]
fn test_pinned_bytes_accounting() {
let cache = BlockCache::new(1024 * 1024);
assert_eq!(cache.pinned_bytes(), 0);
cache.insert_pinned(1, 0, vec![0; 100]);
assert_eq!(cache.pinned_bytes(), 100);
cache.insert_pinned(1, 0, vec![0; 150]);
assert_eq!(cache.pinned_bytes(), 150);
cache.insert_pinned(1, 0, vec![0; 60]);
assert_eq!(cache.pinned_bytes(), 60);
cache.insert_pinned(2, 0, vec![0; 40]);
assert_eq!(cache.pinned_bytes(), 100);
cache.unpin_file(1);
assert_eq!(cache.pinned_bytes(), 40);
cache.unpin_file(2);
assert_eq!(cache.pinned_bytes(), 0);
}
#[test]
fn test_disabled_pool_disables_every_member() {
let pool = Arc::new(BlockCachePool::new(0));
let a = pool.attach();
let arc = a.insert(1, 0, vec![1, 2, 3]);
assert_eq!(*arc, vec![1, 2, 3]); assert!(a.get(1, 0).is_none());
let arc = a.insert_pinned(1, 0, vec![4]);
assert_eq!(*arc, vec![4]);
assert!(a.get(1, 0).is_none());
assert_eq!(a.entry_count(), 0);
}
}