use std::fmt;
use std::{
io,
path::PathBuf,
sync::{Arc, atomic::Ordering::Relaxed},
};
use waof::{AofEntryType, WalRecord};
use wdev::Device;
use wkv::{CheckpointManager, CheckpointType, WedbStore};
use super::{garnet_database::GarnetDatabase, i_database_manager::HybridLogStats};
use crate::storage::session::objectstore::common::{OBJ_TAG_HASH, OBJ_TAG_SORTED_SET};
pub const AOF_OP_UPSERT: u8 = 0;
pub const AOF_OP_DELETE: u8 = 1;
pub type RecoveredStore<D> = Option<Arc<WedbStore<D>>>;
pub struct DatabaseManagerBase<D: Device> {
pub checkpoint_mgr: CheckpointManager<D>,
pub checkpoint_dir: PathBuf,
}
impl<D: Device> DatabaseManagerBase<D> {
pub fn new(checkpoint_dir: PathBuf) -> Self {
Self {
checkpoint_mgr: CheckpointManager::new(),
checkpoint_dir,
}
}
pub fn try_get_or_add_database(
&self,
db: &Arc<GarnetDatabase<D>>,
) -> wkv::Result<(Arc<GarnetDatabase<D>>, bool)> {
Ok((Arc::clone(db), false))
}
pub fn try_pause_checkpoints(&self, db: &GarnetDatabase<D>) -> bool {
!db.checkpoint_paused.swap(true, Relaxed)
}
pub fn resume_checkpoints(&self, db: &GarnetDatabase<D>) {
db.checkpoint_paused.store(false, Relaxed);
}
pub async fn recover_database_checkpoint_async(
&self,
db: &GarnetDatabase<D>,
recover_from_token: Option<u128>,
) -> wkv::Result<RecoveredStore<D>> {
let token = match recover_from_token {
Some(t) => Some(t),
None => CheckpointManager::<D>::find_latest_checkpoint(&db.checkpoint_dir)
.map_err(wkv::Error::from)?,
};
match token {
Some(t) => Ok(Some(Arc::new(
CheckpointManager::recover(&db.checkpoint_dir, t, Arc::clone(&db.device))
.await
.map_err(wkv::Error::from)?,
))),
None => Ok(None),
}
}
pub async fn recover_database_aof_async(&self, db: &GarnetDatabase<D>) -> wkv::Result<u64> {
let from = db.last_save_store_tail_address.load(Relaxed);
self.replay_database_aof(db, from, u64::MAX).await
}
pub async fn replay_database_aof(
&self,
db: &GarnetDatabase<D>,
from: u64,
until: u64,
) -> wkv::Result<u64> {
let Some(aof) = &db.aof else {
return Ok(0);
};
let session = db.store.new_session()?;
session.set_active_db(db.id.max(0) as u64);
let mut replayed = 0u64;
let end = until.min(aof.tail_address());
let mut it = aof.scan(from, end);
while let Some(rec) = it.next().await.map_err(aof_err)? {
if apply_aof_record(&session, &rec).await? {
replayed += 1;
}
}
Ok(replayed)
}
pub async fn take_database_checkpoint_async(&self, db: &GarnetDatabase<D>) -> wkv::Result<bool> {
if db.checkpoint_paused.load(Relaxed) {
return Ok(false);
}
let token = coarsetime::Clock::now_since_epoch().as_u64() as u128;
let meta = self
.checkpoint_mgr
.create_checkpoint_with_token(
&*db.store,
&db.checkpoint_dir,
CheckpointType::FoldOver,
token,
)
.await
.map_err(wkv::Error::from)?;
db.update_last_save(meta.created_at);
Ok(true)
}
pub async fn take_checkpoint_helper_async(
&self,
db: &GarnetDatabase<D>,
entry_ms: u64,
) -> wkv::Result<bool> {
if db.last_save_ms() < entry_ms {
self.take_database_checkpoint_async(db).await
} else {
Ok(false)
}
}
pub async fn take_on_demand_checkpoint_async(
&self,
db: &GarnetDatabase<D>,
entry_ms: u64,
) -> wkv::Result<()> {
self.take_checkpoint_helper_async(db, entry_ms).await?;
Ok(())
}
pub async fn checkpoint_if_aof_exceeds(
&self,
db: &GarnetDatabase<D>,
aof_size_limit: u64,
) -> wkv::Result<bool> {
if db.aof_size() >= aof_size_limit {
self.take_database_checkpoint_async(db).await
} else {
Ok(false)
}
}
pub fn commit_aof(&self, db: &GarnetDatabase<D>) -> wkv::Result<()> {
if let Some(aof) = &db.aof {
db.last_save_store_tail_address
.store(aof.tail_address(), Relaxed);
}
Ok(())
}
pub fn enqueue_upsert(
&self,
db: &GarnetDatabase<D>,
key: &[u8],
value: &[u8],
) -> wkv::Result<()> {
enqueue_record(
db,
AofEntryType::MainStoreStoreCommand,
AOF_OP_UPSERT,
key,
Some(value),
)
}
pub fn enqueue_delete(&self, db: &GarnetDatabase<D>, key: &[u8]) -> wkv::Result<()> {
enqueue_record(
db,
AofEntryType::MainStoreStoreCommand,
AOF_OP_DELETE,
key,
None,
)
}
pub async fn reset_database(&self, db: &GarnetDatabase<D>) -> wkv::Result<()> {
let session = db.store.new_session()?;
session.set_active_db(db.id.max(0) as u64);
let batch = session.enter_batch();
let ss = crate::storage::session::storage_session::StorageSession::new(batch);
let (_, keys) = ss.string_snapshot().await?;
for key in keys {
ss.delete_string(&key).await?;
}
ss.clear_watches();
db.last_save_ms.store(0, Relaxed);
db.last_save_store_tail_address.store(0, Relaxed);
Ok(())
}
pub async fn execute_hash_collect(&self, db: &GarnetDatabase<D>) -> wkv::Result<usize> {
let (_, n) = execute_collect(self, db, OBJ_TAG_HASH).await?;
Ok(n)
}
pub async fn execute_sorted_set_collect(&self, db: &GarnetDatabase<D>) -> wkv::Result<usize> {
let (_, n) = execute_collect(self, db, OBJ_TAG_SORTED_SET).await?;
Ok(n)
}
pub fn grow_index_if_needed_async(&self, db: &GarnetDatabase<D>) -> wkv::Result<bool> {
Ok(!db.store_index_maxed_out.load(Relaxed))
}
pub async fn initiate_checkpoint_async(&self, db: &GarnetDatabase<D>) -> wkv::Result<bool> {
self.take_database_checkpoint_async(db).await
}
pub fn run_post_checkpoint_cleanup(&self, db: &GarnetDatabase<D>) -> wkv::Result<usize> {
let purged =
CheckpointManager::<D>::purge_outdated(&db.checkpoint_dir, 2).map_err(wkv::Error::from)?;
Ok(purged.len())
}
pub async fn store_expired_key_deletion_scan(
&self,
db: &GarnetDatabase<D>,
) -> wkv::Result<(u64, u64)> {
db.store
.expired_key_deletion_scan(if db.id == 0 { None } else { Some(db.id as u64) })
.await
}
pub async fn get_database_keyspace_stats(
&self,
db: &GarnetDatabase<D>,
) -> wkv::Result<(u64, u64)> {
db.store.keyspace_stats().await
}
pub async fn collect_hybrid_log_stats_for_db(
&self,
db: &GarnetDatabase<D>,
) -> wkv::Result<HybridLogStats> {
let (key_count, expire_count) = self.get_database_keyspace_stats(db).await?;
Ok(HybridLogStats {
begin_address: db.store.begin_address(),
read_only_address: db.store.read_only_address(),
head_address: db.store.head_address(),
tail_address: db.store.tail_address(),
key_count,
expire_count,
})
}
pub async fn collect_hybrid_log_stats(
&self,
db: &GarnetDatabase<D>,
) -> wkv::Result<Vec<(i64, HybridLogStats)>> {
Ok(vec![(
db.id,
self.collect_hybrid_log_stats_for_db(db).await?,
)])
}
}
async fn execute_collect<D: Device>(
_base: &DatabaseManagerBase<D>,
db: &GarnetDatabase<D>,
tag: u8,
) -> wkv::Result<(u8, usize)> {
let session = db.store.new_session()?;
session.set_active_db(db.id.max(0) as u64);
let batch = session.enter_batch();
let ss = crate::storage::session::storage_session::StorageSession::new(batch);
let n = ss.object_collect(|t, _| t == tag).await?;
Ok((tag, n))
}
pub fn aof_err(e: impl fmt::Display) -> wkv::Error {
wkv::Error::Io(io::Error::other(e.to_string()))
}
pub fn enqueue_record<D: Device>(
db: &GarnetDatabase<D>,
entry_type: AofEntryType,
op: u8,
key: &[u8],
value: Option<&[u8]>,
) -> wkv::Result<()> {
let Some(aof) = &db.aof else {
return Ok(()); };
let value = value.unwrap_or_default();
let mut payload = Vec::with_capacity(1 + 8 + key.len() + value.len());
payload.push(op);
payload.extend_from_slice(&(key.len() as u64).to_be_bytes());
payload.extend_from_slice(key);
payload.extend_from_slice(value);
aof.enqueue(&payload).map_err(aof_err)?;
let _ = entry_type; Ok(())
}
pub async fn apply_aof_record<D: Device>(
session: &wkv::StoreSession<D>,
rec: &WalRecord,
) -> wkv::Result<bool> {
let payload = rec.as_slice();
if payload.len() < 9 {
return Ok(false);
}
let op = payload[0];
let key_len = u64::from_be_bytes(payload[1..9].try_into().map_err(aof_err)?) as usize;
if payload.len() < 9 + key_len {
return Ok(false);
}
let key = &payload[9..9 + key_len];
match op {
AOF_OP_UPSERT => {
session.upsert(key, &payload[9 + key_len..]).await?;
Ok(true)
}
AOF_OP_DELETE => {
session.delete(key).await?;
Ok(true)
}
_ => Ok(false),
}
}