python-saga-orchestrator 0.5.0__tar.gz → 0.6.0__tar.gz
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.
- python_saga_orchestrator-0.6.0/Makefile +16 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/PKG-INFO +1 -1
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/python_saga_orchestrator.egg-info/PKG-INFO +1 -1
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/_version.py +2 -2
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/core/engine.py +16 -27
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/domain/models/context.py +0 -3
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/integration/helpers.py +15 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/integration/test_context_persistence.py +8 -3
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/integration/test_core_flow.py +127 -8
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/integration/test_notification_flow.py +4 -3
- python_saga_orchestrator-0.5.0/Makefile +0 -12
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/.github/workflows/ci.yml +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/.github/workflows/publish.yml +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/.gitignore +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/Dockerfile +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/LICENSE +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/README.md +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/docker-compose.yaml +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/examples/admin_skip.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/examples/common.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/examples/compensation_flow.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/examples/http_and_queue.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/examples/llm_deploy.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/examples/retry_recovery.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/pyproject.toml +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/python_saga_orchestrator.egg-info/SOURCES.txt +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/python_saga_orchestrator.egg-info/dependency_links.txt +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/python_saga_orchestrator.egg-info/requires.txt +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/python_saga_orchestrator.egg-info/top_level.txt +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/__init__.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/admin/__init__.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/admin/api.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/core/__init__.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/core/builder.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/core/orchestrator.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/core/repository.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/domain/__init__.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/domain/exceptions/__init__.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/domain/exceptions/saga.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/domain/mixins/__init__.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/domain/mixins/saga_state.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/domain/mixins/saga_step_histrory.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/domain/mixins/types.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/domain/models/__init__.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/domain/models/builder.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/domain/models/enums/__init__.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/domain/models/enums/base_str_enum.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/domain/models/enums/saga_status.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/domain/models/enums/saga_step_phase.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/domain/models/enums/saga_step_status.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/domain/models/notify.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/domain/models/retry.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/domain/models/saga_snapshot.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/domain/models/step.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/inbox/__init__.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/inbox/contracts.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/inbox/dispatcher.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/inbox/models.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/inbox/repository.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/inbox/retry.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/outbox/__init__.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/outbox/contracts.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/outbox/dispatcher.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/outbox/event.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/outbox/factory.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/outbox/models.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/outbox/repository.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/outbox/retry.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/outbox/serialization.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/setup.cfg +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/task.md +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/__init__.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/conftest.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/integration/__init__.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/integration/conftest.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/integration/models.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/integration/test_admin_api.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/integration/test_compensation_flow.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/integration/test_inbox_flow.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/integration/test_lifecycle_hooks.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/integration/test_outbox_flow.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/integration/test_repository.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/unit/__init__.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/unit/test_builder.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/unit/test_inbox_extensibility.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/unit/test_input_context.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/unit/test_orchestrator_helpers.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/unit/test_outbox_extensibility.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/unit/test_retry.py +0 -0
- {python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/unit/test_step_type_resolution.py +0 -0
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/_version.py
RENAMED
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.6.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 6, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/core/engine.py
RENAMED
|
@@ -261,23 +261,17 @@ class SagaEngine(Generic[ModelT, HistoryModelT]):
|
|
|
261
261
|
return NotifyResult.DUPLICATE
|
|
262
262
|
|
|
263
263
|
expected_types: tuple[str, ...] = context.awaiting_event_types
|
|
264
|
-
|
|
265
|
-
if normalized_event is None and
|
|
266
|
-
expected_type is not None
|
|
267
|
-
or (
|
|
268
|
-
isinstance(expected_types, (list, tuple))
|
|
269
|
-
and len(expected_types) > 0
|
|
270
|
-
)
|
|
271
|
-
):
|
|
264
|
+
|
|
265
|
+
if normalized_event is None and expected_types:
|
|
272
266
|
self._append_notify_log(
|
|
273
267
|
saga=saga,
|
|
274
268
|
event=normalized_event,
|
|
275
269
|
result=NotifyResult.EVENT_TYPE_MISMATCH,
|
|
276
270
|
)
|
|
277
271
|
return NotifyResult.EVENT_TYPE_MISMATCH
|
|
272
|
+
|
|
278
273
|
if (
|
|
279
274
|
normalized_event is not None
|
|
280
|
-
and isinstance(expected_types, (list, tuple))
|
|
281
275
|
and expected_types
|
|
282
276
|
and normalized_event.event_type not in expected_types
|
|
283
277
|
):
|
|
@@ -287,17 +281,6 @@ class SagaEngine(Generic[ModelT, HistoryModelT]):
|
|
|
287
281
|
result=NotifyResult.EVENT_TYPE_MISMATCH,
|
|
288
282
|
)
|
|
289
283
|
return NotifyResult.EVENT_TYPE_MISMATCH
|
|
290
|
-
if (
|
|
291
|
-
expected_type is not None
|
|
292
|
-
and normalized_event is not None
|
|
293
|
-
and normalized_event.event_type != expected_type
|
|
294
|
-
):
|
|
295
|
-
self._append_notify_log(
|
|
296
|
-
saga=saga,
|
|
297
|
-
event=normalized_event,
|
|
298
|
-
result=NotifyResult.EVENT_TYPE_MISMATCH,
|
|
299
|
-
)
|
|
300
|
-
return NotifyResult.EVENT_TYPE_MISMATCH
|
|
301
284
|
|
|
302
285
|
expected_correlation: str | None = context.awaiting_correlation_id
|
|
303
286
|
if (
|
|
@@ -497,16 +480,17 @@ class SagaEngine(Generic[ModelT, HistoryModelT]):
|
|
|
497
480
|
|
|
498
481
|
def _is_waiting_for_events(self, saga) -> bool:
|
|
499
482
|
"""Проверяет, ожидает ли сага внешних событий."""
|
|
500
|
-
context = saga.context
|
|
501
|
-
return bool(context.
|
|
483
|
+
context: SagaContext = saga.context
|
|
484
|
+
return bool(context.awaiting_event_types or context.awaiting_correlation_id)
|
|
502
485
|
|
|
503
486
|
def _handle_saga_timeout(self, saga, now: datetime) -> None:
|
|
504
|
-
"""
|
|
487
|
+
"""Логика обработки таймаута ожидания событий."""
|
|
505
488
|
context = saga.context
|
|
506
|
-
event_types = context.awaiting_event_types or context.awaiting_event_type
|
|
507
489
|
|
|
508
490
|
saga.status = SagaStatus.TIMEOUT
|
|
509
|
-
saga.last_error =
|
|
491
|
+
saga.last_error = (
|
|
492
|
+
f"Timed out waiting for event(s): {context.awaiting_event_types}"
|
|
493
|
+
)
|
|
510
494
|
saga.deadline_at = None
|
|
511
495
|
saga.step_execution_token = uuid.uuid4()
|
|
512
496
|
|
|
@@ -528,6 +512,7 @@ class SagaEngine(Generic[ModelT, HistoryModelT]):
|
|
|
528
512
|
skipped=False,
|
|
529
513
|
)
|
|
530
514
|
)
|
|
515
|
+
context.clear_awaiting_state()
|
|
531
516
|
|
|
532
517
|
async def get_snapshot(self, saga_id: UUID) -> SagaSnapshot:
|
|
533
518
|
"""Return the snapshot view of one saga."""
|
|
@@ -612,6 +597,7 @@ class SagaEngine(Generic[ModelT, HistoryModelT]):
|
|
|
612
597
|
step_def,
|
|
613
598
|
now=datetime.now(UTC),
|
|
614
599
|
)
|
|
600
|
+
saga.context.clear_awaiting_state()
|
|
615
601
|
|
|
616
602
|
await self._drive(saga_id)
|
|
617
603
|
|
|
@@ -623,12 +609,13 @@ class SagaEngine(Generic[ModelT, HistoryModelT]):
|
|
|
623
609
|
if saga.status not in {
|
|
624
610
|
SagaStatus.SUSPENDED,
|
|
625
611
|
SagaStatus.FAILED,
|
|
612
|
+
SagaStatus.TIMEOUT,
|
|
626
613
|
SagaStatus.COMPENSATING,
|
|
627
614
|
SagaStatus.COMPENSATING_SUSPENDED,
|
|
628
615
|
}:
|
|
629
616
|
raise SagaStateError(
|
|
630
617
|
"Cannot start compensation unless saga is suspended, failed, "
|
|
631
|
-
f"or already compensating (status={saga.status})"
|
|
618
|
+
f"timed out, or already compensating (status={saga.status})"
|
|
632
619
|
)
|
|
633
620
|
if saga.current_step_index <= 0:
|
|
634
621
|
raise SagaStateError(
|
|
@@ -639,6 +626,7 @@ class SagaEngine(Generic[ModelT, HistoryModelT]):
|
|
|
639
626
|
saga.retry_counter = 0
|
|
640
627
|
saga.step_execution_token = uuid.uuid4()
|
|
641
628
|
saga.deadline_at = datetime.now(UTC) + self._execution_lease
|
|
629
|
+
saga.context.clear_awaiting_state()
|
|
642
630
|
|
|
643
631
|
await self._run_compensation(saga_id)
|
|
644
632
|
|
|
@@ -679,6 +667,7 @@ class SagaEngine(Generic[ModelT, HistoryModelT]):
|
|
|
679
667
|
saga.deadline_at = None
|
|
680
668
|
saga.last_error = error_message
|
|
681
669
|
saga.step_execution_token = uuid.uuid4()
|
|
670
|
+
saga.context.clear_awaiting_state()
|
|
682
671
|
|
|
683
672
|
async def skip_step(
|
|
684
673
|
self,
|
|
@@ -731,7 +720,7 @@ class SagaEngine(Generic[ModelT, HistoryModelT]):
|
|
|
731
720
|
saga.context.save_step_output(
|
|
732
721
|
step_def.step_id, output_model.model_dump(mode="json")
|
|
733
722
|
)
|
|
734
|
-
|
|
723
|
+
saga.context.clear_awaiting_state()
|
|
735
724
|
saga.current_step_index += 1
|
|
736
725
|
saga.retry_counter = 0
|
|
737
726
|
saga.last_error = None
|
|
@@ -78,7 +78,6 @@ class SagaContext(BaseModel):
|
|
|
78
78
|
) # For idempotency
|
|
79
79
|
|
|
80
80
|
# -- Awaiting state --
|
|
81
|
-
awaiting_event_type: str | None = None
|
|
82
81
|
awaiting_event_types: tuple[str, ...] = Field(default_factory=tuple)
|
|
83
82
|
awaiting_correlation_id: str | None = None
|
|
84
83
|
awaiting_until: str | None = None # ISO 8601 format
|
|
@@ -111,12 +110,10 @@ class SagaContext(BaseModel):
|
|
|
111
110
|
until: str | None,
|
|
112
111
|
) -> None:
|
|
113
112
|
self.awaiting_event_types = event_types
|
|
114
|
-
self.awaiting_event_type = event_types[0] if event_types else None
|
|
115
113
|
self.awaiting_correlation_id = correlation_id
|
|
116
114
|
self.awaiting_until = until
|
|
117
115
|
|
|
118
116
|
def clear_awaiting_state(self) -> None:
|
|
119
|
-
self.awaiting_event_type = None
|
|
120
117
|
self.awaiting_event_types = ()
|
|
121
118
|
self.awaiting_correlation_id = None
|
|
122
119
|
self.awaiting_until = None
|
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/integration/helpers.py
RENAMED
|
@@ -334,6 +334,21 @@ class WaitingWithTimeoutStep(BaseStep[StartInput, StartOutput]):
|
|
|
334
334
|
)
|
|
335
335
|
|
|
336
336
|
|
|
337
|
+
class RetryWaitInput(BaseModel):
|
|
338
|
+
value: int
|
|
339
|
+
event_type: str | None = None
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class RetryWaitStep(BaseStep[RetryWaitInput, StartOutput]):
|
|
343
|
+
async def execute(self, inp: RetryWaitInput) -> StepAwaitEvent | StartOutput:
|
|
344
|
+
if inp.event_type is None:
|
|
345
|
+
return StepAwaitEvent(
|
|
346
|
+
event_types=("some.event",),
|
|
347
|
+
until=timedelta(milliseconds=10),
|
|
348
|
+
)
|
|
349
|
+
return StartOutput(value=inp.value + 1)
|
|
350
|
+
|
|
351
|
+
|
|
337
352
|
class SagaStartedEvent(BaseModel):
|
|
338
353
|
saga_id: uuid.UUID
|
|
339
354
|
initial_value: int
|
|
@@ -7,6 +7,7 @@ import pytest
|
|
|
7
7
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
8
|
from sqlalchemy.orm import selectinload
|
|
9
9
|
|
|
10
|
+
from saga_orchestrator import SagaStateMixin
|
|
10
11
|
from saga_orchestrator.domain.models.context import SagaContext
|
|
11
12
|
from saga_orchestrator.domain.models.enums import SagaStepPhase, SagaStepStatus
|
|
12
13
|
from tests.integration.helpers import StartInput
|
|
@@ -70,12 +71,16 @@ async def test_top_level_attribute_change_is_persisted(session_maker):
|
|
|
70
71
|
|
|
71
72
|
async with session_maker() as session:
|
|
72
73
|
saga = await session.get(IntegrationSagaState, saga_id)
|
|
73
|
-
saga.context.
|
|
74
|
+
saga.context.awaiting_event_types = (new_awaiting_type,)
|
|
74
75
|
await session.commit()
|
|
75
76
|
|
|
76
77
|
async with session_maker() as session:
|
|
77
|
-
reloaded_saga = await session.get(
|
|
78
|
-
|
|
78
|
+
reloaded_saga: SagaStateMixin | None = await session.get(
|
|
79
|
+
IntegrationSagaState, saga_id
|
|
80
|
+
)
|
|
81
|
+
assert reloaded_saga is not None
|
|
82
|
+
assert len(reloaded_saga.context.awaiting_event_types) == 1
|
|
83
|
+
assert new_awaiting_type in reloaded_saga.context.awaiting_event_types
|
|
79
84
|
|
|
80
85
|
|
|
81
86
|
@pytest.mark.asyncio
|
|
@@ -5,7 +5,13 @@ from datetime import timedelta
|
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
7
|
|
|
8
|
-
from saga_orchestrator import
|
|
8
|
+
from saga_orchestrator import (
|
|
9
|
+
SagaAdmin,
|
|
10
|
+
SagaAdminSnapshot,
|
|
11
|
+
SagaBuilder,
|
|
12
|
+
SagaStepPhase,
|
|
13
|
+
SagaStepStatus,
|
|
14
|
+
)
|
|
9
15
|
from saga_orchestrator.core.orchestrator import SagaOrchestrator
|
|
10
16
|
from saga_orchestrator.domain.exceptions import ActiveSagaAlreadyExistsError
|
|
11
17
|
from saga_orchestrator.domain.models import ExponentialRetry
|
|
@@ -17,6 +23,7 @@ from tests.integration.helpers import (
|
|
|
17
23
|
ActivateQueueStep,
|
|
18
24
|
AddOneStep,
|
|
19
25
|
AlwaysFailStep,
|
|
26
|
+
CompensatingStep,
|
|
20
27
|
FailsOnceStep,
|
|
21
28
|
FlakyStep,
|
|
22
29
|
HttpInput,
|
|
@@ -25,6 +32,8 @@ from tests.integration.helpers import (
|
|
|
25
32
|
RecoverableStep,
|
|
26
33
|
ReserveQueueInput,
|
|
27
34
|
ReserveQueueStep,
|
|
35
|
+
RetryWaitInput,
|
|
36
|
+
RetryWaitStep,
|
|
28
37
|
StartInput,
|
|
29
38
|
WaitingWithTimeoutStep,
|
|
30
39
|
)
|
|
@@ -308,10 +317,12 @@ async def test_three_step_http_and_queue_style_flow(session_maker):
|
|
|
308
317
|
aggregation_id="agg-http-queue-3",
|
|
309
318
|
)
|
|
310
319
|
|
|
311
|
-
state_after_start = await admin.get_saga(saga_id)
|
|
320
|
+
state_after_start: SagaAdminSnapshot = await admin.get_saga(saga_id)
|
|
312
321
|
assert state_after_start.status == SagaStatus.SUSPENDED
|
|
313
322
|
assert state_after_start.current_step_index == 1
|
|
314
|
-
assert state_after_start.context.
|
|
323
|
+
assert len(state_after_start.context.awaiting_event_types) == 2
|
|
324
|
+
assert "reserve.success" in state_after_start.context.awaiting_event_types
|
|
325
|
+
assert "reserve.failed" in state_after_start.context.awaiting_event_types
|
|
315
326
|
|
|
316
327
|
processed = await dispatcher.run_once(limit=10)
|
|
317
328
|
assert processed == 1
|
|
@@ -323,11 +334,11 @@ async def test_three_step_http_and_queue_style_flow(session_maker):
|
|
|
323
334
|
reserve_token = (await admin.get_saga(saga_id)).step_execution_token
|
|
324
335
|
await orchestrator.notify(
|
|
325
336
|
saga_id=saga_id,
|
|
326
|
-
token=reserve_token,
|
|
337
|
+
token=reserve_token,
|
|
327
338
|
event=NotifyEvent(
|
|
328
339
|
event_id="evt-reserve-1",
|
|
329
340
|
event_type="reserve.success",
|
|
330
|
-
correlation_id=first_headers["correlation_id"],
|
|
341
|
+
correlation_id=first_headers["correlation_id"],
|
|
331
342
|
payload={"reservation_id": "res-200"},
|
|
332
343
|
),
|
|
333
344
|
)
|
|
@@ -335,7 +346,9 @@ async def test_three_step_http_and_queue_style_flow(session_maker):
|
|
|
335
346
|
state_after_reserve = await admin.get_saga(saga_id)
|
|
336
347
|
assert state_after_reserve.status == SagaStatus.SUSPENDED
|
|
337
348
|
assert state_after_reserve.current_step_index == 2
|
|
338
|
-
assert
|
|
349
|
+
assert len(state_after_start.context.awaiting_event_types) == 2
|
|
350
|
+
assert "activate.success" in state_after_reserve.context.awaiting_event_types
|
|
351
|
+
assert "activate.failed" in state_after_reserve.context.awaiting_event_types
|
|
339
352
|
|
|
340
353
|
processed = await dispatcher.run_once(limit=10)
|
|
341
354
|
assert processed == 1
|
|
@@ -347,11 +360,11 @@ async def test_three_step_http_and_queue_style_flow(session_maker):
|
|
|
347
360
|
activate_token = (await admin.get_saga(saga_id)).step_execution_token
|
|
348
361
|
await orchestrator.notify(
|
|
349
362
|
saga_id=saga_id,
|
|
350
|
-
token=activate_token,
|
|
363
|
+
token=activate_token,
|
|
351
364
|
event=NotifyEvent(
|
|
352
365
|
event_id="evt-activate-1",
|
|
353
366
|
event_type="activate.success",
|
|
354
|
-
correlation_id=second_headers["correlation_id"],
|
|
367
|
+
correlation_id=second_headers["correlation_id"],
|
|
355
368
|
payload={"deployment_id": "dep-200"},
|
|
356
369
|
),
|
|
357
370
|
)
|
|
@@ -446,3 +459,109 @@ async def test_timed_out_status_on_await_event_deadline(session_maker):
|
|
|
446
459
|
assert state_after.step_history[1].status == SagaStepStatus.TIMEOUT
|
|
447
460
|
assert state_after.step_history[1].phase == SagaStepPhase.EXECUTE
|
|
448
461
|
assert "Timed out" in state_after.step_history[1].error
|
|
462
|
+
|
|
463
|
+
assert state_after.context.awaiting_event_types == ()
|
|
464
|
+
assert state_after.context.awaiting_correlation_id is None
|
|
465
|
+
assert state_after.context.awaiting_until is None
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@pytest.mark.asyncio
|
|
469
|
+
async def test_compensate_after_timeout_resolves_deadlock(session_maker):
|
|
470
|
+
"""
|
|
471
|
+
Проверяет возможность запуска компенсации для саги, зависшей со статусом TIMEOUT.
|
|
472
|
+
Убеждается, что админ может спасти сагу без правки БД.
|
|
473
|
+
"""
|
|
474
|
+
first_step = CompensatingStep()
|
|
475
|
+
builder = SagaBuilder()
|
|
476
|
+
ref = builder.add_step(
|
|
477
|
+
step=first_step,
|
|
478
|
+
input_map=lambda ctx: StartInput(value=ctx.initial_data["value"]),
|
|
479
|
+
)
|
|
480
|
+
builder.add_step(
|
|
481
|
+
step=WaitingWithTimeoutStep(),
|
|
482
|
+
depends_on=ref,
|
|
483
|
+
input_map=lambda out: StartInput(value=out.value),
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
orchestrator = SagaOrchestrator[IntegrationSagaState, IntegrationSagaHistory](
|
|
487
|
+
model_class=IntegrationSagaState,
|
|
488
|
+
history_model_class=IntegrationSagaHistory,
|
|
489
|
+
session_maker=session_maker,
|
|
490
|
+
)
|
|
491
|
+
admin = SagaAdmin[IntegrationSagaState, IntegrationSagaHistory](
|
|
492
|
+
engine=orchestrator.engine
|
|
493
|
+
)
|
|
494
|
+
orchestrator.register("comp-timeout", builder.build())
|
|
495
|
+
|
|
496
|
+
saga_id = await orchestrator.start(
|
|
497
|
+
saga_name="comp-timeout",
|
|
498
|
+
initial_data={"value": 1},
|
|
499
|
+
aggregation_id="agg-comp-timeout",
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
await asyncio.sleep(0.1)
|
|
503
|
+
await orchestrator.run_due()
|
|
504
|
+
|
|
505
|
+
state = await admin.get_saga(saga_id)
|
|
506
|
+
assert state.status == SagaStatus.TIMEOUT
|
|
507
|
+
|
|
508
|
+
await admin.compensate_step(saga_id)
|
|
509
|
+
|
|
510
|
+
final_state = await admin.get_saga(saga_id)
|
|
511
|
+
assert final_state.status == SagaStatus.COMPENSATED
|
|
512
|
+
assert first_step.compensated is True
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
@pytest.mark.asyncio
|
|
516
|
+
async def test_retry_after_timeout_processes_successfully(session_maker):
|
|
517
|
+
"""
|
|
518
|
+
Проверяет успешный ретрай саги после таймаута. Убеждается, что нет мгновенного
|
|
519
|
+
повторного таймаута из-за старого неочищенного кеша ожидания.
|
|
520
|
+
"""
|
|
521
|
+
|
|
522
|
+
builder = SagaBuilder()
|
|
523
|
+
builder.add_step(
|
|
524
|
+
step=RetryWaitStep(),
|
|
525
|
+
input_map=lambda ctx: RetryWaitInput(
|
|
526
|
+
value=ctx.initial_data["value"],
|
|
527
|
+
event_type=ctx.latest_event_type,
|
|
528
|
+
),
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
orchestrator = SagaOrchestrator[IntegrationSagaState, IntegrationSagaHistory](
|
|
532
|
+
model_class=IntegrationSagaState,
|
|
533
|
+
history_model_class=IntegrationSagaHistory,
|
|
534
|
+
session_maker=session_maker,
|
|
535
|
+
)
|
|
536
|
+
admin = SagaAdmin[IntegrationSagaState, IntegrationSagaHistory](
|
|
537
|
+
engine=orchestrator.engine
|
|
538
|
+
)
|
|
539
|
+
orchestrator.register("retry-timeout", builder.build())
|
|
540
|
+
|
|
541
|
+
saga_id = await orchestrator.start(
|
|
542
|
+
saga_name="retry-timeout",
|
|
543
|
+
initial_data={"value": 1},
|
|
544
|
+
aggregation_id="agg-retry-timeout",
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
await asyncio.sleep(0.1)
|
|
548
|
+
await orchestrator.run_due()
|
|
549
|
+
state1 = await admin.get_saga(saga_id)
|
|
550
|
+
assert state1.status == SagaStatus.TIMEOUT
|
|
551
|
+
|
|
552
|
+
await admin.retry_step(saga_id)
|
|
553
|
+
state2 = await admin.get_saga(saga_id)
|
|
554
|
+
|
|
555
|
+
assert state2.status == SagaStatus.SUSPENDED
|
|
556
|
+
assert state2.context.awaiting_event_types == ("some.event",)
|
|
557
|
+
|
|
558
|
+
await orchestrator.notify(
|
|
559
|
+
saga_id=saga_id,
|
|
560
|
+
token=state2.step_execution_token,
|
|
561
|
+
event=NotifyEvent(
|
|
562
|
+
event_id="evt-retry-123", event_type="some.event", payload={"value": 42}
|
|
563
|
+
),
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
final_state = await admin.get_saga(saga_id)
|
|
567
|
+
assert final_state.status == SagaStatus.COMPLETED
|
|
@@ -5,7 +5,7 @@ from datetime import timedelta
|
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
7
|
|
|
8
|
-
from saga_orchestrator import SagaAdmin, SagaBuilder
|
|
8
|
+
from saga_orchestrator import SagaAdmin, SagaAdminSnapshot, SagaBuilder
|
|
9
9
|
from saga_orchestrator.core.orchestrator import SagaOrchestrator
|
|
10
10
|
from saga_orchestrator.domain.models import ExponentialRetry
|
|
11
11
|
from saga_orchestrator.domain.models.enums import SagaStatus
|
|
@@ -94,12 +94,13 @@ async def test_await_event_configures_wait_contract(session_maker):
|
|
|
94
94
|
),
|
|
95
95
|
)
|
|
96
96
|
|
|
97
|
-
state_after = await admin.get_saga(saga_id)
|
|
97
|
+
state_after: SagaAdminSnapshot = await admin.get_saga(saga_id)
|
|
98
98
|
assert state_after.status == SagaStatus.SUSPENDED
|
|
99
99
|
assert state_after.step_execution_token == new_token
|
|
100
100
|
assert state_after.step_execution_token != state_before.step_execution_token
|
|
101
101
|
assert state_after.deadline_at is None
|
|
102
|
-
assert state_after.context.
|
|
102
|
+
assert len(state_after.context.awaiting_event_types) == 1
|
|
103
|
+
assert "model.approved" in state_after.context.awaiting_event_types
|
|
103
104
|
assert state_after.context.awaiting_correlation_id == "corr-await"
|
|
104
105
|
assert state_after.context.awaiting_until == "2999-01-01T00:00:00+00:00"
|
|
105
106
|
|
|
File without changes
|
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/.github/workflows/publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/examples/compensation_flow.py
RENAMED
|
File without changes
|
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/examples/http_and_queue.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/examples/retry_recovery.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/admin/api.py
RENAMED
|
File without changes
|
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/core/__init__.py
RENAMED
|
File without changes
|
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/core/builder.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/inbox/models.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/inbox/retry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/outbox/event.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/outbox/models.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/saga_orchestrator/outbox/retry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/integration/__init__.py
RENAMED
|
File without changes
|
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/integration/conftest.py
RENAMED
|
File without changes
|
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/integration/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/unit/test_builder.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.5.0 → python_saga_orchestrator-0.6.0}/tests/unit/test_input_context.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|