eventsourcing 9.4.4__py3-none-any.whl → 9.4.6__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/application.py +92 -68
- eventsourcing/domain.py +479 -226
- eventsourcing/interface.py +10 -2
- eventsourcing/persistence.py +121 -33
- eventsourcing/popo.py +16 -14
- eventsourcing/postgres.py +20 -20
- eventsourcing/projection.py +25 -20
- eventsourcing/sqlite.py +28 -18
- eventsourcing/system.py +130 -90
- eventsourcing/tests/application.py +14 -97
- eventsourcing/tests/persistence.py +86 -18
- eventsourcing/tests/postgres_utils.py +27 -7
- eventsourcing/utils.py +1 -1
- {eventsourcing-9.4.4.dist-info → eventsourcing-9.4.6.dist-info}/METADATA +18 -12
- eventsourcing-9.4.6.dist-info/RECORD +26 -0
- eventsourcing-9.4.4.dist-info/RECORD +0 -26
- {eventsourcing-9.4.4.dist-info → eventsourcing-9.4.6.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.4.4.dist-info → eventsourcing-9.4.6.dist-info}/LICENSE +0 -0
- {eventsourcing-9.4.4.dist-info → eventsourcing-9.4.6.dist-info}/WHEEL +0 -0
eventsourcing/system.py
CHANGED
|
@@ -5,16 +5,10 @@ import threading
|
|
|
5
5
|
import traceback
|
|
6
6
|
from abc import ABC, abstractmethod
|
|
7
7
|
from collections import defaultdict
|
|
8
|
+
from collections.abc import Sequence
|
|
8
9
|
from queue import Full, Queue
|
|
9
10
|
from types import FrameType, ModuleType
|
|
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
|
|
16
|
-
|
|
17
|
-
from typing_extensions import Self
|
|
11
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Generic, Optional, Union, cast
|
|
18
12
|
|
|
19
13
|
from eventsourcing.application import (
|
|
20
14
|
Application,
|
|
@@ -23,7 +17,11 @@ from eventsourcing.application import (
|
|
|
23
17
|
Section,
|
|
24
18
|
TApplication,
|
|
25
19
|
)
|
|
26
|
-
from eventsourcing.domain import
|
|
20
|
+
from eventsourcing.domain import (
|
|
21
|
+
DomainEventProtocol,
|
|
22
|
+
MutableOrImmutableAggregate,
|
|
23
|
+
TAggregateID,
|
|
24
|
+
)
|
|
27
25
|
from eventsourcing.persistence import (
|
|
28
26
|
Mapper,
|
|
29
27
|
Notification,
|
|
@@ -31,27 +29,34 @@ from eventsourcing.persistence import (
|
|
|
31
29
|
Recording,
|
|
32
30
|
Tracking,
|
|
33
31
|
)
|
|
32
|
+
from eventsourcing.projection import EventSourcedProjection
|
|
34
33
|
from eventsourcing.utils import EnvType, get_topic, resolve_topic
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from collections.abc import Iterable, Iterator
|
|
37
|
+
|
|
38
|
+
from typing_extensions import Self
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
ProcessingJob = tuple[DomainEventProtocol[TAggregateID], Tracking]
|
|
37
42
|
|
|
38
43
|
|
|
39
|
-
class RecordingEvent:
|
|
44
|
+
class RecordingEvent(Generic[TAggregateID]):
|
|
40
45
|
def __init__(
|
|
41
46
|
self,
|
|
42
47
|
application_name: str,
|
|
43
|
-
recordings: list[Recording],
|
|
48
|
+
recordings: list[Recording[TAggregateID]],
|
|
44
49
|
previous_max_notification_id: int | None,
|
|
45
50
|
):
|
|
46
51
|
self.application_name = application_name
|
|
47
|
-
self.recordings = recordings
|
|
52
|
+
self.recordings: list[Recording[TAggregateID]] = recordings
|
|
48
53
|
self.previous_max_notification_id = previous_max_notification_id
|
|
49
54
|
|
|
50
55
|
|
|
51
|
-
ConvertingJob = Optional[Union[RecordingEvent,
|
|
56
|
+
ConvertingJob = Optional[Union[RecordingEvent[TAggregateID], Sequence[Notification]]]
|
|
52
57
|
|
|
53
58
|
|
|
54
|
-
class Follower(EventSourcedProjection):
|
|
59
|
+
class Follower(EventSourcedProjection[TAggregateID]):
|
|
55
60
|
"""Extends the :class:`~eventsourcing.projection.EventSourcedProjection` class
|
|
56
61
|
by pulling notification objects from its notification log readers, by converting
|
|
57
62
|
the notification objects to domain events and tracking objects and by processing
|
|
@@ -68,7 +73,7 @@ class Follower(EventSourcedProjection):
|
|
|
68
73
|
def __init__(self, env: EnvType | None = None) -> None:
|
|
69
74
|
super().__init__(env)
|
|
70
75
|
self.readers: dict[str, NotificationLogReader] = {}
|
|
71
|
-
self.mappers: dict[str, Mapper] = {}
|
|
76
|
+
self.mappers: dict[str, Mapper[TAggregateID]] = {}
|
|
72
77
|
self.is_threading_enabled = False
|
|
73
78
|
|
|
74
79
|
def follow(self, name: str, log: NotificationLog) -> None:
|
|
@@ -103,7 +108,7 @@ class Follower(EventSourcedProjection):
|
|
|
103
108
|
self.process_event(domain_event, tracking)
|
|
104
109
|
|
|
105
110
|
def process_event(
|
|
106
|
-
self, domain_event: DomainEventProtocol, tracking: Tracking
|
|
111
|
+
self, domain_event: DomainEventProtocol[TAggregateID], tracking: Tracking
|
|
107
112
|
) -> None:
|
|
108
113
|
with self.processing_lock:
|
|
109
114
|
super().process_event(domain_event, tracking)
|
|
@@ -115,7 +120,7 @@ class Follower(EventSourcedProjection):
|
|
|
115
120
|
stop: int | None = None,
|
|
116
121
|
*,
|
|
117
122
|
inclusive_of_start: bool = True,
|
|
118
|
-
) -> Iterator[
|
|
123
|
+
) -> Iterator[Sequence[Notification]]:
|
|
119
124
|
"""Pulls batches of unseen :class:`~eventsourcing.persistence.Notification`
|
|
120
125
|
objects from the notification log reader of the named application.
|
|
121
126
|
"""
|
|
@@ -127,15 +132,15 @@ class Follower(EventSourcedProjection):
|
|
|
127
132
|
)
|
|
128
133
|
|
|
129
134
|
def filter_received_notifications(
|
|
130
|
-
self, notifications:
|
|
131
|
-
) ->
|
|
135
|
+
self, notifications: Sequence[Notification]
|
|
136
|
+
) -> Sequence[Notification]:
|
|
132
137
|
if self.topics:
|
|
133
138
|
return [n for n in notifications if n.topic in self.topics]
|
|
134
139
|
return notifications
|
|
135
140
|
|
|
136
141
|
def convert_notifications(
|
|
137
142
|
self, leader_name: str, notifications: Iterable[Notification]
|
|
138
|
-
) -> list[ProcessingJob]:
|
|
143
|
+
) -> list[ProcessingJob[TAggregateID]]:
|
|
139
144
|
"""Uses the given :class:`~eventsourcing.persistence.Mapper` to convert
|
|
140
145
|
each received :class:`~eventsourcing.persistence.Notification`
|
|
141
146
|
object to an :class:`~eventsourcing.domain.AggregateEvent` object
|
|
@@ -144,7 +149,9 @@ class Follower(EventSourcedProjection):
|
|
|
144
149
|
mapper = self.mappers[leader_name]
|
|
145
150
|
processing_jobs = []
|
|
146
151
|
for notification in notifications:
|
|
147
|
-
domain_event: DomainEventProtocol = mapper.to_domain_event(
|
|
152
|
+
domain_event: DomainEventProtocol[TAggregateID] = mapper.to_domain_event(
|
|
153
|
+
notification
|
|
154
|
+
)
|
|
148
155
|
tracking = Tracking(
|
|
149
156
|
application_name=leader_name,
|
|
150
157
|
notification_id=notification.id,
|
|
@@ -153,15 +160,17 @@ class Follower(EventSourcedProjection):
|
|
|
153
160
|
return processing_jobs
|
|
154
161
|
|
|
155
162
|
|
|
156
|
-
class RecordingEventReceiver(ABC):
|
|
163
|
+
class RecordingEventReceiver(Generic[TAggregateID], ABC):
|
|
157
164
|
"""Abstract base class for objects that may receive recording events."""
|
|
158
165
|
|
|
159
166
|
@abstractmethod
|
|
160
|
-
def receive_recording_event(
|
|
167
|
+
def receive_recording_event(
|
|
168
|
+
self, new_recording_event: RecordingEvent[TAggregateID]
|
|
169
|
+
) -> None:
|
|
161
170
|
"""Receives a recording event."""
|
|
162
171
|
|
|
163
172
|
|
|
164
|
-
class Leader(Application):
|
|
173
|
+
class Leader(Application[TAggregateID]):
|
|
165
174
|
"""Extends the :class:`~eventsourcing.application.Application`
|
|
166
175
|
class by also being responsible for keeping track of
|
|
167
176
|
followers, and prompting followers when there are new
|
|
@@ -171,22 +180,24 @@ class Leader(Application):
|
|
|
171
180
|
def __init__(self, env: EnvType | None = None) -> None:
|
|
172
181
|
super().__init__(env)
|
|
173
182
|
self.previous_max_notification_id: int | None = None
|
|
174
|
-
self.followers: list[RecordingEventReceiver] = []
|
|
183
|
+
self.followers: list[RecordingEventReceiver[TAggregateID]] = []
|
|
175
184
|
|
|
176
|
-
def lead(self, follower: RecordingEventReceiver) -> None:
|
|
185
|
+
def lead(self, follower: RecordingEventReceiver[TAggregateID]) -> None:
|
|
177
186
|
"""Adds given follower to a list of followers."""
|
|
178
187
|
self.followers.append(follower)
|
|
179
188
|
|
|
180
189
|
def save(
|
|
181
190
|
self,
|
|
182
|
-
*objs: MutableOrImmutableAggregate
|
|
191
|
+
*objs: MutableOrImmutableAggregate[TAggregateID]
|
|
192
|
+
| DomainEventProtocol[TAggregateID]
|
|
193
|
+
| None,
|
|
183
194
|
**kwargs: Any,
|
|
184
|
-
) -> list[Recording]:
|
|
195
|
+
) -> list[Recording[TAggregateID]]:
|
|
185
196
|
if self.previous_max_notification_id is None:
|
|
186
197
|
self.previous_max_notification_id = self.recorder.max_notification_id()
|
|
187
198
|
return super().save(*objs, **kwargs)
|
|
188
199
|
|
|
189
|
-
def _notify(self, recordings: list[Recording]) -> None:
|
|
200
|
+
def _notify(self, recordings: list[Recording[TAggregateID]]) -> None:
|
|
190
201
|
"""Calls :func:`receive_recording_event` on each follower
|
|
191
202
|
whenever new events have just been saved.
|
|
192
203
|
"""
|
|
@@ -206,7 +217,7 @@ class Leader(Application):
|
|
|
206
217
|
follower.receive_recording_event(recording_event)
|
|
207
218
|
|
|
208
219
|
|
|
209
|
-
class ProcessApplication(Leader, Follower):
|
|
220
|
+
class ProcessApplication(Leader[TAggregateID], Follower[TAggregateID]):
|
|
210
221
|
"""Base class for event processing applications
|
|
211
222
|
that are both "leaders" and followers".
|
|
212
223
|
"""
|
|
@@ -219,7 +230,7 @@ class System:
|
|
|
219
230
|
|
|
220
231
|
def __init__(
|
|
221
232
|
self,
|
|
222
|
-
pipes: Iterable[Iterable[type[Application]]],
|
|
233
|
+
pipes: Iterable[Iterable[type[Application[Any]]]],
|
|
223
234
|
):
|
|
224
235
|
# Remember the caller frame's module, so that we might identify a topic.
|
|
225
236
|
caller_frame = cast(FrameType, inspect.currentframe()).f_back
|
|
@@ -228,7 +239,7 @@ class System:
|
|
|
228
239
|
|
|
229
240
|
# Build nodes and edges.
|
|
230
241
|
self.edges: list[tuple[str, str]] = []
|
|
231
|
-
classes: dict[str, type[Application]] = {}
|
|
242
|
+
classes: dict[str, type[Application[Any]]] = {}
|
|
232
243
|
for pipe in pipes:
|
|
233
244
|
follower_cls = None
|
|
234
245
|
for cls in pipe:
|
|
@@ -287,12 +298,12 @@ class System:
|
|
|
287
298
|
def processors(self) -> list[str]:
|
|
288
299
|
return [name for name in self.leads if name in self.follows]
|
|
289
300
|
|
|
290
|
-
def get_app_cls(self, name: str) -> type[Application]:
|
|
301
|
+
def get_app_cls(self, name: str) -> type[Application[Any]]:
|
|
291
302
|
cls = resolve_topic(self.nodes[name])
|
|
292
303
|
assert issubclass(cls, Application)
|
|
293
304
|
return cls
|
|
294
305
|
|
|
295
|
-
def leader_cls(self, name: str) -> type[Leader]:
|
|
306
|
+
def leader_cls(self, name: str) -> type[Leader[Any]]:
|
|
296
307
|
cls = self.get_app_cls(name)
|
|
297
308
|
if issubclass(cls, Leader):
|
|
298
309
|
return cls
|
|
@@ -300,7 +311,7 @@ class System:
|
|
|
300
311
|
assert issubclass(cls, Leader)
|
|
301
312
|
return cls
|
|
302
313
|
|
|
303
|
-
def follower_cls(self, name: str) -> type[Follower]:
|
|
314
|
+
def follower_cls(self, name: str) -> type[Follower[Any]]:
|
|
304
315
|
cls = self.get_app_cls(name)
|
|
305
316
|
assert issubclass(cls, Follower)
|
|
306
317
|
return cls
|
|
@@ -322,7 +333,7 @@ class System:
|
|
|
322
333
|
return topic
|
|
323
334
|
|
|
324
335
|
|
|
325
|
-
class Runner(ABC):
|
|
336
|
+
class Runner(Generic[TAggregateID], ABC):
|
|
326
337
|
"""Abstract base class for system runners."""
|
|
327
338
|
|
|
328
339
|
def __init__(self, system: System, env: EnvType | None = None):
|
|
@@ -369,14 +380,14 @@ class EventProcessingError(Exception):
|
|
|
369
380
|
"""Raised when event processing fails."""
|
|
370
381
|
|
|
371
382
|
|
|
372
|
-
class SingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
383
|
+
class SingleThreadedRunner(Runner[TAggregateID], RecordingEventReceiver[TAggregateID]):
|
|
373
384
|
"""Runs a :class:`System` in a single thread."""
|
|
374
385
|
|
|
375
386
|
def __init__(self, system: System, env: EnvType | None = None):
|
|
376
387
|
"""Initialises runner with the given :class:`System`."""
|
|
377
388
|
super().__init__(system=system, env=env)
|
|
378
|
-
self.apps: dict[str, Application] = {}
|
|
379
|
-
self._recording_events_received: list[RecordingEvent] = []
|
|
389
|
+
self.apps: dict[str, Application[TAggregateID]] = {}
|
|
390
|
+
self._recording_events_received: list[RecordingEvent[TAggregateID]] = []
|
|
380
391
|
self._prompted_names_lock = threading.Lock()
|
|
381
392
|
self._prompted_names: set[str] = set()
|
|
382
393
|
self._processing_lock = threading.Lock()
|
|
@@ -407,19 +418,21 @@ class SingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
407
418
|
for edge in self.system.edges:
|
|
408
419
|
leader_name = edge[0]
|
|
409
420
|
follower_name = edge[1]
|
|
410
|
-
leader = cast("Leader", self.apps[leader_name])
|
|
411
|
-
follower = cast(
|
|
421
|
+
leader = cast("Leader[Any]", self.apps[leader_name])
|
|
422
|
+
follower = cast(Follower[Any], self.apps[follower_name])
|
|
412
423
|
assert isinstance(leader, Leader)
|
|
413
424
|
assert isinstance(follower, Follower)
|
|
414
425
|
follower.follow(leader_name, leader.notification_log)
|
|
415
426
|
|
|
416
427
|
# Setup leaders to lead this runner.
|
|
417
428
|
for name in self.system.leaders:
|
|
418
|
-
leader = cast("Leader", self.apps[name])
|
|
429
|
+
leader = cast("Leader[Any]", self.apps[name])
|
|
419
430
|
assert isinstance(leader, Leader)
|
|
420
431
|
leader.lead(self)
|
|
421
432
|
|
|
422
|
-
def receive_recording_event(
|
|
433
|
+
def receive_recording_event(
|
|
434
|
+
self, new_recording_event: RecordingEvent[TAggregateID]
|
|
435
|
+
) -> None:
|
|
423
436
|
"""Receives recording event by appending the name of the leader
|
|
424
437
|
to a list of prompted names.
|
|
425
438
|
|
|
@@ -446,7 +459,7 @@ class SingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
446
459
|
|
|
447
460
|
for leader_name in prompted_names:
|
|
448
461
|
for follower_name in self.system.leads[leader_name]:
|
|
449
|
-
follower = cast(
|
|
462
|
+
follower = cast(Follower[Any], self.apps[follower_name])
|
|
450
463
|
follower.pull_and_process(leader_name)
|
|
451
464
|
|
|
452
465
|
finally:
|
|
@@ -463,14 +476,16 @@ class SingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
463
476
|
return app
|
|
464
477
|
|
|
465
478
|
|
|
466
|
-
class NewSingleThreadedRunner(
|
|
479
|
+
class NewSingleThreadedRunner(
|
|
480
|
+
Runner[TAggregateID], RecordingEventReceiver[TAggregateID]
|
|
481
|
+
):
|
|
467
482
|
"""Runs a :class:`System` in a single thread."""
|
|
468
483
|
|
|
469
484
|
def __init__(self, system: System, env: EnvType | None = None):
|
|
470
485
|
"""Initialises runner with the given :class:`System`."""
|
|
471
486
|
super().__init__(system=system, env=env)
|
|
472
|
-
self.apps: dict[str, Application] = {}
|
|
473
|
-
self._recording_events_received: list[RecordingEvent] = []
|
|
487
|
+
self.apps: dict[str, Application[Any]] = {}
|
|
488
|
+
self._recording_events_received: list[RecordingEvent[TAggregateID]] = []
|
|
474
489
|
self._recording_events_received_lock = threading.Lock()
|
|
475
490
|
self._processing_lock = threading.Lock()
|
|
476
491
|
self._previous_max_notification_ids: dict[str, int] = {}
|
|
@@ -504,19 +519,21 @@ class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
504
519
|
for edge in self.system.edges:
|
|
505
520
|
leader_name = edge[0]
|
|
506
521
|
follower_name = edge[1]
|
|
507
|
-
leader = cast("Leader", self.apps[leader_name])
|
|
508
|
-
follower = cast(
|
|
522
|
+
leader = cast("Leader[Any]", self.apps[leader_name])
|
|
523
|
+
follower = cast(Follower[Any], self.apps[follower_name])
|
|
509
524
|
assert isinstance(leader, Leader)
|
|
510
525
|
assert isinstance(follower, Follower)
|
|
511
526
|
follower.follow(leader_name, leader.notification_log)
|
|
512
527
|
|
|
513
528
|
# Setup leaders to notify followers.
|
|
514
529
|
for name in self.system.leaders:
|
|
515
|
-
leader = cast("Leader", self.apps[name])
|
|
530
|
+
leader = cast("Leader[Any]", self.apps[name])
|
|
516
531
|
assert isinstance(leader, Leader)
|
|
517
532
|
leader.lead(self)
|
|
518
533
|
|
|
519
|
-
def receive_recording_event(
|
|
534
|
+
def receive_recording_event(
|
|
535
|
+
self, new_recording_event: RecordingEvent[TAggregateID]
|
|
536
|
+
) -> None:
|
|
520
537
|
"""Receives recording event by appending it to list of received recording
|
|
521
538
|
events.
|
|
522
539
|
|
|
@@ -603,7 +620,7 @@ class NewSingleThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
603
620
|
return app
|
|
604
621
|
|
|
605
622
|
|
|
606
|
-
class MultiThreadedRunner(Runner):
|
|
623
|
+
class MultiThreadedRunner(Runner[TAggregateID]):
|
|
607
624
|
"""Runs a :class:`System` with one :class:`MultiThreadedRunnerThread`
|
|
608
625
|
for each :class:`Follower` in the system definition.
|
|
609
626
|
"""
|
|
@@ -611,8 +628,8 @@ class MultiThreadedRunner(Runner):
|
|
|
611
628
|
def __init__(self, system: System, env: EnvType | None = None):
|
|
612
629
|
"""Initialises runner with the given :class:`System`."""
|
|
613
630
|
super().__init__(system=system, env=env)
|
|
614
|
-
self.apps: dict[str, Application] = {}
|
|
615
|
-
self.threads: dict[str, MultiThreadedRunnerThread] = {}
|
|
631
|
+
self.apps: dict[str, Application[Any]] = {}
|
|
632
|
+
self.threads: dict[str, MultiThreadedRunnerThread[TAggregateID]] = {}
|
|
616
633
|
self.has_errored = threading.Event()
|
|
617
634
|
|
|
618
635
|
# Construct followers.
|
|
@@ -647,8 +664,9 @@ class MultiThreadedRunner(Runner):
|
|
|
647
664
|
super().start()
|
|
648
665
|
|
|
649
666
|
# Construct followers.
|
|
667
|
+
thread: MultiThreadedRunnerThread[TAggregateID]
|
|
650
668
|
for follower_name in self.system.followers:
|
|
651
|
-
follower = cast(
|
|
669
|
+
follower = cast(Follower[Any], self.apps[follower_name])
|
|
652
670
|
|
|
653
671
|
thread = MultiThreadedRunnerThread(
|
|
654
672
|
follower=follower,
|
|
@@ -663,8 +681,8 @@ class MultiThreadedRunner(Runner):
|
|
|
663
681
|
|
|
664
682
|
# Lead and follow.
|
|
665
683
|
for edge in self.system.edges:
|
|
666
|
-
leader = cast("Leader", self.apps[edge[0]])
|
|
667
|
-
follower = cast(
|
|
684
|
+
leader = cast("Leader[Any]", self.apps[edge[0]])
|
|
685
|
+
follower = cast(Follower[Any], self.apps[edge[1]])
|
|
668
686
|
follower.follow(leader.name, leader.notification_log)
|
|
669
687
|
thread = self.threads[follower.name]
|
|
670
688
|
leader.lead(thread)
|
|
@@ -696,14 +714,14 @@ class MultiThreadedRunner(Runner):
|
|
|
696
714
|
return app
|
|
697
715
|
|
|
698
716
|
|
|
699
|
-
class MultiThreadedRunnerThread(RecordingEventReceiver, threading.Thread):
|
|
717
|
+
class MultiThreadedRunnerThread(RecordingEventReceiver[TAggregateID], threading.Thread):
|
|
700
718
|
"""Runs one :class:`~eventsourcing.system.Follower` application in
|
|
701
719
|
a :class:`~eventsourcing.system.MultiThreadedRunner`.
|
|
702
720
|
"""
|
|
703
721
|
|
|
704
722
|
def __init__(
|
|
705
723
|
self,
|
|
706
|
-
follower: Follower,
|
|
724
|
+
follower: Follower[Any],
|
|
707
725
|
has_errored: threading.Event,
|
|
708
726
|
):
|
|
709
727
|
super().__init__(daemon=True)
|
|
@@ -740,7 +758,9 @@ class MultiThreadedRunnerThread(RecordingEventReceiver, threading.Thread):
|
|
|
740
758
|
self.error.__cause__ = e
|
|
741
759
|
self.has_errored.set()
|
|
742
760
|
|
|
743
|
-
def receive_recording_event(
|
|
761
|
+
def receive_recording_event(
|
|
762
|
+
self, new_recording_event: RecordingEvent[TAggregateID]
|
|
763
|
+
) -> None:
|
|
744
764
|
"""Receives prompt by appending name of
|
|
745
765
|
leader to list of prompted names.
|
|
746
766
|
"""
|
|
@@ -755,7 +775,9 @@ class MultiThreadedRunnerThread(RecordingEventReceiver, threading.Thread):
|
|
|
755
775
|
self.is_prompted.set()
|
|
756
776
|
|
|
757
777
|
|
|
758
|
-
class NewMultiThreadedRunner(
|
|
778
|
+
class NewMultiThreadedRunner(
|
|
779
|
+
Runner[TAggregateID], RecordingEventReceiver[TAggregateID]
|
|
780
|
+
):
|
|
759
781
|
"""Runs a :class:`System` with multiple threads in a new way."""
|
|
760
782
|
|
|
761
783
|
QUEUE_MAX_SIZE: int = 0
|
|
@@ -767,10 +789,16 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
767
789
|
):
|
|
768
790
|
"""Initialises runner with the given :class:`System`."""
|
|
769
791
|
super().__init__(system=system, env=env)
|
|
770
|
-
self.apps: dict[str, Application] = {}
|
|
771
|
-
self.pulling_threads: dict[str, list[PullingThread]] = {}
|
|
772
|
-
self.processing_queues: dict[
|
|
773
|
-
|
|
792
|
+
self.apps: dict[str, Application[TAggregateID]] = {}
|
|
793
|
+
self.pulling_threads: dict[str, list[PullingThread[TAggregateID]]] = {}
|
|
794
|
+
self.processing_queues: dict[
|
|
795
|
+
str, Queue[list[ProcessingJob[TAggregateID]] | None]
|
|
796
|
+
] = {}
|
|
797
|
+
self.all_threads: list[
|
|
798
|
+
PullingThread[TAggregateID]
|
|
799
|
+
| ConvertingThread[TAggregateID]
|
|
800
|
+
| ProcessingThread[TAggregateID]
|
|
801
|
+
] = []
|
|
774
802
|
self.has_errored = threading.Event()
|
|
775
803
|
|
|
776
804
|
# Construct followers.
|
|
@@ -807,8 +835,8 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
807
835
|
|
|
808
836
|
# Start the processing threads.
|
|
809
837
|
for follower_name in self.system.followers:
|
|
810
|
-
follower = cast(
|
|
811
|
-
processing_queue: Queue[list[ProcessingJob] | None] = Queue(
|
|
838
|
+
follower = cast(Follower[Any], self.apps[follower_name])
|
|
839
|
+
processing_queue: Queue[list[ProcessingJob[TAggregateID]] | None] = Queue(
|
|
812
840
|
maxsize=self.QUEUE_MAX_SIZE
|
|
813
841
|
)
|
|
814
842
|
self.processing_queues[follower_name] = processing_queue
|
|
@@ -823,13 +851,15 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
823
851
|
for edge in self.system.edges:
|
|
824
852
|
# Set up follower to pull notifications from leader.
|
|
825
853
|
leader_name = edge[0]
|
|
826
|
-
leader = cast("Leader", self.apps[leader_name])
|
|
854
|
+
leader = cast("Leader[Any]", self.apps[leader_name])
|
|
827
855
|
follower_name = edge[1]
|
|
828
|
-
follower = cast(
|
|
856
|
+
follower = cast(Follower[Any], self.apps[follower_name])
|
|
829
857
|
follower.follow(leader.name, leader.notification_log)
|
|
830
858
|
|
|
831
859
|
# Create converting queue.
|
|
832
|
-
converting_queue: Queue[ConvertingJob] = Queue(
|
|
860
|
+
converting_queue: Queue[ConvertingJob[TAggregateID]] = Queue(
|
|
861
|
+
maxsize=self.QUEUE_MAX_SIZE
|
|
862
|
+
)
|
|
833
863
|
|
|
834
864
|
# Start converting thread.
|
|
835
865
|
converting_thread = ConvertingThread(
|
|
@@ -861,7 +891,7 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
861
891
|
|
|
862
892
|
# Subscribe for notifications from leaders.
|
|
863
893
|
for leader_name in self.system.leaders:
|
|
864
|
-
leader = cast("Leader", self.apps[leader_name])
|
|
894
|
+
leader = cast("Leader[Any]", self.apps[leader_name])
|
|
865
895
|
assert isinstance(leader, Leader)
|
|
866
896
|
leader.lead(self)
|
|
867
897
|
|
|
@@ -890,29 +920,33 @@ class NewMultiThreadedRunner(Runner, RecordingEventReceiver):
|
|
|
890
920
|
assert isinstance(app, cls)
|
|
891
921
|
return app
|
|
892
922
|
|
|
893
|
-
def receive_recording_event(
|
|
923
|
+
def receive_recording_event(
|
|
924
|
+
self, new_recording_event: RecordingEvent[TAggregateID]
|
|
925
|
+
) -> None:
|
|
894
926
|
for pulling_thread in self.pulling_threads[
|
|
895
927
|
new_recording_event.application_name
|
|
896
928
|
]:
|
|
897
929
|
pulling_thread.receive_recording_event(new_recording_event)
|
|
898
930
|
|
|
899
931
|
|
|
900
|
-
class PullingThread(threading.Thread):
|
|
932
|
+
class PullingThread(threading.Thread, Generic[TAggregateID]):
|
|
901
933
|
"""Receives or pulls notifications from the given leader, and
|
|
902
934
|
puts them on a queue for conversion into processing jobs.
|
|
903
935
|
"""
|
|
904
936
|
|
|
905
937
|
def __init__(
|
|
906
938
|
self,
|
|
907
|
-
converting_queue: Queue[ConvertingJob],
|
|
908
|
-
follower: Follower,
|
|
939
|
+
converting_queue: Queue[ConvertingJob[TAggregateID]],
|
|
940
|
+
follower: Follower[Any],
|
|
909
941
|
leader_name: str,
|
|
910
942
|
has_errored: threading.Event,
|
|
911
943
|
):
|
|
912
944
|
super().__init__(daemon=True)
|
|
913
945
|
self.overflow_event = threading.Event()
|
|
914
|
-
self.recording_event_queue: Queue[RecordingEvent | None] = Queue(
|
|
915
|
-
|
|
946
|
+
self.recording_event_queue: Queue[RecordingEvent[TAggregateID] | None] = Queue(
|
|
947
|
+
maxsize=100
|
|
948
|
+
)
|
|
949
|
+
self.converting_queue: Queue[ConvertingJob[TAggregateID]] = converting_queue
|
|
916
950
|
self.receive_lock = threading.Lock()
|
|
917
951
|
self.follower = follower
|
|
918
952
|
self.leader_name = leader_name
|
|
@@ -968,7 +1002,9 @@ class PullingThread(threading.Thread):
|
|
|
968
1002
|
self.error.__cause__ = e
|
|
969
1003
|
self.has_errored.set()
|
|
970
1004
|
|
|
971
|
-
def receive_recording_event(
|
|
1005
|
+
def receive_recording_event(
|
|
1006
|
+
self, recording_event: RecordingEvent[TAggregateID]
|
|
1007
|
+
) -> None:
|
|
972
1008
|
try:
|
|
973
1009
|
self.recording_event_queue.put(recording_event, timeout=0)
|
|
974
1010
|
except Full:
|
|
@@ -979,20 +1015,22 @@ class PullingThread(threading.Thread):
|
|
|
979
1015
|
self.recording_event_queue.put(None)
|
|
980
1016
|
|
|
981
1017
|
|
|
982
|
-
class ConvertingThread(threading.Thread):
|
|
1018
|
+
class ConvertingThread(threading.Thread, Generic[TAggregateID]):
|
|
983
1019
|
"""Converts notifications into processing jobs."""
|
|
984
1020
|
|
|
985
1021
|
def __init__(
|
|
986
1022
|
self,
|
|
987
|
-
converting_queue: Queue[ConvertingJob],
|
|
988
|
-
processing_queue: Queue[list[ProcessingJob] | None],
|
|
989
|
-
follower: Follower,
|
|
1023
|
+
converting_queue: Queue[ConvertingJob[TAggregateID]],
|
|
1024
|
+
processing_queue: Queue[list[ProcessingJob[TAggregateID]] | None],
|
|
1025
|
+
follower: Follower[Any],
|
|
990
1026
|
leader_name: str,
|
|
991
1027
|
has_errored: threading.Event,
|
|
992
1028
|
):
|
|
993
1029
|
super().__init__(daemon=True)
|
|
994
|
-
self.converting_queue = converting_queue
|
|
995
|
-
self.processing_queue =
|
|
1030
|
+
self.converting_queue: Queue[ConvertingJob[TAggregateID]] = converting_queue
|
|
1031
|
+
self.processing_queue: Queue[list[ProcessingJob[TAggregateID]] | None] = (
|
|
1032
|
+
processing_queue
|
|
1033
|
+
)
|
|
996
1034
|
self.follower = follower
|
|
997
1035
|
self.leader_name = leader_name
|
|
998
1036
|
self.error: Exception | None = None
|
|
@@ -1046,19 +1084,21 @@ class ConvertingThread(threading.Thread):
|
|
|
1046
1084
|
self.converting_queue.put(None)
|
|
1047
1085
|
|
|
1048
1086
|
|
|
1049
|
-
class ProcessingThread(threading.Thread):
|
|
1087
|
+
class ProcessingThread(threading.Thread, Generic[TAggregateID]):
|
|
1050
1088
|
"""A processing thread gets events from a processing queue, and
|
|
1051
1089
|
calls the application's process_event() method.
|
|
1052
1090
|
"""
|
|
1053
1091
|
|
|
1054
1092
|
def __init__(
|
|
1055
1093
|
self,
|
|
1056
|
-
processing_queue: Queue[list[ProcessingJob] | None],
|
|
1057
|
-
follower: Follower,
|
|
1094
|
+
processing_queue: Queue[list[ProcessingJob[TAggregateID]] | None],
|
|
1095
|
+
follower: Follower[Any],
|
|
1058
1096
|
has_errored: threading.Event,
|
|
1059
1097
|
):
|
|
1060
1098
|
super().__init__(daemon=True)
|
|
1061
|
-
self.processing_queue =
|
|
1099
|
+
self.processing_queue: Queue[list[ProcessingJob[TAggregateID]] | None] = (
|
|
1100
|
+
processing_queue
|
|
1101
|
+
)
|
|
1062
1102
|
self.follower = follower
|
|
1063
1103
|
self.error: Exception | None = None
|
|
1064
1104
|
self.has_errored = has_errored
|
|
@@ -1132,7 +1172,7 @@ class NotificationLogReader:
|
|
|
1132
1172
|
stop: int | None = None,
|
|
1133
1173
|
topics: Sequence[str] = (),
|
|
1134
1174
|
inclusive_of_start: bool = True,
|
|
1135
|
-
) -> Iterator[
|
|
1175
|
+
) -> Iterator[Sequence[Notification]]:
|
|
1136
1176
|
"""Returns a generator that yields lists of event notifications
|
|
1137
1177
|
from the reader's notification log, starting from given start
|
|
1138
1178
|
position (a notification ID).
|