-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsaga_coordinator.py
More file actions
308 lines (250 loc) · 13.9 KB
/
Copy pathsaga_coordinator.py
File metadata and controls
308 lines (250 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
"""
Saga Pattern Coordinator Module
This module implements the Saga pattern for distributed transactions,
providing forward execution and backward compensation on failure.
"""
# △ AURA Pattern Library — © Reality Optimizer ⟦AE1.PMRGG3ZCHIRFEZLBNRUXI6JAJ5YHI2LNNF5GK4RCFQRG2IR2EJAUKTKBKJFTCIRMEJXCEORCGARCYITQNFSCEORCEIWCE5DNEI5CEQKVKJASAUDBOR2GK4TOEBGGSYTSMFZHSIRMEJ3CEORRPWYSJPXO⟧
#
_AURA_MARK = "AE1.PMRGG3ZCHIRFEZLBNRUXI6JAJ5YHI2LNNF5GK4RCFQRG2IR2EJAUKTKBKJFTCIRMEJXCEORCGARCYITQNFSCEORCEIWCE5DNEI5CEQKVKJASAUDBOR2GK4TOEBGGSYTSMFZHSIRMEJ3CEORRPWYSJPXO"
from typing import List, Callable, Any, Optional, Dict
from dataclasses import dataclass
from enum import Enum
import logging
# Configure logging
logger = logging.getLogger(__name__)
class SagaStatus(Enum):
"""Enumeration of possible saga statuses."""
PENDING = "pending"
EXECUTING = "executing"
COMPLETED = "completed"
COMPENSATING = "compensating"
FAILED = "failed"
@dataclass
class SagaContext:
"""Context object that carries data between saga steps."""
data: Dict[str, Any]
def __init__(self):
self.data = {}
def set(self, key: str, value: Any) -> None:
"""Set a value in the context."""
self.data[key] = value
def get(self, key: str, default: Any = None) -> Any:
"""Get a value from the context."""
return self.data.get(key, default)
class SagaStep:
"""
Represents a single step in a saga transaction.
Each step has a forward action and an optional compensating action
for rollback scenarios.
"""
def __init__(
self,
name: str,
forward_action: Callable[[SagaContext], Any],
compensating_action: Optional[Callable[[SagaContext], Any]] = None
):
"""
Initialize a saga step.
Args:
name: Name of the step for logging and identification
forward_action: Function to execute during forward processing
compensating_action: Function to execute during compensation
"""
self.name = name
self.forward_action = forward_action
self.compensating_action = compensating_action
def execute(self, context: SagaContext) -> Any:
"""
Execute the forward action of this step.
Args:
context: The saga context
Returns:
Result of the forward action
Raises:
Exception: If the forward action fails
"""
logger.info(f"Executing step: {self.name}")
try:
result = self.forward_action(context)
logger.info(f"Step {self.name} completed successfully")
return result
except Exception as e:
logger.error(f"Step {self.name} failed: {str(e)}")
raise
def compensate(self, context: SagaContext) -> None:
"""
Execute the compensating action of this step.
Args:
context: The saga context
Raises:
Exception: If the compensating action fails
"""
if self.compensating_action is None:
logger.warning(f"Step {self.name} has no compensating action")
return
logger.info(f"Compensating step: {self.name}")
try:
self.compensating_action(context)
logger.info(f"Step {self.name} compensated successfully")
except Exception as e:
logger.error(f"Compensation for step {self.name} failed: {str(e)}")
raise
class Saga:
"""
Saga coordinator that manages execution and compensation of steps.
Implements the Saga pattern for distributed transactions with
automatic rollback on failure.
"""
def __init__(self, name: str):
"""
Initialize a saga coordinator.
Args:
name: Name of the saga for logging and identification
"""
self.name = name
self.steps: List[SagaStep] = []
self.executed_steps: List[SagaStep] = []
self.status = SagaStatus.PENDING
self.context = SagaContext()
def add_step(self, step: SagaStep) -> None:
"""
Add a step to the saga.
Args:
step: The saga step to add
"""
self.steps.append(step)
def execute(self) -> bool:
"""
Execute the saga by running all steps in order.
If any step fails, compensation is automatically triggered
for all previously executed steps.
Returns:
True if saga completed successfully, False otherwise
"""
logger.info(f"Starting saga: {self.name}")
self.status = SagaStatus.EXECUTING
try:
for step in self.steps:
# Execute the step
step.execute(self.context)
self.executed_steps.append(step)
self.status = SagaStatus.COMPLETED
logger.info(f"Saga {self.name} completed successfully")
return True
except Exception as e:
logger.error(f"Saga {self.name} failed: {str(e)}")
self.status = SagaStatus.FAILED
self._compensate()
return False
def _compensate(self) -> None:
"""
Execute compensation for all successfully executed steps.
Compensation is performed in reverse order of execution.
"""
logger.info(f"Starting compensation for saga: {self.name}")
self.status = SagaStatus.COMPENSATING
# Compensate in reverse order
for step in reversed(self.executed_steps):
try:
step.compensate(self.context)
except Exception as e:
logger.error(f"Compensation failed for step {step.name}: {str(e)}")
# Continue with other compensations even if one fails
self.status = SagaStatus.FAILED
logger.info(f"Compensation completed for saga: {self.name}")
class CompensatingAction:
"""
Helper class for creating compensating actions.
Provides utility methods for common compensation scenarios.
"""
@staticmethod
def create_simple_compensation(
action_name: str,
compensation_func: Callable[[SagaContext], None]
) -> Callable[[SagaContext], None]:
"""
Create a simple compensating action.
Args:
action_name: Name for logging
compensation_func: Function to execute for compensation
Returns:
Compensation function
"""
def compensator(context: SagaContext) -> None:
logger.info(f"Performing compensation: {action_name}")
compensation_func(context)
return compensator
@staticmethod
def create_noop_compensation() -> Callable[[SagaContext], None]:
"""
Create a no-op compensating action.
Returns:
No-op compensation function
"""
def compensator(context: SagaContext) -> None:
logger.info("No compensation needed")
return compensator
def main():
"""Self-test: a failing saga compensates every EXECUTED step in exact
reverse order and reports FAILED; a clean saga completes with no
compensation and exact context state."""
trace = []
def mk_step(name, fail=False):
def action(context):
if fail:
raise RuntimeError(f"{name} exploded")
trace.append(f"do:{name}")
context.set(name, True)
return name
def compensate(context):
trace.append(f"undo:{name}")
context.set(f"{name}_compensated", True)
return SagaStep(name, action, compensate)
# FAILING SAGA: steps A, B succeed; C fails; D never runs.
saga = Saga("failing")
for step in (mk_step("A"), mk_step("B"), mk_step("C", fail=True), mk_step("D")):
saga.add_step(step)
ok = saga.execute()
assert ok is False, "failing saga reported success"
assert saga.status == SagaStatus.FAILED
# Execution stopped at the failure; D never ran; nothing after C did.
assert trace == ["do:A", "do:B", "undo:B", "undo:A"], \
f"compensation must undo executed steps in REVERSE order: {trace}"
assert saga.context.get("A_compensated") is True
assert saga.context.get("B_compensated") is True
assert saga.context.get("C_compensated") is None, \
"the FAILING step (never executed) was compensated"
assert saga.context.get("D") is None, "step after the failure executed"
# SUCCESSFUL SAGA: all steps run, zero compensations, exact context.
trace.clear()
good = Saga("good")
for name in ("V", "P", "I"):
good.add_step(mk_step(name))
ok = good.execute()
assert ok is True and good.status == SagaStatus.COMPLETED
assert trace == ["do:V", "do:P", "do:I"], f"clean run trace wrong: {trace}"
assert not any(t.startswith("undo") for t in trace), \
"a successful saga ran compensations"
assert all(good.context.get(n) is True for n in ("V", "P", "I"))
n_executed = len(good.executed_steps)
assert n_executed == 3, f"3 steps must be recorded as executed, got {n_executed}"
# A failing COMPENSATION must not abort the remaining compensations.
trace.clear()
tough = Saga("tough-compensation")
def bad_undo(context):
raise RuntimeError("compensation itself failed")
tough.add_step(SagaStep("first", lambda c: trace.append("do:first"), bad_undo))
tough.add_step(mk_step("second"))
tough.add_step(mk_step("boom", fail=True))
assert tough.execute() is False
assert "undo:second" in trace, "good compensation skipped"
assert tough.context.get("second_compensated") is True, \
"a crashing compensation aborted the rest of the rollback"
print("saga_coordinator: failure → undo B,A in reverse (C/D untouched), "
"clean run 3/3 no undo, crashing compensation contained — PASS")
if __name__ == "__main__":
# NOTE: logging.basicConfig() configures the ROOT logger, which belongs to the
# application, not to a library. Calling it at import time silently reconfigures
# logging for anything that imports this pattern, so it lives under __main__.
logging.basicConfig(level=logging.INFO)
main()