use std::{
fs::{read_dir, remove_dir_all, remove_file},
future::Future,
marker::PhantomData,
path::Path,
str,
sync::{
Arc, Mutex,
atomic::{AtomicBool, AtomicU64, Ordering},
},
time::Duration,
};
use compio::{
fs::{File, create_dir_all, metadata, read, rename},
io::AsyncWriteAtExt,
time::sleep,
};
use log::{info, warn};
use wbase::time::{now_ms, now_nanos};
use wdev::Device;
use wepoch::LightEpoch;
use whlog::{HybridLog, HybridLogConfig};
use windex::HashIndex;
use super::{
error::{Error, Result},
index_ckpt::{read_index_checkpoint_truncated, write_index_checkpoint},
meta::{
CheckpointMeta, CheckpointType, FORMAT_VERSION, HlogMeta, INDEX_EXT, INTEGRITY_FROM_VERSION,
IndexMeta, META_EXT, META_PREFIX, StoreMeta, TMP_EXT, index_filename, index_tmp_filename,
meta_filename, meta_tmp_filename,
},
};
pub struct RecoveredCheckpoint<D: Device> {
pub meta: CheckpointMeta,
pub index: Arc<HashIndex>,
pub hlog: Arc<HybridLog<D>>,
pub epoch: Arc<LightEpoch>,
}
pub trait CprStore {
type Device: Device;
fn hlog(&self) -> &HybridLog<Self::Device>;
fn index(&self) -> &HashIndex;
fn epoch(&self) -> &LightEpoch;
fn tail_address(&self) -> u64;
fn begin_address(&self) -> u64;
fn head_address(&self) -> u64;
fn shift_read_only_address(&self, target: u64);
fn flush_all(&self) -> impl Future<Output = Result<()>>;
fn entry_count(&self) -> usize;
fn skip_read_cache(&self, addr: u64) -> u64;
fn take_range_index_checkpoints(&self, dir: &Path, token: u128) -> Result<usize>;
fn take_bftree_checkpoint(&self, dir: &Path, token: u128) -> Result<usize>;
fn checkpoint_store_meta(&self) -> StoreMeta;
}
pub trait CprRecover: CprStore {
fn from_recovered(
recovered: RecoveredCheckpoint<Self::Device>,
checkpoint_dir: &Path,
device: Arc<Self::Device>,
) -> impl Future<Output = Result<Self>>
where
Self: Sized;
}
impl<S: CprStore> CprStore for Arc<S> {
type Device = S::Device;
#[inline]
fn hlog(&self) -> &HybridLog<Self::Device> {
(**self).hlog()
}
#[inline]
fn index(&self) -> &HashIndex {
(**self).index()
}
#[inline]
fn epoch(&self) -> &LightEpoch {
(**self).epoch()
}
#[inline]
fn tail_address(&self) -> u64 {
(**self).tail_address()
}
#[inline]
fn begin_address(&self) -> u64 {
(**self).begin_address()
}
#[inline]
fn head_address(&self) -> u64 {
(**self).head_address()
}
#[inline]
fn shift_read_only_address(&self, target: u64) {
(**self).shift_read_only_address(target)
}
#[inline]
fn flush_all(&self) -> impl Future<Output = Result<()>> {
(**self).flush_all()
}
#[inline]
fn entry_count(&self) -> usize {
(**self).entry_count()
}
#[inline]
fn skip_read_cache(&self, addr: u64) -> u64 {
(**self).skip_read_cache(addr)
}
#[inline]
fn take_range_index_checkpoints(&self, dir: &Path, token: u128) -> Result<usize> {
(**self).take_range_index_checkpoints(dir, token)
}
#[inline]
fn take_bftree_checkpoint(&self, dir: &Path, token: u128) -> Result<usize> {
(**self).take_bftree_checkpoint(dir, token)
}
#[inline]
fn checkpoint_store_meta(&self) -> StoreMeta {
(**self).checkpoint_store_meta()
}
}
impl<S: CprStore> CprStore for &S {
type Device = S::Device;
#[inline]
fn hlog(&self) -> &HybridLog<Self::Device> {
(**self).hlog()
}
#[inline]
fn index(&self) -> &HashIndex {
(**self).index()
}
#[inline]
fn epoch(&self) -> &LightEpoch {
(**self).epoch()
}
#[inline]
fn tail_address(&self) -> u64 {
(**self).tail_address()
}
#[inline]
fn begin_address(&self) -> u64 {
(**self).begin_address()
}
#[inline]
fn head_address(&self) -> u64 {
(**self).head_address()
}
#[inline]
fn shift_read_only_address(&self, target: u64) {
(**self).shift_read_only_address(target)
}
#[inline]
fn flush_all(&self) -> impl Future<Output = Result<()>> {
(**self).flush_all()
}
#[inline]
fn entry_count(&self) -> usize {
(**self).entry_count()
}
#[inline]
fn skip_read_cache(&self, addr: u64) -> u64 {
(**self).skip_read_cache(addr)
}
#[inline]
fn take_range_index_checkpoints(&self, dir: &Path, token: u128) -> Result<usize> {
(**self).take_range_index_checkpoints(dir, token)
}
#[inline]
fn take_bftree_checkpoint(&self, dir: &Path, token: u128) -> Result<usize> {
(**self).take_bftree_checkpoint(dir, token)
}
#[inline]
fn checkpoint_store_meta(&self) -> StoreMeta {
(**self).checkpoint_store_meta()
}
}
pub async fn take_index_checkpoint(
index: &HashIndex,
entry_count: usize,
checkpoint_dir: impl AsRef<Path>,
token: u128,
rc_skip: impl Fn(u64) -> u64,
) -> Result<IndexMeta> {
write_index_checkpoint(index, entry_count, checkpoint_dir, token, &rc_skip).await
}
static TOKEN_SEQ: AtomicU64 = AtomicU64::new(1);
static LAST_TOKEN: Mutex<Option<u128>> = Mutex::new(None);
#[inline]
fn candidate_token() -> u128 {
let now = now_nanos() as u128;
let seq = TOKEN_SEQ.fetch_add(1, Ordering::Relaxed) as u128;
(now << 64) | seq
}
fn issue_token_after(candidate: u128, floor: u128) -> u128 {
let mut last = LAST_TOKEN.lock().unwrap_or_else(|e| e.into_inner());
let mut token = match *last {
Some(issued) if candidate <= issued => issued.checked_add(1).unwrap_or(candidate),
_ => candidate,
};
if token <= floor {
token = floor.checked_add(1).unwrap_or(token);
}
*last = Some(token);
token
}
static CKPT_GATE: AtomicBool = AtomicBool::new(false);
async fn lock_ckpt_gate() -> CkptGate {
while CKPT_GATE.swap(true, Ordering::AcqRel) {
sleep(Duration::from_micros(FENCE_POLL_INTERVAL_US)).await;
}
CkptGate
}
struct CkptGate;
impl Drop for CkptGate {
fn drop(&mut self) {
CKPT_GATE.store(false, Ordering::Release);
}
}
const FENCE_POLL_INTERVAL_US: u64 = 200;
#[inline]
fn addr_violation(name: &str, val: u64, cond: &str, bound: u64, note: &str) -> Error {
use core::fmt::Write;
let mut s = String::with_capacity(96);
let _ = write!(s, "{name} ({val:#x}) {cond} ({bound:#x}){note}");
Error::InvalidRecoveryAddress(s)
}
#[inline]
fn index_mismatch(desc: &str, actual: u64, expected: u64) -> Error {
let mut s = String::with_capacity(desc.len() + 40);
s.push_str(desc);
s.push_str(": ");
let mut buf = itoa::Buffer::new();
s.push_str(buf.format(actual));
s.push_str(" vs ");
s.push_str(buf.format(expected));
Error::InvalidIndexCkpt(s)
}
pub fn next_token() -> u128 {
issue_token_after(candidate_token(), 0)
}
async fn sync_file_data(path: &Path) -> Result<()> {
#[cfg(windows)]
let file = compio::fs::OpenOptions::new()
.write(true)
.open(path)
.await?;
#[cfg(not(windows))]
let file = File::open(path).await?;
file.sync_all().await?;
Ok(())
}
async fn sync_dir_handle(dir: &Path) -> Result<()> {
#[cfg(unix)]
{
File::open(dir).await?.sync_all().await?;
}
#[cfg(not(unix))]
{
let _ = dir;
}
Ok(())
}
pub(crate) async fn sync_checkpoint_dir(dir: &Path) -> Result<()> {
sync_dir_handle(dir).await
}
pub(crate) async fn sync_dir_tree(dir: &Path) -> Result<()> {
for entry in read_dir(dir)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
Box::pin(sync_dir_tree(&entry.path())).await?;
} else {
sync_file_data(&entry.path()).await?;
}
}
sync_dir_handle(dir).await
}
fn rm_path_best_effort(path: &Path) {
if path.is_dir() {
let _ = remove_dir_all(path);
} else {
let _ = remove_file(path);
}
}
#[derive(Debug)]
pub struct CheckpointManager<D: Device = wdev::SegmentedDevice> {
_marker: PhantomData<D>,
}
impl<D: Device> Default for CheckpointManager<D> {
fn default() -> Self {
Self {
_marker: PhantomData,
}
}
}
impl<D: Device> CheckpointManager<D> {
pub const fn new() -> Self {
Self {
_marker: PhantomData,
}
}
pub const fn with_device() -> Self {
Self::new()
}
pub async fn take_index_checkpoint(
&self,
index: &HashIndex,
entry_count: usize,
checkpoint_dir: impl AsRef<Path>,
token: u128,
rc_skip: impl Fn(u64) -> u64,
) -> Result<IndexMeta> {
take_index_checkpoint(index, entry_count, checkpoint_dir, token, rc_skip).await
}
pub async fn create_checkpoint<S: CprStore<Device = D>>(
&self,
store: &S,
checkpoint_dir: impl AsRef<Path>,
cp_type: CheckpointType,
) -> Result<CheckpointMeta> {
let dir = checkpoint_dir.as_ref();
let _gate = lock_ckpt_gate().await;
let floor = Self::list_checkpoints(dir)?.last().copied().unwrap_or(0);
let token = issue_token_after(candidate_token(), floor);
Self::create_gated(store, dir, cp_type, token).await
}
pub async fn create_checkpoint_with_token<S: CprStore<Device = D>>(
&self,
store: &S,
checkpoint_dir: impl AsRef<Path>,
cp_type: CheckpointType,
token: u128,
) -> Result<CheckpointMeta> {
let dir = checkpoint_dir.as_ref();
let _gate = lock_ckpt_gate().await;
Self::create_gated(store, dir, cp_type, token).await
}
async fn create_gated<S: CprStore<Device = D>>(
store: &S,
dir: &Path,
cp_type: CheckpointType,
token: u128,
) -> Result<CheckpointMeta> {
let res = Self::create_checkpoint_inner(store, dir, cp_type, token).await;
if let Err(e) = &res {
warn!("Checkpoint 创建失败,已回收本 Token 残留文件: token={token:#x}, err={e}");
let _ = Self::purge_checkpoint(dir, token);
}
res
}
async fn create_checkpoint_inner<S: CprStore<Device = D>>(
store: &S,
dir: &Path,
cp_type: CheckpointType,
token: u128,
) -> Result<CheckpointMeta> {
if store.epoch().this_instance_protected() {
return Err(Error::CheckpointWhileEpochProtected);
}
create_dir_all(dir).await?;
let tail = store.tail_address();
store.shift_read_only_address(tail);
let fence_epoch = store.epoch().current_epoch();
store.epoch().bump_epoch();
while store.hlog().safe_read_only_address() < tail
|| !store.epoch().is_safe_to_reclaim(fence_epoch)
{
store.epoch().drain();
sleep(Duration::from_micros(FENCE_POLL_INTERVAL_US)).await;
}
store.epoch().drain();
store.flush_all().await?;
let _ri_count = store.take_range_index_checkpoints(dir, token)?;
let _bftree_count = store.take_bftree_checkpoint(dir, token)?;
let mut token_buf = itoa::Buffer::new();
let token_dir = dir.join(token_buf.format(token));
if metadata(&token_dir).await.is_ok() {
sync_dir_tree(&token_dir).await?;
}
let index_meta =
take_index_checkpoint(store.index(), store.entry_count(), dir, token, |addr| {
store.skip_read_cache(addr)
})
.await?;
let hlog_meta = HlogMeta {
begin_address: store.begin_address(),
head_address: store.head_address(),
flushed_until_address: store.hlog().flushed_until_address(),
tail_address: tail,
};
let store_meta = store.checkpoint_store_meta();
let mut meta = CheckpointMeta {
token,
cp_type,
index_meta,
hlog_meta,
store_meta,
created_at: now_ms(),
format_version: FORMAT_VERSION,
integrity_crc32: 0,
};
meta.seal();
let json_bytes = meta.encode_json()?;
let tmp_meta_path = dir.join(meta_tmp_filename(token));
let final_meta_path = dir.join(meta_filename(token));
let mut file = File::create(&tmp_meta_path).await?;
file.write_all_at(json_bytes, 0).await.0?;
file.sync_all().await?;
rename(&tmp_meta_path, &final_meta_path).await?;
sync_checkpoint_dir(dir).await?;
info!(
"成功创建 Checkpoint: token={token:#x}, type={cp_type:?}, entry_count={}, tail={tail:#x}",
index_meta.entry_count
);
Ok(meta)
}
pub async fn recover_checkpoint_components(
checkpoint_dir: impl AsRef<Path>,
token: u128,
device: Arc<D>,
) -> Result<RecoveredCheckpoint<D>> {
let dir = checkpoint_dir.as_ref();
let meta_path = dir.join(meta_filename(token));
if metadata(&meta_path).await.is_err() {
return Err(Error::MetaNotFound(meta_path));
}
let meta_bytes = read(&meta_path).await?;
let meta = CheckpointMeta::decode_auto(&meta_bytes)?;
if meta.token != token {
return Err(Error::TokenMismatch {
expected: token,
actual: meta.token,
});
}
if meta.format_version > FORMAT_VERSION {
return Err(Error::UnsupportedMetaVersion {
actual: meta.format_version,
supported: FORMAT_VERSION,
});
}
if meta.format_version >= INTEGRITY_FROM_VERSION {
let digest = meta.integrity_digest();
if digest != meta.integrity_crc32 {
return Err(Error::MetaChecksumMismatch {
expected: meta.integrity_crc32,
actual: digest,
});
}
}
let epoch = Arc::new(LightEpoch::new(meta.store_meta.max_sessions));
let hlog_config = HybridLogConfig::new(
meta.store_meta.page_size,
meta.store_meta.num_pages,
meta.store_meta.mutable_fraction,
)?;
if meta.index_meta.size != meta.store_meta.index_size {
return Err(index_mismatch(
"索引元数据大小与配置大小不匹配",
meta.index_meta.size as u64,
meta.store_meta.index_size as u64,
));
}
let begin = meta.hlog_meta.begin_address;
let tail = meta.hlog_meta.tail_address;
let flushed = meta.hlog_meta.flushed_until_address;
if tail < hlog_config.initial_address {
return Err(addr_violation(
"TailAddress",
tail,
"小于日志起始基准地址",
hlog_config.initial_address,
"",
));
}
if begin > tail {
return Err(addr_violation(
"BeginAddress",
begin,
"超出 TailAddress",
tail,
"",
));
}
if meta.hlog_meta.head_address > tail {
return Err(addr_violation(
"HeadAddress",
meta.hlog_meta.head_address,
"超出 TailAddress",
tail,
"",
));
}
if meta.hlog_meta.head_address < begin {
return Err(addr_violation(
"HeadAddress",
meta.hlog_meta.head_address,
"低于 BeginAddress",
begin,
"",
));
}
if flushed < begin {
return Err(addr_violation(
"FlushedUntilAddress",
flushed,
"小于 BeginAddress",
begin,
"",
));
}
if flushed < meta.hlog_meta.head_address {
return Err(addr_violation(
"FlushedUntilAddress",
flushed,
"低于 HeadAddress",
meta.hlog_meta.head_address,
",存在未落盘的已驱逐页",
));
}
let index_path = dir.join(index_filename(token));
let (index, index_meta) =
read_index_checkpoint_truncated(&index_path, token, Some(tail)).await?;
if index_meta.size != meta.store_meta.index_size {
return Err(index_mismatch(
"索引快照实际大小与引擎配置大小不匹配",
index_meta.size as u64,
meta.store_meta.index_size as u64,
));
}
if index_meta.overflow_count != meta.index_meta.overflow_count {
return Err(index_mismatch(
"索引快照溢出桶数量与元数据不一致",
index_meta.overflow_count,
meta.index_meta.overflow_count,
));
}
if index_meta.entry_count != meta.index_meta.entry_count {
return Err(index_mismatch(
"索引快照条目总数与元数据不一致",
index_meta.entry_count as u64,
meta.index_meta.entry_count as u64,
));
}
let hlog = Arc::new(HybridLog::new(
hlog_config.clone(),
Arc::clone(&device),
Arc::clone(&epoch),
)?);
let curr_page = hlog_config.page_id(tail);
let offset = hlog_config.page_offset(tail);
let page_start = hlog_config.page_start_address(curr_page);
if offset > 0 && tail > hlog_config.initial_address {
let tail_buf = device.read_range(page_start, hlog_config.page_size).await?;
{
let mut guard = hlog.buffer.write_page(curr_page);
guard.copy_from_slice(&tail_buf);
}
hlog.buffer.set_page_id(curr_page);
} else {
hlog.buffer.clear_page(curr_page);
}
let head = page_start.max(meta.hlog_meta.head_address);
let ro = if meta.cp_type == CheckpointType::FoldOver {
tail
} else {
hlog_config
.calculate_read_only_address(head, tail)
.max(head)
};
let flushed = flushed.min(tail);
hlog.addresses.begin_address.store(begin, Ordering::Release);
hlog.addresses.head_address.store(head, Ordering::Release);
hlog
.addresses
.safe_head_address
.store(head, Ordering::Release);
hlog
.addresses
.read_only_address
.store(ro, Ordering::Release);
hlog
.addresses
.safe_read_only_address
.store(ro, Ordering::Release);
hlog
.addresses
.flushed_until_address
.store(flushed, Ordering::Release);
hlog.addresses.tail_address.store(tail, Ordering::Release);
info!(
"成功完成 Checkpoint 崩溃恢复组件加载: token={token:#x}, entry_count={}, tail={tail:#x}, head={head:#x}, ro={ro:#x}",
meta.index_meta.entry_count
);
Ok(RecoveredCheckpoint {
meta,
index: Arc::new(index),
hlog,
epoch,
})
}
pub async fn recover<S: CprRecover<Device = D>>(
checkpoint_dir: impl AsRef<Path>,
token: u128,
device: Arc<D>,
) -> Result<S> {
let dir = checkpoint_dir.as_ref();
let recovered = Self::recover_checkpoint_components(dir, token, Arc::clone(&device)).await?;
S::from_recovered(recovered, dir, device).await
}
pub async fn recover_store<S: CprRecover<Device = D>>(
&self,
checkpoint_dir: impl AsRef<Path>,
token: u128,
device: Arc<D>,
) -> Result<S> {
Self::recover(checkpoint_dir, token, device).await
}
pub async fn recover_latest<S: CprRecover<Device = D>>(
checkpoint_dir: impl AsRef<Path>,
device: Arc<D>,
) -> Result<S> {
let dir = checkpoint_dir.as_ref();
let tokens = Self::list_checkpoints(dir)?;
let mut first_err = None;
for token in tokens.into_iter().rev() {
match Self::recover::<S>(dir, token, Arc::clone(&device)).await {
Ok(store) => return Ok(store),
Err(e) => {
warn!("跳过无效 Checkpoint(回退至更早版本): token={token:#x}, err={e}");
first_err.get_or_insert(e);
}
}
}
Err(first_err.unwrap_or(Error::NoValidCheckpoint(dir.to_path_buf())))
}
pub async fn recover_latest_store<S: CprRecover<Device = D>>(
&self,
checkpoint_dir: impl AsRef<Path>,
device: Arc<D>,
) -> Result<S> {
Self::recover_latest(checkpoint_dir, device).await
}
pub fn list_checkpoints(checkpoint_dir: impl AsRef<Path>) -> Result<Vec<u128>> {
let dir = checkpoint_dir.as_ref();
if !dir.exists() {
return Ok(Vec::new());
}
let mut tokens: Vec<u128> = read_dir(dir)?
.flatten()
.filter_map(|entry| {
let name = entry.file_name();
let name_str = name.to_str()?;
name_str
.strip_prefix(META_PREFIX)
.and_then(|s| s.strip_suffix(META_EXT))
.and_then(|s| s.parse::<u128>().ok())
})
.collect();
tokens.sort_unstable();
Ok(tokens)
}
pub fn find_latest_checkpoint(checkpoint_dir: impl AsRef<Path>) -> Result<Option<u128>> {
let tokens = Self::list_checkpoints(checkpoint_dir)?;
Ok(tokens.last().copied())
}
pub fn purge_checkpoint(checkpoint_dir: impl AsRef<Path>, token: u128) -> Result<()> {
let dir = checkpoint_dir.as_ref();
let mut itoa_buf = itoa::Buffer::new();
let token_str = itoa_buf.format(token);
let meta_path = dir.join(meta_filename(token));
let index_path = dir.join(index_filename(token));
let meta_tmp = dir.join(meta_tmp_filename(token));
let index_tmp = dir.join(index_tmp_filename(token));
let token_dir = dir.join(token_str);
rm_path_best_effort(&meta_path);
rm_path_best_effort(&index_path);
rm_path_best_effort(&meta_tmp);
rm_path_best_effort(&index_tmp);
rm_path_best_effort(&token_dir);
Ok(())
}
fn sweep_checkpoint_residue(dir: &Path) {
let ri_dir = dir.join("rangeindex");
if ri_dir.exists() {
let _ = remove_dir_all(ri_dir);
}
if let Ok(entries) = read_dir(dir) {
for entry in entries.flatten() {
let name = entry.file_name();
if let Some(name_str) = name.to_str() {
if name_str.ends_with(TMP_EXT)
|| name_str.ends_with(INDEX_EXT)
|| name_str.ends_with(META_EXT)
{
let _ = remove_file(entry.path());
} else if name_str.parse::<u128>().is_ok() && entry.file_type().is_ok_and(|t| t.is_dir())
{
let _ = remove_dir_all(entry.path());
}
}
}
}
}
pub fn purge_all(checkpoint_dir: impl AsRef<Path>) -> Result<()> {
let dir = checkpoint_dir.as_ref();
if !dir.exists() {
return Ok(());
}
let tokens = Self::list_checkpoints(dir)?;
for token in tokens {
Self::purge_checkpoint(dir, token)?;
}
Self::sweep_checkpoint_residue(dir);
Ok(())
}
pub fn purge_outdated(checkpoint_dir: impl AsRef<Path>, keep: usize) -> Result<Vec<u128>> {
let dir = checkpoint_dir.as_ref();
let tokens = Self::list_checkpoints(dir)?;
let boundary = tokens.len().saturating_sub(keep);
for &token in &tokens[..boundary] {
Self::purge_checkpoint(dir, token)?;
}
Ok(tokens[..boundary].to_vec())
}
pub fn purge(&self, checkpoint_dir: impl AsRef<Path>, token: u128) -> Result<()> {
Self::purge_checkpoint(checkpoint_dir, token)
}
pub fn purge_all_checkpoints(&self, checkpoint_dir: impl AsRef<Path>) -> Result<()> {
Self::purge_all(checkpoint_dir)
}
pub fn purge_outdated_checkpoints(
&self,
checkpoint_dir: impl AsRef<Path>,
keep: usize,
) -> Result<Vec<u128>> {
Self::purge_outdated(checkpoint_dir, keep)
}
}
#[cfg(test)]
mod tests {
use std::fs::{create_dir_all, write};
use compio::runtime::Runtime;
use tempfile::tempdir;
use super::{issue_token_after, next_token, sync_dir_tree};
#[test]
fn token_gate_monotonic_under_rollback_and_floor() {
let a = next_token();
let b = issue_token_after(a.saturating_sub(1), 0);
assert!(b > a, "回拨候选必须续发: {a} -> {b}");
let c = issue_token_after(0, b);
assert!(c > b, "目录下界必须抬升签发值: {b} -> {c}");
let d = issue_token_after(b, c);
assert!(d > c, "叠加路径必须续发: {c} -> {d}");
let e = next_token();
assert!(e > d, "常规签发必须严格递增: {d} -> {e}");
let f = issue_token_after(u128::MAX - 5, 0);
let g = next_token();
assert!(f > e && g > f, "高位候选后仍须递增: {e} -> {f} -> {g}");
}
#[test]
fn sync_dir_tree_handles_nested_tree_and_is_idempotent() {
let rt = Runtime::new().unwrap();
rt.block_on(async {
let dir = tempdir().unwrap();
let deep = dir.path().join("token/rangeindex/prefix");
create_dir_all(&deep).unwrap();
write(deep.join("data.bftree"), b"payload").unwrap();
write(deep.join("empty.bftree"), b"").unwrap();
create_dir_all(dir.path().join("token/empty_dir")).unwrap();
sync_dir_tree(dir.path()).await.unwrap();
sync_dir_tree(dir.path()).await.unwrap();
});
}
#[test]
fn sync_dir_tree_fails_on_missing_dir() {
let rt = Runtime::new().unwrap();
rt.block_on(async {
let dir = tempdir().unwrap();
assert!(sync_dir_tree(&dir.path().join("missing")).await.is_err());
});
}
}