use crate::error::{Result, TdbError};
use anyhow::Context;
use chrono::{DateTime, Utc};
use parking_lot::{Mutex, RwLock};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SagaStatus {
Created,
Executing,
Completed,
Compensating,
Compensated,
Failed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StepStatus {
Pending,
Executing,
Completed,
Failed,
Compensating,
Compensated,
CompensationFailed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SagaStep {
pub name: String,
pub compensatable: bool,
pub retriable: bool,
pub max_retries: u32,
pub retry_count: u32,
pub status: StepStatus,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
pub result_data: Option<Vec<u8>>,
pub metadata: HashMap<String, String>,
}
impl Default for SagaStep {
fn default() -> Self {
Self {
name: String::new(),
compensatable: true,
retriable: true,
max_retries: 3,
retry_count: 0,
status: StepStatus::Pending,
started_at: None,
completed_at: None,
result_data: None,
metadata: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SagaStrategy {
ForwardRecovery,
BestEffort,
RetryFirst,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SagaConfig {
pub strategy: SagaStrategy,
pub timeout: Duration,
pub step_timeout: Duration,
pub auto_compensate: bool,
pub compensation_delay: Duration,
}
impl Default for SagaConfig {
fn default() -> Self {
Self {
strategy: SagaStrategy::ForwardRecovery,
timeout: Duration::from_secs(300), step_timeout: Duration::from_secs(30),
auto_compensate: true,
compensation_delay: Duration::from_millis(100),
}
}
}
pub struct SagaOrchestrator {
id: String,
config: SagaConfig,
steps: Arc<RwLock<Vec<SagaStep>>>,
current_step: Arc<Mutex<usize>>,
status: Arc<RwLock<SagaStatus>>,
start_time: DateTime<Utc>,
stats: Arc<Mutex<SagaStats>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SagaStats {
pub total_sagas: u64,
pub successful_sagas: u64,
pub failed_sagas: u64,
pub compensated_sagas: u64,
pub total_steps: u64,
pub failed_steps: u64,
pub compensated_steps: u64,
pub avg_saga_duration_ms: f64,
total_duration_ms: f64,
}
impl SagaOrchestrator {
pub fn new(id: String, config: SagaConfig) -> Self {
Self {
id,
config,
steps: Arc::new(RwLock::new(Vec::new())),
current_step: Arc::new(Mutex::new(0)),
status: Arc::new(RwLock::new(SagaStatus::Created)),
start_time: Utc::now(),
stats: Arc::new(Mutex::new(SagaStats::default())),
}
}
pub fn add_step(&mut self, step: SagaStep) {
self.steps.write().push(step);
}
pub async fn execute(&mut self) -> Result<bool> {
*self.status.write() = SagaStatus::Executing;
{
let mut stats = self.stats.lock();
stats.total_sagas += 1;
}
let start = Utc::now();
let forward_result = self.execute_forward().await;
match forward_result {
Ok(_) => {
*self.status.write() = SagaStatus::Completed;
let duration = Utc::now().signed_duration_since(start).num_milliseconds() as f64;
let mut stats = self.stats.lock();
stats.successful_sagas += 1;
stats.total_duration_ms += duration;
stats.avg_saga_duration_ms = stats.total_duration_ms / stats.total_sagas as f64;
Ok(true)
}
Err(_) => {
if self.config.auto_compensate {
self.compensate().await?;
let duration =
Utc::now().signed_duration_since(start).num_milliseconds() as f64;
let mut stats = self.stats.lock();
stats.compensated_sagas += 1;
stats.total_duration_ms += duration;
stats.avg_saga_duration_ms = stats.total_duration_ms / stats.total_sagas as f64;
Ok(false)
} else {
*self.status.write() = SagaStatus::Failed;
let mut stats = self.stats.lock();
stats.failed_sagas += 1;
Err(TdbError::Other("Saga execution failed".to_string()))
}
}
}
}
async fn execute_forward(&self) -> Result<()> {
let step_count = self.steps.read().len();
for step_idx in 0..step_count {
*self.current_step.lock() = step_idx;
let step_result = self.execute_step(step_idx).await;
match step_result {
Ok(_) => {
let mut stats = self.stats.lock();
stats.total_steps += 1;
}
Err(_) => {
let mut stats = self.stats.lock();
stats.failed_steps += 1;
match self.config.strategy {
SagaStrategy::ForwardRecovery => {
return Err(TdbError::Other(format!(
"Step {} failed",
self.steps.read()[step_idx].name
)));
}
SagaStrategy::BestEffort => {
continue;
}
SagaStrategy::RetryFirst => {
return Err(TdbError::Other(format!(
"Step {} failed after retries",
self.steps.read()[step_idx].name
)));
}
}
}
}
}
Ok(())
}
async fn execute_step(&self, step_idx: usize) -> Result<()> {
{
let mut steps = self.steps.write();
let step = &mut steps[step_idx];
step.status = StepStatus::Executing;
step.started_at = Some(Utc::now());
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
let success = true;
{
let mut steps = self.steps.write();
let step = &mut steps[step_idx];
if success {
step.status = StepStatus::Completed;
step.completed_at = Some(Utc::now());
Ok(())
} else {
step.status = StepStatus::Failed;
Err(TdbError::Other(format!("Step {} failed", step.name)))
}
}
}
async fn compensate(&mut self) -> Result<()> {
*self.status.write() = SagaStatus::Compensating;
let current_step = *self.current_step.lock();
for step_idx in (0..current_step).rev() {
let step = {
let steps = self.steps.read();
steps[step_idx].clone()
};
if step.status == StepStatus::Completed && step.compensatable {
self.compensate_step(step_idx).await?;
let mut stats = self.stats.lock();
stats.compensated_steps += 1;
}
tokio::time::sleep(self.config.compensation_delay).await;
}
*self.status.write() = SagaStatus::Compensated;
Ok(())
}
async fn compensate_step(&self, step_idx: usize) -> Result<()> {
{
let mut steps = self.steps.write();
let step = &mut steps[step_idx];
step.status = StepStatus::Compensating;
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
{
let mut steps = self.steps.write();
let step = &mut steps[step_idx];
step.status = StepStatus::Compensated;
}
Ok(())
}
pub fn id(&self) -> &str {
&self.id
}
pub fn status(&self) -> SagaStatus {
*self.status.read()
}
pub fn step_count(&self) -> usize {
self.steps.read().len()
}
pub fn current_step_index(&self) -> usize {
*self.current_step.lock()
}
pub fn stats(&self) -> SagaStats {
self.stats.lock().clone()
}
pub fn get_step_status(&self, step_idx: usize) -> Option<StepStatus> {
self.steps.read().get(step_idx).map(|s| s.status)
}
pub fn get_steps(&self) -> Vec<SagaStep> {
self.steps.read().clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_saga_creation() {
let config = SagaConfig::default();
let saga = SagaOrchestrator::new("saga-001".to_string(), config);
assert_eq!(saga.id(), "saga-001");
assert_eq!(saga.status(), SagaStatus::Created);
assert_eq!(saga.step_count(), 0);
}
#[tokio::test]
async fn test_add_steps() {
let config = SagaConfig::default();
let mut saga = SagaOrchestrator::new("saga-002".to_string(), config);
saga.add_step(SagaStep {
name: "step1".to_string(),
..Default::default()
});
saga.add_step(SagaStep {
name: "step2".to_string(),
..Default::default()
});
assert_eq!(saga.step_count(), 2);
}
#[tokio::test]
async fn test_successful_saga() {
let config = SagaConfig::default();
let mut saga = SagaOrchestrator::new("saga-003".to_string(), config);
saga.add_step(SagaStep {
name: "reserve-inventory".to_string(),
compensatable: true,
retriable: true,
..Default::default()
});
saga.add_step(SagaStep {
name: "charge-payment".to_string(),
compensatable: true,
retriable: false,
..Default::default()
});
}
#[tokio::test]
async fn test_saga_compensation() {
let config = SagaConfig {
strategy: SagaStrategy::ForwardRecovery,
..Default::default()
};
let mut saga = SagaOrchestrator::new("saga-004".to_string(), config);
saga.add_step(SagaStep {
name: "step1".to_string(),
compensatable: true,
..Default::default()
});
}
#[test]
fn test_saga_config() {
let config = SagaConfig::default();
assert_eq!(config.strategy, SagaStrategy::ForwardRecovery);
assert!(config.auto_compensate);
assert_eq!(config.timeout, Duration::from_secs(300));
}
#[test]
fn test_step_default() {
let step = SagaStep::default();
assert!(step.compensatable);
assert!(step.retriable);
assert_eq!(step.max_retries, 3);
assert_eq!(step.status, StepStatus::Pending);
}
#[tokio::test]
async fn test_saga_stats() {
let config = SagaConfig::default();
let saga = SagaOrchestrator::new("saga-005".to_string(), config);
let stats = saga.stats();
assert_eq!(stats.total_sagas, 0);
assert_eq!(stats.successful_sagas, 0);
}
#[test]
fn test_saga_status_enum() {
assert_eq!(SagaStatus::Created, SagaStatus::Created);
assert_ne!(SagaStatus::Created, SagaStatus::Executing);
}
#[test]
fn test_step_status_enum() {
assert_eq!(StepStatus::Pending, StepStatus::Pending);
assert_ne!(StepStatus::Pending, StepStatus::Executing);
}
#[tokio::test]
async fn test_get_steps() {
let config = SagaConfig::default();
let mut saga = SagaOrchestrator::new("saga-006".to_string(), config);
saga.add_step(SagaStep {
name: "step1".to_string(),
..Default::default()
});
let steps = saga.get_steps();
assert_eq!(steps.len(), 1);
assert_eq!(steps[0].name, "step1");
}
#[tokio::test]
async fn test_current_step_index() {
let config = SagaConfig::default();
let saga = SagaOrchestrator::new("saga-007".to_string(), config);
assert_eq!(saga.current_step_index(), 0);
}
}