mod common;
use common::{FaultyConnection, FaultyConnectionFactory, MockConnection, MockConnectionFactory};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use sz_orm_core::TxError;
use sz_orm_core::{Pool, PoolConfigBuilder};
use sz_orm_core::{TransactOptions, Transaction, TransactionManager, TransactionState};
use tokio::sync::Mutex;
fn make_tx() -> Transaction {
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let conn = MockConnection::new(db);
Transaction::new(Box::new(conn), TransactOptions::default())
}
fn make_faulty_tx(
fail_commit: bool,
fail_rollback: bool,
fail_execute_n: Option<u32>,
) -> Transaction {
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let mut conn = FaultyConnection::new(db);
conn.fail_on_commit = fail_commit;
conn.fail_on_rollback = fail_rollback;
conn.fail_on_execute_n = fail_execute_n;
Transaction::new(Box::new(conn), TransactOptions::default())
}
#[tokio::test]
async fn jepsen_transaction_state_machine_commit() {
let mut tx = make_tx();
assert_eq!(tx.state(), TransactionState::Active);
assert!(tx.is_active());
tx.execute("INSERT").await.unwrap();
tx.commit().await.unwrap();
assert_eq!(tx.state(), TransactionState::Committed);
assert!(!tx.is_active());
let result = tx.commit().await;
assert!(result.is_err(), "double commit should fail");
let result = tx.rollback().await;
assert!(result.is_err(), "rollback after commit should fail");
let result = tx.execute("SELECT").await;
assert!(result.is_err(), "execute after commit should fail");
}
#[tokio::test]
async fn jepsen_transaction_state_machine_rollback() {
let mut tx = make_tx();
assert_eq!(tx.state(), TransactionState::Active);
tx.execute("INSERT").await.unwrap();
tx.rollback().await.unwrap();
assert_eq!(tx.state(), TransactionState::RolledBack);
assert!(!tx.is_active());
let result = tx.rollback().await;
assert!(result.is_err(), "double rollback should fail");
let result = tx.commit().await;
assert!(result.is_err(), "commit after rollback should fail");
let result = tx.execute("SELECT").await;
assert!(result.is_err(), "execute after rollback should fail");
}
#[tokio::test]
async fn jepsen_savepoint_nested_names() {
let mut tx = make_tx();
assert_eq!(tx.state(), TransactionState::Active);
let sp1 = tx.savepoint().await.unwrap();
assert_eq!(sp1, "sp_1");
let sp2 = tx.savepoint().await.unwrap();
assert_eq!(sp2, "sp_2");
let sp3 = tx.savepoint().await.unwrap();
assert_eq!(sp3, "sp_3");
tx.rollback_to_savepoint(&sp2).await.unwrap();
tx.release_savepoint(&sp3).await.unwrap();
let sp4 = tx.savepoint().await.unwrap();
assert_eq!(sp4, "sp_4");
tx.commit().await.unwrap();
}
#[tokio::test]
async fn jepsen_savepoint_after_commit() {
let mut tx = make_tx();
tx.commit().await.unwrap();
let result = tx.savepoint().await;
assert!(result.is_err(), "savepoint after commit should fail");
assert!(matches!(result, Err(TxError::NotActive(_))));
}
#[tokio::test]
async fn jepsen_savepoint_after_rollback() {
let mut tx = make_tx();
tx.rollback().await.unwrap();
let result = tx.savepoint().await;
assert!(result.is_err(), "savepoint after rollback should fail");
}
#[tokio::test]
async fn jepsen_rollback_to_savepoint_after_commit() {
let mut tx = make_tx();
let sp = tx.savepoint().await.unwrap();
tx.commit().await.unwrap();
let result = tx.rollback_to_savepoint(&sp).await;
assert!(
result.is_err(),
"rollback_to_savepoint after commit should fail"
);
}
#[tokio::test]
async fn jepsen_release_savepoint_after_commit() {
let mut tx = make_tx();
let sp = tx.savepoint().await.unwrap();
tx.commit().await.unwrap();
let result = tx.release_savepoint(&sp).await;
assert!(
result.is_err(),
"release_savepoint after commit should fail"
);
}
#[tokio::test]
async fn jepsen_commit_failure_keeps_active() {
let mut tx = make_faulty_tx(true, false, None);
assert_eq!(tx.state(), TransactionState::Active);
tx.execute("INSERT").await.unwrap();
let result = tx.commit().await;
assert!(result.is_err(), "commit should fail");
assert!(matches!(result, Err(TxError::CommitFailed(_))));
assert_eq!(
tx.state(),
TransactionState::Active,
"tx must remain Active after commit failure"
);
assert!(tx.is_active());
let result = tx.rollback().await;
assert!(
result.is_ok(),
"rollback after failed commit should succeed"
);
assert_eq!(tx.state(), TransactionState::RolledBack);
}
#[tokio::test]
async fn jepsen_rollback_failure_keeps_active() {
let mut tx = make_faulty_tx(false, true, None);
assert_eq!(tx.state(), TransactionState::Active);
tx.execute("INSERT").await.unwrap();
let result = tx.rollback().await;
assert!(result.is_err(), "rollback should fail");
assert!(matches!(result, Err(TxError::RollbackFailed(_))));
assert_eq!(
tx.state(),
TransactionState::Active,
"tx must remain Active after rollback failure"
);
let result = tx.commit().await;
assert!(
result.is_ok(),
"commit after failed rollback should succeed"
);
assert_eq!(tx.state(), TransactionState::Committed);
}
#[tokio::test]
async fn jepsen_execute_failure_keeps_active() {
let mut tx = make_faulty_tx(false, false, Some(2));
assert_eq!(tx.state(), TransactionState::Active);
tx.execute("INSERT 1").await.unwrap();
let result = tx.execute("INSERT 2").await;
assert!(result.is_err(), "second execute should fail");
assert_eq!(tx.state(), TransactionState::Active);
assert!(tx.is_active());
tx.rollback().await.unwrap();
assert_eq!(tx.state(), TransactionState::RolledBack);
}
#[tokio::test]
async fn jepsen_connection_disconnect_during_tx() {
let mut tx = make_faulty_tx(false, false, Some(2));
tx.execute("INSERT 1").await.unwrap();
let result = tx.execute("INSERT 2").await;
assert!(result.is_err());
assert_eq!(tx.state(), TransactionState::Active);
let result = tx.rollback().await;
assert!(
result.is_ok(),
"rollback should succeed even if connection disconnected"
);
assert_eq!(tx.state(), TransactionState::RolledBack);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn jepsen_transaction_manager_concurrent_isolation() {
let mgr = Arc::new(TransactionManager::new());
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let mut handles = Vec::new();
let success = Arc::new(AtomicU64::new(0));
for i in 0..10 {
let mgr_clone = mgr.clone();
let db_clone = db.clone();
let s = success.clone();
handles.push(tokio::spawn(async move {
let tx_id = format!("tx_{}", i);
let conn = MockConnection::new(db_clone);
mgr_clone
.begin(tx_id.clone(), Box::new(conn), TransactOptions::default())
.await
.unwrap();
let state = mgr_clone.state(&tx_id).await;
assert_eq!(state, Some(TransactionState::Active));
if i % 2 == 0 {
mgr_clone.commit(&tx_id).await.unwrap();
let state = mgr_clone.state(&tx_id).await;
assert_eq!(state, Some(TransactionState::Committed));
} else {
mgr_clone.rollback(&tx_id).await.unwrap();
let state = mgr_clone.state(&tx_id).await;
assert_eq!(state, Some(TransactionState::RolledBack));
}
s.fetch_add(1, Ordering::Relaxed);
}));
}
for h in handles {
h.await.unwrap();
}
assert_eq!(success.load(Ordering::Relaxed), 10);
let list = mgr.list().await;
assert_eq!(list.len(), 10);
}
#[tokio::test]
async fn jepsen_transaction_manager_not_found() {
let mgr = TransactionManager::new();
let result = mgr.commit("nonexistent").await;
assert!(result.is_err());
let result = mgr.rollback("nonexistent").await;
assert!(result.is_err());
let state = mgr.state("nonexistent").await;
assert_eq!(state, None);
}
#[tokio::test]
async fn jepsen_transaction_manager_remove() {
let mgr = TransactionManager::new();
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let conn = MockConnection::new(db);
mgr.begin(
"tx1".to_string(),
Box::new(conn),
TransactOptions::default(),
)
.await
.unwrap();
let removed = mgr.remove("tx1").await;
assert!(removed.is_some());
let state = mgr.state("tx1").await;
assert_eq!(state, None);
let list = mgr.list().await;
assert!(list.is_empty());
}
#[tokio::test]
async fn jepsen_pool_factory_fault() {
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let factory = Arc::new(FaultyConnectionFactory::new(db, 1));
let config = PoolConfigBuilder::new()
.max_size(5)
.min_idle(0)
.acquire_timeout(2)
.build()
.unwrap();
let pool = Pool::new(config, factory).unwrap();
let result = pool.acquire().await;
assert!(result.is_err(), "first acquire should fail");
let status = pool.status().await;
assert_eq!(
status.active, status.idle,
"no borrowed conns after failure"
);
let conn = pool.acquire().await;
assert!(conn.is_ok(), "second acquire should succeed");
}
#[tokio::test]
async fn jepsen_pool_close_all_then_acquire() {
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let factory = Arc::new(MockConnectionFactory::new(db));
let config = PoolConfigBuilder::new()
.max_size(5)
.min_idle(0)
.acquire_timeout(2)
.build()
.unwrap();
let pool = Pool::new(config, factory).unwrap();
let c1 = pool.acquire().await.unwrap();
let c2 = pool.acquire().await.unwrap();
pool.release(c1).await;
pool.release(c2).await;
pool.close_all().await;
let status = pool.status().await;
assert_eq!(status.idle, 0);
assert_eq!(status.active, 0);
let result = pool.acquire().await;
assert!(
result.is_err(),
"acquire after close_all should be rejected"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn jepsen_pool_concurrent_with_faults() {
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let factory = Arc::new(FaultyConnectionFactory::new(db, 3));
let config = PoolConfigBuilder::new()
.max_size(10)
.min_idle(0)
.acquire_timeout(3)
.build()
.unwrap();
let pool = Arc::new(Pool::new(config, factory).unwrap());
let success = Arc::new(AtomicU64::new(0));
let failure = Arc::new(AtomicU64::new(0));
let mut handles = Vec::new();
for _ in 0..8 {
let s = success.clone();
let f = failure.clone();
let p = pool.clone();
handles.push(tokio::spawn(async move {
for _ in 0..20 {
match p.acquire().await {
Ok(conn) => {
p.release(conn).await;
s.fetch_add(1, Ordering::Relaxed);
}
Err(_) => {
f.fetch_add(1, Ordering::Relaxed);
}
}
}
}));
}
for h in handles {
h.await.unwrap();
}
let total = success.load(Ordering::Relaxed) + failure.load(Ordering::Relaxed);
assert_eq!(total, 160, "all ops must complete");
let status = pool.status().await;
assert_eq!(status.active, status.idle, "no borrowed conns");
}
#[tokio::test]
async fn jepsen_transaction_drop_without_commit() {
let mut tx = make_tx();
tx.execute("INSERT").await.unwrap();
assert_eq!(tx.state(), TransactionState::Active);
drop(tx);
}
#[tokio::test]
async fn jepsen_transaction_drop_after_commit() {
let mut tx = make_tx();
tx.commit().await.unwrap();
assert_eq!(tx.state(), TransactionState::Committed);
drop(tx);
}
#[tokio::test]
async fn jepsen_savepoint_partial_rollback() {
let mut tx = make_tx();
tx.execute("INSERT 1").await.unwrap();
let sp1 = tx.savepoint().await.unwrap();
tx.execute("INSERT 2").await.unwrap();
let _sp2 = tx.savepoint().await.unwrap();
tx.execute("INSERT 3").await.unwrap();
tx.rollback_to_savepoint(&sp1).await.unwrap();
assert_eq!(tx.state(), TransactionState::Active);
tx.execute("INSERT 4").await.unwrap();
let sp3 = tx.savepoint().await.unwrap();
assert_eq!(sp3, "sp_3");
tx.commit().await.unwrap();
assert_eq!(tx.state(), TransactionState::Committed);
}
#[tokio::test]
async fn jepsen_savepoint_release() {
let mut tx = make_tx();
let sp1 = tx.savepoint().await.unwrap();
tx.execute("INSERT").await.unwrap();
tx.release_savepoint(&sp1).await.unwrap();
tx.execute("INSERT 2").await.unwrap();
tx.commit().await.unwrap();
}
#[tokio::test]
async fn jepsen_long_transaction_multiple_ops() {
let mut tx = make_tx();
for i in 0..100 {
tx.execute(&format!("INSERT {}", i)).await.unwrap();
}
tx.commit().await.unwrap();
assert_eq!(tx.state(), TransactionState::Committed);
}
#[tokio::test]
async fn jepsen_long_transaction_rollback() {
let mut tx = make_tx();
for i in 0..100 {
tx.execute(&format!("INSERT {}", i)).await.unwrap();
}
tx.rollback().await.unwrap();
assert_eq!(tx.state(), TransactionState::RolledBack);
}
#[tokio::test]
async fn jepsen_transact_options() {
use sz_orm_core::IsolationLevel;
let opts = TransactOptions::default()
.with_isolation(IsolationLevel::Serializable)
.read_only()
.with_timeout(Duration::from_secs(30));
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let conn = MockConnection::new(db);
let tx = Transaction::new(Box::new(conn), opts);
let tx_opts = tx.options();
assert_eq!(tx_opts.isolation_level, Some(IsolationLevel::Serializable));
assert!(tx_opts.read_only);
assert_eq!(tx_opts.timeout, Some(Duration::from_secs(30)));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn jepsen_concurrent_tx_partial_failure() {
let success = Arc::new(AtomicU64::new(0));
let mut handles = Vec::new();
for i in 0..10 {
let s = success.clone();
handles.push(tokio::spawn(async move {
let mut tx = if i % 2 == 0 {
make_tx()
} else {
make_faulty_tx(true, false, None)
};
tx.execute("INSERT").await.unwrap();
let result = tx.commit().await;
if i % 2 == 0 {
assert!(result.is_ok(), "normal tx should commit");
assert_eq!(tx.state(), TransactionState::Committed);
} else {
assert!(result.is_err(), "faulty tx should fail to commit");
assert_eq!(
tx.state(),
TransactionState::Active,
"faulty tx stays active"
);
tx.rollback().await.unwrap();
assert_eq!(tx.state(), TransactionState::RolledBack);
}
s.fetch_add(1, Ordering::Relaxed);
}));
}
for h in handles {
h.await.unwrap();
}
assert_eq!(success.load(Ordering::Relaxed), 10);
}
#[tokio::test]
async fn jepsen_deep_savepoint_nesting() {
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let conn = MockConnection::new(db);
let opts = TransactOptions::default().with_max_nesting_depth(30);
let mut tx = Transaction::new(Box::new(conn), opts);
let mut savepoints = Vec::new();
for _ in 0..20 {
let sp = tx.savepoint().await.unwrap();
savepoints.push(sp);
tx.execute("INSERT").await.unwrap();
}
tx.rollback_to_savepoint(&savepoints[9]).await.unwrap();
assert_eq!(tx.state(), TransactionState::Active);
for sp in savepoints[10..20].iter() {
tx.release_savepoint(sp).await.unwrap();
}
tx.commit().await.unwrap();
}
#[tokio::test]
async fn jepsen_empty_transaction_commit() {
let mut tx = make_tx();
tx.commit().await.unwrap();
assert_eq!(tx.state(), TransactionState::Committed);
}
#[tokio::test]
async fn jepsen_empty_transaction_rollback() {
let mut tx = make_tx();
tx.rollback().await.unwrap();
assert_eq!(tx.state(), TransactionState::RolledBack);
}
#[tokio::test]
async fn jepsen_query_in_transaction() {
let mut tx = make_tx();
let result = tx.query("SELECT * FROM t").await;
assert!(result.is_ok(), "query should succeed in active tx");
tx.commit().await.unwrap();
let result = tx.query("SELECT").await;
assert!(result.is_err(), "query after commit should fail");
}