Skip to content

Commit cd2e902

Browse files
author
Вадим Козыревский
committed
Обновил документацию по саге
1 parent 9d2090e commit cd2e902

4 files changed

Lines changed: 183 additions & 45 deletions

File tree

docs/mermaid/chain_of_responsibility.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ The Sequence diagram visualizes the complete execution flow of a handler chain u
4949
- The diagram shows all possible paths through the chain using nested conditional blocks
5050
- **Any handler can process the request and stop the chain**, not just the last one
5151
- Handler names (not aliases) are used in `alt` conditions for better readability
52+
- **After a handler successfully processes a request**, events are collected from the handler's `events` property and processed by the EventMediator
5253

5354
### Example Handler Chain Code
5455

@@ -92,6 +93,7 @@ handlers = [CreditCardHandler, PayPalHandler, DefaultPaymentHandler]
9293
```text
9394
sequenceDiagram
9495
participant C as Chain
96+
participant E as EventMediator
9597
participant H1 as CreditCardHandler
9698
participant H2 as PayPalHandler
9799
participant H3 as DefaultPaymentHandler
@@ -102,18 +104,30 @@ sequenceDiagram
102104
alt CreditCardHandler can handle
103105
H1-->>C: result
104106
Note over H1: Handler processed, chain stops
107+
C->>H1: events
108+
H1-->>C: List[Event]
109+
C->>E: process events
110+
Note over E: Events from CreditCardHandler processed
105111
else
106112
H1->>H2: next(request)
107113
Note over H1: Cannot handle, passing to next
108114
alt PayPalHandler can handle
109115
H2-->>C: result
110116
Note over H2: Handler processed, chain stops
117+
C->>H2: events
118+
H2-->>C: List[Event]
119+
C->>E: process events
120+
Note over E: Events from PayPalHandler processed
111121
else
112122
H2->>H3: next(request)
113123
Note over H2: Cannot handle, passing to next
114124
alt DefaultPaymentHandler can handle
115125
H3-->>C: result
116126
Note over H3: Handler processed (default)
127+
C->>H3: events
128+
H3-->>C: List[Event]
129+
C->>E: process events
130+
Note over E: Events from DefaultPaymentHandler processed
117131
end
118132
end
119133
end
@@ -124,6 +138,7 @@ sequenceDiagram
124138
```mermaid
125139
sequenceDiagram
126140
participant C as Chain
141+
participant E as EventMediator
127142
participant H1 as CreditCardHandler
128143
participant H2 as PayPalHandler
129144
participant H3 as DefaultPaymentHandler
@@ -134,18 +149,30 @@ sequenceDiagram
134149
alt CreditCardHandler can handle
135150
H1-->>C: result
136151
Note over H1: Handler processed, chain stops
152+
C->>H1: events
153+
H1-->>C: List[Event]
154+
C->>E: process events
155+
Note over E: Events from CreditCardHandler processed
137156
else
138157
H1->>H2: next(request)
139158
Note over H1: Cannot handle, passing to next
140159
alt PayPalHandler can handle
141160
H2-->>C: result
142161
Note over H2: Handler processed, chain stops
162+
C->>H2: events
163+
H2-->>C: List[Event]
164+
C->>E: process events
165+
Note over E: Events from PayPalHandler processed
143166
else
144167
H2->>H3: next(request)
145168
Note over H2: Cannot handle, passing to next
146169
alt DefaultPaymentHandler can handle
147170
H3-->>C: result
148171
Note over H3: Handler processed (default)
172+
C->>H3: events
173+
H3-->>C: List[Event]
174+
C->>E: process events
175+
Note over E: Events from DefaultPaymentHandler processed
149176
end
150177
end
151178
end

docs/saga/examples.md

Lines changed: 92 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@
33
## Basic Saga Example
44

55
```python
6+
import dataclasses
7+
import uuid
8+
import di
9+
10+
import cqrs
11+
from cqrs.saga import bootstrap
612
from cqrs.saga.saga import Saga
713
from cqrs.saga.step import SagaStepHandler, SagaStepResult
814
from cqrs.saga.storage.memory import MemorySagaStorage
915
from cqrs.saga.models import SagaContext
10-
import dataclasses
11-
import uuid
16+
from cqrs.response import Response
1217

1318
@dataclasses.dataclass
1419
class OrderContext(SagaContext):
@@ -35,17 +40,34 @@ class ReserveInventoryStep(SagaStepHandler[OrderContext, Response]):
3540
context.inventory_reservation_id
3641
)
3742

38-
# Create and execute saga
43+
# Define saga class with steps
44+
class OrderSaga(Saga[OrderContext]):
45+
steps = [ReserveInventoryStep, ProcessPaymentStep]
46+
47+
# Setup DI container
48+
di_container = di.Container()
49+
# ... register services ...
50+
51+
# Create saga storage
3952
storage = MemorySagaStorage()
40-
saga = Saga(
41-
steps=[ReserveInventoryStep, ProcessPaymentStep],
42-
container=container,
43-
storage=storage,
53+
54+
# Register saga in SagaMap
55+
def saga_mapper(mapper: cqrs.SagaMap) -> None:
56+
mapper.bind(OrderContext, OrderSaga)
57+
58+
# Create saga mediator using bootstrap
59+
mediator = bootstrap.bootstrap(
60+
di_container=di_container,
61+
sagas_mapper=saga_mapper,
62+
saga_storage=storage,
4463
)
4564

46-
async with saga.transaction(context=context, saga_id=uuid.uuid4()) as transaction:
47-
async for step_result in transaction:
48-
print(f"Step completed: {step_result.step_type.__name__}")
65+
# Execute saga
66+
context = OrderContext(order_id="123", items=["item_1"], total_amount=100.0)
67+
saga_id = uuid.uuid4()
68+
69+
async for step_result in mediator.stream(context, saga_id=saga_id):
70+
print(f"Step completed: {step_result.step_type.__name__}")
4971
```
5072

5173
**Complete example:** [`examples/saga.py`](https://github.com/vadikko2/cqrs/blob/master/examples/saga.py)
@@ -57,11 +79,20 @@ async with saga.transaction(context=context, saga_id=uuid.uuid4()) as transactio
5779
```python
5880
from cqrs.saga.recovery import recover_saga
5981

82+
# Get saga instance (or keep reference to saga class)
83+
saga = OrderSaga()
84+
6085
# Recover interrupted saga
6186
saga_id = uuid.UUID("550e8400-e29b-41d4-a716-446655440000")
6287

6388
try:
64-
await recover_saga(saga, saga_id, OrderContext)
89+
await recover_saga(
90+
saga=saga,
91+
saga_id=saga_id,
92+
context_builder=OrderContext,
93+
container=di_container, # Same container used in bootstrap
94+
storage=storage,
95+
)
6596
print("Saga recovered successfully!")
6697
except RuntimeError:
6798
# Expected if saga was in COMPENSATING/FAILED state
@@ -77,19 +108,30 @@ except RuntimeError:
77108
```python
78109
import fastapi
79110
import json
80-
from cqrs.saga.saga import Saga
111+
import uuid
112+
from cqrs.saga import bootstrap
113+
114+
def mediator_factory() -> cqrs.SagaMediator:
115+
"""Create saga mediator using bootstrap."""
116+
return bootstrap.bootstrap(
117+
di_container=di_container,
118+
sagas_mapper=saga_mapper,
119+
saga_storage=storage,
120+
)
81121

82122
@app.post("/process-order")
83-
async def process_order(request: ProcessOrderRequest):
123+
async def process_order(
124+
request: ProcessOrderRequest,
125+
mediator: cqrs.SagaMediator = fastapi.Depends(mediator_factory),
126+
):
84127
async def generate_sse():
85-
saga = Saga(steps=[...], container=container, storage=storage)
86128
saga_id = uuid.uuid4()
129+
context = OrderContext(...)
87130

88131
yield f"data: {json.dumps({'type': 'start', 'saga_id': str(saga_id)})}\n\n"
89132

90-
async with saga.transaction(context=context, saga_id=saga_id) as transaction:
91-
async for step_result in transaction:
92-
yield f"data: {json.dumps({'type': 'step_progress', 'step': step_result.step_type.__name__})}\n\n"
133+
async for step_result in mediator.stream(context, saga_id=saga_id):
134+
yield f"data: {json.dumps({'type': 'step_progress', 'step': step_result.step_type.__name__})}\n\n"
93135

94136
yield f"data: {json.dumps({'type': 'complete'})}\n\n"
95137

@@ -107,6 +149,7 @@ async def process_order(request: ProcessOrderRequest):
107149
```python
108150
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
109151
from cqrs.saga.storage.sqlalchemy import SqlAlchemySagaStorage, Base
152+
from cqrs.saga import bootstrap
110153

111154
# Setup
112155
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")
@@ -117,11 +160,24 @@ async def create_storage() -> SqlAlchemySagaStorage:
117160

118161
# Usage
119162
storage = await create_storage()
120-
saga = Saga(steps=[...], container=container, storage=storage)
121163

122-
async with saga.transaction(context=context, saga_id=uuid.uuid4()) as transaction:
123-
async for step_result in transaction:
124-
print(f"Step: {step_result.step_type.__name__}")
164+
# Register saga in SagaMap
165+
def saga_mapper(mapper: cqrs.SagaMap) -> None:
166+
mapper.bind(OrderContext, OrderSaga)
167+
168+
# Create saga mediator using bootstrap
169+
mediator = bootstrap.bootstrap(
170+
di_container=di_container,
171+
sagas_mapper=saga_mapper,
172+
saga_storage=storage,
173+
)
174+
175+
# Execute saga
176+
context = OrderContext(...)
177+
saga_id = uuid.uuid4()
178+
179+
async for step_result in mediator.stream(context, saga_id=saga_id):
180+
print(f"Step: {step_result.step_type.__name__}")
125181

126182
await storage.session.commit()
127183
```
@@ -130,15 +186,14 @@ await storage.session.commit()
130186

131187
## Compensation Retry Configuration
132188

189+
Compensation retry configuration is handled at the transaction level. When using `SagaMediator`, retry settings can be configured when creating the saga transaction. However, the recommended approach is to configure retry settings in your saga class or use the default settings.
190+
191+
For advanced retry configuration, you can access the transaction directly:
192+
133193
```python
134-
saga = Saga(
135-
steps=[...],
136-
container=container,
137-
storage=storage,
138-
compensation_retry_count=5, # Retry up to 5 times
139-
compensation_retry_delay=2.0, # Start with 2 second delay
140-
compensation_retry_backoff=1.5, # Multiply delay by 1.5 each time
141-
)
194+
# Note: Compensation retry is configured at the SagaTransaction level
195+
# When using mediator.stream(), default retry settings are used
196+
# For custom retry configuration, you may need to access the transaction directly
142197
```
143198

144199
## Background Recovery Job
@@ -148,11 +203,18 @@ import asyncio
148203
from cqrs.saga.recovery import recover_saga
149204

150205
async def recovery_job():
206+
saga = OrderSaga() # Get saga instance
151207
while True:
152208
incomplete_sagas = await find_incomplete_sagas()
153209
for saga_id in incomplete_sagas:
154210
try:
155-
await recover_saga(saga, saga_id, OrderContext)
211+
await recover_saga(
212+
saga=saga,
213+
saga_id=saga_id,
214+
context_builder=OrderContext,
215+
container=di_container,
216+
storage=storage,
217+
)
156218
except RuntimeError:
157219
pass # Compensation completed
158220
await asyncio.sleep(60) # Scan every minute

docs/saga/index.md

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,17 @@ graph TD
5555
## Basic Example
5656

5757
```python
58+
import dataclasses
59+
import uuid
60+
import di
61+
62+
import cqrs
63+
from cqrs.saga import bootstrap
5864
from cqrs.saga.saga import Saga
5965
from cqrs.saga.step import SagaStepHandler, SagaStepResult
6066
from cqrs.saga.storage.memory import MemorySagaStorage
6167
from cqrs.saga.models import SagaContext
6268
from cqrs.response import Response
63-
import dataclasses
64-
import uuid
6569

6670
# Context
6771
@dataclasses.dataclass
@@ -90,17 +94,34 @@ class ReserveInventoryStep(SagaStepHandler[OrderContext, Response]):
9094
context.inventory_reservation_id
9195
)
9296

93-
# Create and execute saga
97+
# Define saga class with steps
98+
class OrderSaga(Saga[OrderContext]):
99+
steps = [ReserveInventoryStep]
100+
101+
# Setup DI container
102+
di_container = di.Container()
103+
# ... register your services ...
104+
105+
# Create saga storage
94106
storage = MemorySagaStorage()
95-
saga = Saga(
96-
steps=[ReserveInventoryStep],
97-
container=container,
98-
storage=storage,
107+
108+
# Register saga in SagaMap
109+
def saga_mapper(mapper: cqrs.SagaMap) -> None:
110+
mapper.bind(OrderContext, OrderSaga)
111+
112+
# Create saga mediator using bootstrap
113+
mediator = bootstrap.bootstrap(
114+
di_container=di_container,
115+
sagas_mapper=saga_mapper,
116+
saga_storage=storage,
99117
)
100118

101-
async with saga.transaction(context=context, saga_id=uuid.uuid4()) as transaction:
102-
async for step_result in transaction:
103-
print(f"Step completed: {step_result.step_type.__name__}")
119+
# Execute saga
120+
context = OrderContext(order_id="123", items=["item_1"], total_amount=100.0)
121+
saga_id = uuid.uuid4()
122+
123+
async for step_result in mediator.stream(context, saga_id=saga_id):
124+
print(f"Step completed: {step_result.step_type.__name__}")
104125
```
105126

106127
## Key Features

0 commit comments

Comments
 (0)