use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use web_time::{Duration, SystemTime};
use crate::catalog::providers::DatabaseProvider;
use crate::dbs::{Capabilities, Session};
use crate::key::root::rc::{ReclaimKey, ReclaimState};
use crate::kvs::LockType::Optimistic;
use crate::kvs::TransactionType::{Read, Write};
use crate::kvs::{Datastore, KVValue};
async fn mem_ds() -> Arc<Datastore> {
Arc::new(
Datastore::builder()
.with_capabilities(Capabilities::all())
.build_with_path("memory")
.await
.unwrap(),
)
}
async fn count_range(ds: &Datastore, beg: Vec<u8>, end: Vec<u8>) -> usize {
let tx = ds.transaction(Read, Optimistic).await.unwrap();
let res = tx.getr(beg..end, None).await.unwrap();
let _ = tx.cancel().await;
res.len()
}
async fn reclaim_queue_len(ds: &Datastore) -> usize {
let (beg, end) = ReclaimKey::range();
count_range(ds, beg, end).await
}
async fn reclaim_entry_observed_ms(ds: &Datastore) -> u64 {
let (beg, end) = ReclaimKey::range();
let tx = ds.transaction(Read, Optimistic).await.unwrap();
let items = tx.getr(beg..end, None).await.unwrap();
let _ = tx.cancel().await;
assert_eq!(items.len(), 1, "expected exactly one reclaim queue entry");
ReclaimState::kv_decode_value(&items[0].1, ()).unwrap().observed_ms
}
fn now_ms() -> u64 {
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis() as u64
}
#[tokio::test]
async fn remove_database_defers_data_reclaim() {
let ds = mem_ds().await;
let ses = Session::owner().with_ns("test").with_db("tenant");
ds.execute(
"DEFINE NAMESPACE test; DEFINE DATABASE tenant; CREATE thing:1 SET v = 1; CREATE thing:2 SET v = 2;",
&ses,
None,
)
.await
.unwrap();
let (ns_id, db_id) = {
let tx = ds.transaction(Read, Optimistic).await.unwrap();
let db = tx.get_db_by_name("test", "tenant", None).await.unwrap().unwrap();
let _ = tx.cancel().await;
(db.namespace_id, db.database_id)
};
let db_prefix = crate::key::database::all::new(ns_id, db_id);
let range = crate::kvs::util::to_prefix_range(&db_prefix).unwrap();
assert!(
count_range(&ds, range.start.clone(), range.end.clone()).await > 0,
"the database prefix should hold data before reclaim"
);
assert_eq!(reclaim_queue_len(&ds).await, 0, "no reclaim jobs before removal");
ds.execute("REMOVE DATABASE tenant;", &ses, None).await.unwrap();
{
let tx = ds.transaction(Read, Optimistic).await.unwrap();
let gone = tx.get_db_by_name("test", "tenant", None).await.unwrap();
let _ = tx.cancel().await;
assert!(gone.is_none(), "database must be invisible immediately after REMOVE");
}
assert_eq!(reclaim_queue_len(&ds).await, 1, "REMOVE DATABASE must enqueue one reclaim job");
assert!(
count_range(&ds, range.start.clone(), range.end.clone()).await > 0,
"data must still be present before the reclaim task runs"
);
Datastore::reclaim_tombstones(
Arc::clone(&ds),
Duration::from_secs(1),
Duration::ZERO,
CancellationToken::new(),
)
.await
.unwrap();
assert_eq!(
count_range(&ds, range.start.clone(), range.end.clone()).await,
0,
"the reclaim task must destroy the database data prefix"
);
assert_eq!(reclaim_queue_len(&ds).await, 0, "the reclaim task must drain the reclaim queue");
ds.execute("DEFINE DATABASE tenant;", &ses, None).await.unwrap();
let new_db_id = {
let tx = ds.transaction(Read, Optimistic).await.unwrap();
let db = tx.get_db_by_name("test", "tenant", None).await.unwrap().unwrap();
let _ = tx.cancel().await;
db.database_id
};
assert_ne!(new_db_id, db_id, "recreated database must get a fresh, never-reused id");
let new_prefix = crate::key::database::all::new(ns_id, new_db_id);
let new_range = crate::kvs::util::to_prefix_range(&new_prefix).unwrap();
assert_eq!(
count_range(&ds, new_range.start, new_range.end).await,
0,
"recreated database must start empty with no data leaked from the removed one"
);
}
#[tokio::test]
async fn reclaim_is_idempotent_and_safe_when_empty() {
let ds = mem_ds().await;
let (iters, errors) = Datastore::reclaim_tombstones(
Arc::clone(&ds),
Duration::from_secs(1),
Duration::ZERO,
CancellationToken::new(),
)
.await
.unwrap();
assert_eq!(errors, 0);
assert_eq!(iters, 0, "an empty queue performs no reclaim iterations");
}
#[tokio::test]
async fn reclaim_respects_grace_period() {
let ds = mem_ds().await;
let ses = Session::owner().with_ns("test").with_db("tenant");
ds.execute(
"DEFINE NAMESPACE test; DEFINE DATABASE tenant; CREATE thing:1 SET v = 1; CREATE thing:2 SET v = 2;",
&ses,
None,
)
.await
.unwrap();
let (ns_id, db_id) = {
let tx = ds.transaction(Read, Optimistic).await.unwrap();
let db = tx.get_db_by_name("test", "tenant", None).await.unwrap().unwrap();
let _ = tx.cancel().await;
(db.namespace_id, db.database_id)
};
let db_prefix = crate::key::database::all::new(ns_id, db_id);
let range = crate::kvs::util::to_prefix_range(&db_prefix).unwrap();
let reader = ds.transaction(Read, Optimistic).await.unwrap();
let reader_seen_before =
reader.getr(range.start.clone()..range.end.clone(), None).await.unwrap().len();
assert!(reader_seen_before > 0, "reader should see the data before removal");
ds.execute("REMOVE DATABASE tenant;", &ses, None).await.unwrap();
assert_eq!(
reclaim_entry_observed_ms(&ds).await,
0,
"a freshly-enqueued reclaim entry must start unobserved"
);
Datastore::reclaim_tombstones(
Arc::clone(&ds),
Duration::from_secs(1),
Duration::from_secs(3600),
CancellationToken::new(),
)
.await
.unwrap();
assert!(
count_range(&ds, range.start.clone(), range.end.clone()).await > 0,
"data must NOT be destroyed while inside the grace window"
);
assert_eq!(reclaim_queue_len(&ds).await, 1, "the reclaim job must remain queued during grace");
assert_ne!(
reclaim_entry_observed_ms(&ds).await,
0,
"the first pass must stamp an observation time instead of reclaiming"
);
assert_eq!(
reader.getr(range.start.clone()..range.end.clone(), None).await.unwrap().len(),
reader_seen_before,
"an in-flight reader opened before REMOVE must still see its data"
);
let _ = reader.cancel().await;
Datastore::reclaim_tombstones(
Arc::clone(&ds),
Duration::from_secs(1),
Duration::ZERO,
CancellationToken::new(),
)
.await
.unwrap();
assert_eq!(
count_range(&ds, range.start.clone(), range.end.clone()).await,
0,
"data must be reclaimed once past the grace window"
);
assert_eq!(reclaim_queue_len(&ds).await, 0, "the reclaim job must be drained after reclaim");
}
#[tokio::test]
async fn reclaim_runs_once_observation_ages_past_grace() {
let ds = mem_ds().await;
let ses = Session::owner().with_ns("test").with_db("tenant");
ds.execute(
"DEFINE NAMESPACE test; DEFINE DATABASE tenant; CREATE thing:1 SET v = 1;",
&ses,
None,
)
.await
.unwrap();
let (ns_id, db_id) = {
let tx = ds.transaction(Read, Optimistic).await.unwrap();
let db = tx.get_db_by_name("test", "tenant", None).await.unwrap().unwrap();
let _ = tx.cancel().await;
(db.namespace_id, db.database_id)
};
let range =
crate::kvs::util::to_prefix_range(&crate::key::database::all::new(ns_id, db_id)).unwrap();
ds.execute("REMOVE DATABASE tenant;", &ses, None).await.unwrap();
{
let (beg, end) = ReclaimKey::range();
let tx = ds.transaction(Write, Optimistic).await.unwrap();
let items = tx.getr(beg..end, None).await.unwrap();
assert_eq!(items.len(), 1);
let rc = ReclaimKey::decode_key(&items[0].0).unwrap();
tx.set(
&rc,
&ReclaimState {
observed_ms: now_ms().saturating_sub(3_600_000),
},
)
.await
.unwrap();
tx.commit().await.unwrap();
}
Datastore::reclaim_tombstones(
Arc::clone(&ds),
Duration::from_secs(1),
Duration::from_secs(60),
CancellationToken::new(),
)
.await
.unwrap();
assert_eq!(
count_range(&ds, range.start.clone(), range.end.clone()).await,
0,
"an entry observed longer than the grace ago must be reclaimed"
);
assert_eq!(reclaim_queue_len(&ds).await, 0, "the reclaim job must be drained");
}