Files
foxhunt/database/src/transaction.rs
jgrusewski 6bd5b18465 🔧 Wave 33: Test Compilation Improvements - 57 errors remaining
**Progress: 1,178 → 57 test errors (95% reduction)**

## Status Summary
-  Production code: Compiles cleanly (0 errors)
- ⚠️  Test code: 57 errors remain (massive improvement)
- ⚙️  All services build successfully
- 📊 Warning count: 253 (target: <20) - AGENTS WILL FIX

## Remaining Test Errors (57 total)
### Primary Issues:
1. 23× E0308 mismatched types
2. 17× E0433 undeclared Decimal
3. 15× E0433 compliance module not found
4. 6× E0624 private method access
5. Various import and type issues

## Next Phase: Wave 33-2
Launch 10+ parallel agents to:
- Fix remaining 57 test compilation errors
- Reduce 253 warnings to <20
- Achieve 95% test coverage
- Ensure all tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-01 21:24:28 +02:00

577 lines
19 KiB
Rust

use crate::error::{DatabaseError, DatabaseResult, ErrorContext};
use crate::pool::DatabasePool;
use config::database::TransactionConfig;
use sqlx::FromRow;
use std::future::Future;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tracing::{debug, error, info, warn};
use uuid::Uuid;
// TransactionConfig is now imported from the config crate
/// Transaction manager for handling database transactions
#[derive(Debug)]
pub struct TransactionManager {
pool: DatabasePool,
config: TransactionConfig,
active_transactions: Arc<AtomicU64>,
total_transactions: Arc<AtomicU64>,
failed_transactions: Arc<AtomicU64>,
retry_attempts: Arc<AtomicU64>,
}
impl TransactionManager {
/// Create a new transaction manager
pub fn new(pool: DatabasePool, config: TransactionConfig) -> Self {
Self {
pool,
config,
active_transactions: Arc::new(AtomicU64::new(0)),
total_transactions: Arc::new(AtomicU64::new(0)),
failed_transactions: Arc::new(AtomicU64::new(0)),
retry_attempts: Arc::new(AtomicU64::new(0)),
}
}
/// Begin a new transaction with default timeout
pub async fn begin(&self) -> DatabaseResult<DatabaseTransaction> {
self.begin_with_timeout(Duration::from_secs(self.config.default_timeout_secs))
.await
}
/// Begin a new transaction with custom timeout
pub async fn begin_with_timeout(
&self,
timeout_duration: Duration,
) -> DatabaseResult<DatabaseTransaction> {
let start_time = Instant::now();
self.total_transactions.fetch_add(1, Ordering::Relaxed);
self.active_transactions.fetch_add(1, Ordering::Relaxed);
debug!(
"Beginning new database transaction with {}s timeout",
timeout_duration.as_secs()
);
// Get the transaction directly from the pool to avoid lifetime issues
let transaction = self.pool.inner().begin().await?;
let transaction_id = Uuid::new_v4();
debug!("Transaction {} started successfully", transaction_id);
Ok(DatabaseTransaction {
inner: Some(transaction),
id: transaction_id,
start_time,
timeout: timeout_duration,
savepoints: Vec::new(),
config: self.config.clone(),
manager_stats: TransactionManagerStats {
active_transactions: self.active_transactions.clone(),
failed_transactions: self.failed_transactions.clone(),
},
})
}
/// Execute a closure within a transaction with retry logic
pub async fn with_transaction<F, R, Fut>(&self, f: F) -> DatabaseResult<R>
where
F: Fn(DatabaseTransaction) -> Fut + Send + Sync,
Fut: Future<Output = DatabaseResult<(R, DatabaseTransaction)>> + Send,
R: Send,
{
self.with_transaction_timeout(f, Duration::from_secs(self.config.default_timeout_secs))
.await
}
/// Execute a closure within a transaction with custom timeout and retry logic
pub async fn with_transaction_timeout<F, R, Fut>(
&self,
f: F,
timeout_duration: Duration,
) -> DatabaseResult<R>
where
F: Fn(DatabaseTransaction) -> Fut + Send + Sync,
Fut: Future<Output = DatabaseResult<(R, DatabaseTransaction)>> + Send,
R: Send,
{
let mut attempts = 0;
let max_attempts = if self.config.enable_retry {
self.config.max_retries + 1
} else {
1
};
loop {
attempts += 1;
let tx = self.begin_with_timeout(timeout_duration).await?;
let tx_id = tx.id();
match f(tx).await {
Ok((result, tx)) => match tx.commit().await {
Ok(_) => {
debug!(
"Transaction {} completed successfully on attempt {}",
tx_id, attempts
);
return Ok(result);
},
Err(e) if e.is_retryable() && attempts < max_attempts => {
self.retry_attempts.fetch_add(1, Ordering::Relaxed);
warn!(
"Transaction {} commit failed (attempt {}): {}. Retrying...",
tx_id, attempts, e
);
self.delay_retry(attempts).await;
continue;
},
Err(e) => {
error!(
"Transaction {} commit failed on attempt {}: {}",
tx_id, attempts, e
);
return Err(e);
},
},
Err(e) if e.is_retryable() && attempts < max_attempts => {
self.retry_attempts.fetch_add(1, Ordering::Relaxed);
warn!(
"Transaction {} failed (attempt {}): {}. Retrying...",
tx_id, attempts, e
);
self.delay_retry(attempts).await;
continue;
},
Err(e) => {
error!(
"Transaction {} failed on attempt {}: {}",
tx_id, attempts, e
);
return Err(e);
},
}
}
}
/// Get transaction statistics
pub fn stats(&self) -> TransactionStats {
TransactionStats {
active_transactions: self.active_transactions.load(Ordering::Relaxed),
total_transactions: self.total_transactions.load(Ordering::Relaxed),
failed_transactions: self.failed_transactions.load(Ordering::Relaxed),
retry_attempts: self.retry_attempts.load(Ordering::Relaxed),
}
}
/// Reset transaction statistics
pub fn reset_stats(&self) {
self.active_transactions.store(0, Ordering::Relaxed);
self.total_transactions.store(0, Ordering::Relaxed);
self.failed_transactions.store(0, Ordering::Relaxed);
self.retry_attempts.store(0, Ordering::Relaxed);
info!("Transaction manager statistics reset");
}
/// Delay before retry with exponential backoff
async fn delay_retry(&self, attempt: u32) {
let delay = Duration::from_millis(
self.config.retry_delay_ms * (2_u64.pow(attempt.saturating_sub(1))),
);
debug!(
"Waiting {}ms before retry attempt {}",
delay.as_millis(),
attempt + 1
);
tokio::time::sleep(delay).await;
}
}
impl Clone for TransactionManager {
fn clone(&self) -> Self {
Self {
pool: self.pool.clone(),
config: self.config.clone(),
active_transactions: self.active_transactions.clone(),
total_transactions: self.total_transactions.clone(),
failed_transactions: self.failed_transactions.clone(),
retry_attempts: self.retry_attempts.clone(),
}
}
}
/// A database transaction wrapper with enhanced features
pub struct DatabaseTransaction {
inner: Option<sqlx::Transaction<'static, sqlx::Postgres>>,
id: Uuid,
start_time: Instant,
timeout: Duration,
savepoints: Vec<String>,
config: TransactionConfig,
manager_stats: TransactionManagerStats,
}
/// Statistics tracking for transaction manager
#[derive(Debug, Clone)]
struct TransactionManagerStats {
active_transactions: Arc<AtomicU64>,
failed_transactions: Arc<AtomicU64>,
}
impl DatabaseTransaction {
/// Get the transaction ID
pub fn id(&self) -> Uuid {
self.id
}
/// Get the elapsed time since transaction start
pub fn elapsed(&self) -> Duration {
self.start_time.elapsed()
}
/// Check if the transaction has timed out
pub fn is_timed_out(&self) -> bool {
self.elapsed() > self.timeout
}
/// Get remaining time before timeout
pub fn remaining_time(&self) -> Duration {
self.timeout.saturating_sub(self.elapsed())
}
/// Create a savepoint
pub async fn savepoint(&mut self, name: &str) -> DatabaseResult<()> {
if self.savepoints.len() >= self.config.max_savepoints as usize {
return Err(DatabaseError::Transaction {
message: format!(
"Maximum number of savepoints ({}) exceeded",
self.config.max_savepoints
),
});
}
if self.is_timed_out() {
return Err(DatabaseError::Timeout {
operation: format!("savepoint_{}", name),
timeout_secs: self.timeout.as_secs(),
});
}
let savepoint_name = format!("sp_{}_{}", self.id.simple(), name);
sqlx::query(&format!("SAVEPOINT {}", savepoint_name))
.execute(&mut **self.inner.as_mut().expect("Transaction already consumed"))
.await
.with_context(&format!("Failed to create savepoint {}", savepoint_name))?;
self.savepoints.push(savepoint_name.clone());
debug!(
"Created savepoint {} for transaction {}",
savepoint_name, self.id
);
Ok(())
}
/// Rollback to a savepoint
pub async fn rollback_to_savepoint(&mut self, name: &str) -> DatabaseResult<()> {
let savepoint_name = format!("sp_{}_{}", self.id.simple(), name);
if !self.savepoints.contains(&savepoint_name) {
return Err(DatabaseError::Transaction {
message: format!("Savepoint {} not found", savepoint_name),
});
}
if self.is_timed_out() {
return Err(DatabaseError::Timeout {
operation: format!("rollback_to_savepoint_{}", name),
timeout_secs: self.timeout.as_secs(),
});
}
sqlx::query(&format!("ROLLBACK TO SAVEPOINT {}", savepoint_name))
.execute(&mut **self.inner.as_mut().expect("Transaction already consumed"))
.await
.with_context(&format!(
"Failed to rollback to savepoint {}",
savepoint_name
))?;
// Remove this savepoint and all subsequent ones
if let Some(pos) = self.savepoints.iter().position(|sp| sp == &savepoint_name) {
self.savepoints.truncate(pos);
}
debug!(
"Rolled back to savepoint {} for transaction {}",
savepoint_name, self.id
);
Ok(())
}
/// Release a savepoint
pub async fn release_savepoint(&mut self, name: &str) -> DatabaseResult<()> {
let savepoint_name = format!("sp_{}_{}", self.id.simple(), name);
if !self.savepoints.contains(&savepoint_name) {
return Err(DatabaseError::Transaction {
message: format!("Savepoint {} not found", savepoint_name),
});
}
sqlx::query(&format!("RELEASE SAVEPOINT {}", savepoint_name))
.execute(&mut **self.inner.as_mut().expect("Transaction already consumed"))
.await
.with_context(&format!("Failed to release savepoint {}", savepoint_name))?;
self.savepoints.retain(|sp| sp != &savepoint_name);
debug!(
"Released savepoint {} for transaction {}",
savepoint_name, self.id
);
Ok(())
}
/// Execute a query within the transaction
pub async fn execute(&mut self, query: &str) -> DatabaseResult<u64> {
if self.is_timed_out() {
return Err(DatabaseError::Timeout {
operation: "execute_query".to_string(),
timeout_secs: self.timeout.as_secs(),
});
}
let result = sqlx::query(query)
.execute(&mut **self.inner.as_mut().expect("Transaction already consumed"))
.await
.with_query_context(query)?;
Ok(result.rows_affected())
}
/// Fetch all rows from a query
pub async fn fetch_all<T>(&mut self, query: &str) -> DatabaseResult<Vec<T>>
where
T: for<'r> FromRow<'r, sqlx::postgres::PgRow> + Send + Unpin,
{
if self.is_timed_out() {
return Err(DatabaseError::Timeout {
operation: "fetch_all".to_string(),
timeout_secs: self.timeout.as_secs(),
});
}
let rows = sqlx::query_as(query)
.fetch_all(&mut **self.inner.as_mut().expect("Transaction already consumed"))
.await
.with_query_context(query)?;
Ok(rows)
}
/// Fetch one row from a query
pub async fn fetch_one<T>(&mut self, query: &str) -> DatabaseResult<T>
where
T: for<'r> FromRow<'r, sqlx::postgres::PgRow> + Send + Unpin,
{
if self.is_timed_out() {
return Err(DatabaseError::Timeout {
operation: "fetch_one".to_string(),
timeout_secs: self.timeout.as_secs(),
});
}
let row = sqlx::query_as(query)
.fetch_one(&mut **self.inner.as_mut().expect("Transaction already consumed"))
.await
.with_query_context(query)?;
Ok(row)
}
/// Fetch optional row from a query
pub async fn fetch_optional<T>(&mut self, query: &str) -> DatabaseResult<Option<T>>
where
T: for<'r> FromRow<'r, sqlx::postgres::PgRow> + Send + Unpin,
{
if self.is_timed_out() {
return Err(DatabaseError::Timeout {
operation: "fetch_optional".to_string(),
timeout_secs: self.timeout.as_secs(),
});
}
let row = sqlx::query_as(query)
.fetch_optional(&mut **self.inner.as_mut().expect("Transaction already consumed"))
.await
.with_query_context(query)?;
Ok(row)
}
/// Commit the transaction
pub async fn commit(mut self) -> DatabaseResult<()> {
if self.is_timed_out() {
return Err(DatabaseError::Timeout {
operation: "commit_transaction".to_string(),
timeout_secs: self.timeout.as_secs(),
});
}
let elapsed = self.elapsed();
let tx_id = self.id;
match self.inner.take().unwrap().commit().await {
Ok(_) => {
self.manager_stats
.active_transactions
.fetch_sub(1, Ordering::Relaxed);
info!(
"Transaction {} committed successfully in {}ms",
tx_id,
elapsed.as_millis()
);
Ok(())
},
Err(e) => {
self.manager_stats
.active_transactions
.fetch_sub(1, Ordering::Relaxed);
self.manager_stats
.failed_transactions
.fetch_add(1, Ordering::Relaxed);
error!(
"Transaction {} commit failed after {}ms: {}",
tx_id,
elapsed.as_millis(),
e
);
Err(DatabaseError::from(e))
},
}
}
/// Rollback the transaction
pub async fn rollback(mut self) -> DatabaseResult<()> {
let elapsed = self.elapsed();
let tx_id = self.id;
match self.inner.take().unwrap().rollback().await {
Ok(_) => {
self.manager_stats
.active_transactions
.fetch_sub(1, Ordering::Relaxed);
info!(
"Transaction {} rolled back successfully in {}ms",
tx_id,
elapsed.as_millis()
);
Ok(())
},
Err(e) => {
self.manager_stats
.active_transactions
.fetch_sub(1, Ordering::Relaxed);
self.manager_stats
.failed_transactions
.fetch_add(1, Ordering::Relaxed);
error!(
"Transaction {} rollback failed after {}ms: {}",
tx_id,
elapsed.as_millis(),
e
);
Err(DatabaseError::from(e))
},
}
}
}
impl Drop for DatabaseTransaction {
fn drop(&mut self) {
// Only warn if transaction is still active (not None)
if self.inner.is_some() {
self.manager_stats
.active_transactions
.fetch_sub(1, Ordering::Relaxed);
warn!("Transaction {} was dropped without explicit commit/rollback - automatic rollback will occur", self.id);
}
}
}
/// Transaction statistics
#[derive(Debug, Clone)]
pub struct TransactionStats {
/// Number of currently active transactions
pub active_transactions: u64,
/// Total number of transactions started
pub total_transactions: u64,
/// Number of failed transactions
pub failed_transactions: u64,
/// Number of retry attempts
pub retry_attempts: u64,
}
impl TransactionStats {
/// Calculate success rate as percentage
pub fn success_rate(&self) -> f64 {
if self.total_transactions == 0 {
0.0
} else {
((self.total_transactions - self.failed_transactions) as f64
/ self.total_transactions as f64)
* 100.0
}
}
/// Calculate retry rate as percentage
pub fn retry_rate(&self) -> f64 {
if self.total_transactions == 0 {
0.0
} else {
(self.retry_attempts as f64 / self.total_transactions as f64) * 100.0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transaction_config_default() {
let config = TransactionConfig::default();
assert_eq!(config.default_timeout_secs, 30);
assert_eq!(config.max_savepoints, 10);
assert!(config.enable_retry);
assert_eq!(config.max_retries, 3);
}
#[test]
fn test_transaction_stats_calculation() {
let stats = TransactionStats {
active_transactions: 2,
total_transactions: 100,
failed_transactions: 5,
retry_attempts: 10,
};
assert_eq!(stats.success_rate(), 95.0);
assert_eq!(stats.retry_rate(), 10.0);
}
#[test]
fn test_transaction_stats_zero_transactions() {
let stats = TransactionStats {
active_transactions: 0,
total_transactions: 0,
failed_transactions: 0,
retry_attempts: 0,
};
assert_eq!(stats.success_rate(), 0.0);
assert_eq!(stats.retry_rate(), 0.0);
}
}