use super::lock_manager::LockManager;
use super::wal::WalWriter;
use super::{IsolationLevel, Operation, Transaction, TransactionId};
use crate::error::{DbError, DbResult};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use std::time::Duration;
#[allow(dead_code)]
pub struct TransactionManager {
active_transactions: Arc<RwLock<HashMap<TransactionId, Arc<RwLock<Transaction>>>>>,
wal: Arc<WalWriter>,
lock_manager: Arc<LockManager>,
timeout: Duration,
wal_batch_size: usize,
}
impl TransactionManager {
pub fn new(wal_path: PathBuf) -> DbResult<Self> {
Self::with_wal_batch_size(wal_path, 100)
}
pub fn with_wal_batch_size(wal_path: PathBuf, batch_size: usize) -> DbResult<Self> {
let wal = WalWriter::with_batch_size(&wal_path, batch_size)?;
Ok(Self {
active_transactions: Arc::new(RwLock::new(HashMap::new())),
wal: Arc::new(wal),
lock_manager: Arc::new(LockManager::new()),
timeout: Duration::from_secs(300),
wal_batch_size: batch_size,
})
}
pub fn set_timeout(&mut self, timeout: Duration) {
self.timeout = timeout;
}
pub fn begin(&self, isolation_level: IsolationLevel) -> DbResult<TransactionId> {
let tx = Transaction::new(isolation_level);
let tx_id = tx.id;
if isolation_level.requires_wal() {
self.wal.write_begin(tx_id)?;
}
{
let mut active = self.active_transactions.write().unwrap();
active.insert(tx_id, Arc::new(RwLock::new(tx)));
}
tracing::debug!("Transaction {} started", tx_id);
Ok(tx_id)
}
pub fn get(&self, tx_id: TransactionId) -> DbResult<Arc<RwLock<Transaction>>> {
let active = self.active_transactions.read().unwrap();
active
.get(&tx_id)
.cloned()
.ok_or_else(|| DbError::TransactionNotFound(tx_id.to_string()))
}
pub fn is_active(&self, tx_id: TransactionId) -> bool {
let active = self.active_transactions.read().unwrap();
active
.get(&tx_id)
.map(|tx| tx.read().unwrap().is_active())
.unwrap_or(false)
}
pub fn validate(&self, tx_id: TransactionId) -> DbResult<()> {
let tx_arc = self.get(tx_id)?;
let errors = {
let tx = tx_arc.read().unwrap();
let mut validation_errors = Vec::new();
let mut seen_keys: std::collections::HashMap<String, Vec<Operation>> =
std::collections::HashMap::new();
for op in &tx.operations {
let key = format!("{}:{}:{}", op.database(), op.collection(), op.key());
seen_keys.entry(key.clone()).or_default().push(op.clone());
}
for (key, ops) in seen_keys.iter() {
let inserts: Vec<_> = ops
.iter()
.filter(|op| matches!(op, Operation::Insert { .. }))
.collect();
if inserts.len() > 1 {
let error = format!("Duplicate insert for key {} within transaction", key);
validation_errors.push(error);
}
let deletes: Vec<_> = ops
.iter()
.filter(|op| matches!(op, Operation::Delete { .. }))
.collect();
if !deletes.is_empty() {
let updates_after_delete: Vec<_> = ops
.iter()
.skip_while(|op| !matches!(op, Operation::Delete { .. }))
.filter(|op| matches!(op, Operation::Update { .. }))
.collect();
if !updates_after_delete.is_empty() {
let error =
format!("Cannot update deleted document {} within transaction", key);
validation_errors.push(error);
}
}
}
validation_errors
};
{
let mut tx = tx_arc.write().unwrap();
tx.clear_validation_errors();
for error in errors {
tx.add_validation_error(error);
}
if tx.has_validation_errors() {
let error_msg = tx.get_validation_errors().join("; ");
return Err(DbError::TransactionConflict(format!(
"Transaction validation failed: {}",
error_msg
)));
}
}
Ok(())
}
pub fn commit(&self, tx_id: TransactionId) -> DbResult<()> {
self.validate(tx_id)?;
let tx_arc = self.get(tx_id)?;
let requires_wal = {
let mut tx = tx_arc.write().unwrap();
if !tx.is_active() {
return Err(DbError::TransactionConflict(format!(
"Transaction {} is not active (state: {:?})",
tx_id, tx.state
)));
}
tx.prepare();
tx.isolation_level.requires_wal()
};
if requires_wal {
self.wal.write_commit(tx_id)?;
}
{
let mut tx = tx_arc.write().unwrap();
tx.commit();
}
{
let mut active = self.active_transactions.write().unwrap();
active.remove(&tx_id);
}
self.lock_manager.release_locks(tx_id);
tracing::debug!("Transaction {} committed", tx_id);
Ok(())
}
pub fn rollback(&self, tx_id: TransactionId) -> DbResult<()> {
let tx_arc = self.get(tx_id)?;
let requires_wal = {
let tx = tx_arc.read().unwrap();
tx.isolation_level.requires_wal()
};
if requires_wal {
self.wal.write_abort(tx_id)?;
}
{
let mut tx = tx_arc.write().unwrap();
tx.abort();
}
{
let mut active = self.active_transactions.write().unwrap();
active.remove(&tx_id);
}
self.lock_manager.release_locks(tx_id);
tracing::debug!("Transaction {} rolled back", tx_id);
Ok(())
}
pub fn active_transaction_ids(&self) -> Vec<TransactionId> {
let active = self.active_transactions.read().unwrap();
active.keys().copied().collect()
}
pub fn transaction_count(&self) -> usize {
let active = self.active_transactions.read().unwrap();
active.len()
}
pub fn cleanup_expired(&self) -> usize {
let now = chrono::Utc::now();
let mut expired = Vec::new();
{
let active = self.active_transactions.read().unwrap();
for (tx_id, tx_arc) in active.iter() {
let tx = tx_arc.read().unwrap();
if now
.signed_duration_since(tx.created_at)
.to_std()
.unwrap_or(Duration::ZERO)
> self.timeout
{
expired.push(*tx_id);
}
}
}
let count = expired.len();
for tx_id in expired {
tracing::warn!("Aborting expired transaction {}", tx_id);
let _ = self.rollback(tx_id);
}
count
}
pub fn wal(&self) -> &Arc<WalWriter> {
&self.wal
}
pub fn lock_manager(&self) -> &Arc<LockManager> {
&self.lock_manager
}
pub fn checkpoint(&self) -> DbResult<()> {
self.wal.write_checkpoint()
}
}
impl std::fmt::Debug for TransactionManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TransactionManager")
.field("active_count", &self.transaction_count())
.field("timeout", &self.timeout)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_begin_transaction() {
let dir = tempdir().unwrap();
let wal_path = dir.path().join("test.wal");
let manager = TransactionManager::new(wal_path).unwrap();
let tx_id = manager.begin(IsolationLevel::ReadCommitted).unwrap();
assert!(manager.is_active(tx_id));
assert_eq!(manager.transaction_count(), 1);
}
#[test]
fn test_commit_transaction() {
let dir = tempdir().unwrap();
let wal_path = dir.path().join("test.wal");
let manager = TransactionManager::new(wal_path).unwrap();
let tx_id = manager.begin(IsolationLevel::ReadCommitted).unwrap();
manager.commit(tx_id).unwrap();
assert!(!manager.is_active(tx_id));
assert_eq!(manager.transaction_count(), 0);
}
#[test]
fn test_rollback_transaction() {
let dir = tempdir().unwrap();
let wal_path = dir.path().join("test.wal");
let manager = TransactionManager::new(wal_path).unwrap();
let tx_id = manager.begin(IsolationLevel::ReadCommitted).unwrap();
manager.rollback(tx_id).unwrap();
assert!(!manager.is_active(tx_id));
assert_eq!(manager.transaction_count(), 0);
}
#[test]
fn test_multiple_transactions() {
let dir = tempdir().unwrap();
let wal_path = dir.path().join("test.wal");
let manager = TransactionManager::new(wal_path).unwrap();
let tx1 = manager.begin(IsolationLevel::ReadCommitted).unwrap();
let tx2 = manager.begin(IsolationLevel::Serializable).unwrap();
assert_eq!(manager.transaction_count(), 2);
assert!(manager.is_active(tx1));
assert!(manager.is_active(tx2));
manager.commit(tx1).unwrap();
assert_eq!(manager.transaction_count(), 1);
manager.rollback(tx2).unwrap();
assert_eq!(manager.transaction_count(), 0);
}
#[test]
fn test_transaction_not_found() {
let dir = tempdir().unwrap();
let wal_path = dir.path().join("test.wal");
let manager = TransactionManager::new(wal_path).unwrap();
let fake_id = TransactionId::new();
assert!(manager.get(fake_id).is_err());
}
#[test]
fn test_double_commit() {
let dir = tempdir().unwrap();
let wal_path = dir.path().join("test.wal");
let manager = TransactionManager::new(wal_path).unwrap();
let tx_id = manager.begin(IsolationLevel::ReadCommitted).unwrap();
manager.commit(tx_id).unwrap();
assert!(manager.commit(tx_id).is_err());
}
}