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