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,652 @@
|
|
1
|
+
"""Saga Coordinator Node for orchestrating distributed transactions.
|
2
|
+
|
3
|
+
The Saga pattern provides a way to manage distributed transactions by breaking them
|
4
|
+
into a series of local transactions, each with a compensating action for rollback.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import json
|
8
|
+
import logging
|
9
|
+
import time
|
10
|
+
import uuid
|
11
|
+
from datetime import UTC, datetime
|
12
|
+
from enum import Enum
|
13
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
14
|
+
|
15
|
+
from kailash.nodes.base import NodeParameter, register_node
|
16
|
+
from kailash.nodes.base_async import AsyncNode
|
17
|
+
from kailash.sdk_exceptions import NodeExecutionError
|
18
|
+
|
19
|
+
from .saga_state_storage import SagaStateStorage, StorageFactory
|
20
|
+
|
21
|
+
logger = logging.getLogger(__name__)
|
22
|
+
|
23
|
+
|
24
|
+
class SagaState(Enum):
|
25
|
+
"""Saga execution states."""
|
26
|
+
|
27
|
+
PENDING = "pending"
|
28
|
+
RUNNING = "running"
|
29
|
+
COMPENSATING = "compensating"
|
30
|
+
COMPLETED = "completed"
|
31
|
+
FAILED = "failed"
|
32
|
+
COMPENSATED = "compensated"
|
33
|
+
|
34
|
+
|
35
|
+
class SagaStep:
|
36
|
+
"""Represents a single step in a saga."""
|
37
|
+
|
38
|
+
def __init__(
|
39
|
+
self,
|
40
|
+
step_id: str,
|
41
|
+
name: str,
|
42
|
+
node_id: str,
|
43
|
+
parameters: Dict[str, Any],
|
44
|
+
compensation_node_id: Optional[str] = None,
|
45
|
+
compensation_parameters: Optional[Dict[str, Any]] = None,
|
46
|
+
):
|
47
|
+
self.step_id = step_id
|
48
|
+
self.name = name
|
49
|
+
self.node_id = node_id
|
50
|
+
self.parameters = parameters
|
51
|
+
self.compensation_node_id = compensation_node_id
|
52
|
+
self.compensation_parameters = compensation_parameters or {}
|
53
|
+
self.state: str = "pending"
|
54
|
+
self.result: Optional[Any] = None
|
55
|
+
self.error: Optional[str] = None
|
56
|
+
self.start_time: Optional[float] = None
|
57
|
+
self.end_time: Optional[float] = None
|
58
|
+
|
59
|
+
|
60
|
+
@register_node()
|
61
|
+
class SagaCoordinatorNode(AsyncNode):
|
62
|
+
"""Orchestrates distributed transactions using the Saga pattern.
|
63
|
+
|
64
|
+
The Saga Coordinator manages the execution of a series of steps, each representing
|
65
|
+
a local transaction. If any step fails, the coordinator executes compensating
|
66
|
+
actions for all previously completed steps in reverse order.
|
67
|
+
|
68
|
+
Features:
|
69
|
+
- Step-by-step transaction execution
|
70
|
+
- Automatic compensation on failure
|
71
|
+
- State persistence and recovery
|
72
|
+
- Monitoring and observability
|
73
|
+
- Configurable retry policies
|
74
|
+
|
75
|
+
Examples:
|
76
|
+
>>> # Create a saga
|
77
|
+
>>> saga = SagaCoordinatorNode()
|
78
|
+
>>> result = await saga.execute(
|
79
|
+
... operation="create_saga",
|
80
|
+
... saga_name="order_processing",
|
81
|
+
... timeout=600.0
|
82
|
+
... )
|
83
|
+
|
84
|
+
>>> # Add steps
|
85
|
+
>>> result = await saga.execute(
|
86
|
+
... operation="add_step",
|
87
|
+
... name="validate_order",
|
88
|
+
... node_id="ValidationNode",
|
89
|
+
... compensation_node_id="CancelOrderNode"
|
90
|
+
... )
|
91
|
+
|
92
|
+
>>> # Execute saga
|
93
|
+
>>> result = await saga.execute(operation="execute_saga")
|
94
|
+
"""
|
95
|
+
|
96
|
+
def __init__(self, **kwargs):
|
97
|
+
# Set node-specific attributes before calling parent
|
98
|
+
self.saga_id = kwargs.pop("saga_id", None) or str(uuid.uuid4())
|
99
|
+
self.saga_name = kwargs.pop("saga_name", "distributed_transaction")
|
100
|
+
self.timeout = kwargs.pop("timeout", 3600.0) # 1 hour default
|
101
|
+
self.retry_policy = kwargs.pop(
|
102
|
+
"retry_policy", {"max_attempts": 3, "delay": 1.0}
|
103
|
+
)
|
104
|
+
self.enable_monitoring = kwargs.pop("enable_monitoring", True)
|
105
|
+
self.state_storage_type = kwargs.pop(
|
106
|
+
"state_storage", "memory"
|
107
|
+
) # or "redis", "database"
|
108
|
+
|
109
|
+
# Initialize internal state
|
110
|
+
self.steps: List[SagaStep] = []
|
111
|
+
self.state = SagaState.PENDING
|
112
|
+
self.current_step_index = -1
|
113
|
+
self.saga_context: Dict[str, Any] = {}
|
114
|
+
self.start_time: Optional[float] = None
|
115
|
+
self.end_time: Optional[float] = None
|
116
|
+
self.saga_history: List[Dict[str, Any]] = []
|
117
|
+
|
118
|
+
# State persistence
|
119
|
+
storage_config = kwargs.pop("storage_config", {})
|
120
|
+
self._state_storage: SagaStateStorage = StorageFactory.create_storage(
|
121
|
+
self.state_storage_type, **storage_config
|
122
|
+
)
|
123
|
+
|
124
|
+
super().__init__(**kwargs)
|
125
|
+
|
126
|
+
def get_parameters(self) -> Dict[str, NodeParameter]:
|
127
|
+
"""Define the parameters for the Saga Coordinator node."""
|
128
|
+
return {
|
129
|
+
"operation": NodeParameter(
|
130
|
+
name="operation",
|
131
|
+
type=str,
|
132
|
+
default="execute_saga",
|
133
|
+
description="Operation to perform",
|
134
|
+
),
|
135
|
+
"saga_name": NodeParameter(
|
136
|
+
name="saga_name",
|
137
|
+
type=str,
|
138
|
+
required=False,
|
139
|
+
description="Name of the saga",
|
140
|
+
),
|
141
|
+
"saga_id": NodeParameter(
|
142
|
+
name="saga_id",
|
143
|
+
type=str,
|
144
|
+
required=False,
|
145
|
+
description="Unique saga identifier",
|
146
|
+
),
|
147
|
+
"timeout": NodeParameter(
|
148
|
+
name="timeout",
|
149
|
+
type=float,
|
150
|
+
default=3600.0,
|
151
|
+
description="Saga timeout in seconds",
|
152
|
+
),
|
153
|
+
"name": NodeParameter(
|
154
|
+
name="name",
|
155
|
+
type=str,
|
156
|
+
required=False,
|
157
|
+
description="Step name (for add_step)",
|
158
|
+
),
|
159
|
+
"node_id": NodeParameter(
|
160
|
+
name="node_id",
|
161
|
+
type=str,
|
162
|
+
required=False,
|
163
|
+
description="Node ID to execute (for add_step)",
|
164
|
+
),
|
165
|
+
"parameters": NodeParameter(
|
166
|
+
name="parameters",
|
167
|
+
type=dict,
|
168
|
+
default={},
|
169
|
+
description="Parameters for the step",
|
170
|
+
),
|
171
|
+
"compensation_node_id": NodeParameter(
|
172
|
+
name="compensation_node_id",
|
173
|
+
type=str,
|
174
|
+
required=False,
|
175
|
+
description="Node ID for compensation",
|
176
|
+
),
|
177
|
+
"compensation_parameters": NodeParameter(
|
178
|
+
name="compensation_parameters",
|
179
|
+
type=dict,
|
180
|
+
default={},
|
181
|
+
description="Parameters for compensation",
|
182
|
+
),
|
183
|
+
"context": NodeParameter(
|
184
|
+
name="context",
|
185
|
+
type=dict,
|
186
|
+
default={},
|
187
|
+
description="Saga context data",
|
188
|
+
),
|
189
|
+
}
|
190
|
+
|
191
|
+
async def async_run(self, **runtime_inputs) -> Dict[str, Any]:
|
192
|
+
"""Execute the Saga coordinator based on the requested operation."""
|
193
|
+
operation = runtime_inputs.get("operation", "execute_saga")
|
194
|
+
|
195
|
+
operations = {
|
196
|
+
"create_saga": self._create_saga,
|
197
|
+
"add_step": self._add_step,
|
198
|
+
"execute_saga": self._execute_saga,
|
199
|
+
"get_status": self._get_status,
|
200
|
+
"compensate": self._compensate,
|
201
|
+
"resume": self._resume_saga,
|
202
|
+
"cancel": self._cancel_saga,
|
203
|
+
"get_history": self._get_history,
|
204
|
+
"load_saga": self._load_saga,
|
205
|
+
"list_sagas": self._list_sagas,
|
206
|
+
}
|
207
|
+
|
208
|
+
if operation not in operations:
|
209
|
+
raise NodeExecutionError(f"Unknown operation: {operation}")
|
210
|
+
|
211
|
+
try:
|
212
|
+
return await operations[operation](runtime_inputs)
|
213
|
+
except Exception as e:
|
214
|
+
logger.error(f"Saga coordinator error: {e}")
|
215
|
+
return {
|
216
|
+
"status": "error",
|
217
|
+
"saga_id": self.saga_id,
|
218
|
+
"error": str(e),
|
219
|
+
"operation": operation,
|
220
|
+
}
|
221
|
+
|
222
|
+
async def _create_saga(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
223
|
+
"""Create a new saga instance."""
|
224
|
+
self.saga_id = inputs.get("saga_id", str(uuid.uuid4()))
|
225
|
+
self.saga_name = inputs.get("saga_name", self.saga_name)
|
226
|
+
self.timeout = inputs.get("timeout", self.timeout)
|
227
|
+
|
228
|
+
# Initialize saga
|
229
|
+
self.state = SagaState.PENDING
|
230
|
+
self.steps = []
|
231
|
+
self.saga_context = inputs.get("context", {})
|
232
|
+
|
233
|
+
# Persist initial state
|
234
|
+
await self._persist_state()
|
235
|
+
|
236
|
+
return {
|
237
|
+
"status": "success",
|
238
|
+
"saga_id": self.saga_id,
|
239
|
+
"saga_name": self.saga_name,
|
240
|
+
"state": self.state.value,
|
241
|
+
"message": "Saga created successfully",
|
242
|
+
}
|
243
|
+
|
244
|
+
async def _add_step(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
245
|
+
"""Add a step to the saga."""
|
246
|
+
if self.state != SagaState.PENDING:
|
247
|
+
raise NodeExecutionError(
|
248
|
+
f"Cannot add steps to saga in state: {self.state.value}"
|
249
|
+
)
|
250
|
+
|
251
|
+
step = SagaStep(
|
252
|
+
step_id=inputs.get("step_id", str(uuid.uuid4())),
|
253
|
+
name=inputs.get("name", f"step_{len(self.steps) + 1}"),
|
254
|
+
node_id=inputs.get("node_id"),
|
255
|
+
parameters=inputs.get("parameters", {}),
|
256
|
+
compensation_node_id=inputs.get("compensation_node_id"),
|
257
|
+
compensation_parameters=inputs.get("compensation_parameters", {}),
|
258
|
+
)
|
259
|
+
|
260
|
+
if not step.node_id:
|
261
|
+
raise NodeExecutionError("node_id is required for saga step")
|
262
|
+
|
263
|
+
self.steps.append(step)
|
264
|
+
await self._persist_state()
|
265
|
+
|
266
|
+
return {
|
267
|
+
"status": "success",
|
268
|
+
"saga_id": self.saga_id,
|
269
|
+
"step_id": step.step_id,
|
270
|
+
"step_index": len(self.steps) - 1,
|
271
|
+
"total_steps": len(self.steps),
|
272
|
+
}
|
273
|
+
|
274
|
+
async def _execute_saga(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
275
|
+
"""Execute all steps in the saga."""
|
276
|
+
if self.state not in [SagaState.PENDING, SagaState.RUNNING]:
|
277
|
+
raise NodeExecutionError(
|
278
|
+
f"Cannot execute saga in state: {self.state.value}"
|
279
|
+
)
|
280
|
+
|
281
|
+
if not self.steps:
|
282
|
+
raise NodeExecutionError("No steps defined in saga")
|
283
|
+
|
284
|
+
self.state = SagaState.RUNNING
|
285
|
+
self.start_time = time.time()
|
286
|
+
await self._persist_state()
|
287
|
+
|
288
|
+
# Execute steps sequentially
|
289
|
+
for i, step in enumerate(self.steps):
|
290
|
+
self.current_step_index = i
|
291
|
+
|
292
|
+
try:
|
293
|
+
# Execute step
|
294
|
+
result = await self._execute_step(step, inputs)
|
295
|
+
|
296
|
+
if result.get("status") != "success":
|
297
|
+
# Step failed, start compensation
|
298
|
+
self.state = SagaState.COMPENSATING
|
299
|
+
await self._persist_state()
|
300
|
+
compensation_result = await self._compensate(inputs)
|
301
|
+
|
302
|
+
return {
|
303
|
+
"status": "failed",
|
304
|
+
"saga_id": self.saga_id,
|
305
|
+
"failed_step": step.name,
|
306
|
+
"error": result.get("error", "Step execution failed"),
|
307
|
+
"compensation": compensation_result,
|
308
|
+
}
|
309
|
+
|
310
|
+
# Update saga context with step results
|
311
|
+
if "output_key" in step.parameters:
|
312
|
+
self.saga_context[step.parameters["output_key"]] = result.get(
|
313
|
+
"data"
|
314
|
+
)
|
315
|
+
|
316
|
+
except Exception as e:
|
317
|
+
logger.error(f"Error executing step {step.name}: {e}")
|
318
|
+
self.state = SagaState.COMPENSATING
|
319
|
+
await self._persist_state()
|
320
|
+
|
321
|
+
compensation_result = await self._compensate(inputs)
|
322
|
+
|
323
|
+
return {
|
324
|
+
"status": "failed",
|
325
|
+
"saga_id": self.saga_id,
|
326
|
+
"failed_step": step.name,
|
327
|
+
"error": str(e),
|
328
|
+
"compensation": compensation_result,
|
329
|
+
}
|
330
|
+
|
331
|
+
# All steps completed successfully
|
332
|
+
self.state = SagaState.COMPLETED
|
333
|
+
self.end_time = time.time()
|
334
|
+
await self._persist_state()
|
335
|
+
|
336
|
+
return {
|
337
|
+
"status": "success",
|
338
|
+
"saga_id": self.saga_id,
|
339
|
+
"saga_name": self.saga_name,
|
340
|
+
"state": self.state.value,
|
341
|
+
"steps_completed": len(self.steps),
|
342
|
+
"duration": self.end_time - self.start_time,
|
343
|
+
"context": self.saga_context,
|
344
|
+
}
|
345
|
+
|
346
|
+
async def _execute_step(
|
347
|
+
self, step: SagaStep, inputs: Dict[str, Any]
|
348
|
+
) -> Dict[str, Any]:
|
349
|
+
"""Execute a single saga step."""
|
350
|
+
step.state = "running"
|
351
|
+
step.start_time = time.time()
|
352
|
+
|
353
|
+
# Log step execution
|
354
|
+
self._log_event(
|
355
|
+
"step_started",
|
356
|
+
{
|
357
|
+
"step_id": step.step_id,
|
358
|
+
"step_name": step.name,
|
359
|
+
"node_id": step.node_id,
|
360
|
+
},
|
361
|
+
)
|
362
|
+
|
363
|
+
try:
|
364
|
+
# Simulate step execution (in real implementation, would call actual node)
|
365
|
+
# For now, return success
|
366
|
+
result = {
|
367
|
+
"status": "success",
|
368
|
+
"data": {"step_result": f"Result of {step.name}"},
|
369
|
+
}
|
370
|
+
|
371
|
+
step.state = "completed"
|
372
|
+
step.result = result
|
373
|
+
step.end_time = time.time()
|
374
|
+
|
375
|
+
self._log_event(
|
376
|
+
"step_completed",
|
377
|
+
{
|
378
|
+
"step_id": step.step_id,
|
379
|
+
"duration": step.end_time - step.start_time,
|
380
|
+
},
|
381
|
+
)
|
382
|
+
|
383
|
+
return result
|
384
|
+
|
385
|
+
except Exception as e:
|
386
|
+
step.state = "failed"
|
387
|
+
step.error = str(e)
|
388
|
+
step.end_time = time.time()
|
389
|
+
|
390
|
+
self._log_event(
|
391
|
+
"step_failed",
|
392
|
+
{
|
393
|
+
"step_id": step.step_id,
|
394
|
+
"error": str(e),
|
395
|
+
},
|
396
|
+
)
|
397
|
+
|
398
|
+
raise
|
399
|
+
|
400
|
+
async def _compensate(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
401
|
+
"""Execute compensation for all completed steps in reverse order."""
|
402
|
+
if self.state not in [SagaState.COMPENSATING, SagaState.FAILED]:
|
403
|
+
self.state = SagaState.COMPENSATING
|
404
|
+
await self._persist_state()
|
405
|
+
|
406
|
+
compensated_steps = []
|
407
|
+
compensation_errors = []
|
408
|
+
|
409
|
+
# Compensate in reverse order
|
410
|
+
for i in range(self.current_step_index, -1, -1):
|
411
|
+
step = self.steps[i]
|
412
|
+
|
413
|
+
if step.state != "completed":
|
414
|
+
continue
|
415
|
+
|
416
|
+
if not step.compensation_node_id:
|
417
|
+
logger.warning(f"No compensation defined for step {step.name}")
|
418
|
+
continue
|
419
|
+
|
420
|
+
try:
|
421
|
+
# Execute compensation
|
422
|
+
self._log_event(
|
423
|
+
"compensation_started",
|
424
|
+
{
|
425
|
+
"step_id": step.step_id,
|
426
|
+
"step_name": step.name,
|
427
|
+
},
|
428
|
+
)
|
429
|
+
|
430
|
+
# Simulate compensation (in real implementation, would call actual node)
|
431
|
+
step.state = "compensated"
|
432
|
+
compensated_steps.append(step.name)
|
433
|
+
|
434
|
+
self._log_event(
|
435
|
+
"compensation_completed",
|
436
|
+
{
|
437
|
+
"step_id": step.step_id,
|
438
|
+
},
|
439
|
+
)
|
440
|
+
|
441
|
+
except Exception as e:
|
442
|
+
logger.error(f"Compensation failed for step {step.name}: {e}")
|
443
|
+
compensation_errors.append(
|
444
|
+
{
|
445
|
+
"step": step.name,
|
446
|
+
"error": str(e),
|
447
|
+
}
|
448
|
+
)
|
449
|
+
|
450
|
+
# Update saga state
|
451
|
+
self.state = (
|
452
|
+
SagaState.COMPENSATED if not compensation_errors else SagaState.FAILED
|
453
|
+
)
|
454
|
+
self.end_time = time.time()
|
455
|
+
await self._persist_state()
|
456
|
+
|
457
|
+
return {
|
458
|
+
"status": (
|
459
|
+
"compensated" if not compensation_errors else "partial_compensation"
|
460
|
+
),
|
461
|
+
"saga_id": self.saga_id,
|
462
|
+
"compensated_steps": compensated_steps,
|
463
|
+
"compensation_errors": compensation_errors,
|
464
|
+
"duration": self.end_time - self.start_time if self.start_time else 0,
|
465
|
+
}
|
466
|
+
|
467
|
+
async def _resume_saga(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
468
|
+
"""Resume a saga from where it left off."""
|
469
|
+
if self.state not in [SagaState.RUNNING, SagaState.FAILED]:
|
470
|
+
raise NodeExecutionError(f"Cannot resume saga in state: {self.state.value}")
|
471
|
+
|
472
|
+
# Find the next pending step
|
473
|
+
next_step_index = -1
|
474
|
+
for i, step in enumerate(self.steps):
|
475
|
+
if step.state == "pending":
|
476
|
+
next_step_index = i
|
477
|
+
break
|
478
|
+
|
479
|
+
if next_step_index == -1:
|
480
|
+
return {
|
481
|
+
"status": "no_pending_steps",
|
482
|
+
"saga_id": self.saga_id,
|
483
|
+
"message": "No pending steps to resume",
|
484
|
+
}
|
485
|
+
|
486
|
+
# Resume from the next pending step
|
487
|
+
self.current_step_index = next_step_index - 1
|
488
|
+
return await self._execute_saga(inputs)
|
489
|
+
|
490
|
+
async def _cancel_saga(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
491
|
+
"""Cancel the saga and trigger compensation."""
|
492
|
+
if self.state == SagaState.COMPLETED:
|
493
|
+
raise NodeExecutionError("Cannot cancel completed saga")
|
494
|
+
|
495
|
+
self.state = SagaState.COMPENSATING
|
496
|
+
await self._persist_state()
|
497
|
+
|
498
|
+
return await self._compensate(inputs)
|
499
|
+
|
500
|
+
async def _get_status(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
501
|
+
"""Get the current status of the saga."""
|
502
|
+
steps_status = []
|
503
|
+
for step in self.steps:
|
504
|
+
steps_status.append(
|
505
|
+
{
|
506
|
+
"step_id": step.step_id,
|
507
|
+
"name": step.name,
|
508
|
+
"state": step.state,
|
509
|
+
"error": step.error,
|
510
|
+
"duration": (
|
511
|
+
(step.end_time - step.start_time)
|
512
|
+
if step.start_time and step.end_time
|
513
|
+
else None
|
514
|
+
),
|
515
|
+
}
|
516
|
+
)
|
517
|
+
|
518
|
+
return {
|
519
|
+
"status": "success",
|
520
|
+
"saga_id": self.saga_id,
|
521
|
+
"saga_name": self.saga_name,
|
522
|
+
"state": self.state.value,
|
523
|
+
"current_step_index": self.current_step_index,
|
524
|
+
"total_steps": len(self.steps),
|
525
|
+
"steps": steps_status,
|
526
|
+
"start_time": self.start_time,
|
527
|
+
"end_time": self.end_time,
|
528
|
+
"duration": (
|
529
|
+
(self.end_time - self.start_time)
|
530
|
+
if self.start_time and self.end_time
|
531
|
+
else None
|
532
|
+
),
|
533
|
+
"context": self.saga_context,
|
534
|
+
}
|
535
|
+
|
536
|
+
async def _get_history(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
537
|
+
"""Get the execution history of the saga."""
|
538
|
+
return {
|
539
|
+
"status": "success",
|
540
|
+
"saga_id": self.saga_id,
|
541
|
+
"history": self.saga_history,
|
542
|
+
"total_events": len(self.saga_history),
|
543
|
+
}
|
544
|
+
|
545
|
+
async def _persist_state(self):
|
546
|
+
"""Persist saga state for recovery."""
|
547
|
+
state_data = {
|
548
|
+
"saga_id": self.saga_id,
|
549
|
+
"saga_name": self.saga_name,
|
550
|
+
"state": self.state.value,
|
551
|
+
"current_step_index": self.current_step_index,
|
552
|
+
"steps": [
|
553
|
+
{
|
554
|
+
"step_id": step.step_id,
|
555
|
+
"name": step.name,
|
556
|
+
"node_id": step.node_id,
|
557
|
+
"parameters": step.parameters,
|
558
|
+
"compensation_node_id": step.compensation_node_id,
|
559
|
+
"compensation_parameters": step.compensation_parameters,
|
560
|
+
"state": step.state,
|
561
|
+
"result": step.result,
|
562
|
+
"error": step.error,
|
563
|
+
"start_time": step.start_time,
|
564
|
+
"end_time": step.end_time,
|
565
|
+
}
|
566
|
+
for step in self.steps
|
567
|
+
],
|
568
|
+
"context": self.saga_context,
|
569
|
+
"start_time": self.start_time,
|
570
|
+
"end_time": self.end_time,
|
571
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
572
|
+
"saga_history": self.saga_history,
|
573
|
+
}
|
574
|
+
|
575
|
+
success = await self._state_storage.save_state(self.saga_id, state_data)
|
576
|
+
if not success:
|
577
|
+
logger.error(f"Failed to persist state for saga {self.saga_id}")
|
578
|
+
|
579
|
+
def _log_event(self, event_type: str, data: Dict[str, Any]):
|
580
|
+
"""Log an event to saga history."""
|
581
|
+
event = {
|
582
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
583
|
+
"event_type": event_type,
|
584
|
+
"saga_id": self.saga_id,
|
585
|
+
"data": data,
|
586
|
+
}
|
587
|
+
|
588
|
+
self.saga_history.append(event)
|
589
|
+
|
590
|
+
if self.enable_monitoring:
|
591
|
+
logger.info(f"Saga event: {event_type}", extra=event)
|
592
|
+
|
593
|
+
async def _load_saga(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
594
|
+
"""Load saga state from persistence."""
|
595
|
+
saga_id = inputs.get("saga_id", self.saga_id)
|
596
|
+
|
597
|
+
state_data = await self._state_storage.load_state(saga_id)
|
598
|
+
if not state_data:
|
599
|
+
return {
|
600
|
+
"status": "not_found",
|
601
|
+
"saga_id": saga_id,
|
602
|
+
"message": f"Saga {saga_id} not found",
|
603
|
+
}
|
604
|
+
|
605
|
+
# Restore saga state
|
606
|
+
self.saga_id = state_data["saga_id"]
|
607
|
+
self.saga_name = state_data["saga_name"]
|
608
|
+
self.state = SagaState(state_data["state"])
|
609
|
+
self.current_step_index = state_data["current_step_index"]
|
610
|
+
self.saga_context = state_data["context"]
|
611
|
+
self.start_time = state_data.get("start_time")
|
612
|
+
self.end_time = state_data.get("end_time")
|
613
|
+
self.saga_history = state_data.get("saga_history", [])
|
614
|
+
|
615
|
+
# Restore steps
|
616
|
+
self.steps = []
|
617
|
+
for step_data in state_data["steps"]:
|
618
|
+
step = SagaStep(
|
619
|
+
step_id=step_data["step_id"],
|
620
|
+
name=step_data["name"],
|
621
|
+
node_id=step_data["node_id"],
|
622
|
+
parameters=step_data["parameters"],
|
623
|
+
compensation_node_id=step_data.get("compensation_node_id"),
|
624
|
+
compensation_parameters=step_data.get("compensation_parameters", {}),
|
625
|
+
)
|
626
|
+
step.state = step_data["state"]
|
627
|
+
step.result = step_data.get("result")
|
628
|
+
step.error = step_data.get("error")
|
629
|
+
step.start_time = step_data.get("start_time")
|
630
|
+
step.end_time = step_data.get("end_time")
|
631
|
+
self.steps.append(step)
|
632
|
+
|
633
|
+
return {
|
634
|
+
"status": "success",
|
635
|
+
"saga_id": self.saga_id,
|
636
|
+
"saga_name": self.saga_name,
|
637
|
+
"state": self.state.value,
|
638
|
+
"message": "Saga loaded successfully",
|
639
|
+
}
|
640
|
+
|
641
|
+
async def _list_sagas(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
642
|
+
"""List sagas based on filter criteria."""
|
643
|
+
filter_criteria = inputs.get("filter", {})
|
644
|
+
|
645
|
+
saga_ids = await self._state_storage.list_sagas(filter_criteria)
|
646
|
+
|
647
|
+
return {
|
648
|
+
"status": "success",
|
649
|
+
"saga_ids": saga_ids,
|
650
|
+
"count": len(saga_ids),
|
651
|
+
"filter": filter_criteria,
|
652
|
+
}
|