#![cfg(feature = "sqlite")]
use persistant_core::{Config, Reason};
use persistant_kv::conformance::{Leaf, certify_leaves};
use persistant_kv::{BatchOp, KvBackend, KvError, KvOnRelational};
use persistant_relational::{Isolation, RelationalBackend, RelationalTx};
use persistant_sqlx::{Bounded, SqliteBackend};
use std::time::{Duration, Instant};
const BOUND: Duration = Duration::from_millis(2000);
async fn guard<T>(call: impl std::future::Future<Output = T>) -> T {
tokio::time::timeout(Duration::from_secs(20), call)
.await
.expect("the call hung: the bound did not fire")
}
fn temp_db(tag: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!(
"persistant-bounded-{tag}-{}-{}.db",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
))
}
fn cleanup(path: &std::path::Path) {
for ext in ["", "-wal", "-shm"] {
let mut p = path.as_os_str().to_owned();
p.push(ext);
let _ = std::fs::remove_file(p);
}
}
fn put(key: &str) -> Vec<BatchOp> {
vec![BatchOp::Put {
key: key.to_string(),
value: b"v".to_vec(),
ttl: None,
}]
}
#[tokio::test]
async fn a_batch_blocked_on_a_real_lock_times_out_and_applies_nothing() {
let path = temp_db("lock");
let kv = KvOnRelational::open(Bounded::new(
SqliteBackend::open(&path).await.unwrap(),
BOUND,
))
.await
.unwrap();
let blocker_db = SqliteBackend::open(&path).await.unwrap();
let mut blocker = blocker_db.begin(Isolation::ReadCommitted).await.unwrap();
blocker
.execute("UPDATE persistant_kv_batch SET id = id WHERE id = 1", &[])
.await
.unwrap();
let started = Instant::now();
let r = guard(kv.batch(&put("blocked"))).await;
assert!(
matches!(r, Err(KvError::TimedOut(d)) if d == BOUND),
"got {r:?}"
);
assert!(
started.elapsed() < BOUND * 5,
"took {:?}",
started.elapsed()
);
let plain = KvOnRelational::open(SqliteBackend::open(&path).await.unwrap_or_else(|e| {
panic!("open: {e}");
}));
if let Ok(Ok(plain)) = tokio::time::timeout(BOUND * 2, plain).await {
let waited = tokio::time::timeout(BOUND, plain.batch(&put("plain"))).await;
assert!(waited.is_err(), "the unbounded batch returned: {waited:?}");
}
blocker.rollback().await.unwrap();
assert_eq!(kv.get("blocked").await.unwrap(), None);
let mut ok = false;
for _ in 0..20 {
if matches!(kv.batch(&put("after")).await, Ok(true)) {
ok = true;
break;
}
}
assert!(ok, "the store did not recover once the lock was gone");
cleanup(&path);
}
#[tokio::test]
async fn bounded_call_is_on_with_a_timeout_and_off_without() {
let leaves = [Leaf::BoundedCall];
let none = Config::new();
let path = temp_db("on");
let kv = KvOnRelational::open(Bounded::new(
SqliteBackend::open(&path).await.unwrap(),
BOUND,
))
.await
.unwrap();
let rows = certify_leaves(&kv, &none, &none, &leaves).await;
assert!(rows[0].row.enabled, "{}", rows[0].row.note);
cleanup(&path);
let path = temp_db("off");
let kv = KvOnRelational::open(SqliteBackend::open(&path).await.unwrap())
.await
.unwrap();
let rows = certify_leaves(&kv, &none, &none, &leaves).await;
assert!(!rows[0].row.enabled);
assert_eq!(rows[0].row.reason, Some(Reason::Missing));
cleanup(&path);
}