use crate::{
db_cache::{CachedEntry, CachedKey, DbCache},
error::SlateDBError,
};
use async_trait::async_trait;
use log::info;
pub struct FoyerHybridCache {
inner: foyer::HybridCache<CachedKey, CachedEntry>,
}
impl FoyerHybridCache {
pub fn new_with_cache(cache: foyer::HybridCache<CachedKey, CachedEntry>) -> Self {
Self { inner: cache }
}
}
impl FoyerHybridCache {
async fn get(&self, key: &CachedKey) -> Result<Option<CachedEntry>, crate::Error> {
self.inner
.get(key)
.await
.map_err(|e| SlateDBError::from(e).into())
.map(|maybe_v| maybe_v.map(|v| v.value().clone()))
}
}
#[async_trait]
impl DbCache for FoyerHybridCache {
async fn get_block(&self, key: &CachedKey) -> Result<Option<CachedEntry>, crate::Error> {
self.get(key).await
}
async fn get_index(&self, key: &CachedKey) -> Result<Option<CachedEntry>, crate::Error> {
self.get(key).await
}
async fn get_filter(&self, key: &CachedKey) -> Result<Option<CachedEntry>, crate::Error> {
self.get(key).await
}
async fn get_stats(&self, key: &CachedKey) -> Result<Option<CachedEntry>, crate::Error> {
self.get(key).await
}
async fn insert(&self, key: CachedKey, value: CachedEntry) {
self.inner.insert(key, value);
}
async fn remove(&self, key: &CachedKey) {
self.inner.remove(key);
}
fn entry_count(&self) -> u64 {
0
}
async fn close(&self) -> Result<(), crate::Error> {
let memory_bytes = self.inner.memory().usage();
info!(
"foyer hybrid cache: closing \
(flushing {:.2} MB from memory to disk)",
memory_bytes as f64 / (1024.0 * 1024.0)
);
let result = self
.inner
.close()
.await
.map_err(|e| crate::Error::from(SlateDBError::from(e)));
match &result {
Ok(()) => info!("foyer hybrid cache: closed successfully"),
Err(e) => info!("foyer hybrid cache: close failed [error={e:?}]"),
}
result
}
}
#[cfg(test)]
mod tests {
use crate::db_cache::foyer_hybrid::FoyerHybridCache;
use crate::db_cache::{CachedEntry, CachedKey, DbCache};
use crate::db_state::SsTableId;
use crate::format::sst::BlockBuilder;
use foyer::{
BlockEngineConfig, DeviceBuilder, FsDeviceBuilder, HybridCacheBuilder, PsyncIoEngineConfig,
};
use rand::RngCore;
use std::collections::HashMap;
use std::sync::Arc;
use tempfile::{tempdir, TempDir};
const SST_ID: SsTableId = SsTableId::Wal(123);
#[tokio::test]
async fn test_hybrid_cache() {
let (cache, _dir) = setup().await;
let mut items = HashMap::new();
for b in 0u64..256 {
let k = CachedKey::from((SST_ID, b));
let v = build_block();
cache.insert(k.clone(), v.clone()).await;
items.insert(k, v);
}
let mut found = 0;
let mut notfound = 0;
for (k, v) in items {
let cached_v = cache.get_block(&k).await.unwrap();
if let Some(cached_v) = cached_v {
assert!(v.block().unwrap().as_ref() == cached_v.block().unwrap().as_ref());
found += 1;
} else {
notfound += 1;
}
}
println!("found: {}, notfound: {}", found, notfound);
assert!(found > 0);
}
fn build_block() -> CachedEntry {
let mut rng = rand::rng();
let mut builder = BlockBuilder::new_latest(1024);
loop {
let mut k = vec![0u8; 32];
rng.fill_bytes(&mut k);
let mut v = vec![0u8; 128];
rng.fill_bytes(&mut v);
if builder.add_value(&k, &v, None, None) {
break;
}
}
let block = Arc::new(builder.build().unwrap());
CachedEntry::with_block(block)
}
async fn setup() -> (FoyerHybridCache, TempDir) {
let tempdir = tempdir().unwrap();
let cache = open_cache(tempdir.path()).await;
(cache, tempdir)
}
async fn open_cache(path: &std::path::Path) -> FoyerHybridCache {
let cache = HybridCacheBuilder::new()
.with_name("hybrid_cache_test")
.memory(1024 * 1024)
.with_weighter(|_, v: &CachedEntry| v.size())
.storage()
.with_io_engine_config(PsyncIoEngineConfig::new())
.with_engine_config(
BlockEngineConfig::new(
FsDeviceBuilder::new(path)
.with_capacity(4 * 1024 * 1024)
.build()
.unwrap(),
)
.with_block_size(64 * 1024),
)
.build()
.await
.unwrap();
FoyerHybridCache::new_with_cache(cache)
}
#[tokio::test]
async fn should_persist_blocks_to_disk_on_close() {
let dir = tempdir().unwrap();
let cache = open_cache(dir.path()).await;
let mut keys = Vec::new();
for b in 0u64..64 {
let k = CachedKey::from((SST_ID, b));
cache.insert(k.clone(), build_block()).await;
keys.push(k);
}
cache.close().await.unwrap();
drop(cache);
let cache = open_cache(dir.path()).await;
let mut found = 0;
for k in &keys {
if cache.get_block(k).await.unwrap().is_some() {
found += 1;
}
}
assert_eq!(found, keys.len(), "all entries should survive close+reopen");
}
}