Skip to content

Commit f80a417

Browse files
author
Вадим Козыревский
committed
Add fallback documentation
1 parent 4f0e116 commit f80a417

28 files changed

Lines changed: 675 additions & 33 deletions

docs/bootstrap/advanced.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
Core concepts for commands, queries, and request handlers.
1414

15-
[:octicons-arrow-right-24: Read More](../request_handler.md)
15+
[:octicons-arrow-right-24: Read More](../request_handler/index.md)
1616

1717
</div>
1818

docs/bootstrap/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,15 @@
5757
The `bootstrap` utilities simplify the initial configuration of your CQRS application. They automatically set up:
5858

5959
- **Dependency Injection Container** — Resolves handlers and their dependencies (see [Dependency Injection](../di.md))
60-
- **Request Mapping** — Maps commands and queries to their handlers (see [Request Handlers](../request_handler.md))
60+
- **Request Mapping** — Maps commands and queries to their handlers (see [Request Handlers](../request_handler/index.md))
6161
- **Event Mapping** — Maps domain events to their handlers (see [Event Handling](../event_handler/index.md))
6262
- **Saga Mapping** — Maps saga context types to saga classes (see [Saga Pattern](../saga/index.md))
6363
- **Message Broker** — Configures event publishing (see [Event Producing](../event_producing.md))
6464
- **Middlewares** — Adds logging and custom middlewares
6565
- **Event Processing** — Configures parallel event processing
6666

6767
!!! tip "Getting Started"
68-
If you're new to `python-cqrs`, start here! Bootstrap is the foundation for all other features. After configuring bootstrap, proceed to [Request Handlers](../request_handler.md) to learn how to create command and query handlers.
68+
If you're new to `python-cqrs`, start here! Bootstrap is the foundation for all other features. After configuring bootstrap, proceed to [Request Handlers](../request_handler/index.md) to learn how to create command and query handlers.
6969

7070
!!! note "Navigation"
7171
Use the navigation menu on the left to explore different mediator types and configuration options. Each section covers a specific aspect of bootstrap configuration.

docs/bootstrap/middlewares.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
Request pipeline and handler execution where middlewares apply.
1414

15-
[:octicons-arrow-right-24: Read More](../request_handler.md)
15+
[:octicons-arrow-right-24: Read More](../request_handler/index.md)
1616

1717
</div>
1818

docs/bootstrap/request_mediator.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
Commands, queries, handlers, and request/response mapping.
1414

15-
[:octicons-arrow-right-24: Read More](../request_handler.md)
15+
[:octicons-arrow-right-24: Read More](../request_handler/index.md)
1616

1717
</div>
1818

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Chain of Responsibility Fallback
2+
3+
<div class="grid cards" markdown>
4+
5+
- :material-home: **Back to Chain of Responsibility Overview**
6+
7+
Return to the Chain of Responsibility overview page with all topics.
8+
9+
[:octicons-arrow-left-24: Back to Overview](index.md)
10+
11+
</div>
12+
13+
---
14+
15+
## Overview
16+
17+
You can combine **Chain of Responsibility** with **Request Handler Fallback**: the primary handler is a `RequestHandler` that delegates to a COR chain; when the chain raises (e.g. downstream failure), the fallback handler is invoked. This is useful when a request is first tried through a chain (e.g. cache → DB → external API) and you want a default/cached response if the whole chain fails.
18+
19+
| Concept | Description |
20+
|---------|-------------|
21+
| **Primary** | A `RequestHandler` that runs the COR chain (e.g. injects chain entry via DI) |
22+
| **Fallback** | A `RequestHandler` used when the chain raises |
23+
| **Flow** | `mediator.send(request)` dispatches to primary; primary calls the chain; on exception, fallback runs |
24+
25+
!!! tip "When to Use"
26+
Use COR + Fallback when you have a chain of strategies (CoR) and a clear fallback path if the entire chain fails (e.g. connection errors to the last handler).
27+
28+
## Registration
29+
30+
Bind the request type to `RequestHandlerFallback` with a **wrapper** handler as primary (the one that runs the chain) and a simple handler as fallback:
31+
32+
```python
33+
import cqrs
34+
from cqrs.requests.cor_request_handler import CORRequestHandler, build_chain
35+
36+
def commands_mapper(mapper: cqrs.RequestMap) -> None:
37+
mapper.bind(
38+
FetchDataCommand,
39+
cqrs.RequestHandlerFallback(
40+
primary=CORChainWrapperHandler, # RequestHandler that delegates to chain
41+
fallback=FallbackFetchDataHandler,
42+
failure_exceptions=(ConnectionError, TimeoutError),
43+
),
44+
)
45+
```
46+
47+
Build the chain and inject the chain entry (first handler) so the wrapper receives it:
48+
49+
```python
50+
source_a = SourceAHandler()
51+
source_b = SourceBHandler()
52+
default = DefaultChainHandler()
53+
build_chain([source_a, source_b, default])
54+
55+
di_container.bind(
56+
di.bind_by_type(
57+
dependent.Dependent(lambda: source_a, scope="request"),
58+
SourceAHandler,
59+
),
60+
)
61+
62+
mediator = bootstrap.bootstrap(
63+
di_container=di_container,
64+
commands_mapper=commands_mapper,
65+
)
66+
```
67+
68+
## Wrapper Handler
69+
70+
The primary handler is a normal `RequestHandler` that holds the chain entry and delegates:
71+
72+
```python
73+
class CORChainWrapperHandler(
74+
cqrs.RequestHandler[FetchDataCommand, FetchDataResult],
75+
):
76+
"""Primary handler: runs the COR chain; chain entry injected via DI."""
77+
78+
def __init__(self, chain_entry: SourceAHandler) -> None:
79+
self._chain_entry = chain_entry
80+
81+
@property
82+
def events(self) -> list[cqrs.Event]:
83+
return []
84+
85+
async def handle(self, request: FetchDataCommand) -> FetchDataResult:
86+
result = await self._chain_entry.handle(request)
87+
if result is None:
88+
raise ValueError("COR chain did not handle the request")
89+
return result
90+
```
91+
92+
When any handler in the chain raises (e.g. `ConnectionError`), the dispatcher catches it and invokes the fallback handler. Use `failure_exceptions` to restrict fallback to specific exception types.
93+
94+
## Fallback Handler
95+
96+
The fallback is a simple `RequestHandler` that returns a default or cached response:
97+
98+
```python
99+
class FallbackFetchDataHandler(
100+
cqrs.RequestHandler[FetchDataCommand, FetchDataResult],
101+
):
102+
@property
103+
def events(self) -> list[cqrs.Event]:
104+
return []
105+
106+
async def handle(self, request: FetchDataCommand) -> FetchDataResult:
107+
return FetchDataResult(
108+
data="cached_or_default",
109+
source="fallback",
110+
)
111+
```
112+
113+
## Circuit Breaker configuration
114+
115+
CoR + Fallback uses `RequestHandlerFallback`, so Circuit Breaker is configured the same way as for [Request Handler Fallback](../request_handler/fallback.md#circuit-breaker-optional).
116+
117+
- **Adapter:** `AioBreakerAdapter` from `cqrs.adapters.circuit_breaker`.
118+
- **Parameters:** `fail_max` (default `5`), `timeout_duration` (seconds, default `60`), `exclude` (exceptions that do not open the circuit), optional `storage_factory` for distributed state.
119+
- **One instance per domain** — e.g. one adapter for request fallbacks (including COR wrapper); the adapter creates an isolated circuit per handler type.
120+
121+
Example:
122+
123+
```python
124+
from cqrs.adapters.circuit_breaker import AioBreakerAdapter
125+
126+
request_cb = AioBreakerAdapter(fail_max=5, timeout_duration=60)
127+
mapper.bind(
128+
FetchDataCommand,
129+
cqrs.RequestHandlerFallback(
130+
primary=CORChainWrapperHandler,
131+
fallback=FallbackFetchDataHandler,
132+
failure_exceptions=(ConnectionError, TimeoutError),
133+
circuit_breaker=request_cb,
134+
),
135+
)
136+
```
137+
138+
Full configuration (exclude, storage_factory, failure_exceptions) is described in [Request Handler Fallback — Circuit Breaker configuration](../request_handler/fallback.md#circuit-breaker-configuration) and [Saga Fallback — Circuit Breaker](../saga/fallback/circuit_breaker.md).
139+
140+
## Related
141+
142+
- [Request Handler Fallback](../request_handler/fallback.md) — Fallback for command/query handlers
143+
- [Chain of Responsibility Examples](examples.md) — Registering and building COR chains
144+
- [Saga Fallback Pattern](../saga/fallback/index.md) — Fallback for saga steps

docs/chain_of_responsibility/index.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ The Chain of Responsibility pattern allows multiple handlers to process a reques
2424

2525
[:octicons-arrow-right-24: Read More](../mermaid/chain_of_responsibility.md)
2626

27+
- :material-backup-restore: **Fallback**
28+
29+
Combine CoR with Request Handler Fallback when the chain raises.
30+
31+
[:octicons-arrow-right-24: Read More](fallback.md)
32+
2733
</div>
2834

2935
`CORRequestHandler` implements the Chain of Responsibility pattern, allowing multiple handlers to process a request sequentially. Each handler decides whether to process the request or pass it to the next handler in the chain. The chain stops when a handler successfully processes the request or when all handlers have been exhausted.
@@ -36,10 +42,10 @@ The Chain of Responsibility pattern allows multiple handlers to process a reques
3642
- **Easy to extend** — Add new handlers without modifying existing ones
3743

3844
!!! note "Prerequisites"
39-
Understanding of [Request Handlers](../request_handler.md) and [Bootstrap](../bootstrap/index.md) is recommended.
45+
Understanding of [Request Handlers](../request_handler/index.md) and [Bootstrap](../bootstrap/index.md) is recommended.
4046

4147
!!! tip "When to Use"
42-
Use Chain of Responsibility when you have multiple processing strategies or need fallback mechanisms. For standard request handling, use regular [Request Handlers](../request_handler.md).
48+
Use Chain of Responsibility when you have multiple processing strategies or need fallback mechanisms. For standard request handling, use regular [Request Handlers](../request_handler/index.md).
4349

4450
## Pattern Description
4551

docs/di.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Both libraries allow you to bind implementations to interfaces and automatically
1515
This section assumes you've already configured [Bootstrap](bootstrap/index.md). The DI container is passed to bootstrap functions to resolve handlers and their dependencies.
1616

1717
!!! tip "Next Steps"
18-
After understanding DI, proceed to [Request Handlers](request_handler.md) to learn how handlers use dependency injection.
18+
After understanding DI, proceed to [Request Handlers](request_handler/index.md) to learn how handlers use dependency injection.
1919

2020
## Supported Libraries
2121

docs/event_handler/fallback.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Event Handler Fallback
2+
3+
<div class="grid cards" markdown>
4+
5+
- :material-home: **Back to Event Handling Overview**
6+
7+
Return to the Event Handling overview page with all topics.
8+
9+
[:octicons-arrow-left-24: Back to Overview](index.md)
10+
11+
</div>
12+
13+
---
14+
15+
## Overview
16+
17+
The **Event Handler Fallback** pattern allows you to register an alternative event handler that runs when the primary event handler fails (or when the circuit breaker is open). This provides resilience for side effects such as sending notifications or updating read models when the primary path (e.g. external API) is unavailable.
18+
19+
| Concept | Description |
20+
|---------|-------------|
21+
| **Primary handler** | Main event handler that executes first |
22+
| **Fallback handler** | Alternative handler invoked on primary failure or circuit open |
23+
| **failure_exceptions** | Optional tuple of exception types that trigger fallback; if empty, any exception |
24+
| **Circuit Breaker** | Optional; after threshold failures, primary is not called, fallback runs immediately |
25+
26+
!!! tip "When to Use"
27+
Use Event Handler Fallback when domain event side effects (notifications, read model updates, integrations) must degrade gracefully: e.g. enqueue for later or log when the primary handler (e.g. external notification API) fails.
28+
29+
## Registration
30+
31+
Bind the event type to `EventHandlerFallback(primary, fallback, ...)` in your domain events mapper:
32+
33+
```python
34+
import cqrs
35+
36+
def events_mapper(mapper: cqrs.EventMap) -> None:
37+
mapper.bind(
38+
NotificationSent,
39+
cqrs.EventHandlerFallback(
40+
primary=PrimaryNotificationSentHandler,
41+
fallback=FallbackNotificationSentHandler,
42+
failure_exceptions=(ConnectionError, TimeoutError), # optional
43+
circuit_breaker=event_cb, # optional
44+
),
45+
)
46+
```
47+
48+
- **primary** — The primary event handler class (`EventHandler[EventType]`).
49+
- **fallback** — The fallback event handler class; must handle the same event type.
50+
- **failure_exceptions** — If non-empty, only these exception types trigger fallback; otherwise any exception triggers fallback.
51+
- **circuit_breaker** — Optional `ICircuitBreaker` instance (e.g. `AioBreakerAdapter`). Use one instance per domain (e.g. one for events). When the circuit is open, the primary handler is not called and the fallback runs immediately.
52+
53+
## Basic Example
54+
55+
```python
56+
class NotificationSent(cqrs.DomainEvent, frozen=True):
57+
user_id: str
58+
message: str
59+
60+
class PrimaryNotificationSentHandler(cqrs.EventHandler[NotificationSent]):
61+
async def handle(self, event: NotificationSent) -> None:
62+
# e.g. call external notification API
63+
raise RuntimeError("External notification service unavailable")
64+
65+
class FallbackNotificationSentHandler(cqrs.EventHandler[NotificationSent]):
66+
async def handle(self, event: NotificationSent) -> None:
67+
# e.g. enqueue for later or log
68+
logger.info("Enqueue notification for user %s: %s", event.user_id, event.message)
69+
70+
# In events mapper:
71+
mapper.bind(
72+
NotificationSent,
73+
cqrs.EventHandlerFallback(
74+
primary=PrimaryNotificationSentHandler,
75+
fallback=FallbackNotificationSentHandler,
76+
),
77+
)
78+
```
79+
80+
When a command handler emits `NotificationSent`, the event emitter runs the primary handler first. On exception (or when the circuit is open), the fallback handler is invoked. Events from the handler that actually ran are collected and processed.
81+
82+
## Circuit Breaker (optional)
83+
84+
To use a circuit breaker with event handlers:
85+
86+
```bash
87+
pip install aiobreaker
88+
# or: pip install python-cqrs[aiobreaker]
89+
```
90+
91+
```python
92+
from cqrs.adapters.circuit_breaker import AioBreakerAdapter
93+
94+
event_cb = AioBreakerAdapter(fail_max=5, timeout_duration=60)
95+
mapper.bind(
96+
NotificationSent,
97+
cqrs.EventHandlerFallback(
98+
primary=PrimaryNotificationSentHandler,
99+
fallback=FallbackNotificationSentHandler,
100+
circuit_breaker=event_cb,
101+
),
102+
)
103+
```
104+
105+
After `fail_max` failures, the circuit opens and the fallback runs without calling the primary handler. See [Saga Fallback — Circuit Breaker](../saga/fallback/circuit_breaker.md) for the three-state pattern (CLOSED / OPEN / HALF_OPEN).
106+
107+
### Circuit Breaker configuration
108+
109+
Use the same `AioBreakerAdapter` as for request handlers. Parameters:
110+
111+
| Parameter | Description | Default |
112+
|-----------|-------------|---------|
113+
| `fail_max` | Number of failures before opening the circuit | `5` |
114+
| `timeout_duration` | Seconds to wait before attempting HALF_OPEN (retry) | `60` |
115+
| `exclude` | Exception types that **do not** count as failures (e.g. business/validation errors) | `[]` |
116+
| `storage_factory` | Factory `(name: str) -> storage` for circuit state; default is in-memory | in-memory |
117+
118+
**Example with `exclude`** — e.g. invalid payload should not open the circuit:
119+
120+
```python
121+
event_cb = AioBreakerAdapter(
122+
fail_max=5,
123+
timeout_duration=60,
124+
exclude=[ValidationError], # invalid events don't open the circuit
125+
)
126+
mapper.bind(
127+
NotificationSent,
128+
cqrs.EventHandlerFallback(
129+
primary=PrimaryNotificationSentHandler,
130+
fallback=FallbackNotificationSentHandler,
131+
circuit_breaker=event_cb,
132+
),
133+
)
134+
```
135+
136+
**One instance per domain** — use one `AioBreakerAdapter` for all event handler fallbacks that share the same policy. The adapter creates an isolated circuit per handler type.
137+
138+
**Storage:** Default is in-memory. For multiple instances (e.g. several workers), pass a `storage_factory` that returns Redis storage so the circuit state is shared. See [Saga Fallback — Circuit Breaker: Storage Configuration](../saga/fallback/circuit_breaker.md#storage-configuration-memory-vs-redis).
139+
140+
**Failure filtering:** Use `failure_exceptions` on `EventHandlerFallback` to restrict which exceptions trigger fallback; use `exclude` on `AioBreakerAdapter` so certain exceptions do not open the circuit. See [Saga Fallback — Circuit Breaker](../saga/fallback/circuit_breaker.md#failure-exception-filtering).
141+
142+
## Related
143+
144+
- [Request Handler Fallback](../request_handler/fallback.md) — Fallback for command/query handlers
145+
- [Stream Handling Fallback](../stream_handling/fallback.md) — Fallback for streaming handlers
146+
- [Saga Fallback Pattern](../saga/fallback/index.md) — Fallback for saga steps

docs/event_handler/index.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@
4040

4141
[:octicons-arrow-right-24: Read More](best_practices.md)
4242

43+
- :material-backup-restore: **Fallback**
44+
45+
Fallback handler when primary event handler fails or circuit breaker is open.
46+
47+
[:octicons-arrow-right-24: Read More](fallback.md)
48+
4349
</div>
4450

4551
Event handlers process domain events that are emitted from command handlers. These events represent something that happened in the domain and trigger side effects like sending notifications, updating read models, or triggering other workflows.
@@ -55,7 +61,7 @@ When a command handler processes a request, it can emit domain events through th
5561
| **Side Effects** | Event handlers perform side effects without blocking the main command flow |
5662

5763
!!! note "Prerequisites"
58-
Understanding of [Request Handlers](../request_handler.md) and [Bootstrap](../bootstrap/index.md) is required. Events are emitted by command handlers and processed by event handlers.
64+
Understanding of [Request Handlers](../request_handler/index.md) and [Bootstrap](../bootstrap/index.md) is required. Events are emitted by command handlers and processed by event handlers.
5965

6066
!!! tip "Related Topics"
6167
- [Transaction Outbox](../outbox/index.md) — For reliable event delivery to message brokers

0 commit comments

Comments
 (0)