You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Note over Processor: Process follow-ups in same pipeline (BFS or parallel)
45
47
else NotificationEvent
46
48
Emitter->>Broker: 8. Send to Message Broker
47
49
Broker-->>Emitter: 9. Complete
50
+
Emitter-->>Processor: 10. No follow-ups
48
51
end
49
52
50
-
Emitter-->>Processor: 10. Complete
51
53
Processor-->>Mediator: 11. Complete
52
54
Mediator-->>Client: 12. Return Response
53
55
```
54
56
57
+
!!! note "Follow-up events"
58
+
For domain events, handlers can return follow-up events via the `events` property. The processor continues emitting these in the same pipeline (sequential BFS or parallel with semaphore) until the queue is empty.
59
+
55
60
### Detailed Event Processing Flow
56
61
57
62
```mermaid
@@ -63,32 +68,35 @@ graph TD
63
68
D -->|No| E[Return Response]
64
69
D -->|Yes| F[EventProcessor.emit_events]
65
70
66
-
F -->|For Each Event| G{Parallel Enabled?}
71
+
F -->|Queue: initial events| G{Parallel Enabled?}
72
+
73
+
G -->|No| H[Sequential: BFS]
74
+
G -->|Yes| I[Parallel: Semaphore + FIRST_COMPLETED]
67
75
68
-
G -->|No| H[Sequential: EventEmitter.emit]
69
-
G -->|Yes| I[Parallel: Create Task with Semaphore]
70
-
I --> J[EventEmitter.emit]
76
+
H --> J[Pop Event from Queue]
77
+
I --> J
71
78
72
-
H --> K{Event Type?}
73
-
J --> K
79
+
J --> K[EventEmitter.emit]
80
+
K --> L{Event Type?}
74
81
75
-
K -->|DomainEvent| L[EventEmitter: Find Handlers]
76
-
K -->|NotificationEvent| M[EventEmitter: Send to Broker]
82
+
L -->|DomainEvent| M[EventMap Lookup]
83
+
L -->|NotificationEvent| N[Send to Broker]
77
84
78
-
L --> N[EventMap Lookup]
79
-
N --> O[Resolve Handler from DI]
80
-
O --> P[Execute Event Handler]
81
-
P --> Q{More Events?}
85
+
M --> O[Resolve Handler from DI]
86
+
O --> P[Execute handler.handle]
87
+
P --> Q[Collect handler.events - follow-ups]
88
+
Q --> R[Return follow-ups to Processor]
82
89
83
-
M --> Q
84
-
Q -->|Yes| F
85
-
Q -->|No| E
90
+
N --> R
91
+
R --> S{More in Queue?}
92
+
S -->|Yes| G
93
+
S -->|No| E
86
94
87
95
style A fill:#e1f5ff
88
96
style B fill:#fff3e0
89
97
style F fill:#c8e6c9
90
-
style L fill:#c8e6c9
91
98
style P fill:#c8e6c9
99
+
style Q fill:#fff9c4
92
100
style E fill:#f3e5f5
93
101
```
94
102
@@ -134,7 +142,7 @@ The `EventProcessor` handles parallel or sequential processing based on configur
134
142
135
143
### 3. Event Processing via EventEmitter
136
144
137
-
Events are processed through `EventEmitter`, which routes them based on event type:
145
+
Events are processed through `EventEmitter`, which routes them based on event type. For domain events, after each handler runs, follow-up events from `handler.events` are collected and returned; the processor then continues with these in the same pipeline (BFS in sequential mode, or under the same semaphore in parallel mode).
138
146
139
147
```mermaid
140
148
graph TD
@@ -145,25 +153,54 @@ graph TD
145
153
D -->|No| E[Log Warning]
146
154
D -->|Yes| F[Loop Through Handlers]
147
155
F -->|3. Resolve Handler| G[DI Container]
148
-
G -->|4. Execute Handler| H[Handler.handle]
149
-
H -->|5. Process Side Effects| I[Complete]
156
+
G -->|4. Execute handler.handle| H[Handler.handle]
157
+
H -->|5. Collect handler.events| I[Follow-up events]
158
+
I --> J[Return follow-ups to Processor]
150
159
151
-
B -->|NotificationEvent| J{Message Broker?}
152
-
J -->|No| K[Raise RuntimeError]
153
-
J -->|Yes| L[Send to Message Broker]
154
-
L --> I
160
+
B -->|NotificationEvent| K{Message Broker?}
161
+
K -->|No| L[Raise RuntimeError]
162
+
K -->|Yes| M[Send to Message Broker]
163
+
M --> N[Return empty - no follow-ups]
155
164
156
165
style A fill:#e1f5ff
157
166
style H fill:#c8e6c9
158
-
style I fill:#fff3e0
167
+
style I fill:#fff9c4
168
+
style J fill:#fff3e0
169
+
```
170
+
171
+
### 3.1. Follow-up events from event handlers (event propagation)
172
+
173
+
Event handlers can produce **follow-up events** by implementing the `events` property. After `handle()` is called, the emitter reads `handler.events` and returns them to the processor. These follow-ups are processed in the **same pipeline**:
174
+
175
+
| Mode | Behavior |
176
+
|------|----------|
177
+
|**Sequential** (`concurrent_event_handle_enable=False`) | Events and follow-ups are processed in **BFS order**: one event at a time, then its follow-ups are appended to the queue. |
178
+
|**Parallel** (`concurrent_event_handle_enable=True`) | Events are processed under a semaphore; as soon as any task completes, its follow-ups are queued and started (FIRST_COMPLETED), without waiting for sibling events. |
179
+
180
+
This allows **multi-level event chains**: e.g. `OrderCreated` → handler emits `InventoryReserved` → handler emits `NotificationScheduled`, all in one run.
`EventEmitter` automatically routes events based on their type:
164
201
165
-
-**DomainEvent** — Processed by event handlers registered in EventMap (in-process, synchronous)
166
-
-**NotificationEvent** — Sent to message broker (Kafka, RabbitMQ, etc.) for asynchronous processing
202
+
-**DomainEvent** — Processed by event handlers registered in EventMap (in-process). Handlers may return follow-up events via the `events` property; these are processed in the same pipeline (BFS or parallel with semaphore).
203
+
-**NotificationEvent** — Sent to message broker (Kafka, RabbitMQ, etc.) for asynchronous processing; no follow-ups.
167
204
168
205
!!! important "Single Processing"
169
-
Events are processed **only once** through EventEmitter. There is no duplicate processing - DomainEvents are handled by event handlers, and NotificationEvents are sent to message brokers.
206
+
Each event instance is processed **only once** through EventEmitter. Follow-up events returned by handlers are **new** events that are then processed in the same run (same pipeline) until the queue is empty.
Copy file name to clipboardExpand all lines: docs/event_handler/event_types.md
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -22,7 +22,7 @@
22
22
23
23
### DomainEvent
24
24
25
-
Domain events represent something that happened in the domain. They are processed by event handlers:
25
+
Domain events represent something that happened in the domain. They are processed by event handlers. Handlers can return **follow-up events** via the `events` property; these are processed in the same pipeline (see [Event Flow](event_flow.md)).
26
26
27
27
```python
28
28
classUserJoined(cqrs.DomainEvent, frozen=True):
@@ -32,6 +32,7 @@ class UserJoined(cqrs.DomainEvent, frozen=True):
Event handlers can produce **follow-up events** via the `events` property. These are processed in the same pipeline (BFS in sequential mode, or under the same semaphore in parallel mode), enabling multi-level chains (e.g. L1 → L2 → L3).
91
+
92
+
```python
93
+
import typing
94
+
import cqrs
95
+
from cqrs.events.event import IEvent
96
+
97
+
# Level 1: emitted by command handler
98
+
classEventL1(cqrs.DomainEvent, frozen=True):
99
+
seed: str
100
+
101
+
# Level 2: emitted by HandlerL1
102
+
classEventL2(cqrs.DomainEvent, frozen=True):
103
+
seed: str
104
+
105
+
# Level 3: emitted by HandlerL2 (terminal)
106
+
classEventL3(cqrs.DomainEvent, frozen=True):
107
+
seed: str
108
+
109
+
classHandlerL1(cqrs.EventHandler[EventL1]):
110
+
def__init__(self) -> None:
111
+
self._follow_ups: list[IEvent] = []
112
+
113
+
@property
114
+
defevents(self) -> typing.Sequence[IEvent]:
115
+
returntuple(self._follow_ups)
116
+
117
+
asyncdefhandle(self, event: EventL1) -> None:
118
+
# Side effects...
119
+
self._follow_ups.append(EventL2(seed=event.seed))
120
+
121
+
classHandlerL2(cqrs.EventHandler[EventL2]):
122
+
def__init__(self) -> None:
123
+
self._follow_ups: list[IEvent] = []
124
+
125
+
@property
126
+
defevents(self) -> typing.Sequence[IEvent]:
127
+
returntuple(self._follow_ups)
128
+
129
+
asyncdefhandle(self, event: EventL2) -> None:
130
+
# Side effects...
131
+
self._follow_ups.append(EventL3(seed=event.seed))
132
+
133
+
classHandlerL3(cqrs.EventHandler[EventL3]):
134
+
asyncdefhandle(self, event: EventL3) -> None:
135
+
# Terminal handler — no follow-ups (default events = ())
When you emit `EventL1(seed="x")`, the processor runs: L1 → HandlerL1 emits L2 → HandlerL2 emits L3 → HandlerL3 runs. All in the same `emit_events()` call (sequential BFS or parallel with FIRST_COMPLETED).
Copy file name to clipboardExpand all lines: docs/event_handler/index.md
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -44,12 +44,13 @@
44
44
45
45
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.
46
46
47
-
When a command handler processes a request, it can emit domain events through the `events` property. These events are automatically collected and processed by event handlers registered in the system.
47
+
When a command handler processes a request, it can emit domain events through the `events` property. These events are automatically collected and processed by event handlers registered in the system.**Event handlers** can in turn produce **follow-up events** via their own `events` property; these follow-ups are processed in the same pipeline (sequential BFS or parallel with semaphore), enabling multi-level event chains.
48
48
49
49
| Aspect | Description |
50
50
|--------|-------------|
51
51
|**Runtime Processing**| Events are processed synchronously in the same request context, not asynchronously |
52
52
|**Automatic Dispatch**| Events are automatically dispatched to registered handlers after command execution |
53
+
|**Event Propagation**| Handlers can return follow-up events via `events`; they are processed in the same run (BFS or parallel) |
53
54
|**Parallel Support**| Multiple events can be processed in parallel with configurable concurrency limits |
54
55
|**Side Effects**| Event handlers perform side effects without blocking the main command flow |
Copy file name to clipboardExpand all lines: docs/event_handler/parallel_processing.md
+41-33Lines changed: 41 additions & 33 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -22,34 +22,39 @@ Events can be processed in parallel to improve performance. This is controlled b
22
22
23
23
### How Parallel Processing Works
24
24
25
+
In **sequential** mode, events and follow-ups (from `handler.events`) are processed in **BFS order**: one event at a time, then its follow-ups are appended to the queue. In **parallel** mode, events are processed under a semaphore; as soon as any task completes (FIRST_COMPLETED), its follow-up events are queued and started without waiting for sibling events. `emit_events` returns only when all events and follow-ups are done.
RoutePar -->|NotificationEvent| BrokerPar[Send to Broker]
48
-
HandlerPar --> ReleaseSem[Release Semaphore]
49
-
BrokerPar --> ReleaseSem
50
-
ReleaseSem --> NextPar{More Events?}
51
-
NextPar -->|Yes| LoopPar
52
-
NextPar -->|No| End2[End]
51
+
HandlerPar --> CollectPar[Collect follow-ups]
52
+
BrokerPar --> CollectPar
53
+
CollectPar --> QueuePar[Queue follow-ups, start new tasks]
54
+
QueuePar --> WaitPar[Wait FIRST_COMPLETED]
55
+
WaitPar --> MorePar{Pending or queue?}
56
+
MorePar -->|Yes| PopPar
57
+
MorePar -->|No| End2[End]
53
58
54
59
style Start fill:#e1f5ff
55
60
style Sequential fill:#fff3e0
@@ -59,7 +64,7 @@ graph TD
59
64
60
65
### Implementation
61
66
62
-
The `EventProcessor` handles parallel or sequential event emission:
67
+
The `EventProcessor` handles parallel or sequential event emission. Follow-up events returned by handlers (via `handler.events`) are processed in the **same pipeline**: BFS in sequential mode, or under the same semaphore with FIRST_COMPLETED in parallel mode. The method returns when all events and follow-ups are done.
0 commit comments