use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use web_time::{Duration, SystemTime};
use crate::catalog::providers::DatabaseProvider;
use crate::catalog::{DatabaseId, NamespaceId};
use crate::dbs::{Capabilities, Session};
use crate::key::root::rc::{ReclaimKey, ReclaimKind, 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
}
async fn keys_in_range(ds: &Datastore, beg: Vec<u8>, end: Vec<u8>) -> Vec<Vec<u8>> {
let tx = ds.transaction(Read, Optimistic).await.unwrap();
let res = tx.keysr(beg..end, 1000, 0, None).await.unwrap();
let _ = tx.cancel().await;
res
}
async fn drain_reclaim(ds: &Arc<Datastore>, budget: u64) {
for _ in 0..256 {
let (iterations, errors) = Datastore::reclaim_tombstones_with_budget(
Arc::clone(ds),
Duration::from_secs(1),
Duration::ZERO,
budget,
CancellationToken::new(),
)
.await
.unwrap();
assert_eq!(errors, 0, "a reclaim pass must not error");
if iterations == 0 {
break;
}
}
}
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 remove_namespace_reclaims_every_key_under_the_namespace() {
let ds = mem_ds().await;
let ses = Session::owner().with_ns("test").with_db("tenant");
ds.execute("DEFINE NAMESPACE test; DEFINE DATABASE tenant;", &ses, None).await.unwrap();
for i in 0..40 {
ds.execute(&format!("CREATE thing:{i} SET v = {i};"), &ses, None).await.unwrap();
}
let ns_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
};
let range = crate::kvs::util::to_prefix_range(&crate::key::namespace::all::new(ns_id)).unwrap();
assert!(
count_range(&ds, range.start.clone(), range.end.clone()).await > 0,
"the namespace prefix should hold keys before reclaim"
);
ds.execute("REMOVE DATABASE tenant;", &ses, None).await.unwrap();
ds.execute("REMOVE NAMESPACE test;", &Session::owner(), None).await.unwrap();
drain_reclaim(&ds, 4).await;
let leftover = keys_in_range(&ds, range.start.clone(), range.end.clone()).await;
assert!(
leftover.is_empty(),
"the reclaim task must destroy every key under the namespace prefix, left {} behind: {:?}",
leftover.len(),
leftover.iter().map(crate::key::debug::Sprintable::sprint).collect::<Vec<_>>(),
);
assert_eq!(reclaim_queue_len(&ds).await, 0, "the reclaim task must drain the reclaim queue");
}
#[tokio::test]
async fn a_doc_id_entry_is_left_queued_and_takes_no_candidate_slot() {
let ds = mem_ds().await;
let rc = ReclaimKey::of_kind(
ReclaimKind::DocKey,
NamespaceId(9),
DatabaseId(9),
uuid::Uuid::from_u128(1),
);
{
let tx = ds.transaction(Write, Optimistic).await.unwrap();
tx.set(&rc, &ReclaimState::enqueued()).await.unwrap();
tx.commit().await.unwrap();
}
let (_, errors) = Datastore::reclaim_tombstones(
Arc::clone(&ds),
Duration::from_secs(1),
Duration::from_secs(3600),
CancellationToken::new(),
)
.await
.unwrap();
assert_eq!(errors, 0, "an entry this version cannot act on is not an error");
assert_eq!(
reclaim_queue_len(&ds).await,
1,
"the entry must stay queued for a node that understands its layout"
);
assert_eq!(
reclaim_entry_observed_ms(&ds).await,
0,
"it must not be observed, so it cannot occupy a bounded candidate slot"
);
}
#[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),
cursor: None,
},
)
.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");
}