use std::{
path::{Path, PathBuf},
sync::Arc,
thread,
};
use parking_lot::Mutex;
use wkv::{BfTreeService, RangeIndexManager as EngineRangeIndexManager};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlushFileEntry {
pub path: PathBuf,
pub key_hash: String,
pub address: i64,
}
pub struct RangeIndexManager {
engine: Arc<EngineRangeIndexManager>,
recovered_checkpoint_token: Mutex<Option<String>>,
}
impl RangeIndexManager {
pub fn new(ri_log_root: impl Into<PathBuf>, cpr_dir: impl Into<PathBuf>) -> Self {
Self::from_engine(Arc::new(EngineRangeIndexManager::new(ri_log_root, cpr_dir)))
}
pub fn from_engine(engine: Arc<EngineRangeIndexManager>) -> Self {
Self {
engine,
recovered_checkpoint_token: Mutex::new(None),
}
}
#[inline]
pub fn engine(&self) -> &Arc<EngineRangeIndexManager> {
&self.engine
}
pub fn try_claim_snapshot(&self, key: &[u8]) -> bool {
let key_id = EngineRangeIndexManager::key_id_of(key);
self
.engine
.live_indexes()
.pin()
.get(&key_id)
.is_some_and(|entry| entry.try_claim_snapshot())
}
pub fn release_snapshot(&self, key: &[u8]) {
let key_id = EngineRangeIndexManager::key_id_of(key);
if let Some(entry) = self.engine.live_indexes().pin().get(&key_id).cloned() {
entry.release_snapshot();
}
}
pub fn log_data_path(&self, hash_prefix: &str) -> PathBuf {
self.engine.data_file_path(hash_prefix)
}
pub fn log_flush_path(&self, hash_prefix: &str, logical_address: i64) -> PathBuf {
self.engine.log_flush_path(hash_prefix, logical_address)
}
pub fn checkpoint_snapshot_path(&self, hash_prefix: &str, checkpoint_token: &str) -> PathBuf {
self
.engine
.checkpoint_snapshot_path(checkpoint_token, hash_prefix)
}
pub fn checkpoint_snapshot_dir(&self, checkpoint_token: &str) -> PathBuf {
self
.engine
.checkpoint_snapshot_path(checkpoint_token, "")
.parent()
.map_or_else(PathBuf::new, Path::to_path_buf)
}
#[inline]
pub const fn round_up_to_power_of2(v: u32) -> u32 {
let mut x = v.wrapping_sub(1);
x |= x >> 1;
x |= x >> 2;
x |= x >> 4;
x |= x >> 8;
x |= x >> 16;
x.wrapping_add(1)
}
pub fn dispose_bf_tree_deferred(&self, tree: Arc<BfTreeService>) {
thread::spawn(move || {
if let Err(e) = tree.dispose_quiesced() {
log::warn!("Deferred dispose failed: {e}");
}
});
}
pub fn register_pending(&self, key: &[u8], src_flush_address: i64) -> Result<bool, String> {
self
.engine
.pre_stage_and_register_pending(key, src_flush_address)
.map(|()| true)
.map_err(|e| e.to_string())
}
pub fn set_recovered_checkpoint_token(&self, token: impl Into<String>) {
*self.recovered_checkpoint_token.lock() = Some(token.into());
}
pub fn recovered_checkpoint_token(&self) -> Option<String> {
self.recovered_checkpoint_token.lock().clone()
}
pub fn enumerate_flush_files(&self) -> Vec<FlushFileEntry> {
self
.engine
.enumerate_files_for_replication("", i64::MIN, i64::MAX)
.unwrap_or_default()
.into_iter()
.filter(|entry| entry.is_flush_file)
.map(|entry| FlushFileEntry {
path: entry.path,
key_hash: entry.key_hash,
address: entry.address,
})
.collect()
}
}
#[cfg(test)]
mod tests {
use std::fs;
use tempfile::tempdir;
use wkv::RangeIndexManager as Engine;
use super::*;
fn prefix_of(key: &[u8]) -> String {
Engine::hash_prefix_of(key)
}
#[test]
fn round_up_matches_std_next_power_of_two() {
assert_eq!(RangeIndexManager::round_up_to_power_of2(0), 0);
for v in [1u32, 2, 3, 4, 5, 1023, 1024, 1025, 4095, 65536, 0x7FFF_FFFF] {
assert_eq!(
RangeIndexManager::round_up_to_power_of2(v),
v.next_power_of_two()
);
}
assert_eq!(Engine::compute_leaf_page_size(2049), 8192);
assert_eq!(Engine::compute_leaf_page_size(2048), 4096);
}
#[test]
fn path_helpers_follow_layout() {
let dir = tempdir().unwrap();
let mgr = RangeIndexManager::new(dir.path().join("ri"), dir.path().join("cpr"));
let prefix = prefix_of(b"key-a");
assert_eq!(
mgr.log_data_path(&prefix),
dir.path().join("ri").join(format!("{prefix}.data.bftree"))
);
let flush = mgr.log_flush_path(&prefix, 4096);
assert_eq!(flush.parent(), mgr.log_data_path(&prefix).parent());
let name = flush.file_name().unwrap().to_str().unwrap();
assert!(
name
.strip_prefix(&prefix)
.is_some_and(|rest| rest.starts_with('.') && rest.ends_with(".flush.bftree"))
);
assert_ne!(flush, mgr.log_flush_path(&prefix, 8192));
let dir_cp = mgr.checkpoint_snapshot_dir("token-1");
let path_cp = mgr.checkpoint_snapshot_path(&prefix, "token-1");
assert_eq!(path_cp.parent(), Some(dir_cp.as_path()));
assert!(path_cp.starts_with(&dir_cp));
assert!(dir_cp.ends_with("rangeindex"));
}
#[test]
fn try_claim_release_roundtrip_without_entry() {
let dir = tempdir().unwrap();
let mgr = RangeIndexManager::new(dir.path().join("ri"), dir.path().join("cpr"));
assert!(!mgr.try_claim_snapshot(b"absent"));
mgr.release_snapshot(b"absent");
}
#[test]
fn try_claim_is_mutually_exclusive_until_released() {
let dir = tempdir().unwrap();
let mgr = RangeIndexManager::new(dir.path().join("ri"), dir.path().join("cpr"));
mgr
.engine
.create_bftree(
b"idx",
wkv::StorageBackend::Memory,
wkv::TreeTuning::default(),
)
.unwrap();
assert!(mgr.try_claim_snapshot(b"idx"));
assert!(!mgr.try_claim_snapshot(b"idx"));
mgr.release_snapshot(b"idx");
assert!(mgr.try_claim_snapshot(b"idx"));
mgr.release_snapshot(b"idx");
}
#[test]
fn recovered_checkpoint_token_roundtrip() {
let dir = tempdir().unwrap();
let mgr = RangeIndexManager::new(dir.path().join("ri"), dir.path().join("cpr"));
assert_eq!(mgr.recovered_checkpoint_token(), None);
mgr.set_recovered_checkpoint_token("ckpt-42");
assert_eq!(mgr.recovered_checkpoint_token().as_deref(), Some("ckpt-42"));
mgr.set_recovered_checkpoint_token("ckpt-43");
assert_eq!(mgr.recovered_checkpoint_token().as_deref(), Some("ckpt-43"));
}
#[test]
fn enumerate_flush_files_parses_embedded_address() {
let dir = tempdir().unwrap();
let mgr = RangeIndexManager::new(dir.path().join("ri"), dir.path().join("cpr"));
let p1 = prefix_of(b"k1");
let p2 = prefix_of(b"k2");
let f1 = mgr.log_flush_path(&p1, 0x100);
let f2 = mgr.log_flush_path(&p2, 0x200);
fs::write(&f1, b"x").unwrap();
fs::write(&f2, b"y").unwrap();
fs::write(dir.path().join("ri").join("garbage.bftree"), b"z").unwrap();
let mut files = mgr.enumerate_flush_files();
files.sort_by_key(|f| f.address);
assert_eq!(files.len(), 2);
assert_eq!(files[0].address, 0x100);
assert_eq!(files[0].key_hash, p1);
assert_eq!(files[1].address, 0x200);
assert_eq!(files[1].key_hash, p2);
}
}