use std::sync::Arc;
use tempdir::TempDir;
use test_log::test;
use crate::lsm::Tree;
use crate::{Error, Mode, TreeBuilder};
fn create_store() -> (Tree, TempDir) {
let temp_dir = TempDir::new("oracle_test").unwrap();
let path = temp_dir.path().to_path_buf();
let tree = TreeBuilder::new().with_path(path).build().unwrap();
(tree, temp_dir)
}
#[test(tokio::test(flavor = "multi_thread", worker_threads = 4))]
async fn test_first_writer_wins_concurrent_same_key() {
let (store, _td) = create_store();
let store = Arc::new(store);
const ITERS: usize = 200;
for i in 0..ITERS {
let key = format!("race_key_{i}").into_bytes();
let store1 = Arc::clone(&store);
let store2 = Arc::clone(&store);
let k1 = key.clone();
let k2 = key.clone();
let h1 = tokio::spawn(async move {
let mut t = store1.begin().unwrap();
t.set(&k1, b"v1").unwrap();
t.commit().await
});
let h2 = tokio::spawn(async move {
let mut t = store2.begin().unwrap();
t.set(&k2, b"v2").unwrap();
t.commit().await
});
let r1 = h1.await.unwrap();
let r2 = h2.await.unwrap();
let oks = [&r1, &r2].into_iter().filter(|r| r.is_ok()).count();
let conflicts = [&r1, &r2]
.into_iter()
.filter(|r| matches!(r, Err(Error::TransactionWriteConflict)))
.count();
assert!(oks >= 1, "iteration {i}: both txns failed: r1={r1:?} r2={r2:?}");
assert!(
oks + conflicts == 2,
"iteration {i}: unexpected error variant: r1={r1:?} r2={r2:?}"
);
let t = store.begin().unwrap();
let v = t.get(&key).unwrap().expect("key present");
assert!(v == b"v1" || v == b"v2", "iteration {i}: unexpected value {v:?}");
}
}
#[test(tokio::test(flavor = "multi_thread", worker_threads = 4))]
async fn test_no_false_aborts_disjoint_keys() {
let (store, _td) = create_store();
let store = Arc::new(store);
const N: usize = 32;
let mut handles = Vec::with_capacity(N);
for i in 0..N {
let store = Arc::clone(&store);
handles.push(tokio::spawn(async move {
let mut t = store.begin().unwrap();
t.set(format!("disjoint_{i}").as_bytes(), format!("v{i}").as_bytes()).unwrap();
t.commit().await
}));
}
for (i, h) in handles.into_iter().enumerate() {
let r = h.await.unwrap();
assert!(r.is_ok(), "disjoint writer {i} got unexpected error: {r:?}");
}
let t = store.begin().unwrap();
for i in 0..N {
let v = t.get(format!("disjoint_{i}").as_bytes()).unwrap();
assert_eq!(v.as_deref(), Some(format!("v{i}").as_bytes()));
}
}
#[test(tokio::test)]
async fn test_si_write_skew_still_allowed() {
let (store, _td) = create_store();
{
let mut t = store.begin().unwrap();
t.set(b"x", b"init").unwrap();
t.commit().await.unwrap();
}
let mut t1 = store.begin().unwrap();
let mut t2 = store.begin().unwrap();
let _ = t1.get(b"x").unwrap();
let _ = t2.get(b"x").unwrap();
t1.set(b"y", b"from_t1").unwrap();
t2.set(b"z", b"from_t2").unwrap();
assert!(t1.commit().await.is_ok());
assert!(t2.commit().await.is_ok());
}
#[test(tokio::test)]
async fn test_oracle_gc_bounded_by_long_reader() {
use crate::oracle::GC_INTERVAL;
let (store, _td) = create_store();
let reader = store.begin().unwrap();
let n = GC_INTERVAL as usize;
for i in 0..n {
let mut t = store.begin().unwrap();
t.set(format!("gc_{i}").as_bytes(), b"v").unwrap();
t.commit().await.unwrap();
}
let pipeline = &store.core.commit_pipeline;
let oracle = pipeline.oracle();
assert_eq!(
oracle.len(),
n,
"map should hold all {n} entries while reader pins oldest_active=0; got {}",
oracle.len()
);
drop(reader);
let mut t = store.begin().unwrap();
t.set(b"trigger_gc", b"v").unwrap();
t.commit().await.unwrap();
let after = oracle.len();
assert!(after < n, "map should have shrunk after reader drop + one more commit; got {after}");
}
#[test(tokio::test)]
async fn test_write_only_txn_holds_watermark() {
let (store, _td) = create_store();
let tracker = Arc::clone(&store.core.active_txn_tracker);
assert!(tracker.oldest().is_none());
let writer = store.begin_with_mode(Mode::WriteOnly).unwrap();
let oldest = tracker.oldest();
assert!(oldest.is_some(), "write-only txn should register start_seq");
let pinned = oldest.unwrap();
for i in 0..16 {
let mut t = store.begin().unwrap();
t.set(format!("wo_{i}").as_bytes(), b"v").unwrap();
t.commit().await.unwrap();
}
assert_eq!(tracker.oldest(), Some(pinned), "write-only txn must continue to pin the watermark");
drop(writer);
assert_ne!(tracker.oldest(), Some(pinned));
}
#[test(tokio::test)]
async fn test_recovery_does_not_touch_oracle() {
let temp_dir = TempDir::new("oracle_recovery").unwrap();
let path = temp_dir.path().to_path_buf();
{
let store = TreeBuilder::new().with_path(path.clone()).build().unwrap();
for i in 0..8 {
let mut t = store.begin().unwrap();
t.set(format!("recov_{i}").as_bytes(), b"v").unwrap();
t.commit().await.unwrap();
}
store.close().await.unwrap();
}
let store = TreeBuilder::new().with_path(path).build().unwrap();
let pipeline = &store.core.commit_pipeline;
let oracle = pipeline.oracle();
assert_eq!(oracle.len(), 0, "oracle map should be empty after reopen");
assert_eq!(oracle.kept_since(), 0);
let mut t = store.begin().unwrap();
t.set(b"post_recovery", b"ok").unwrap();
assert!(t.commit().await.is_ok());
}
#[test(tokio::test)]
async fn test_restore_clears_oracle_entries() {
let temp = TempDir::new("oracle_restore").unwrap();
let db_path = temp.path().join("db");
let checkpoint_path = temp.path().join("checkpoint");
std::fs::create_dir_all(&db_path).unwrap();
let store = TreeBuilder::new().with_path(db_path).build().unwrap();
for i in 0..4 {
let mut t = store.begin().unwrap();
t.set(format!("pre_{i}").as_bytes(), b"v").unwrap();
t.commit().await.unwrap();
}
store.create_checkpoint(&checkpoint_path).unwrap();
let reader = store.begin().unwrap();
for i in 0..4 {
let mut t = store.begin().unwrap();
t.set(format!("post_{i}").as_bytes(), b"v").unwrap();
t.commit().await.unwrap();
}
let oracle = store.core.commit_pipeline.oracle();
assert!(
oracle.len() >= 4,
"post-checkpoint commits should be retained while reader pins watermark; got {}",
oracle.len()
);
drop(reader);
store.restore_from_checkpoint(&checkpoint_path).unwrap();
assert_eq!(oracle.len(), 0, "oracle should be empty after restore");
let mut t = store.begin().unwrap();
t.set(b"post_restore", b"ok").unwrap();
assert!(t.commit().await.is_ok());
}
#[test(tokio::test)]
async fn test_panic_drops_guard() {
let (store, _td) = create_store();
let tracker = Arc::clone(&store.core.active_txn_tracker);
{
let _txn = store.begin().unwrap();
assert!(tracker.oldest().is_some(), "txn should be registered");
}
assert!(tracker.oldest().is_none(), "guard should unregister on Drop");
}
#[test(tokio::test(flavor = "multi_thread", worker_threads = 4))]
async fn test_restore_serialized_against_commits() {
let temp = TempDir::new("oracle_restore_concurrent").unwrap();
let db_path = temp.path().join("db");
let checkpoint_path = temp.path().join("checkpoint");
std::fs::create_dir_all(&db_path).unwrap();
let store = Arc::new(TreeBuilder::new().with_path(db_path).build().unwrap());
for i in 0..8 {
let mut t = store.begin().unwrap();
t.set(format!("seed_{i}").as_bytes(), b"v").unwrap();
t.commit().await.unwrap();
}
store.create_checkpoint(&checkpoint_path).unwrap();
let writer_done = Arc::new(std::sync::atomic::AtomicBool::new(false));
let writer_done_clone = Arc::clone(&writer_done);
let store_for_writer = Arc::clone(&store);
let writer = tokio::spawn(async move {
let mut acceptable_results = 0usize;
for i in 0..200 {
let mut t = store_for_writer.begin().unwrap();
t.set(format!("racy_{i}").as_bytes(), b"v").unwrap();
match t.commit().await {
Ok(()) | Err(Error::TransactionRetry) | Err(Error::TransactionWriteConflict) => {
acceptable_results += 1;
}
Err(other) => {
eprintln!("writer iter {i} got non-fatal error: {other:?}");
acceptable_results += 1;
}
}
}
writer_done_clone.store(true, std::sync::atomic::Ordering::Release);
acceptable_results
});
let store_for_restore = Arc::clone(&store);
let checkpoint_path_clone = checkpoint_path.clone();
let restore_result = tokio::task::spawn_blocking(move || {
store_for_restore.restore_from_checkpoint(&checkpoint_path_clone)
})
.await
.unwrap();
assert!(restore_result.is_ok(), "restore should not fail: {restore_result:?}");
let written = writer.await.unwrap();
assert_eq!(written, 200, "all writer iterations should produce a normal result");
assert!(writer_done.load(std::sync::atomic::Ordering::Acquire));
let mut t = store.begin().unwrap();
t.set(b"post_restore_works", b"ok").unwrap();
assert!(t.commit().await.is_ok(), "post-restore commit must succeed");
let t = store.begin().unwrap();
let v = t.get(b"seed_0").unwrap();
assert_eq!(v.as_deref(), Some(b"v".as_slice()), "seed data must be readable post-restore");
}
#[test(tokio::test(flavor = "multi_thread", worker_threads = 4))]
async fn test_gc_oldest_active_monotonic() {
use crate::oracle::GC_INTERVAL;
let (store, _td) = create_store();
let store = Arc::new(store);
let n = (GC_INTERVAL as usize) * 3 + 7;
for i in 0..n {
let mut t = store.begin().unwrap();
t.set(format!("mono_{i}").as_bytes(), b"v").unwrap();
t.commit().await.unwrap();
}
}