tatara_engine/domain/
saga_executor.rs1use anyhow::Result;
8use async_trait::async_trait;
9use tracing::{debug, error, info};
10
11use tatara_core::domain::saga::SagaResult;
12
13#[async_trait]
15pub trait SagaStep: Send + Sync {
16 fn name(&self) -> &str;
18
19 async fn execute(&self) -> Result<serde_json::Value>;
21
22 async fn compensate(&self, output: &serde_json::Value) -> Result<()>;
24}
25
26pub struct SagaExecutor {
28 steps: Vec<Box<dyn SagaStep>>,
29}
30
31impl SagaExecutor {
32 pub fn new(steps: Vec<Box<dyn SagaStep>>) -> Self {
33 Self { steps }
34 }
35
36 pub async fn run(&self) -> SagaResult {
38 let mut completed: Vec<(usize, serde_json::Value)> = Vec::new();
39
40 for (i, step) in self.steps.iter().enumerate() {
41 debug!(step = step.name(), index = i, "saga: executing step");
42
43 match step.execute().await {
44 Ok(output) => {
45 info!(step = step.name(), "saga: step completed");
46 completed.push((i, output));
47 }
48 Err(e) => {
49 error!(
50 step = step.name(),
51 error = %e,
52 "saga: step failed — compensating"
53 );
54
55 let mut compensation_errors = Vec::new();
57 for (j, output) in completed.iter().rev() {
58 let comp_step = &self.steps[*j];
59 debug!(step = comp_step.name(), "saga: compensating");
60 if let Err(comp_err) = comp_step.compensate(output).await {
61 error!(
62 step = comp_step.name(),
63 error = %comp_err,
64 "saga: compensation failed"
65 );
66 compensation_errors.push(format!("{}: {}", comp_step.name(), comp_err));
67 }
68 }
69
70 return SagaResult::Compensated {
71 failed_step: step.name().to_string(),
72 error: e.to_string(),
73 steps_completed: completed.len(),
74 compensations_run: completed.len() - compensation_errors.len(),
75 compensation_errors,
76 };
77 }
78 }
79 }
80
81 SagaResult::Completed {
82 steps_run: self.steps.len(),
83 }
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use std::sync::{Arc, Mutex};
91
92 struct SuccessStep {
93 name: String,
94 log: Arc<Mutex<Vec<String>>>,
95 }
96
97 #[async_trait]
98 impl SagaStep for SuccessStep {
99 fn name(&self) -> &str {
100 &self.name
101 }
102 async fn execute(&self) -> Result<serde_json::Value> {
103 self.log.lock().unwrap().push(format!("exec:{}", self.name));
104 Ok(serde_json::json!({"step": self.name}))
105 }
106 async fn compensate(&self, _output: &serde_json::Value) -> Result<()> {
107 self.log.lock().unwrap().push(format!("comp:{}", self.name));
108 Ok(())
109 }
110 }
111
112 struct FailStep {
113 name: String,
114 log: Arc<Mutex<Vec<String>>>,
115 }
116
117 #[async_trait]
118 impl SagaStep for FailStep {
119 fn name(&self) -> &str {
120 &self.name
121 }
122 async fn execute(&self) -> Result<serde_json::Value> {
123 self.log.lock().unwrap().push(format!("exec:{}", self.name));
124 Err(anyhow::anyhow!("step failed"))
125 }
126 async fn compensate(&self, _output: &serde_json::Value) -> Result<()> {
127 self.log.lock().unwrap().push(format!("comp:{}", self.name));
128 Ok(())
129 }
130 }
131
132 #[tokio::test]
133 async fn test_all_success() {
134 let log = Arc::new(Mutex::new(Vec::new()));
135 let executor = SagaExecutor::new(vec![
136 Box::new(SuccessStep {
137 name: "a".into(),
138 log: log.clone(),
139 }),
140 Box::new(SuccessStep {
141 name: "b".into(),
142 log: log.clone(),
143 }),
144 Box::new(SuccessStep {
145 name: "c".into(),
146 log: log.clone(),
147 }),
148 ]);
149
150 let result = executor.run().await;
151 assert!(matches!(result, SagaResult::Completed { steps_run: 3 }));
152 assert_eq!(*log.lock().unwrap(), vec!["exec:a", "exec:b", "exec:c"]);
153 }
154
155 #[tokio::test]
156 async fn test_fail_at_step_2_compensates_step_1() {
157 let log = Arc::new(Mutex::new(Vec::new()));
158 let executor = SagaExecutor::new(vec![
159 Box::new(SuccessStep {
160 name: "a".into(),
161 log: log.clone(),
162 }),
163 Box::new(FailStep {
164 name: "b".into(),
165 log: log.clone(),
166 }),
167 Box::new(SuccessStep {
168 name: "c".into(),
169 log: log.clone(),
170 }),
171 ]);
172
173 let result = executor.run().await;
174 assert!(matches!(result, SagaResult::Compensated { .. }));
175 let events = log.lock().unwrap().clone();
178 assert!(events.contains(&"exec:a".to_string()));
179 assert!(events.contains(&"exec:b".to_string()));
180 assert!(!events.contains(&"exec:c".to_string()));
181 assert!(events.contains(&"comp:a".to_string()));
182 }
183
184 #[tokio::test]
185 async fn test_empty_saga() {
186 let executor = SagaExecutor::new(vec![]);
187 let result = executor.run().await;
188 assert!(matches!(result, SagaResult::Completed { steps_run: 0 }));
189 }
190
191 #[tokio::test]
192 async fn test_single_step_success() {
193 let log = Arc::new(Mutex::new(Vec::new()));
194 let executor = SagaExecutor::new(vec![Box::new(SuccessStep {
195 name: "only".into(),
196 log: log.clone(),
197 })]);
198
199 let result = executor.run().await;
200 assert!(matches!(result, SagaResult::Completed { steps_run: 1 }));
201 }
202
203 #[tokio::test]
204 async fn test_first_step_fails_no_compensation() {
205 let log = Arc::new(Mutex::new(Vec::new()));
206 let executor = SagaExecutor::new(vec![
207 Box::new(FailStep {
208 name: "first".into(),
209 log: log.clone(),
210 }),
211 Box::new(SuccessStep {
212 name: "second".into(),
213 log: log.clone(),
214 }),
215 ]);
216
217 let result = executor.run().await;
218 assert!(matches!(
219 result,
220 SagaResult::Compensated {
221 steps_completed: 0,
222 ..
223 }
224 ));
225 let events = log.lock().unwrap().clone();
227 assert!(!events.contains(&"comp:first".to_string()));
228 }
229}