eventsourcing 9.3.4__py3-none-any.whl → 9.4.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.
Potentially problematic release.
This version of eventsourcing might be problematic. Click here for more details.
- eventsourcing/__init__.py +0 -1
- eventsourcing/application.py +115 -188
- eventsourcing/cipher.py +9 -10
- eventsourcing/compressor.py +2 -6
- eventsourcing/cryptography.py +91 -0
- eventsourcing/dispatch.py +52 -11
- eventsourcing/domain.py +733 -690
- eventsourcing/interface.py +39 -32
- eventsourcing/persistence.py +412 -287
- eventsourcing/popo.py +136 -44
- eventsourcing/postgres.py +404 -187
- eventsourcing/projection.py +428 -0
- eventsourcing/sqlite.py +167 -55
- eventsourcing/system.py +253 -341
- eventsourcing/tests/__init__.py +3 -0
- eventsourcing/tests/application.py +195 -129
- eventsourcing/tests/domain.py +19 -37
- eventsourcing/tests/persistence.py +533 -235
- eventsourcing/tests/postgres_utils.py +12 -9
- eventsourcing/utils.py +39 -47
- {eventsourcing-9.3.4.dist-info → eventsourcing-9.4.0.dist-info}/LICENSE +1 -1
- {eventsourcing-9.3.4.dist-info → eventsourcing-9.4.0.dist-info}/METADATA +14 -13
- eventsourcing-9.4.0.dist-info/RECORD +26 -0
- {eventsourcing-9.3.4.dist-info → eventsourcing-9.4.0.dist-info}/WHEEL +1 -1
- eventsourcing-9.3.4.dist-info/RECORD +0 -24
- {eventsourcing-9.3.4.dist-info → eventsourcing-9.4.0.dist-info}/AUTHORS +0 -0
eventsourcing/system.py
CHANGED
|
@@ -1,42 +1,30 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
+
import threading
|
|
4
5
|
import traceback
|
|
5
6
|
from abc import ABC, abstractmethod
|
|
6
7
|
from collections import defaultdict
|
|
7
8
|
from queue import Full, Queue
|
|
8
|
-
from threading import Event, Lock, RLock, Thread
|
|
9
9
|
from types import FrameType, ModuleType
|
|
10
|
-
from typing import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Iterable,
|
|
16
|
-
Iterator,
|
|
17
|
-
List,
|
|
18
|
-
Optional,
|
|
19
|
-
Sequence,
|
|
20
|
-
Tuple,
|
|
21
|
-
Type,
|
|
22
|
-
Union,
|
|
23
|
-
cast,
|
|
24
|
-
)
|
|
10
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union, cast
|
|
11
|
+
|
|
12
|
+
from eventsourcing.projection import EventSourcedProjection
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Iterable, Iterator, Sequence
|
|
25
16
|
|
|
26
|
-
if TYPE_CHECKING: # pragma: nocover
|
|
27
17
|
from typing_extensions import Self
|
|
28
18
|
|
|
29
19
|
from eventsourcing.application import (
|
|
30
20
|
Application,
|
|
31
21
|
NotificationLog,
|
|
32
|
-
|
|
33
|
-
RecordingEvent,
|
|
22
|
+
ProgrammingError,
|
|
34
23
|
Section,
|
|
35
24
|
TApplication,
|
|
36
25
|
)
|
|
37
|
-
from eventsourcing.domain import DomainEventProtocol
|
|
26
|
+
from eventsourcing.domain import DomainEventProtocol, MutableOrImmutableAggregate
|
|
38
27
|
from eventsourcing.persistence import (
|
|
39
|
-
IntegrityError,
|
|
40
28
|
Mapper,
|
|
41
29
|
Notification,
|
|
42
30
|
ProcessRecorder,
|
|
@@ -45,39 +33,46 @@ from eventsourcing.persistence import (
|
|
|
45
33
|
)
|
|
46
34
|
from eventsourcing.utils import EnvType, get_topic, resolve_topic
|
|
47
35
|
|
|
48
|
-
ProcessingJob =
|
|
49
|
-
ConvertingJob = Optional[Union[RecordingEvent, List[Notification]]]
|
|
36
|
+
ProcessingJob = tuple[DomainEventProtocol, Tracking]
|
|
50
37
|
|
|
51
38
|
|
|
52
|
-
class
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
39
|
+
class RecordingEvent:
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
application_name: str,
|
|
43
|
+
recordings: list[Recording],
|
|
44
|
+
previous_max_notification_id: int | None,
|
|
45
|
+
):
|
|
46
|
+
self.application_name = application_name
|
|
47
|
+
self.recordings = recordings
|
|
48
|
+
self.previous_max_notification_id = previous_max_notification_id
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
ConvertingJob = Optional[Union[RecordingEvent, list[Notification]]]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Follower(EventSourcedProjection):
|
|
55
|
+
"""Extends the :class:`~eventsourcing.projection.EventSourcedProjection` class
|
|
56
|
+
by pulling notification objects from its notification log readers, by converting
|
|
57
|
+
the notification objects to domain events and tracking objects and by processing
|
|
58
|
+
the reconstructed domain event objects.
|
|
58
59
|
"""
|
|
59
60
|
|
|
60
|
-
follow_topics: ClassVar[Sequence[str]] = []
|
|
61
61
|
pull_section_size = 10
|
|
62
62
|
|
|
63
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
64
|
+
super().__init_subclass__(**kwargs)
|
|
65
|
+
# for backwards compatibility, set "topics" if has "follow_topics".
|
|
66
|
+
cls.topics = getattr(cls, "follow_topics", cls.topics)
|
|
67
|
+
|
|
63
68
|
def __init__(self, env: EnvType | None = None) -> None:
|
|
64
69
|
super().__init__(env)
|
|
65
|
-
self.readers:
|
|
66
|
-
self.mappers:
|
|
67
|
-
self.recorder: ProcessRecorder
|
|
70
|
+
self.readers: dict[str, NotificationLogReader] = {}
|
|
71
|
+
self.mappers: dict[str, Mapper] = {}
|
|
68
72
|
self.is_threading_enabled = False
|
|
69
|
-
self.processing_lock = RLock()
|
|
70
|
-
|
|
71
|
-
def construct_recorder(self) -> ProcessRecorder:
|
|
72
|
-
"""
|
|
73
|
-
Constructs and returns a :class:`~eventsourcing.persistence.ProcessRecorder`
|
|
74
|
-
for the application to use as its application recorder.
|
|
75
|
-
"""
|
|
76
|
-
return self.factory.process_recorder()
|
|
77
73
|
|
|
78
74
|
def follow(self, name: str, log: NotificationLog) -> None:
|
|
79
|
-
"""
|
|
80
|
-
Constructs a notification log reader and a mapper for
|
|
75
|
+
"""Constructs a notification log reader and a mapper for
|
|
81
76
|
the named application, and adds them to its collections
|
|
82
77
|
of readers and mappers.
|
|
83
78
|
"""
|
|
@@ -95,13 +90,11 @@ class Follower(Application):
|
|
|
95
90
|
def pull_and_process(
|
|
96
91
|
self, leader_name: str, start: int | None = None, stop: int | None = None
|
|
97
92
|
) -> None:
|
|
98
|
-
"""
|
|
99
|
-
Pull and process new domain event notifications.
|
|
100
|
-
"""
|
|
93
|
+
"""Pull and process new domain event notifications."""
|
|
101
94
|
if start is None:
|
|
102
|
-
start = self.recorder.max_tracking_id(leader_name)
|
|
95
|
+
start = self.recorder.max_tracking_id(leader_name)
|
|
103
96
|
for notifications in self.pull_notifications(
|
|
104
|
-
leader_name, start=start, stop=stop
|
|
97
|
+
leader_name, start=start, stop=stop, inclusive_of_start=False
|
|
105
98
|
):
|
|
106
99
|
notifications_iter = self.filter_received_notifications(notifications)
|
|
107
100
|
for domain_event, tracking in self.convert_notifications(
|
|
@@ -109,29 +102,41 @@ class Follower(Application):
|
|
|
109
102
|
):
|
|
110
103
|
self.process_event(domain_event, tracking)
|
|
111
104
|
|
|
105
|
+
def process_event(
|
|
106
|
+
self, domain_event: DomainEventProtocol, tracking: Tracking
|
|
107
|
+
) -> None:
|
|
108
|
+
with self.processing_lock:
|
|
109
|
+
super().process_event(domain_event, tracking)
|
|
110
|
+
|
|
112
111
|
def pull_notifications(
|
|
113
|
-
self,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
self,
|
|
113
|
+
leader_name: str,
|
|
114
|
+
start: int | None,
|
|
115
|
+
stop: int | None = None,
|
|
116
|
+
*,
|
|
117
|
+
inclusive_of_start: bool = True,
|
|
118
|
+
) -> Iterator[list[Notification]]:
|
|
119
|
+
"""Pulls batches of unseen :class:`~eventsourcing.persistence.Notification`
|
|
117
120
|
objects from the notification log reader of the named application.
|
|
118
121
|
"""
|
|
119
122
|
return self.readers[leader_name].select(
|
|
120
|
-
start=start,
|
|
123
|
+
start=start,
|
|
124
|
+
stop=stop,
|
|
125
|
+
topics=self.topics,
|
|
126
|
+
inclusive_of_start=inclusive_of_start,
|
|
121
127
|
)
|
|
122
128
|
|
|
123
129
|
def filter_received_notifications(
|
|
124
|
-
self, notifications:
|
|
125
|
-
) ->
|
|
126
|
-
if self.
|
|
127
|
-
return [n for n in notifications if n.topic in self.
|
|
130
|
+
self, notifications: list[Notification]
|
|
131
|
+
) -> list[Notification]:
|
|
132
|
+
if self.topics:
|
|
133
|
+
return [n for n in notifications if n.topic in self.topics]
|
|
128
134
|
return notifications
|
|
129
135
|
|
|
130
136
|
def convert_notifications(
|
|
131
137
|
self, leader_name: str, notifications: Iterable[Notification]
|
|
132
|
-
) ->
|
|
133
|
-
"""
|
|
134
|
-
Uses the given :class:`~eventsourcing.persistence.Mapper` to convert
|
|
138
|
+
) -> list[ProcessingJob]:
|
|
139
|
+
"""Uses the given :class:`~eventsourcing.persistence.Mapper` to convert
|
|
135
140
|
each received :class:`~eventsourcing.persistence.Notification`
|
|
136
141
|
object to an :class:`~eventsourcing.domain.AggregateEvent` object
|
|
137
142
|
paired with a :class:`~eventsourcing.persistence.Tracking` object.
|
|
@@ -147,82 +152,17 @@ class Follower(Application):
|
|
|
147
152
|
processing_jobs.append((domain_event, tracking))
|
|
148
153
|
return processing_jobs
|
|
149
154
|
|
|
150
|
-
# @retry(IntegrityError, max_attempts=50000, wait=0.01)
|
|
151
|
-
def process_event(
|
|
152
|
-
self, domain_event: DomainEventProtocol, tracking: Tracking
|
|
153
|
-
) -> None:
|
|
154
|
-
"""
|
|
155
|
-
Calls :func:`~eventsourcing.system.Follower.policy` method with
|
|
156
|
-
the given :class:`~eventsourcing.domain.AggregateEvent` and a
|
|
157
|
-
new :class:`~eventsourcing.application.ProcessingEvent` created from
|
|
158
|
-
the given :class:`~eventsourcing.persistence.Tracking` object.
|
|
159
|
-
|
|
160
|
-
The policy will collect any new aggregate events on the process
|
|
161
|
-
event object.
|
|
162
|
-
|
|
163
|
-
After the policy method returns, the process event object will
|
|
164
|
-
then be recorded by calling
|
|
165
|
-
:func:`~eventsourcing.application.Application.record`, which
|
|
166
|
-
will return new notifications.
|
|
167
|
-
|
|
168
|
-
After calling
|
|
169
|
-
:func:`~eventsourcing.application.Application.take_snapshots`,
|
|
170
|
-
the new notifications are passed to the
|
|
171
|
-
:func:`~eventsourcing.application.Application.notify` method.
|
|
172
|
-
"""
|
|
173
|
-
processing_event = ProcessingEvent(tracking=tracking)
|
|
174
|
-
with self.processing_lock:
|
|
175
|
-
self.policy(domain_event, processing_event)
|
|
176
|
-
try:
|
|
177
|
-
recordings = self._record(processing_event)
|
|
178
|
-
except IntegrityError:
|
|
179
|
-
if self.recorder.has_tracking_id(
|
|
180
|
-
tracking.application_name,
|
|
181
|
-
tracking.notification_id,
|
|
182
|
-
):
|
|
183
|
-
pass
|
|
184
|
-
else:
|
|
185
|
-
raise
|
|
186
|
-
else:
|
|
187
|
-
self._take_snapshots(processing_event)
|
|
188
|
-
self.notify(processing_event.events)
|
|
189
|
-
self._notify(recordings)
|
|
190
|
-
|
|
191
|
-
@abstractmethod
|
|
192
|
-
def policy(
|
|
193
|
-
self,
|
|
194
|
-
domain_event: DomainEventProtocol,
|
|
195
|
-
processing_event: ProcessingEvent,
|
|
196
|
-
) -> None:
|
|
197
|
-
"""
|
|
198
|
-
Abstract domain event processing policy method. Must be
|
|
199
|
-
implemented by event processing applications. When
|
|
200
|
-
processing the given domain event, event processing
|
|
201
|
-
applications must use the :func:`~ProcessingEvent.collect_events`
|
|
202
|
-
method of the given process event object (instead of
|
|
203
|
-
the application's :func:`~eventsourcing.application.Application.save`
|
|
204
|
-
method) to collect pending events from changed aggregates,
|
|
205
|
-
so that the new domain events will be recorded atomically
|
|
206
|
-
with tracking information about the position of the given
|
|
207
|
-
domain event's notification.
|
|
208
|
-
"""
|
|
209
|
-
|
|
210
155
|
|
|
211
156
|
class RecordingEventReceiver(ABC):
|
|
212
|
-
"""
|
|
213
|
-
Abstract base class for objects that may receive recording events.
|
|
214
|
-
"""
|
|
157
|
+
"""Abstract base class for objects that may receive recording events."""
|
|
215
158
|
|
|
216
159
|
@abstractmethod
|
|
217
|
-
def receive_recording_event(self,
|
|
218
|
-
"""
|
|
219
|
-
Receives a recording event.
|
|
220
|
-
"""
|
|
160
|
+
def receive_recording_event(self, new_recording_event: RecordingEvent) -> None:
|
|
161
|
+
"""Receives a recording event."""
|
|
221
162
|
|
|
222
163
|
|
|
223
164
|
class Leader(Application):
|
|
224
|
-
"""
|
|
225
|
-
Extends the :class:`~eventsourcing.application.Application`
|
|
165
|
+
"""Extends the :class:`~eventsourcing.application.Application`
|
|
226
166
|
class by also being responsible for keeping track of
|
|
227
167
|
followers, and prompting followers when there are new
|
|
228
168
|
domain event notifications to be pulled and processed.
|
|
@@ -230,17 +170,24 @@ class Leader(Application):
|
|
|
230
170
|
|
|
231
171
|
def __init__(self, env: EnvType | None = None) -> None:
|
|
232
172
|
super().__init__(env)
|
|
233
|
-
self.
|
|
173
|
+
self.previous_max_notification_id: int | None = None
|
|
174
|
+
self.followers: list[RecordingEventReceiver] = []
|
|
234
175
|
|
|
235
176
|
def lead(self, follower: RecordingEventReceiver) -> None:
|
|
236
|
-
"""
|
|
237
|
-
Adds given follower to a list of followers.
|
|
238
|
-
"""
|
|
177
|
+
"""Adds given follower to a list of followers."""
|
|
239
178
|
self.followers.append(follower)
|
|
240
179
|
|
|
241
|
-
def
|
|
242
|
-
|
|
243
|
-
|
|
180
|
+
def save(
|
|
181
|
+
self,
|
|
182
|
+
*objs: MutableOrImmutableAggregate | DomainEventProtocol | None,
|
|
183
|
+
**kwargs: Any,
|
|
184
|
+
) -> list[Recording]:
|
|
185
|
+
if self.previous_max_notification_id is None:
|
|
186
|
+
self.previous_max_notification_id = self.recorder.max_notification_id()
|
|
187
|
+
return super().save(*objs, **kwargs)
|
|
188
|
+
|
|
189
|
+
def _notify(self, recordings: list[Recording]) -> None:
|
|
190
|
+
"""Calls :func:`receive_recording_event` on each follower
|
|
244
191
|
whenever new events have just been saved.
|
|
245
192
|
"""
|
|
246
193
|
super()._notify(recordings)
|
|
@@ -260,31 +207,28 @@ class Leader(Application):
|
|
|
260
207
|
|
|
261
208
|
|
|
262
209
|
class ProcessApplication(Leader, Follower):
|
|
263
|
-
"""
|
|
264
|
-
Base class for event processing applications
|
|
210
|
+
"""Base class for event processing applications
|
|
265
211
|
that are both "leaders" and followers".
|
|
266
212
|
"""
|
|
267
213
|
|
|
268
214
|
|
|
269
215
|
class System:
|
|
270
|
-
"""
|
|
271
|
-
Defines a system of applications.
|
|
272
|
-
"""
|
|
216
|
+
"""Defines a system of applications."""
|
|
273
217
|
|
|
274
|
-
__caller_modules: ClassVar[
|
|
218
|
+
__caller_modules: ClassVar[dict[int, ModuleType]] = {}
|
|
275
219
|
|
|
276
220
|
def __init__(
|
|
277
221
|
self,
|
|
278
|
-
pipes: Iterable[Iterable[
|
|
222
|
+
pipes: Iterable[Iterable[type[Application]]],
|
|
279
223
|
):
|
|
280
224
|
# Remember the caller frame's module, so that we might identify a topic.
|
|
281
|
-
caller_frame = cast(FrameType,
|
|
225
|
+
caller_frame = cast(FrameType, inspect.currentframe()).f_back
|
|
282
226
|
module = cast(ModuleType, inspect.getmodule(caller_frame))
|
|
283
|
-
type(self).__caller_modules[id(self)] = module
|
|
227
|
+
type(self).__caller_modules[id(self)] = module # noqa: SLF001
|
|
284
228
|
|
|
285
229
|
# Build nodes and edges.
|
|
286
|
-
self.edges:
|
|
287
|
-
classes:
|
|
230
|
+
self.edges: list[tuple[str, str]] = []
|
|
231
|
+
classes: dict[str, type[Application]] = {}
|
|
288
232
|
for pipe in pipes:
|
|
289
233
|
follower_cls = None
|
|
290
234
|
for cls in pipe:
|
|
@@ -298,14 +242,13 @@ class System:
|
|
|
298
242
|
if edge not in self.edges:
|
|
299
243
|
self.edges.append(edge)
|
|
300
244
|
|
|
301
|
-
self.nodes:
|
|
302
|
-
for name in classes:
|
|
303
|
-
|
|
304
|
-
self.nodes[name] = topic
|
|
245
|
+
self.nodes: dict[str, str] = {}
|
|
246
|
+
for name, cls in classes.items():
|
|
247
|
+
self.nodes[name] = get_topic(cls)
|
|
305
248
|
|
|
306
249
|
# Identify leaders and followers.
|
|
307
|
-
self.follows:
|
|
308
|
-
self.leads:
|
|
250
|
+
self.follows: dict[str, list[str]] = defaultdict(list)
|
|
251
|
+
self.leads: dict[str, list[str]] = defaultdict(list)
|
|
309
252
|
for edge in self.edges:
|
|
310
253
|
self.leads[edge[0]].append(edge[1])
|
|
311
254
|
self.follows[edge[1]].append(edge[0])
|
|
@@ -319,35 +262,37 @@ class System:
|
|
|
319
262
|
# Check followers are followers.
|
|
320
263
|
for name in self.follows:
|
|
321
264
|
if not issubclass(classes[name], Follower):
|
|
322
|
-
|
|
265
|
+
msg = f"Not a follower class: {classes[name]}"
|
|
266
|
+
raise TypeError(msg)
|
|
323
267
|
|
|
324
268
|
# Check each process is a process application class.
|
|
325
269
|
for name in self.processors:
|
|
326
270
|
if not issubclass(classes[name], ProcessApplication):
|
|
327
|
-
|
|
271
|
+
msg = f"Not a process application class: {classes[name]}"
|
|
272
|
+
raise TypeError(msg)
|
|
328
273
|
|
|
329
274
|
@property
|
|
330
|
-
def leaders(self) ->
|
|
275
|
+
def leaders(self) -> list[str]:
|
|
331
276
|
return list(self.leads.keys())
|
|
332
277
|
|
|
333
278
|
@property
|
|
334
|
-
def leaders_only(self) ->
|
|
279
|
+
def leaders_only(self) -> list[str]:
|
|
335
280
|
return [name for name in self.leads if name not in self.follows]
|
|
336
281
|
|
|
337
282
|
@property
|
|
338
|
-
def followers(self) ->
|
|
283
|
+
def followers(self) -> list[str]:
|
|
339
284
|
return list(self.follows.keys())
|
|
340
285
|
|
|
341
286
|
@property
|
|
342
|
-
def processors(self) ->
|
|
287
|
+
def processors(self) -> list[str]:
|
|
343
288
|
return [name for name in self.leads if name in self.follows]
|
|
344
289
|
|
|
345
|
-
def get_app_cls(self, name: str) ->
|
|
290
|
+
def get_app_cls(self, name: str) -> type[Application]:
|
|
346
291
|
cls = resolve_topic(self.nodes[name])
|
|
347
292
|
assert issubclass(cls, Application)
|
|
348
293
|
return cls
|
|
349
294
|
|
|
350
|
-
def leader_cls(self, name: str) ->
|
|
295
|
+
def leader_cls(self, name: str) -> type[Leader]:
|
|
351
296
|
cls = self.get_app_cls(name)
|
|
352
297
|
if issubclass(cls, Leader):
|
|
353
298
|
return cls
|
|
@@ -355,13 +300,13 @@ class System:
|
|
|
355
300
|
assert issubclass(cls, Leader)
|
|
356
301
|
return cls
|
|
357
302
|
|
|
358
|
-
def follower_cls(self, name: str) ->
|
|
303
|
+
def follower_cls(self, name: str) -> type[Follower]:
|
|
359
304
|
cls = self.get_app_cls(name)
|
|
360
305
|
assert issubclass(cls, Follower)
|
|
361
306
|
return cls
|
|
362
307
|
|
|
363
308
|
@property
|
|
364
|
-
def topic(self) -> str
|
|
309
|
+
def topic(self) -> str:
|
|
365
310
|
"""
|
|
366
311
|
Returns a topic to the system object, if constructed as a module attribute.
|
|
367
312
|
"""
|
|
@@ -371,13 +316,14 @@ class System:
|
|
|
371
316
|
if value is self:
|
|
372
317
|
topic = module.__name__ + ":" + name
|
|
373
318
|
assert resolve_topic(topic) is self
|
|
319
|
+
if topic is None:
|
|
320
|
+
msg = f"Unable to compute topic for system object: {self}"
|
|
321
|
+
raise ProgrammingError(msg)
|
|
374
322
|
return topic
|
|
375
323
|
|
|
376
324
|
|
|
377
325
|
class Runner(ABC):
|
|
378
|
-
"""
|
|
379
|
-
Abstract base class for system runners.
|
|
380
|
-
"""
|
|
326
|
+
"""Abstract base class for system runners."""
|
|
381
327
|
|
|
382
328
|
def __init__(self, system: System, env: EnvType | None = None):
|
|
383
329
|
self.system = system
|
|
@@ -386,65 +332,54 @@ class Runner(ABC):
|
|
|
386
332
|
|
|
387
333
|
@abstractmethod
|
|
388
334
|
def start(self) -> None:
|
|
389
|
-
"""
|
|
390
|
-
Starts the runner.
|
|
391
|
-
"""
|
|
335
|
+
"""Starts the runner."""
|
|
392
336
|
if self.is_started:
|
|
393
337
|
raise RunnerAlreadyStartedError
|
|
394
338
|
self.is_started = True
|
|
395
339
|
|
|
396
340
|
@abstractmethod
|
|
397
341
|
def stop(self) -> None:
|
|
398
|
-
"""
|
|
399
|
-
Stops the runner.
|
|
400
|
-
"""
|
|
342
|
+
"""Stops the runner."""
|
|
401
343
|
|
|
402
344
|
@abstractmethod
|
|
403
|
-
def get(self, cls:
|
|
404
|
-
"""
|
|
405
|
-
|
|
406
|
-
|
|
345
|
+
def get(self, cls: type[TApplication]) -> TApplication:
|
|
346
|
+
"""Returns an application instance for given application class."""
|
|
347
|
+
|
|
348
|
+
def __enter__(self) -> Self:
|
|
349
|
+
self.start()
|
|
350
|
+
return self
|
|
351
|
+
|
|
352
|
+
def __exit__(self, *args: object, **kwargs: Any) -> None:
|
|
353
|
+
self.stop()
|
|
407
354
|
|
|
408
355
|
|
|
409
356
|
class RunnerAlreadyStartedError(Exception):
|
|
410
|
-
"""
|
|
411
|
-
Raised when runner is already started.
|
|
412
|
-
"""
|
|
357
|
+
"""Raised when runner is already started."""
|
|
413
358
|
|
|
414
359
|
|
|
415
360
|
class NotificationPullingError(Exception):
|
|
416
|
-
"""
|
|
417
|
-
Raised when pulling notifications fails.
|
|
418
|
-
"""
|
|
361
|
+
"""Raised when pulling notifications fails."""
|
|
419
362
|
|
|
420
363
|
|
|
421
364
|
class NotificationConvertingError(Exception):
|
|
422
|
-
"""
|
|
423
|
-
Raised when converting notifications fails.
|
|
424
|
-
"""
|
|
365
|
+
"""Raised when converting notifications fails."""
|
|
425
366
|
|
|
426
367
|
|
|
427
368
|
class EventProcessingError(Exception):
|
|
428
|
-
"""
|
|
429
|
-
Raised when event processing fails.
|
|
430
|
-
"""
|
|
369
|
+
"""Raised when event processing fails."""
|
|
431
370
|
|
|
432
371
|
|
|
433
372
|
class SingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
434
|
-
"""
|
|
435
|
-
Runs a :class:`System` in a single thread.
|
|
436
|
-
"""
|
|
373
|
+
"""Runs a :class:`System` in a single thread."""
|
|
437
374
|
|
|
438
375
|
def __init__(self, system: System, env: EnvType | None = None):
|
|
439
|
-
"""
|
|
440
|
-
Initialises runner with the given :class:`System`.
|
|
441
|
-
"""
|
|
376
|
+
"""Initialises runner with the given :class:`System`."""
|
|
442
377
|
super().__init__(system=system, env=env)
|
|
443
|
-
self.apps:
|
|
444
|
-
self._recording_events_received:
|
|
445
|
-
self._prompted_names_lock = Lock()
|
|
378
|
+
self.apps: dict[str, Application] = {}
|
|
379
|
+
self._recording_events_received: list[RecordingEvent] = []
|
|
380
|
+
self._prompted_names_lock = threading.Lock()
|
|
446
381
|
self._prompted_names: set[str] = set()
|
|
447
|
-
self._processing_lock = Lock()
|
|
382
|
+
self._processing_lock = threading.Lock()
|
|
448
383
|
|
|
449
384
|
# Construct followers.
|
|
450
385
|
for name in self.system.followers:
|
|
@@ -461,34 +396,31 @@ class SingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
461
396
|
self.apps[name] = single
|
|
462
397
|
|
|
463
398
|
def start(self) -> None:
|
|
464
|
-
"""
|
|
465
|
-
Starts the runner. The applications mentioned in the system definition
|
|
399
|
+
"""Starts the runner. The applications mentioned in the system definition
|
|
466
400
|
are constructed. The followers are set up to follow the applications
|
|
467
401
|
they are defined as following in the system definition. And the leaders
|
|
468
402
|
are set up to lead the runner itself.
|
|
469
403
|
"""
|
|
470
|
-
|
|
471
404
|
super().start()
|
|
472
405
|
|
|
473
406
|
# Setup followers to follow leaders.
|
|
474
407
|
for edge in self.system.edges:
|
|
475
408
|
leader_name = edge[0]
|
|
476
409
|
follower_name = edge[1]
|
|
477
|
-
leader = cast(Leader, self.apps[leader_name])
|
|
478
|
-
follower = cast(Follower, self.apps[follower_name])
|
|
410
|
+
leader = cast("Leader", self.apps[leader_name])
|
|
411
|
+
follower = cast("Follower", self.apps[follower_name])
|
|
479
412
|
assert isinstance(leader, Leader)
|
|
480
413
|
assert isinstance(follower, Follower)
|
|
481
414
|
follower.follow(leader_name, leader.notification_log)
|
|
482
415
|
|
|
483
416
|
# Setup leaders to lead this runner.
|
|
484
417
|
for name in self.system.leaders:
|
|
485
|
-
leader = cast(Leader, self.apps[name])
|
|
418
|
+
leader = cast("Leader", self.apps[name])
|
|
486
419
|
assert isinstance(leader, Leader)
|
|
487
420
|
leader.lead(self)
|
|
488
421
|
|
|
489
|
-
def receive_recording_event(self,
|
|
490
|
-
"""
|
|
491
|
-
Receives recording event by appending the name of the leader
|
|
422
|
+
def receive_recording_event(self, new_recording_event: RecordingEvent) -> None:
|
|
423
|
+
"""Receives recording event by appending the name of the leader
|
|
492
424
|
to a list of prompted names.
|
|
493
425
|
|
|
494
426
|
Then, unless this method has previously been called and not yet returned,
|
|
@@ -498,7 +430,7 @@ class SingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
498
430
|
continues until there are no more prompted names. In this way, a system
|
|
499
431
|
of applications will process all events in a single thread.
|
|
500
432
|
"""
|
|
501
|
-
leader_name =
|
|
433
|
+
leader_name = new_recording_event.application_name
|
|
502
434
|
with self._prompted_names_lock:
|
|
503
435
|
self._prompted_names.add(leader_name)
|
|
504
436
|
|
|
@@ -514,7 +446,7 @@ class SingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
514
446
|
|
|
515
447
|
for leader_name in prompted_names:
|
|
516
448
|
for follower_name in self.system.leads[leader_name]:
|
|
517
|
-
follower = cast(Follower, self.apps[follower_name])
|
|
449
|
+
follower = cast("Follower", self.apps[follower_name])
|
|
518
450
|
follower.pull_and_process(leader_name)
|
|
519
451
|
|
|
520
452
|
finally:
|
|
@@ -525,34 +457,23 @@ class SingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
525
457
|
app.close()
|
|
526
458
|
self.apps.clear()
|
|
527
459
|
|
|
528
|
-
def get(self, cls:
|
|
460
|
+
def get(self, cls: type[TApplication]) -> TApplication:
|
|
529
461
|
app = self.apps[cls.name]
|
|
530
462
|
assert isinstance(app, cls)
|
|
531
463
|
return app
|
|
532
464
|
|
|
533
|
-
def __enter__(self) -> Self:
|
|
534
|
-
self.start()
|
|
535
|
-
return self
|
|
536
|
-
|
|
537
|
-
def __exit__(self, *args: object, **kwargs: Any) -> None:
|
|
538
|
-
self.stop()
|
|
539
|
-
|
|
540
465
|
|
|
541
466
|
class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
542
|
-
"""
|
|
543
|
-
Runs a :class:`System` in a single thread.
|
|
544
|
-
"""
|
|
467
|
+
"""Runs a :class:`System` in a single thread."""
|
|
545
468
|
|
|
546
469
|
def __init__(self, system: System, env: EnvType | None = None):
|
|
547
|
-
"""
|
|
548
|
-
Initialises runner with the given :class:`System`.
|
|
549
|
-
"""
|
|
470
|
+
"""Initialises runner with the given :class:`System`."""
|
|
550
471
|
super().__init__(system=system, env=env)
|
|
551
|
-
self.apps:
|
|
552
|
-
self._recording_events_received:
|
|
553
|
-
self._recording_events_received_lock = Lock()
|
|
554
|
-
self._processing_lock = Lock()
|
|
555
|
-
self._previous_max_notification_ids:
|
|
472
|
+
self.apps: dict[str, Application] = {}
|
|
473
|
+
self._recording_events_received: list[RecordingEvent] = []
|
|
474
|
+
self._recording_events_received_lock = threading.Lock()
|
|
475
|
+
self._processing_lock = threading.Lock()
|
|
476
|
+
self._previous_max_notification_ids: dict[str, int] = {}
|
|
556
477
|
|
|
557
478
|
# Construct followers.
|
|
558
479
|
for name in self.system.followers:
|
|
@@ -569,8 +490,7 @@ class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
569
490
|
self.apps[name] = single
|
|
570
491
|
|
|
571
492
|
def start(self) -> None:
|
|
572
|
-
"""
|
|
573
|
-
Starts the runner.
|
|
493
|
+
"""Starts the runner.
|
|
574
494
|
The applications are constructed, and setup to lead and follow
|
|
575
495
|
each other, according to the system definition.
|
|
576
496
|
The followers are setup to follow the applications they follow
|
|
@@ -578,28 +498,26 @@ class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
578
498
|
leader), and their leaders are setup to lead the runner itself
|
|
579
499
|
(send prompts).
|
|
580
500
|
"""
|
|
581
|
-
|
|
582
501
|
super().start()
|
|
583
502
|
|
|
584
503
|
# Setup followers to follow leaders.
|
|
585
504
|
for edge in self.system.edges:
|
|
586
505
|
leader_name = edge[0]
|
|
587
506
|
follower_name = edge[1]
|
|
588
|
-
leader = cast(Leader, self.apps[leader_name])
|
|
589
|
-
follower = cast(Follower, self.apps[follower_name])
|
|
507
|
+
leader = cast("Leader", self.apps[leader_name])
|
|
508
|
+
follower = cast("Follower", self.apps[follower_name])
|
|
590
509
|
assert isinstance(leader, Leader)
|
|
591
510
|
assert isinstance(follower, Follower)
|
|
592
511
|
follower.follow(leader_name, leader.notification_log)
|
|
593
512
|
|
|
594
513
|
# Setup leaders to notify followers.
|
|
595
514
|
for name in self.system.leaders:
|
|
596
|
-
leader = cast(Leader, self.apps[name])
|
|
515
|
+
leader = cast("Leader", self.apps[name])
|
|
597
516
|
assert isinstance(leader, Leader)
|
|
598
517
|
leader.lead(self)
|
|
599
518
|
|
|
600
|
-
def receive_recording_event(self,
|
|
601
|
-
"""
|
|
602
|
-
Receives recording event by appending it to list of received recording
|
|
519
|
+
def receive_recording_event(self, new_recording_event: RecordingEvent) -> None:
|
|
520
|
+
"""Receives recording event by appending it to list of received recording
|
|
603
521
|
events.
|
|
604
522
|
|
|
605
523
|
Unless this method has previously been called and not yet returned, it
|
|
@@ -607,19 +525,19 @@ class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
607
525
|
events, until there are none remaining.
|
|
608
526
|
"""
|
|
609
527
|
with self._recording_events_received_lock:
|
|
610
|
-
self._recording_events_received.append(
|
|
528
|
+
self._recording_events_received.append(new_recording_event)
|
|
611
529
|
|
|
612
530
|
if self._processing_lock.acquire(blocking=False):
|
|
613
531
|
try:
|
|
614
532
|
while True:
|
|
615
533
|
with self._recording_events_received_lock:
|
|
616
|
-
|
|
534
|
+
recording_events = self._recording_events_received
|
|
617
535
|
self._recording_events_received = []
|
|
618
536
|
|
|
619
|
-
if not
|
|
537
|
+
if not recording_events:
|
|
620
538
|
break
|
|
621
539
|
|
|
622
|
-
for recording_event in
|
|
540
|
+
for recording_event in recording_events:
|
|
623
541
|
leader_name = recording_event.application_name
|
|
624
542
|
previous_max_notification_id = (
|
|
625
543
|
self._previous_max_notification_ids.get(leader_name, 0)
|
|
@@ -642,9 +560,7 @@ class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
642
560
|
for follower_name in self.system.leads[leader_name]:
|
|
643
561
|
follower = self.apps[follower_name]
|
|
644
562
|
assert isinstance(follower, Follower)
|
|
645
|
-
start = (
|
|
646
|
-
follower.recorder.max_tracking_id(leader_name) + 1
|
|
647
|
-
)
|
|
563
|
+
start = follower.recorder.max_tracking_id(leader_name)
|
|
648
564
|
stop = recording_event.recordings[0].notification.id - 1
|
|
649
565
|
follower.pull_and_process(
|
|
650
566
|
leader_name=leader_name,
|
|
@@ -656,9 +572,9 @@ class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
656
572
|
follower = self.apps[follower_name]
|
|
657
573
|
assert isinstance(follower, Follower)
|
|
658
574
|
if (
|
|
659
|
-
follower.
|
|
575
|
+
follower.topics
|
|
660
576
|
and recording.notification.topic
|
|
661
|
-
not in follower.
|
|
577
|
+
not in follower.topics
|
|
662
578
|
):
|
|
663
579
|
continue
|
|
664
580
|
follower.process_event(
|
|
@@ -681,26 +597,23 @@ class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
681
597
|
app.close()
|
|
682
598
|
self.apps.clear()
|
|
683
599
|
|
|
684
|
-
def get(self, cls:
|
|
600
|
+
def get(self, cls: type[TApplication]) -> TApplication:
|
|
685
601
|
app = self.apps[cls.name]
|
|
686
602
|
assert isinstance(app, cls)
|
|
687
603
|
return app
|
|
688
604
|
|
|
689
605
|
|
|
690
606
|
class MultiThreadedRunner(Runner):
|
|
691
|
-
"""
|
|
692
|
-
Runs a :class:`System` with one :class:`MultiThreadedRunnerThread`
|
|
607
|
+
"""Runs a :class:`System` with one :class:`MultiThreadedRunnerThread`
|
|
693
608
|
for each :class:`Follower` in the system definition.
|
|
694
609
|
"""
|
|
695
610
|
|
|
696
611
|
def __init__(self, system: System, env: EnvType | None = None):
|
|
697
|
-
"""
|
|
698
|
-
Initialises runner with the given :class:`System`.
|
|
699
|
-
"""
|
|
612
|
+
"""Initialises runner with the given :class:`System`."""
|
|
700
613
|
super().__init__(system=system, env=env)
|
|
701
|
-
self.apps:
|
|
702
|
-
self.threads:
|
|
703
|
-
self.has_errored = Event()
|
|
614
|
+
self.apps: dict[str, Application] = {}
|
|
615
|
+
self.threads: dict[str, MultiThreadedRunnerThread] = {}
|
|
616
|
+
self.has_errored = threading.Event()
|
|
704
617
|
|
|
705
618
|
# Construct followers.
|
|
706
619
|
for follower_name in self.system.followers:
|
|
@@ -722,8 +635,7 @@ class MultiThreadedRunner(Runner):
|
|
|
722
635
|
self.apps[name] = single
|
|
723
636
|
|
|
724
637
|
def start(self) -> None:
|
|
725
|
-
"""
|
|
726
|
-
Starts the runner.
|
|
638
|
+
"""Starts the runner.
|
|
727
639
|
A multi-threaded runner thread is started for each
|
|
728
640
|
'follower' application in the system, and constructs
|
|
729
641
|
an instance of each non-follower leader application in
|
|
@@ -736,7 +648,7 @@ class MultiThreadedRunner(Runner):
|
|
|
736
648
|
|
|
737
649
|
# Construct followers.
|
|
738
650
|
for follower_name in self.system.followers:
|
|
739
|
-
follower = cast(Follower, self.apps[follower_name])
|
|
651
|
+
follower = cast("Follower", self.apps[follower_name])
|
|
740
652
|
|
|
741
653
|
thread = MultiThreadedRunnerThread(
|
|
742
654
|
follower=follower,
|
|
@@ -751,8 +663,8 @@ class MultiThreadedRunner(Runner):
|
|
|
751
663
|
|
|
752
664
|
# Lead and follow.
|
|
753
665
|
for edge in self.system.edges:
|
|
754
|
-
leader = cast(Leader, self.apps[edge[0]])
|
|
755
|
-
follower = cast(Follower, self.apps[edge[1]])
|
|
666
|
+
leader = cast("Leader", self.apps[edge[0]])
|
|
667
|
+
follower = cast("Follower", self.apps[edge[1]])
|
|
756
668
|
follower.follow(leader.name, leader.notification_log)
|
|
757
669
|
thread = self.threads[follower.name]
|
|
758
670
|
leader.lead(thread)
|
|
@@ -778,37 +690,35 @@ class MultiThreadedRunner(Runner):
|
|
|
778
690
|
if thread.error:
|
|
779
691
|
raise thread.error
|
|
780
692
|
|
|
781
|
-
def get(self, cls:
|
|
693
|
+
def get(self, cls: type[TApplication]) -> TApplication:
|
|
782
694
|
app = self.apps[cls.name]
|
|
783
695
|
assert isinstance(app, cls)
|
|
784
696
|
return app
|
|
785
697
|
|
|
786
698
|
|
|
787
|
-
class MultiThreadedRunnerThread(RecordingEventReceiver, Thread):
|
|
788
|
-
"""
|
|
789
|
-
Runs one :class:`~eventsourcing.system.Follower` application in
|
|
699
|
+
class MultiThreadedRunnerThread(RecordingEventReceiver, threading.Thread):
|
|
700
|
+
"""Runs one :class:`~eventsourcing.system.Follower` application in
|
|
790
701
|
a :class:`~eventsourcing.system.MultiThreadedRunner`.
|
|
791
702
|
"""
|
|
792
703
|
|
|
793
704
|
def __init__(
|
|
794
705
|
self,
|
|
795
706
|
follower: Follower,
|
|
796
|
-
has_errored: Event,
|
|
707
|
+
has_errored: threading.Event,
|
|
797
708
|
):
|
|
798
709
|
super().__init__(daemon=True)
|
|
799
710
|
self.follower = follower
|
|
800
711
|
self.has_errored = has_errored
|
|
801
712
|
self.error: Exception | None = None
|
|
802
|
-
self.is_stopping = Event()
|
|
803
|
-
self.has_started = Event()
|
|
804
|
-
self.is_prompted = Event()
|
|
805
|
-
self.prompted_names:
|
|
806
|
-
self.prompted_names_lock = Lock()
|
|
807
|
-
self.is_running = Event()
|
|
713
|
+
self.is_stopping = threading.Event()
|
|
714
|
+
self.has_started = threading.Event()
|
|
715
|
+
self.is_prompted = threading.Event()
|
|
716
|
+
self.prompted_names: list[str] = []
|
|
717
|
+
self.prompted_names_lock = threading.Lock()
|
|
718
|
+
self.is_running = threading.Event()
|
|
808
719
|
|
|
809
720
|
def run(self) -> None:
|
|
810
|
-
"""
|
|
811
|
-
Loops forever until stopped. The loop blocks on waiting
|
|
721
|
+
"""Loops forever until stopped. The loop blocks on waiting
|
|
812
722
|
for the 'is_prompted' event to be set, then calls
|
|
813
723
|
:func:`~Follower.pull_and_process` method for each
|
|
814
724
|
prompted name.
|
|
@@ -830,12 +740,11 @@ class MultiThreadedRunnerThread(RecordingEventReceiver, Thread):
|
|
|
830
740
|
self.error.__cause__ = e
|
|
831
741
|
self.has_errored.set()
|
|
832
742
|
|
|
833
|
-
def receive_recording_event(self,
|
|
834
|
-
"""
|
|
835
|
-
Receives prompt by appending name of
|
|
743
|
+
def receive_recording_event(self, new_recording_event: RecordingEvent) -> None:
|
|
744
|
+
"""Receives prompt by appending name of
|
|
836
745
|
leader to list of prompted names.
|
|
837
746
|
"""
|
|
838
|
-
leader_name =
|
|
747
|
+
leader_name = new_recording_event.application_name
|
|
839
748
|
with self.prompted_names_lock:
|
|
840
749
|
if leader_name not in self.prompted_names:
|
|
841
750
|
self.prompted_names.append(leader_name)
|
|
@@ -847,9 +756,7 @@ class MultiThreadedRunnerThread(RecordingEventReceiver, Thread):
|
|
|
847
756
|
|
|
848
757
|
|
|
849
758
|
class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
|
|
850
|
-
"""
|
|
851
|
-
Runs a :class:`System` with multiple threads in a new way.
|
|
852
|
-
"""
|
|
759
|
+
"""Runs a :class:`System` with multiple threads in a new way."""
|
|
853
760
|
|
|
854
761
|
QUEUE_MAX_SIZE: int = 0
|
|
855
762
|
|
|
@@ -858,15 +765,13 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
858
765
|
system: System,
|
|
859
766
|
env: EnvType | None = None,
|
|
860
767
|
):
|
|
861
|
-
"""
|
|
862
|
-
Initialises runner with the given :class:`System`.
|
|
863
|
-
"""
|
|
768
|
+
"""Initialises runner with the given :class:`System`."""
|
|
864
769
|
super().__init__(system=system, env=env)
|
|
865
|
-
self.apps:
|
|
866
|
-
self.pulling_threads:
|
|
867
|
-
self.processing_queues:
|
|
868
|
-
self.all_threads:
|
|
869
|
-
self.has_errored = Event()
|
|
770
|
+
self.apps: dict[str, Application] = {}
|
|
771
|
+
self.pulling_threads: dict[str, list[PullingThread]] = {}
|
|
772
|
+
self.processing_queues: dict[str, Queue[list[ProcessingJob] | None]] = {}
|
|
773
|
+
self.all_threads: list[PullingThread | ConvertingThread | ProcessingThread] = []
|
|
774
|
+
self.has_errored = threading.Event()
|
|
870
775
|
|
|
871
776
|
# Construct followers.
|
|
872
777
|
for follower_name in self.system.followers:
|
|
@@ -888,8 +793,7 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
888
793
|
self.apps[name] = single
|
|
889
794
|
|
|
890
795
|
def start(self) -> None:
|
|
891
|
-
"""
|
|
892
|
-
Starts the runner.
|
|
796
|
+
"""Starts the runner.
|
|
893
797
|
|
|
894
798
|
A multi-threaded runner thread is started for each
|
|
895
799
|
'follower' application in the system, and constructs
|
|
@@ -903,8 +807,8 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
903
807
|
|
|
904
808
|
# Start the processing threads.
|
|
905
809
|
for follower_name in self.system.followers:
|
|
906
|
-
follower = cast(Follower, self.apps[follower_name])
|
|
907
|
-
processing_queue: Queue[
|
|
810
|
+
follower = cast("Follower", self.apps[follower_name])
|
|
811
|
+
processing_queue: Queue[list[ProcessingJob] | None] = Queue(
|
|
908
812
|
maxsize=self.QUEUE_MAX_SIZE
|
|
909
813
|
)
|
|
910
814
|
self.processing_queues[follower_name] = processing_queue
|
|
@@ -919,9 +823,9 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
919
823
|
for edge in self.system.edges:
|
|
920
824
|
# Set up follower to pull notifications from leader.
|
|
921
825
|
leader_name = edge[0]
|
|
922
|
-
leader = cast(Leader, self.apps[leader_name])
|
|
826
|
+
leader = cast("Leader", self.apps[leader_name])
|
|
923
827
|
follower_name = edge[1]
|
|
924
|
-
follower = cast(Follower, self.apps[follower_name])
|
|
828
|
+
follower = cast("Follower", self.apps[follower_name])
|
|
925
829
|
follower.follow(leader.name, leader.notification_log)
|
|
926
830
|
|
|
927
831
|
# Create converting queue.
|
|
@@ -957,7 +861,7 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
957
861
|
|
|
958
862
|
# Subscribe for notifications from leaders.
|
|
959
863
|
for leader_name in self.system.leaders:
|
|
960
|
-
leader = cast(Leader, self.apps[leader_name])
|
|
864
|
+
leader = cast("Leader", self.apps[leader_name])
|
|
961
865
|
assert isinstance(leader, Leader)
|
|
962
866
|
leader.lead(self)
|
|
963
867
|
|
|
@@ -981,19 +885,20 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
981
885
|
if thread.error:
|
|
982
886
|
raise thread.error
|
|
983
887
|
|
|
984
|
-
def get(self, cls:
|
|
888
|
+
def get(self, cls: type[TApplication]) -> TApplication:
|
|
985
889
|
app = self.apps[cls.name]
|
|
986
890
|
assert isinstance(app, cls)
|
|
987
891
|
return app
|
|
988
892
|
|
|
989
|
-
def receive_recording_event(self,
|
|
990
|
-
for pulling_thread in self.pulling_threads[
|
|
991
|
-
|
|
893
|
+
def receive_recording_event(self, new_recording_event: RecordingEvent) -> None:
|
|
894
|
+
for pulling_thread in self.pulling_threads[
|
|
895
|
+
new_recording_event.application_name
|
|
896
|
+
]:
|
|
897
|
+
pulling_thread.receive_recording_event(new_recording_event)
|
|
992
898
|
|
|
993
899
|
|
|
994
|
-
class PullingThread(Thread):
|
|
995
|
-
"""
|
|
996
|
-
Receives or pulls notifications from the given leader, and
|
|
900
|
+
class PullingThread(threading.Thread):
|
|
901
|
+
"""Receives or pulls notifications from the given leader, and
|
|
997
902
|
puts them on a queue for conversion into processing jobs.
|
|
998
903
|
"""
|
|
999
904
|
|
|
@@ -1002,19 +907,19 @@ class PullingThread(Thread):
|
|
|
1002
907
|
converting_queue: Queue[ConvertingJob],
|
|
1003
908
|
follower: Follower,
|
|
1004
909
|
leader_name: str,
|
|
1005
|
-
has_errored: Event,
|
|
910
|
+
has_errored: threading.Event,
|
|
1006
911
|
):
|
|
1007
912
|
super().__init__(daemon=True)
|
|
1008
|
-
self.overflow_event = Event()
|
|
913
|
+
self.overflow_event = threading.Event()
|
|
1009
914
|
self.recording_event_queue: Queue[RecordingEvent | None] = Queue(maxsize=100)
|
|
1010
915
|
self.converting_queue = converting_queue
|
|
1011
|
-
self.receive_lock = Lock()
|
|
916
|
+
self.receive_lock = threading.Lock()
|
|
1012
917
|
self.follower = follower
|
|
1013
918
|
self.leader_name = leader_name
|
|
1014
919
|
self.error: Exception | None = None
|
|
1015
920
|
self.has_errored = has_errored
|
|
1016
|
-
self.is_stopping = Event()
|
|
1017
|
-
self.has_started = Event()
|
|
921
|
+
self.is_stopping = threading.Event()
|
|
922
|
+
self.has_started = threading.Event()
|
|
1018
923
|
self.mapper = self.follower.mappers[self.leader_name]
|
|
1019
924
|
self.previous_max_notification_id = self.follower.recorder.max_tracking_id(
|
|
1020
925
|
application_name=self.leader_name
|
|
@@ -1031,6 +936,7 @@ class PullingThread(Thread):
|
|
|
1031
936
|
# Ignore recording event if already seen a subsequent.
|
|
1032
937
|
if (
|
|
1033
938
|
recording_event.previous_max_notification_id is not None
|
|
939
|
+
and self.previous_max_notification_id is not None
|
|
1034
940
|
and recording_event.previous_max_notification_id
|
|
1035
941
|
< self.previous_max_notification_id
|
|
1036
942
|
):
|
|
@@ -1039,13 +945,17 @@ class PullingThread(Thread):
|
|
|
1039
945
|
# Catch up if there is a gap in sequence of recording events.
|
|
1040
946
|
if (
|
|
1041
947
|
recording_event.previous_max_notification_id is None
|
|
948
|
+
or self.previous_max_notification_id is None
|
|
1042
949
|
or recording_event.previous_max_notification_id
|
|
1043
950
|
> self.previous_max_notification_id
|
|
1044
951
|
):
|
|
1045
|
-
start = self.previous_max_notification_id
|
|
952
|
+
start = self.previous_max_notification_id
|
|
1046
953
|
stop = recording_event.recordings[0].notification.id - 1
|
|
1047
954
|
for notifications in self.follower.pull_notifications(
|
|
1048
|
-
self.leader_name,
|
|
955
|
+
self.leader_name,
|
|
956
|
+
start=start,
|
|
957
|
+
stop=stop,
|
|
958
|
+
inclusive_of_start=False,
|
|
1049
959
|
):
|
|
1050
960
|
self.converting_queue.put(notifications)
|
|
1051
961
|
self.previous_max_notification_id = notifications[-1].id
|
|
@@ -1069,18 +979,16 @@ class PullingThread(Thread):
|
|
|
1069
979
|
self.recording_event_queue.put(None)
|
|
1070
980
|
|
|
1071
981
|
|
|
1072
|
-
class ConvertingThread(Thread):
|
|
1073
|
-
"""
|
|
1074
|
-
Converts notifications into processing jobs.
|
|
1075
|
-
"""
|
|
982
|
+
class ConvertingThread(threading.Thread):
|
|
983
|
+
"""Converts notifications into processing jobs."""
|
|
1076
984
|
|
|
1077
985
|
def __init__(
|
|
1078
986
|
self,
|
|
1079
987
|
converting_queue: Queue[ConvertingJob],
|
|
1080
|
-
processing_queue: Queue[
|
|
988
|
+
processing_queue: Queue[list[ProcessingJob] | None],
|
|
1081
989
|
follower: Follower,
|
|
1082
990
|
leader_name: str,
|
|
1083
|
-
has_errored: Event,
|
|
991
|
+
has_errored: threading.Event,
|
|
1084
992
|
):
|
|
1085
993
|
super().__init__(daemon=True)
|
|
1086
994
|
self.converting_queue = converting_queue
|
|
@@ -1089,8 +997,8 @@ class ConvertingThread(Thread):
|
|
|
1089
997
|
self.leader_name = leader_name
|
|
1090
998
|
self.error: Exception | None = None
|
|
1091
999
|
self.has_errored = has_errored
|
|
1092
|
-
self.is_stopping = Event()
|
|
1093
|
-
self.has_started = Event()
|
|
1000
|
+
self.is_stopping = threading.Event()
|
|
1001
|
+
self.has_started = threading.Event()
|
|
1094
1002
|
self.mapper = self.follower.mappers[self.leader_name]
|
|
1095
1003
|
|
|
1096
1004
|
def run(self) -> None:
|
|
@@ -1111,9 +1019,8 @@ class ConvertingThread(Thread):
|
|
|
1111
1019
|
recording_event = recording_event_or_notifications
|
|
1112
1020
|
for recording in recording_event.recordings:
|
|
1113
1021
|
if (
|
|
1114
|
-
self.follower.
|
|
1115
|
-
and recording.notification.topic
|
|
1116
|
-
not in self.follower.follow_topics
|
|
1022
|
+
self.follower.topics
|
|
1023
|
+
and recording.notification.topic not in self.follower.topics
|
|
1117
1024
|
):
|
|
1118
1025
|
continue
|
|
1119
1026
|
tracking = Tracking(
|
|
@@ -1129,7 +1036,7 @@ class ConvertingThread(Thread):
|
|
|
1129
1036
|
if processing_jobs:
|
|
1130
1037
|
self.processing_queue.put(processing_jobs)
|
|
1131
1038
|
except Exception as e:
|
|
1132
|
-
print(traceback.format_exc())
|
|
1039
|
+
print(traceback.format_exc()) # noqa: T201
|
|
1133
1040
|
self.error = NotificationConvertingError(str(e))
|
|
1134
1041
|
self.error.__cause__ = e
|
|
1135
1042
|
self.has_errored.set()
|
|
@@ -1139,25 +1046,24 @@ class ConvertingThread(Thread):
|
|
|
1139
1046
|
self.converting_queue.put(None)
|
|
1140
1047
|
|
|
1141
1048
|
|
|
1142
|
-
class ProcessingThread(Thread):
|
|
1143
|
-
"""
|
|
1144
|
-
A processing thread gets events from a processing queue, and
|
|
1049
|
+
class ProcessingThread(threading.Thread):
|
|
1050
|
+
"""A processing thread gets events from a processing queue, and
|
|
1145
1051
|
calls the application's process_event() method.
|
|
1146
1052
|
"""
|
|
1147
1053
|
|
|
1148
1054
|
def __init__(
|
|
1149
1055
|
self,
|
|
1150
|
-
processing_queue: Queue[
|
|
1056
|
+
processing_queue: Queue[list[ProcessingJob] | None],
|
|
1151
1057
|
follower: Follower,
|
|
1152
|
-
has_errored: Event,
|
|
1058
|
+
has_errored: threading.Event,
|
|
1153
1059
|
):
|
|
1154
1060
|
super().__init__(daemon=True)
|
|
1155
1061
|
self.processing_queue = processing_queue
|
|
1156
1062
|
self.follower = follower
|
|
1157
1063
|
self.error: Exception | None = None
|
|
1158
1064
|
self.has_errored = has_errored
|
|
1159
|
-
self.is_stopping = Event()
|
|
1160
|
-
self.has_started = Event()
|
|
1065
|
+
self.is_stopping = threading.Event()
|
|
1066
|
+
self.has_started = threading.Event()
|
|
1161
1067
|
|
|
1162
1068
|
def run(self) -> None:
|
|
1163
1069
|
self.has_started.set()
|
|
@@ -1180,9 +1086,7 @@ class ProcessingThread(Thread):
|
|
|
1180
1086
|
|
|
1181
1087
|
|
|
1182
1088
|
class NotificationLogReader:
|
|
1183
|
-
"""
|
|
1184
|
-
Reads domain event notifications from a notification log.
|
|
1185
|
-
"""
|
|
1089
|
+
"""Reads domain event notifications from a notification log."""
|
|
1186
1090
|
|
|
1187
1091
|
DEFAULT_SECTION_SIZE = 10
|
|
1188
1092
|
|
|
@@ -1191,8 +1095,7 @@ class NotificationLogReader:
|
|
|
1191
1095
|
notification_log: NotificationLog,
|
|
1192
1096
|
section_size: int = DEFAULT_SECTION_SIZE,
|
|
1193
1097
|
):
|
|
1194
|
-
"""
|
|
1195
|
-
Initialises a reader with the given notification log,
|
|
1098
|
+
"""Initialises a reader with the given notification log,
|
|
1196
1099
|
and optionally a section size integer which determines
|
|
1197
1100
|
the requested number of domain event notifications in
|
|
1198
1101
|
each section retrieved from the notification log.
|
|
@@ -1201,8 +1104,7 @@ class NotificationLogReader:
|
|
|
1201
1104
|
self.section_size = section_size
|
|
1202
1105
|
|
|
1203
1106
|
def read(self, *, start: int) -> Iterator[Notification]:
|
|
1204
|
-
"""
|
|
1205
|
-
Returns a generator that yields event notifications
|
|
1107
|
+
"""Returns a generator that yields event notifications
|
|
1206
1108
|
from the reader's notification log, starting from
|
|
1207
1109
|
given start position (a notification ID).
|
|
1208
1110
|
|
|
@@ -1224,10 +1126,14 @@ class NotificationLogReader:
|
|
|
1224
1126
|
section_id = section.next_id
|
|
1225
1127
|
|
|
1226
1128
|
def select(
|
|
1227
|
-
self,
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1129
|
+
self,
|
|
1130
|
+
*,
|
|
1131
|
+
start: int | None,
|
|
1132
|
+
stop: int | None = None,
|
|
1133
|
+
topics: Sequence[str] = (),
|
|
1134
|
+
inclusive_of_start: bool = True,
|
|
1135
|
+
) -> Iterator[list[Notification]]:
|
|
1136
|
+
"""Returns a generator that yields lists of event notifications
|
|
1231
1137
|
from the reader's notification log, starting from given start
|
|
1232
1138
|
position (a notification ID).
|
|
1233
1139
|
|
|
@@ -1240,12 +1146,18 @@ class NotificationLogReader:
|
|
|
1240
1146
|
"""
|
|
1241
1147
|
while True:
|
|
1242
1148
|
notifications = self.notification_log.select(
|
|
1243
|
-
start=start,
|
|
1149
|
+
start=start,
|
|
1150
|
+
stop=stop,
|
|
1151
|
+
limit=self.section_size,
|
|
1152
|
+
topics=topics,
|
|
1153
|
+
inclusive_of_start=inclusive_of_start,
|
|
1244
1154
|
)
|
|
1245
1155
|
# Stop if zero notifications.
|
|
1246
1156
|
if len(notifications) == 0:
|
|
1247
1157
|
break
|
|
1248
1158
|
|
|
1249
1159
|
# Otherwise, yield and continue.
|
|
1160
|
+
start = notifications[-1].id
|
|
1161
|
+
if inclusive_of_start:
|
|
1162
|
+
start += 1
|
|
1250
1163
|
yield notifications
|
|
1251
|
-
start = notifications[-1].id + 1
|