use std::{
env, fs,
path::{Path, PathBuf},
process,
sync::{
Arc, OnceLock,
atomic::{AtomicU64, AtomicUsize, Ordering},
},
};
use compio::runtime::Runtime;
use itoa::Buffer;
use parking_lot::RwLock;
use wbase::time::now_ms;
use wdev::Device;
use wepoch::LightEpoch;
use whasher::{
GxBuildHasher, HashSet,
papaya::{HashMap as PapayaMap, Operation},
};
use whlog::HybridLog;
use windex::{HashBucket, HashBucketEntry, HashIndex};
use wval::{KeyTag, MetaValue, NamespaceDbCodec};
use crate::{
config::{GcConfig, StoreConfig},
error::{Error, Result},
gc,
read_cache::{ReadCache, absolute_address, is_read_cache_addr},
session::{SessionSlot, StoreSession},
ttl::{TTL_VALUE_LEN, TtlProbe},
};
pub type KeyIdVersionsMap = PapayaMap<u64, (u64, bool), GxBuildHasher>;
pub type WriteListenerFn = Arc<dyn Fn(&[u8], &[u8], bool) + Send + Sync>;
pub type RangeIndexListenerFn = Arc<dyn Fn(&[u8], &[u8], &[u8], bool) + Send + Sync>;
pub type TtlPurgeListenerFn = Arc<dyn Fn(u64, u64, &[u8], u64) + Send + Sync>;
#[inline]
pub fn new_key_id_versions_map() -> KeyIdVersionsMap {
PapayaMap::builder()
.hasher(GxBuildHasher::default())
.build()
}
pub const KEY_ID_ASSIGN_MARGIN: u64 = 1 << 20;
pub struct WedbStore<D: Device> {
pub config: StoreConfig,
pub index: Arc<HashIndex>,
pub hlog: Arc<HybridLog<D>>,
pub epoch: Arc<LightEpoch>,
pub device: Arc<D>,
pub next_key_id: AtomicU64,
pub bftree: Arc<wbftree::BfTreeService>,
pub range_index: Arc<wbftree::RangeIndexManager>,
pub reviv_pool: Arc<wreviv::FreeRecordPool>,
pub read_cache: Arc<ReadCache>,
pub key_id_versions: KeyIdVersionsMap,
gc: OnceLock<gc::GcHandle<D>>,
pub(crate) gc_cfg: Arc<RwLock<GcConfig>>,
write_listener: OnceLock<WriteListenerFn>,
range_listener: OnceLock<RangeIndexListenerFn>,
ttl_purge_listener: OnceLock<TtlPurgeListenerFn>,
pub(crate) purge_suppress: AtomicUsize,
temp_range_index_dir: Option<PathBuf>,
temp_bftree_path: Option<PathBuf>,
keyspace_scan_session: SessionSlot<D>,
}
impl<D: Device> WedbStore<D> {
#[inline]
fn generate_initial_key_id() -> u64 {
let now = now_ms();
let rand = fastrand::u16(..) as u64;
((now << 16) | rand).max(1)
}
fn init_bftree(config: &StoreConfig) -> Result<(Arc<wbftree::BfTreeService>, Option<PathBuf>)> {
if let Some(path) = &config.bftree_path {
let mut tmp_os = path.as_os_str().to_os_string();
tmp_os.push(".recovering");
let tmp_path = PathBuf::from(tmp_os);
if tmp_path.exists() {
let _ = fs::remove_file(&tmp_path);
}
if path.exists() && wbftree::file_has_cpr_magic(path) {
match wbftree::BfTreeService::recover_from_cpr_snapshot(
path,
true,
wbftree::StorageBackendType::Disk,
) {
Ok(service) => return Ok((Arc::new(service), None)),
Err(e) => {
log::warn!(
"BfTree 工作文件恢复失败,删除重建: path={}, err={e}",
path.display()
);
}
}
fs::remove_file(path)?;
} else if path.exists() {
log::warn!(
"BfTree 工作文件为无魔数孤儿基文件(未被 Checkpoint 覆盖),删除重建: path={}",
path.display()
);
fs::remove_file(path)?;
}
Ok((Arc::new(wbftree::BfTreeService::open_disk(path, 4)?), None))
} else {
let mut buf = Buffer::new();
let mut name = String::from("wbftree_");
name.push_str(buf.format(process::id()));
name.push('_');
name.push_str(buf.format(now_ms()));
name.push('_');
name.push_str(buf.format(fastrand::u64(..)));
name.push_str(".data");
let tmp = env::temp_dir().join(name);
Ok((
Arc::new(wbftree::BfTreeService::open_disk(&tmp, 4)?),
Some(tmp),
))
}
}
fn init_range_index(config: &StoreConfig) -> (Arc<wbftree::RangeIndexManager>, Option<PathBuf>) {
let (ri_log_root, cpr_dir, temp_range_index_dir) = if let Some(dir) = &config.range_index_dir {
(dir.join("rangeindex"), dir.join("checkpoints"), None)
} else {
let mut buf = Buffer::new();
let mut name = String::from("wedb_rangeindex_");
name.push_str(buf.format(process::id()));
name.push('_');
name.push_str(buf.format(now_ms()));
name.push('_');
name.push_str(buf.format(fastrand::u64(..)));
let tmp = env::temp_dir().join(name);
(tmp.join("log"), tmp.join("cpr"), Some(tmp))
};
(
Arc::new(wbftree::RangeIndexManager::new(ri_log_root, cpr_dir)),
temp_range_index_dir,
)
}
fn check_index_capacity(config: &StoreConfig, index: &HashIndex) -> Result<()> {
if config.index_size != index.size {
return Err(Error::IndexSizeMismatch {
config: config.index_size,
actual: index.size,
});
}
Ok(())
}
fn assemble(
config: StoreConfig,
index: Arc<HashIndex>,
hlog: Arc<HybridLog<D>>,
epoch: Arc<LightEpoch>,
device: Arc<D>,
bftree: Arc<wbftree::BfTreeService>,
temp_bftree_path: Option<PathBuf>,
) -> Self {
let (range_index, temp_range_index_dir) = Self::init_range_index(&config);
let reviv_pool = Arc::new(wreviv::FreeRecordPool::new());
let read_cache = Arc::new(
ReadCache::new(
config.page_size,
config.read_cache_num_pages,
config.enable_read_cache,
)
.unwrap_or_else(|e| {
log::warn!("ReadCache 按会话配置创建失败,降级为默认禁用配置: err={e}");
unsafe { ReadCache::new(4096, 8, false).unwrap_unchecked() }
}),
);
let gc_cfg = Arc::new(RwLock::new(config.gc.clone()));
Self {
config,
index,
hlog,
epoch,
device,
next_key_id: AtomicU64::new(Self::generate_initial_key_id()),
bftree,
range_index,
reviv_pool,
read_cache,
key_id_versions: new_key_id_versions_map(),
gc: OnceLock::new(),
gc_cfg,
write_listener: OnceLock::new(),
range_listener: OnceLock::new(),
ttl_purge_listener: OnceLock::new(),
purge_suppress: AtomicUsize::new(0),
temp_range_index_dir,
temp_bftree_path,
keyspace_scan_session: SessionSlot::new(),
}
}
pub fn open(config: StoreConfig, device: Arc<D>) -> Result<Self> {
config.validate()?;
let index = Arc::new(HashIndex::new(config.index_size)?);
let epoch = Arc::new(LightEpoch::new(config.max_sessions));
let hlog_config = config.to_hlog_config()?;
let hlog = Arc::new(HybridLog::new(
hlog_config,
Arc::clone(&device),
Arc::clone(&epoch),
)?);
let (bftree, temp_bftree_path) = Self::init_bftree(&config)?;
Ok(Self::assemble(
config,
index,
hlog,
epoch,
device,
bftree,
temp_bftree_path,
))
}
pub fn from_components(
config: StoreConfig,
index: Arc<HashIndex>,
hlog: Arc<HybridLog<D>>,
epoch: Arc<LightEpoch>,
device: Arc<D>,
) -> Result<Self> {
config.validate()?;
Self::check_index_capacity(&config, &index)?;
let (bftree, temp_bftree_path) = Self::init_bftree(&config).unwrap_or_else(|e| {
log::warn!("BfTree 初始化失败,降级为内存树(配置的持久工作文件不生效): err={e}");
(
Arc::new(unsafe { wbftree::BfTreeService::open_memory(4).unwrap_unchecked() }),
None,
)
});
Ok(Self::assemble(
config,
index,
hlog,
epoch,
device,
bftree,
temp_bftree_path,
))
}
pub fn from_components_with_bftree(
config: StoreConfig,
index: Arc<HashIndex>,
hlog: Arc<HybridLog<D>>,
epoch: Arc<LightEpoch>,
device: Arc<D>,
bftree: Arc<wbftree::BfTreeService>,
) -> Result<Self> {
config.validate()?;
Self::check_index_capacity(&config, &index)?;
Ok(Self::assemble(
config, index, hlog, epoch, device, bftree, None,
))
}
#[inline]
pub fn raise_key_id_floor(&self, floor: u64) {
self.next_key_id.fetch_max(floor, Ordering::Relaxed);
}
#[inline]
pub fn update_key_id_meta(&self, key_id: u64, version: u64, is_alive: bool) {
self
.key_id_versions
.pin()
.compute(key_id, |entry| match entry {
Some((_, &(curr_ver, curr_alive))) => {
if version > curr_ver || (version == curr_ver && curr_alive && !is_alive) {
Operation::Insert((version, is_alive))
} else {
Operation::Abort(())
}
}
None => Operation::Insert((version, is_alive)),
});
}
#[inline]
pub fn get_key_id_meta(&self, key_id: u64) -> Option<(u64, bool)> {
self.key_id_versions.pin().get(&key_id).copied()
}
#[inline]
pub fn remove_key_id_meta(&self, key_id: u64) {
self.key_id_versions.pin().remove(&key_id);
}
pub fn new_session(self: &Arc<Self>) -> Result<StoreSession<D>> {
let participant = self.epoch.register()?;
Ok(StoreSession::new(Arc::clone(self), participant))
}
pub fn open_shared(config: StoreConfig, device: Arc<D>) -> Result<Arc<Self>>
where
D: Device + 'static,
{
let store = Arc::new(Self::open(config, device)?);
store.start_gc();
Ok(store)
}
pub fn start_gc(self: &Arc<Self>) -> bool
where
D: Device + 'static,
{
if self.gc.get().is_some() {
return true;
}
if !self.gc_cfg.read().enabled || Runtime::try_current().is_none() {
return false;
}
self.gc.set(gc::GcManager::spawn(Arc::clone(self))).is_ok()
}
pub fn update_gc_config(&self, f: impl FnOnce(&mut GcConfig)) {
f(&mut self.gc_cfg.write());
}
pub fn gc_config(&self) -> GcConfig {
self.gc_cfg.read().clone()
}
#[inline]
pub fn gc_handle(&self) -> Option<&gc::GcHandle<D>> {
self.gc.get()
}
pub fn set_write_listener(&self, listener: WriteListenerFn) -> bool {
self.write_listener.set(listener).is_ok()
}
#[inline]
pub(crate) fn write_listener(&self) -> Option<&WriteListenerFn> {
self.write_listener.get()
}
pub fn set_range_listener(&self, listener: RangeIndexListenerFn) -> bool {
self.range_listener.set(listener).is_ok()
}
#[inline]
pub(crate) fn range_listener(&self) -> Option<&RangeIndexListenerFn> {
self.range_listener.get()
}
pub fn set_ttl_purge_listener(&self, listener: TtlPurgeListenerFn) -> bool {
self.ttl_purge_listener.set(listener).is_ok()
}
#[inline]
pub(crate) fn ttl_purge_listener(&self) -> Option<&TtlPurgeListenerFn> {
self.ttl_purge_listener.get()
}
pub async fn flush_all(&self) -> Result<()> {
let head = self.hlog.head_address();
let tail = self.hlog.tail_address();
if tail > head {
let start_page = self.hlog.config.page_id(head);
let end_page = self.hlog.config.page_id(tail.saturating_sub(1));
if start_page <= end_page {
self.hlog.flush_pages_range(start_page, end_page).await?;
}
}
self.device.sync().await.map_err(Error::from)?;
Ok(())
}
pub async fn flush_and_evict_all(&self) -> Result<()> {
let tail = self.tail_address();
self.flush_all().await?;
self.shift_read_only_address(tail);
self.shift_head_address(tail);
Ok(())
}
#[inline]
pub fn temp_range_index_dir(&self) -> Option<&Path> {
self.temp_range_index_dir.as_deref()
}
#[inline]
pub fn temp_bftree_path(&self) -> Option<&Path> {
self.temp_bftree_path.as_deref()
}
pub fn entry_count(&self) -> usize {
let begin_addr = self.hlog.begin_address();
let mut count = 0;
for bucket in self.index.buckets.iter() {
let mut curr_bucket = bucket;
loop {
for item in curr_bucket.entries.iter().take(HashBucket::DATA_ENTRIES) {
let raw = item.load(Ordering::Acquire);
if raw == 0 {
continue;
}
let entry = HashBucketEntry::from_raw(raw);
if entry.is_tentative() {
continue;
}
let addr = entry.address();
if is_read_cache_addr(addr) {
let abs_addr = absolute_address(addr);
if abs_addr >= self.read_cache.head_address()
&& abs_addr < self.read_cache.tail_address()
{
count += 1;
} else {
let real_addr = self.read_cache.skip_read_cache(addr);
if real_addr >= begin_addr {
count += 1;
}
}
} else if addr >= begin_addr {
count += 1;
}
}
let overflow_idx = curr_bucket.overflow_index();
if overflow_idx == 0 {
break;
}
match self.index.overflow_pool.get(overflow_idx) {
Some(next) => curr_bucket = next,
None => break,
}
}
}
count
}
#[inline]
pub fn tail_address(&self) -> u64 {
self.hlog.tail_address()
}
#[inline]
pub fn read_only_address(&self) -> u64 {
self.hlog.read_only_address()
}
#[inline]
pub fn head_address(&self) -> u64 {
self.hlog.head_address()
}
#[inline]
pub fn begin_address(&self) -> u64 {
self.hlog.begin_address()
}
#[inline]
pub fn shift_read_only_address(&self, new_ro: u64) {
self.hlog.shift_read_only_address(new_ro);
if self.config.enable_revivification {
self.reviv_pool.purge_below(new_ro);
}
}
#[inline]
pub fn shift_head_address(&self, new_head: u64) {
self.hlog.shift_head_address(new_head);
}
pub async fn shift_begin_address(&self, new_begin: u64) -> Result<()> {
self
.hlog
.shift_begin_address(new_begin)
.await
.map_err(Error::from)?;
if self.config.enable_revivification {
self.reviv_pool.purge_below(new_begin);
}
if let Err(e) = self.range_index.on_truncate(new_begin as i64) {
log::warn!("range_index on_truncate({new_begin}) 回收快照失败: {e}");
}
Ok(())
}
pub async fn truncate(&self) -> Result<()> {
let begin = self.hlog.begin_address();
self
.device
.truncate_until_address(begin)
.await
.map_err(Error::from)?;
if self.config.enable_revivification {
self.reviv_pool.purge_below(begin);
}
if let Err(e) = self.range_index.on_truncate(begin as i64) {
log::warn!("range_index on_truncate({begin}) 回收快照失败: {e}");
}
Ok(())
}
pub async fn expired_key_deletion_scan(
self: &Arc<Self>,
db_id: Option<u64>,
) -> Result<(u64, u64)> {
let from = self.hlog.read_only_address();
let until = self.hlog.tail_address();
let session = self.new_session()?;
let now = now_ms();
let target_db = db_id.unwrap_or(0);
let mut scanned = 0u64;
let mut to_expire: HashSet<(u64, Box<[u8]>)> = HashSet::with_hasher(GxBuildHasher::default());
self
.hlog
.scan(from, until, |_, rec| {
scanned += 1;
if rec.is_tombstone() {
return Ok(true);
}
if let Some((ns, db, user_key)) = StoreSession::<D>::user_key_from_ttl_key(rec.key)
&& db == target_db
&& let Ok(be) = <[u8; TTL_VALUE_LEN]>::try_from(rec.value)
&& u64::from_be_bytes(be) <= now
{
session.set_context(ns, db);
if matches!(session.probe_ttl(user_key, now), TtlProbe::Pass) {
return Ok(true);
}
to_expire.insert((ns, Box::from(user_key)));
}
Ok(true)
})
.await?;
let mut deleted = 0u64;
for (ns, key) in to_expire {
session.set_context(ns, target_db);
if session.check_expired(&key).await? {
deleted += 1;
}
}
Ok((deleted, scanned))
}
pub async fn keyspace_stats(self: &Arc<Self>) -> Result<(u64, u64)> {
let session = self.keyspace_scan_session.take(self)?;
let from = self.hlog.begin_address();
let until = self.hlog.tail_address();
let now = now_ms();
let mut candidates: HashSet<(u64, u64, Box<[u8]>)> =
HashSet::with_hasher(GxBuildHasher::default());
self
.hlog
.scan(from, until, |_, rec| {
if rec.is_tombstone() {
return Ok(true);
}
if let Ok((ns, db, tag, user_key)) = NamespaceDbCodec::decode_tagged_key(rec.key)
&& matches!(tag, KeyTag::String | KeyTag::Meta)
{
candidates.insert((ns, db, Box::from(user_key)));
}
Ok(true)
})
.await?;
let mut key_count = 0u64;
let mut expire_count = 0u64;
for (ns, db, key) in &candidates {
session.set_context(*ns, *db);
let str_k = session.session_string_key(key);
let alive = session.read_raw_with(&str_k, |_| ()).await?.is_some();
let alive = if alive {
true
} else {
let meta_k = session.session_meta_key(key);
match session.read_raw(&meta_k).await? {
Some(bytes) => matches!(MetaValue::read_size(&bytes), Ok(size) if size > 0),
None => false,
}
};
if !alive {
continue;
}
let ttl = session.ttl_of(key).await?;
if ttl.is_some_and(|exp| exp <= now) {
continue;
}
key_count += 1;
if ttl.is_some() {
expire_count += 1;
}
}
self.keyspace_scan_session.restore(session);
Ok((key_count, expire_count))
}
#[inline]
pub fn hlog(&self) -> &Arc<HybridLog<D>> {
&self.hlog
}
#[inline]
pub fn bftree(&self) -> &Arc<wbftree::BfTreeService> {
&self.bftree
}
pub fn scan_range_callback<F>(&self, start: &[u8], end: &[u8], on_record: F) -> Result<usize>
where
F: FnMut(&[u8], &[u8]) -> bool,
{
self
.bftree
.scan_with_end_key_callback(start, end, wbftree::ScanReturnField::KeyAndValue, on_record)
.map_err(Error::from)
}
}
impl<D: Device> Drop for WedbStore<D> {
fn drop(&mut self) {
if let Some(h) = self.gc.take() {
h.stop();
}
self.bftree.dispose();
if let Some(tmp_path) = &self.temp_bftree_path {
let _ = fs::remove_file(tmp_path);
}
self.range_index.dispose();
if let Some(tmp_dir) = &self.temp_range_index_dir {
let _ = fs::remove_dir_all(tmp_dir);
}
}
}