kailash 0.6.6__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kailash/__init__.py +35 -5
- kailash/access_control.py +64 -46
- kailash/adapters/__init__.py +5 -0
- kailash/adapters/mcp_platform_adapter.py +273 -0
- kailash/api/workflow_api.py +34 -3
- kailash/channels/__init__.py +21 -0
- kailash/channels/api_channel.py +409 -0
- kailash/channels/base.py +271 -0
- kailash/channels/cli_channel.py +661 -0
- kailash/channels/event_router.py +496 -0
- kailash/channels/mcp_channel.py +648 -0
- kailash/channels/session.py +423 -0
- kailash/mcp_server/discovery.py +57 -18
- kailash/middleware/communication/api_gateway.py +23 -3
- kailash/middleware/communication/realtime.py +83 -0
- kailash/middleware/core/agent_ui.py +1 -1
- kailash/middleware/gateway/storage_backends.py +393 -0
- kailash/middleware/mcp/enhanced_server.py +22 -16
- kailash/nexus/__init__.py +21 -0
- kailash/nexus/cli/__init__.py +5 -0
- kailash/nexus/cli/__main__.py +6 -0
- kailash/nexus/cli/main.py +176 -0
- kailash/nexus/factory.py +413 -0
- kailash/nexus/gateway.py +545 -0
- kailash/nodes/__init__.py +8 -5
- kailash/nodes/ai/iterative_llm_agent.py +988 -17
- kailash/nodes/ai/llm_agent.py +29 -9
- kailash/nodes/api/__init__.py +2 -2
- kailash/nodes/api/monitoring.py +1 -1
- kailash/nodes/base.py +29 -5
- kailash/nodes/base_async.py +54 -14
- kailash/nodes/code/async_python.py +1 -1
- kailash/nodes/code/python.py +50 -6
- kailash/nodes/data/async_sql.py +90 -0
- kailash/nodes/data/bulk_operations.py +939 -0
- kailash/nodes/data/query_builder.py +373 -0
- kailash/nodes/data/query_cache.py +512 -0
- kailash/nodes/monitoring/__init__.py +10 -0
- kailash/nodes/monitoring/deadlock_detector.py +964 -0
- kailash/nodes/monitoring/performance_anomaly.py +1078 -0
- kailash/nodes/monitoring/race_condition_detector.py +1151 -0
- kailash/nodes/monitoring/transaction_metrics.py +790 -0
- kailash/nodes/monitoring/transaction_monitor.py +931 -0
- kailash/nodes/security/behavior_analysis.py +414 -0
- kailash/nodes/system/__init__.py +17 -0
- kailash/nodes/system/command_parser.py +820 -0
- kailash/nodes/transaction/__init__.py +48 -0
- kailash/nodes/transaction/distributed_transaction_manager.py +983 -0
- kailash/nodes/transaction/saga_coordinator.py +652 -0
- kailash/nodes/transaction/saga_state_storage.py +411 -0
- kailash/nodes/transaction/saga_step.py +467 -0
- kailash/nodes/transaction/transaction_context.py +756 -0
- kailash/nodes/transaction/two_phase_commit.py +978 -0
- kailash/nodes/transform/processors.py +17 -1
- kailash/nodes/validation/__init__.py +21 -0
- kailash/nodes/validation/test_executor.py +532 -0
- kailash/nodes/validation/validation_nodes.py +447 -0
- kailash/resources/factory.py +1 -1
- kailash/runtime/access_controlled.py +9 -7
- kailash/runtime/async_local.py +84 -21
- kailash/runtime/local.py +21 -2
- kailash/runtime/parameter_injector.py +187 -31
- kailash/runtime/runner.py +6 -4
- kailash/runtime/testing.py +1 -1
- kailash/security.py +22 -3
- kailash/servers/__init__.py +32 -0
- kailash/servers/durable_workflow_server.py +430 -0
- kailash/servers/enterprise_workflow_server.py +522 -0
- kailash/servers/gateway.py +183 -0
- kailash/servers/workflow_server.py +293 -0
- kailash/utils/data_validation.py +192 -0
- kailash/workflow/builder.py +382 -15
- kailash/workflow/cyclic_runner.py +102 -10
- kailash/workflow/validation.py +144 -8
- kailash/workflow/visualization.py +99 -27
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/METADATA +3 -2
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/RECORD +81 -40
- kailash/workflow/builder_improvements.py +0 -207
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/WHEEL +0 -0
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,467 @@
|
|
1
|
+
"""Saga Step Node for executing individual steps in a distributed transaction.
|
2
|
+
|
3
|
+
Each saga step represents a local transaction that can be compensated if needed.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import json
|
7
|
+
import logging
|
8
|
+
import time
|
9
|
+
import uuid
|
10
|
+
from datetime import UTC, datetime
|
11
|
+
from typing import Any, Callable, Dict, Optional
|
12
|
+
|
13
|
+
from kailash.nodes.base import NodeParameter, register_node
|
14
|
+
from kailash.nodes.base_async import AsyncNode
|
15
|
+
from kailash.sdk_exceptions import NodeExecutionError
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
@register_node()
|
21
|
+
class SagaStepNode(AsyncNode):
|
22
|
+
"""Executes individual steps within a Saga transaction.
|
23
|
+
|
24
|
+
Each SagaStepNode represents a single, compensatable unit of work within
|
25
|
+
a distributed transaction. It encapsulates both the forward action and
|
26
|
+
its compensating action.
|
27
|
+
|
28
|
+
Features:
|
29
|
+
- Idempotent execution
|
30
|
+
- Built-in compensation logic
|
31
|
+
- State tracking
|
32
|
+
- Retry support
|
33
|
+
- Monitoring integration
|
34
|
+
|
35
|
+
Examples:
|
36
|
+
>>> # Execute a saga step
|
37
|
+
>>> step = SagaStepNode(step_name="process_payment")
|
38
|
+
>>> result = await step.execute(
|
39
|
+
... operation="execute",
|
40
|
+
... execution_id="exec_123",
|
41
|
+
... saga_context={"order_id": "order_456"},
|
42
|
+
... data={"amount": 100.0, "currency": "USD"}
|
43
|
+
... )
|
44
|
+
|
45
|
+
>>> # Compensate if needed
|
46
|
+
>>> result = await step.execute(
|
47
|
+
... operation="compensate",
|
48
|
+
... execution_id="exec_123"
|
49
|
+
... )
|
50
|
+
"""
|
51
|
+
|
52
|
+
def __init__(self, **kwargs):
|
53
|
+
# Set node-specific attributes before calling parent
|
54
|
+
self.step_name = kwargs.pop("step_name", "saga_step")
|
55
|
+
self.idempotent = kwargs.pop("idempotent", True)
|
56
|
+
self.retry_on_failure = kwargs.pop("retry_on_failure", True)
|
57
|
+
self.max_retries = kwargs.pop("max_retries", 3)
|
58
|
+
self.retry_delay = kwargs.pop("retry_delay", 1.0)
|
59
|
+
self.timeout = kwargs.pop("timeout", 300.0) # 5 minutes default
|
60
|
+
self.enable_monitoring = kwargs.pop("enable_monitoring", True)
|
61
|
+
|
62
|
+
# Compensation settings
|
63
|
+
self.compensation_timeout = kwargs.pop(
|
64
|
+
"compensation_timeout", 600.0
|
65
|
+
) # 10 minutes
|
66
|
+
self.compensation_retries = kwargs.pop("compensation_retries", 5)
|
67
|
+
|
68
|
+
# State tracking
|
69
|
+
self.execution_id: Optional[str] = None
|
70
|
+
self.execution_state: Dict[str, Any] = {}
|
71
|
+
self.compensation_state: Dict[str, Any] = {}
|
72
|
+
|
73
|
+
# Custom action handlers (can be overridden by subclasses)
|
74
|
+
self.forward_action: Optional[Callable] = None
|
75
|
+
self.compensation_action: Optional[Callable] = None
|
76
|
+
|
77
|
+
super().__init__(**kwargs)
|
78
|
+
|
79
|
+
def get_parameters(self) -> Dict[str, NodeParameter]:
|
80
|
+
"""Define the parameters for the Saga Step node."""
|
81
|
+
return {
|
82
|
+
"operation": NodeParameter(
|
83
|
+
name="operation",
|
84
|
+
type=str,
|
85
|
+
default="execute",
|
86
|
+
description="Operation to perform",
|
87
|
+
),
|
88
|
+
"execution_id": NodeParameter(
|
89
|
+
name="execution_id",
|
90
|
+
type=str,
|
91
|
+
required=False,
|
92
|
+
description="Unique execution identifier",
|
93
|
+
),
|
94
|
+
"saga_context": NodeParameter(
|
95
|
+
name="saga_context",
|
96
|
+
type=dict,
|
97
|
+
default={},
|
98
|
+
description="Saga context data",
|
99
|
+
),
|
100
|
+
"action_type": NodeParameter(
|
101
|
+
name="action_type",
|
102
|
+
type=str,
|
103
|
+
default="process",
|
104
|
+
description="Type of action to perform",
|
105
|
+
),
|
106
|
+
"data": NodeParameter(
|
107
|
+
name="data",
|
108
|
+
type=dict,
|
109
|
+
default={},
|
110
|
+
description="Data to process",
|
111
|
+
),
|
112
|
+
"required_inputs": NodeParameter(
|
113
|
+
name="required_inputs",
|
114
|
+
type=list,
|
115
|
+
default=[],
|
116
|
+
description="Required inputs for validation",
|
117
|
+
),
|
118
|
+
}
|
119
|
+
|
120
|
+
def execute(self, **runtime_inputs) -> Dict[str, Any]:
|
121
|
+
"""Execute the saga step based on the requested operation."""
|
122
|
+
# For sync compatibility with LocalRuntime, we don't make this async
|
123
|
+
# The AsyncNode base class handles running async_run in a sync context
|
124
|
+
operation = runtime_inputs.get("operation", "execute")
|
125
|
+
|
126
|
+
operations = {
|
127
|
+
"execute": self._execute_forward,
|
128
|
+
"compensate": self._execute_compensation,
|
129
|
+
"get_status": self._get_status,
|
130
|
+
"validate": self._validate_preconditions,
|
131
|
+
}
|
132
|
+
|
133
|
+
if operation not in operations:
|
134
|
+
raise NodeExecutionError(f"Unknown operation: {operation}")
|
135
|
+
|
136
|
+
try:
|
137
|
+
return operations[operation](runtime_inputs)
|
138
|
+
except Exception as e:
|
139
|
+
logger.error(f"Saga step error in {self.step_name}: {e}")
|
140
|
+
return {
|
141
|
+
"status": "error",
|
142
|
+
"step_name": self.step_name,
|
143
|
+
"error": str(e),
|
144
|
+
"operation": operation,
|
145
|
+
}
|
146
|
+
|
147
|
+
def _execute_forward(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
148
|
+
"""Execute the forward action of the saga step."""
|
149
|
+
self.execution_id = inputs.get("execution_id", str(uuid.uuid4()))
|
150
|
+
saga_context = inputs.get("saga_context", {})
|
151
|
+
|
152
|
+
# Check idempotency
|
153
|
+
if self.idempotent and self._check_already_executed():
|
154
|
+
logger.info(
|
155
|
+
f"Step {self.step_name} already executed for {self.execution_id}"
|
156
|
+
)
|
157
|
+
return self._get_cached_result()
|
158
|
+
|
159
|
+
# Validate preconditions
|
160
|
+
validation_result = self._validate_preconditions(inputs)
|
161
|
+
if validation_result.get("status") != "valid":
|
162
|
+
return validation_result
|
163
|
+
|
164
|
+
# Execute with retries
|
165
|
+
attempt = 0
|
166
|
+
last_error = None
|
167
|
+
|
168
|
+
while attempt < self.max_retries:
|
169
|
+
try:
|
170
|
+
# Log execution start
|
171
|
+
self._log_execution_start()
|
172
|
+
|
173
|
+
# Execute the actual business logic
|
174
|
+
if self.forward_action:
|
175
|
+
result = self.forward_action(inputs, saga_context)
|
176
|
+
else:
|
177
|
+
result = self._default_forward_action(inputs, saga_context)
|
178
|
+
|
179
|
+
# Store result for idempotency
|
180
|
+
self.execution_state = {
|
181
|
+
"execution_id": self.execution_id,
|
182
|
+
"result": result,
|
183
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
184
|
+
"attempts": attempt + 1,
|
185
|
+
}
|
186
|
+
|
187
|
+
self._log_execution_complete(result)
|
188
|
+
|
189
|
+
return {
|
190
|
+
"status": "success",
|
191
|
+
"step_name": self.step_name,
|
192
|
+
"execution_id": self.execution_id,
|
193
|
+
"data": result,
|
194
|
+
"attempts": attempt + 1,
|
195
|
+
}
|
196
|
+
|
197
|
+
except Exception as e:
|
198
|
+
last_error = e
|
199
|
+
attempt += 1
|
200
|
+
logger.warning(
|
201
|
+
f"Step {self.step_name} failed on attempt {attempt}: {e}"
|
202
|
+
)
|
203
|
+
|
204
|
+
if attempt < self.max_retries:
|
205
|
+
time.sleep(self.retry_delay * attempt) # Exponential backoff
|
206
|
+
|
207
|
+
# All retries exhausted
|
208
|
+
self._log_execution_failed(str(last_error))
|
209
|
+
|
210
|
+
return {
|
211
|
+
"status": "failed",
|
212
|
+
"step_name": self.step_name,
|
213
|
+
"execution_id": self.execution_id,
|
214
|
+
"error": str(last_error),
|
215
|
+
"attempts": attempt,
|
216
|
+
}
|
217
|
+
|
218
|
+
def _execute_compensation(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
219
|
+
"""Execute the compensation action of the saga step."""
|
220
|
+
self.execution_id = inputs.get("execution_id", self.execution_id)
|
221
|
+
saga_context = inputs.get("saga_context", {})
|
222
|
+
|
223
|
+
# Check if compensation is needed
|
224
|
+
if not self.execution_state:
|
225
|
+
logger.info(
|
226
|
+
f"No forward execution found for {self.step_name}, skipping compensation"
|
227
|
+
)
|
228
|
+
return {
|
229
|
+
"status": "skipped",
|
230
|
+
"step_name": self.step_name,
|
231
|
+
"message": "No forward execution to compensate",
|
232
|
+
}
|
233
|
+
|
234
|
+
# Check if already compensated
|
235
|
+
if self._check_already_compensated():
|
236
|
+
logger.info(f"Step {self.step_name} already compensated")
|
237
|
+
return {
|
238
|
+
"status": "already_compensated",
|
239
|
+
"step_name": self.step_name,
|
240
|
+
"execution_id": self.execution_id,
|
241
|
+
}
|
242
|
+
|
243
|
+
# Execute compensation with retries
|
244
|
+
attempt = 0
|
245
|
+
last_error = None
|
246
|
+
|
247
|
+
while attempt < self.compensation_retries:
|
248
|
+
try:
|
249
|
+
# Log compensation start
|
250
|
+
self._log_compensation_start()
|
251
|
+
|
252
|
+
# Execute the compensation logic
|
253
|
+
if self.compensation_action:
|
254
|
+
result = self.compensation_action(
|
255
|
+
inputs, saga_context, self.execution_state
|
256
|
+
)
|
257
|
+
else:
|
258
|
+
result = self._default_compensation_action(
|
259
|
+
inputs, saga_context, self.execution_state
|
260
|
+
)
|
261
|
+
|
262
|
+
# Store compensation result
|
263
|
+
self.compensation_state = {
|
264
|
+
"execution_id": self.execution_id,
|
265
|
+
"result": result,
|
266
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
267
|
+
"attempts": attempt + 1,
|
268
|
+
}
|
269
|
+
|
270
|
+
self._log_compensation_complete(result)
|
271
|
+
|
272
|
+
return {
|
273
|
+
"status": "compensated",
|
274
|
+
"step_name": self.step_name,
|
275
|
+
"execution_id": self.execution_id,
|
276
|
+
"compensation_result": result,
|
277
|
+
"attempts": attempt + 1,
|
278
|
+
}
|
279
|
+
|
280
|
+
except Exception as e:
|
281
|
+
last_error = e
|
282
|
+
attempt += 1
|
283
|
+
logger.warning(
|
284
|
+
f"Compensation for {self.step_name} failed on attempt {attempt}: {e}"
|
285
|
+
)
|
286
|
+
|
287
|
+
if attempt < self.compensation_retries:
|
288
|
+
time.sleep(self.retry_delay * attempt)
|
289
|
+
|
290
|
+
# Compensation failed
|
291
|
+
self._log_compensation_failed(str(last_error))
|
292
|
+
|
293
|
+
return {
|
294
|
+
"status": "compensation_failed",
|
295
|
+
"step_name": self.step_name,
|
296
|
+
"execution_id": self.execution_id,
|
297
|
+
"error": str(last_error),
|
298
|
+
"attempts": attempt,
|
299
|
+
}
|
300
|
+
|
301
|
+
def _validate_preconditions(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
302
|
+
"""Validate preconditions before executing the step."""
|
303
|
+
# Override in subclasses for specific validation
|
304
|
+
required_inputs = inputs.get("required_inputs", [])
|
305
|
+
saga_context = inputs.get("saga_context", {})
|
306
|
+
|
307
|
+
missing_inputs = []
|
308
|
+
for required in required_inputs:
|
309
|
+
if required not in saga_context:
|
310
|
+
missing_inputs.append(required)
|
311
|
+
|
312
|
+
if missing_inputs:
|
313
|
+
return {
|
314
|
+
"status": "invalid",
|
315
|
+
"step_name": self.step_name,
|
316
|
+
"missing_inputs": missing_inputs,
|
317
|
+
"message": f"Missing required inputs: {missing_inputs}",
|
318
|
+
}
|
319
|
+
|
320
|
+
return {
|
321
|
+
"status": "valid",
|
322
|
+
"step_name": self.step_name,
|
323
|
+
"message": "All preconditions satisfied",
|
324
|
+
}
|
325
|
+
|
326
|
+
def _get_status(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
327
|
+
"""Get the current status of the saga step."""
|
328
|
+
return {
|
329
|
+
"status": "success",
|
330
|
+
"step_name": self.step_name,
|
331
|
+
"execution_state": self.execution_state,
|
332
|
+
"compensation_state": self.compensation_state,
|
333
|
+
"idempotent": self.idempotent,
|
334
|
+
"retry_settings": {
|
335
|
+
"max_retries": self.max_retries,
|
336
|
+
"retry_delay": self.retry_delay,
|
337
|
+
"compensation_retries": self.compensation_retries,
|
338
|
+
},
|
339
|
+
}
|
340
|
+
|
341
|
+
def _default_forward_action(
|
342
|
+
self, inputs: Dict[str, Any], saga_context: Dict[str, Any]
|
343
|
+
) -> Dict[str, Any]:
|
344
|
+
"""Default forward action implementation."""
|
345
|
+
# Override in subclasses or provide custom forward_action
|
346
|
+
action_type = inputs.get("action_type", "process")
|
347
|
+
data = inputs.get("data", {})
|
348
|
+
|
349
|
+
# Simulate some processing
|
350
|
+
result = {
|
351
|
+
"action": action_type,
|
352
|
+
"processed_data": data,
|
353
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
354
|
+
"step": self.step_name,
|
355
|
+
}
|
356
|
+
|
357
|
+
return result
|
358
|
+
|
359
|
+
def _default_compensation_action(
|
360
|
+
self,
|
361
|
+
inputs: Dict[str, Any],
|
362
|
+
saga_context: Dict[str, Any],
|
363
|
+
execution_state: Dict[str, Any],
|
364
|
+
) -> Dict[str, Any]:
|
365
|
+
"""Default compensation action implementation."""
|
366
|
+
# Override in subclasses or provide custom compensation_action
|
367
|
+
result = {
|
368
|
+
"action": "compensate",
|
369
|
+
"compensated_execution": execution_state.get("execution_id"),
|
370
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
371
|
+
"step": self.step_name,
|
372
|
+
}
|
373
|
+
|
374
|
+
return result
|
375
|
+
|
376
|
+
def _check_already_executed(self) -> bool:
|
377
|
+
"""Check if this step has already been executed."""
|
378
|
+
return bool(self.execution_state)
|
379
|
+
|
380
|
+
def _check_already_compensated(self) -> bool:
|
381
|
+
"""Check if this step has already been compensated."""
|
382
|
+
return bool(self.compensation_state)
|
383
|
+
|
384
|
+
def _get_cached_result(self) -> Dict[str, Any]:
|
385
|
+
"""Get the cached result from a previous execution."""
|
386
|
+
return {
|
387
|
+
"status": "success",
|
388
|
+
"step_name": self.step_name,
|
389
|
+
"execution_id": self.execution_id,
|
390
|
+
"data": self.execution_state.get("result", {}),
|
391
|
+
"cached": True,
|
392
|
+
"cached_at": self.execution_state.get("timestamp"),
|
393
|
+
}
|
394
|
+
|
395
|
+
def _log_execution_start(self):
|
396
|
+
"""Log the start of step execution."""
|
397
|
+
if self.enable_monitoring:
|
398
|
+
logger.info(
|
399
|
+
f"Starting execution of saga step: {self.step_name}",
|
400
|
+
extra={
|
401
|
+
"step_name": self.step_name,
|
402
|
+
"execution_id": self.execution_id,
|
403
|
+
"event": "saga_step_started",
|
404
|
+
},
|
405
|
+
)
|
406
|
+
|
407
|
+
def _log_execution_complete(self, result: Any):
|
408
|
+
"""Log successful completion of step execution."""
|
409
|
+
if self.enable_monitoring:
|
410
|
+
logger.info(
|
411
|
+
f"Completed execution of saga step: {self.step_name}",
|
412
|
+
extra={
|
413
|
+
"step_name": self.step_name,
|
414
|
+
"execution_id": self.execution_id,
|
415
|
+
"event": "saga_step_completed",
|
416
|
+
},
|
417
|
+
)
|
418
|
+
|
419
|
+
def _log_execution_failed(self, error: str):
|
420
|
+
"""Log failure of step execution."""
|
421
|
+
if self.enable_monitoring:
|
422
|
+
logger.error(
|
423
|
+
f"Failed execution of saga step: {self.step_name}",
|
424
|
+
extra={
|
425
|
+
"step_name": self.step_name,
|
426
|
+
"execution_id": self.execution_id,
|
427
|
+
"error": error,
|
428
|
+
"event": "saga_step_failed",
|
429
|
+
},
|
430
|
+
)
|
431
|
+
|
432
|
+
def _log_compensation_start(self):
|
433
|
+
"""Log the start of compensation."""
|
434
|
+
if self.enable_monitoring:
|
435
|
+
logger.info(
|
436
|
+
f"Starting compensation for saga step: {self.step_name}",
|
437
|
+
extra={
|
438
|
+
"step_name": self.step_name,
|
439
|
+
"execution_id": self.execution_id,
|
440
|
+
"event": "saga_compensation_started",
|
441
|
+
},
|
442
|
+
)
|
443
|
+
|
444
|
+
def _log_compensation_complete(self, result: Any):
|
445
|
+
"""Log successful completion of compensation."""
|
446
|
+
if self.enable_monitoring:
|
447
|
+
logger.info(
|
448
|
+
f"Completed compensation for saga step: {self.step_name}",
|
449
|
+
extra={
|
450
|
+
"step_name": self.step_name,
|
451
|
+
"execution_id": self.execution_id,
|
452
|
+
"event": "saga_compensation_completed",
|
453
|
+
},
|
454
|
+
)
|
455
|
+
|
456
|
+
def _log_compensation_failed(self, error: str):
|
457
|
+
"""Log failure of compensation."""
|
458
|
+
if self.enable_monitoring:
|
459
|
+
logger.error(
|
460
|
+
f"Failed compensation for saga step: {self.step_name}",
|
461
|
+
extra={
|
462
|
+
"step_name": self.step_name,
|
463
|
+
"execution_id": self.execution_id,
|
464
|
+
"error": error,
|
465
|
+
"event": "saga_compensation_failed",
|
466
|
+
},
|
467
|
+
)
|