mod common;
use common::setup_logging;
use rocksolid::tx::cf_optimistic_tx_store::{RocksDbCFOptimisticTxnStore, RocksDbCFOptimisticTxnStoreConfig};
use rocksolid::tx::policies::FixedRetry;
use rocksolid::{
CFOperations, StoreResult,
serialization::{deserialize_value, serialize_value},
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use tempfile::TempDir;
const CF_ACCOUNTS: &str = "accounts";
const CF_LOGS: &str = "logs";
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
struct Account {
balance: i64,
version: u64,
}
fn setup_opt_cf_store(test_name: &str) -> (TempDir, RocksDbCFOptimisticTxnStore) {
setup_logging();
let temp_dir = tempfile::tempdir().unwrap();
let db_path = temp_dir.path().join(test_name);
let mut cf_configs = HashMap::new();
cf_configs.insert(CF_ACCOUNTS.to_string(), Default::default());
cf_configs.insert(CF_LOGS.to_string(), Default::default());
let config = RocksDbCFOptimisticTxnStoreConfig {
path: db_path.to_str().unwrap().to_string(),
create_if_missing: true,
column_families_to_open: vec![
rocksdb::DEFAULT_COLUMN_FAMILY_NAME.to_string(),
CF_ACCOUNTS.to_string(),
CF_LOGS.to_string(),
],
column_family_configs: cf_configs,
..Default::default()
};
let store = RocksDbCFOptimisticTxnStore::open(config).unwrap();
(temp_dir, store)
}
#[test]
fn test_optimistic_cf_simple_commit() {
let (_temp_dir, store) = setup_opt_cf_store("opt_cf_simple_commit");
let mut txn_context = store.transaction_context();
let key1 = "acc_key";
let val1 = Account {
balance: 100,
version: 1,
};
let key2 = "log_key";
let val2 = "Transaction started".to_string();
txn_context.put_cf(CF_ACCOUNTS, key1, &val1).unwrap();
txn_context.put_cf(CF_LOGS, key2, &val2).unwrap();
txn_context.commit().unwrap();
let retrieved1: Option<Account> = store.get(CF_ACCOUNTS, key1).unwrap();
let retrieved2: Option<String> = store.get(CF_LOGS, key2).unwrap();
assert_eq!(retrieved1, Some(val1));
assert_eq!(retrieved2, Some(val2));
}
#[test]
fn test_optimistic_cf_rollback() {
let (_temp_dir, store) = setup_opt_cf_store("opt_cf_rollback");
let mut txn_context = store.transaction_context();
txn_context
.put_cf(
CF_ACCOUNTS,
"key1",
&Account {
balance: 50,
version: 1,
},
)
.unwrap();
txn_context
.put_cf(CF_LOGS, "key2", &"this should not be saved".to_string())
.unwrap();
txn_context.rollback().unwrap();
let retrieved1: Option<Account> = store.get(CF_ACCOUNTS, "key1").unwrap();
let retrieved2: Option<String> = store.get(CF_LOGS, "key2").unwrap();
assert!(retrieved1.is_none());
assert!(retrieved2.is_none());
}
#[test]
fn test_optimistic_cf_conflict_and_retry_with_builder() {
let (_temp_dir, store) = setup_opt_cf_store("opt_cf_conflict_retry_builder");
let store = Arc::new(store);
let key = "balance_acc";
store
.put(
CF_ACCOUNTS,
key,
&Account {
balance: 100,
version: 1,
},
)
.unwrap();
println!("Initial state: Account has $100 (v1)");
let store_clone = Arc::clone(&store);
let handle = thread::spawn(move || {
store_clone
.optimistic_transaction()
.with_retry_policy(FixedRetry {
max_attempts: 5,
backoff: Duration::from_millis(20),
})
.execute_with_snapshot(|txn| {
let cf_handle = store_clone.get_cf_handle(CF_ACCOUNTS)?;
let acc_bytes = txn.get_pinned_cf(&cf_handle, key)?.unwrap();
let mut acc: Account = deserialize_value(&acc_bytes)?;
println!("[T1] Read balance: {} (v{})", acc.balance, acc.version);
if acc.version == 1 {
println!("[T1] Pausing to simulate work...");
thread::sleep(Duration::from_millis(100));
}
acc.balance -= 20; acc.version += 1;
txn.put_cf(&cf_handle, key, serialize_value(&acc)?)?;
println!("[T1] Staged new balance: {} (v{})", acc.balance, acc.version);
Ok(())
})
});
thread::sleep(Duration::from_millis(50));
println!("[Main] Concurrent Tx (T2): Modifies and commits...");
let mut concurrent_account: Account = store.get(CF_ACCOUNTS, key).unwrap().unwrap();
concurrent_account.balance += 10; concurrent_account.version += 1;
store.put(CF_ACCOUNTS, key, &concurrent_account).unwrap();
println!("[Main] Committed. Balance is now $110 (v2)");
let result = handle.join().unwrap();
assert!(result.is_ok(), "Transaction should have succeeded after retry");
let final_account: Account = store.get(CF_ACCOUNTS, key).unwrap().unwrap();
println!(
"\nFinal state: Account has ${} (v{})",
final_account.balance, final_account.version
);
assert_eq!(final_account.balance, 90);
assert_eq!(final_account.version, 3);
}