use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use async_trait::async_trait;
use dashmap::DashMap;
use crate::database::Session;
use crate::database::sharding::ShardRouter;
#[derive(Debug)]
pub enum SagaError {
ExecutionFailed(String),
CompensationFailed(String),
Timeout(String),
}
impl fmt::Display for SagaError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ExecutionFailed(msg) => write!(f, "Saga execution failed: {msg}"),
Self::CompensationFailed(msg) => write!(f, "Saga compensation failed: {msg}"),
Self::Timeout(msg) => write!(f, "Saga timeout: {msg}"),
}
}
}
impl std::error::Error for SagaError {}
impl crate::i18n::error_ext::LocalizedMsg for SagaError {
fn message_key(&self) -> &'static str {
match self {
Self::ExecutionFailed(_) => "saga-execution-failed",
Self::CompensationFailed(_) => "saga-compensation-failed",
Self::Timeout(_) => "saga-timeout",
}
}
fn message_args(&self) -> Vec<(&str, String)> {
match self {
Self::ExecutionFailed(reason) => vec![("reason", reason.clone())],
Self::CompensationFailed(reason) => vec![("reason", reason.clone())],
Self::Timeout(reason) => vec![("reason", reason.clone())],
}
}
}
#[async_trait]
pub trait SagaAction: Send + Sync {
async fn execute(&self, session: &Session) -> Result<(), SagaError>;
fn name(&self) -> &str;
}
pub struct SagaStep {
pub name: String,
pub shard_id: u32,
pub action: Box<dyn SagaAction>,
pub compensation: Box<dyn SagaAction>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SagaStatus {
Running,
Completed,
Compensating,
Failed,
}
#[derive(Debug, Clone)]
pub struct SagaStepLog {
pub name: String,
pub shard_id: u32,
pub action_success: bool,
pub compensation_success: Option<bool>,
pub error: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SagaLog {
pub saga_id: String,
pub status: SagaStatus,
pub steps: Vec<SagaStepLog>,
}
pub struct InMemorySagaLog {
logs: DashMap<String, SagaLog>,
}
impl InMemorySagaLog {
pub fn new() -> Self {
Self { logs: DashMap::new() }
}
pub fn get(&self, saga_id: &str) -> Option<SagaLog> {
self.logs.get(saga_id).map(|r| r.value().clone())
}
pub fn insert(&self, log: SagaLog) {
self.logs.insert(log.saga_id.clone(), log);
}
pub fn update_status(&self, saga_id: &str, status: SagaStatus) {
if let Some(mut log) = self.logs.get_mut(saga_id) {
log.status = status;
}
}
}
impl Default for InMemorySagaLog {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct SagaExecutionResult {
pub saga_id: String,
pub success: bool,
pub status: SagaStatus,
pub completed_steps: Vec<String>,
pub compensated_steps: Vec<String>,
pub failure: Option<SagaFailure>,
}
#[derive(Debug)]
pub struct SagaFailure {
pub step_name: String,
pub error: String,
}
pub struct SagaOrchestrator {
router: Arc<ShardRouter>,
saga_log: Arc<InMemorySagaLog>,
}
impl SagaOrchestrator {
pub fn new(router: Arc<ShardRouter>) -> Self {
Self {
router,
saga_log: Arc::new(InMemorySagaLog::new()),
}
}
pub async fn execute_saga(&self, steps: Vec<SagaStep>) -> SagaExecutionResult {
let saga_id = uuid::Uuid::new_v4().to_string();
let mut log = SagaLog {
saga_id: saga_id.clone(),
status: SagaStatus::Running,
steps: Vec::new(),
};
self.saga_log.insert(log.clone());
let mut completed_steps: Vec<(String, u32, Box<dyn SagaAction>)> = Vec::new();
let mut completed_names: Vec<String> = Vec::new();
let step_index_map: HashMap<&str, usize> =
steps.iter().enumerate().map(|(i, s)| (s.name.as_str(), i)).collect();
for step in &steps {
let session_result = self.router.get_session(step.shard_id).await;
match session_result {
Ok(Some(session)) => match step.action.execute(&session).await {
Ok(()) => {
log.steps.push(SagaStepLog {
name: step.name.clone(),
shard_id: step.shard_id,
action_success: true,
compensation_success: None,
error: None,
});
completed_names.push(step.name.clone());
}
Err(e) => {
log.steps.push(SagaStepLog {
name: step.name.clone(),
shard_id: step.shard_id,
action_success: false,
compensation_success: None,
error: Some(e.to_string()),
});
let mut compensated: Vec<String> = Vec::new();
self.saga_log.update_status(&saga_id, SagaStatus::Compensating);
for (completed_name, completed_shard_id, _) in completed_steps.iter().rev() {
if let Ok(Some(session)) = self.router.get_session(*completed_shard_id).await {
if let Some(&idx) = step_index_map.get(completed_name.as_str())
&& let Ok(()) = steps[idx].compensation.execute(&session).await
{
compensated.push(completed_name.clone());
}
}
}
self.saga_log.update_status(&saga_id, SagaStatus::Failed);
return SagaExecutionResult {
saga_id,
success: false,
status: SagaStatus::Failed,
completed_steps: completed_names,
compensated_steps: compensated,
failure: Some(SagaFailure {
step_name: step.name.clone(),
error: e.to_string(),
}),
};
}
},
Err(e) => {
self.saga_log.update_status(&saga_id, SagaStatus::Failed);
return SagaExecutionResult {
saga_id,
success: false,
status: SagaStatus::Failed,
completed_steps: completed_names,
compensated_steps: Vec::new(),
failure: Some(SagaFailure {
step_name: step.name.clone(),
error: e.to_string(),
}),
};
}
Ok(None) => {
self.saga_log.update_status(&saga_id, SagaStatus::Failed);
return SagaExecutionResult {
saga_id,
success: false,
status: SagaStatus::Failed,
completed_steps: completed_names,
compensated_steps: Vec::new(),
failure: Some(SagaFailure {
step_name: step.name.clone(),
error: format!("No session available for shard {}", step.shard_id),
}),
};
}
}
completed_steps.push((step.name.clone(), step.shard_id, {
struct NoopAction;
#[async_trait]
impl SagaAction for NoopAction {
async fn execute(&self, _session: &Session) -> Result<(), SagaError> {
Ok(())
}
fn name(&self) -> &str {
"noop"
}
}
Box::new(NoopAction) as Box<dyn SagaAction>
}));
}
self.saga_log.update_status(&saga_id, SagaStatus::Completed);
SagaExecutionResult {
saga_id,
success: true,
status: SagaStatus::Completed,
completed_steps: completed_names,
compensated_steps: Vec::new(),
failure: None,
}
}
pub fn get_saga_log(&self, saga_id: &str) -> Option<SagaLog> {
self.saga_log.get(saga_id)
}
}