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.
Files changed (82) hide show
  1. kailash/__init__.py +35 -5
  2. kailash/access_control.py +64 -46
  3. kailash/adapters/__init__.py +5 -0
  4. kailash/adapters/mcp_platform_adapter.py +273 -0
  5. kailash/api/workflow_api.py +34 -3
  6. kailash/channels/__init__.py +21 -0
  7. kailash/channels/api_channel.py +409 -0
  8. kailash/channels/base.py +271 -0
  9. kailash/channels/cli_channel.py +661 -0
  10. kailash/channels/event_router.py +496 -0
  11. kailash/channels/mcp_channel.py +648 -0
  12. kailash/channels/session.py +423 -0
  13. kailash/mcp_server/discovery.py +57 -18
  14. kailash/middleware/communication/api_gateway.py +23 -3
  15. kailash/middleware/communication/realtime.py +83 -0
  16. kailash/middleware/core/agent_ui.py +1 -1
  17. kailash/middleware/gateway/storage_backends.py +393 -0
  18. kailash/middleware/mcp/enhanced_server.py +22 -16
  19. kailash/nexus/__init__.py +21 -0
  20. kailash/nexus/cli/__init__.py +5 -0
  21. kailash/nexus/cli/__main__.py +6 -0
  22. kailash/nexus/cli/main.py +176 -0
  23. kailash/nexus/factory.py +413 -0
  24. kailash/nexus/gateway.py +545 -0
  25. kailash/nodes/__init__.py +8 -5
  26. kailash/nodes/ai/iterative_llm_agent.py +988 -17
  27. kailash/nodes/ai/llm_agent.py +29 -9
  28. kailash/nodes/api/__init__.py +2 -2
  29. kailash/nodes/api/monitoring.py +1 -1
  30. kailash/nodes/base.py +29 -5
  31. kailash/nodes/base_async.py +54 -14
  32. kailash/nodes/code/async_python.py +1 -1
  33. kailash/nodes/code/python.py +50 -6
  34. kailash/nodes/data/async_sql.py +90 -0
  35. kailash/nodes/data/bulk_operations.py +939 -0
  36. kailash/nodes/data/query_builder.py +373 -0
  37. kailash/nodes/data/query_cache.py +512 -0
  38. kailash/nodes/monitoring/__init__.py +10 -0
  39. kailash/nodes/monitoring/deadlock_detector.py +964 -0
  40. kailash/nodes/monitoring/performance_anomaly.py +1078 -0
  41. kailash/nodes/monitoring/race_condition_detector.py +1151 -0
  42. kailash/nodes/monitoring/transaction_metrics.py +790 -0
  43. kailash/nodes/monitoring/transaction_monitor.py +931 -0
  44. kailash/nodes/security/behavior_analysis.py +414 -0
  45. kailash/nodes/system/__init__.py +17 -0
  46. kailash/nodes/system/command_parser.py +820 -0
  47. kailash/nodes/transaction/__init__.py +48 -0
  48. kailash/nodes/transaction/distributed_transaction_manager.py +983 -0
  49. kailash/nodes/transaction/saga_coordinator.py +652 -0
  50. kailash/nodes/transaction/saga_state_storage.py +411 -0
  51. kailash/nodes/transaction/saga_step.py +467 -0
  52. kailash/nodes/transaction/transaction_context.py +756 -0
  53. kailash/nodes/transaction/two_phase_commit.py +978 -0
  54. kailash/nodes/transform/processors.py +17 -1
  55. kailash/nodes/validation/__init__.py +21 -0
  56. kailash/nodes/validation/test_executor.py +532 -0
  57. kailash/nodes/validation/validation_nodes.py +447 -0
  58. kailash/resources/factory.py +1 -1
  59. kailash/runtime/access_controlled.py +9 -7
  60. kailash/runtime/async_local.py +84 -21
  61. kailash/runtime/local.py +21 -2
  62. kailash/runtime/parameter_injector.py +187 -31
  63. kailash/runtime/runner.py +6 -4
  64. kailash/runtime/testing.py +1 -1
  65. kailash/security.py +22 -3
  66. kailash/servers/__init__.py +32 -0
  67. kailash/servers/durable_workflow_server.py +430 -0
  68. kailash/servers/enterprise_workflow_server.py +522 -0
  69. kailash/servers/gateway.py +183 -0
  70. kailash/servers/workflow_server.py +293 -0
  71. kailash/utils/data_validation.py +192 -0
  72. kailash/workflow/builder.py +382 -15
  73. kailash/workflow/cyclic_runner.py +102 -10
  74. kailash/workflow/validation.py +144 -8
  75. kailash/workflow/visualization.py +99 -27
  76. {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/METADATA +3 -2
  77. {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/RECORD +81 -40
  78. kailash/workflow/builder_improvements.py +0 -207
  79. {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/WHEEL +0 -0
  80. {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/entry_points.txt +0 -0
  81. {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/licenses/LICENSE +0 -0
  82. {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
+ }