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/tests/__init__.py
CHANGED
|
@@ -5,38 +5,40 @@ import sys
|
|
|
5
5
|
import traceback
|
|
6
6
|
import warnings
|
|
7
7
|
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
-
from datetime import datetime
|
|
9
8
|
from decimal import Decimal
|
|
10
9
|
from threading import Event, get_ident
|
|
11
10
|
from time import sleep
|
|
12
11
|
from timeit import timeit
|
|
13
|
-
from typing import
|
|
12
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
14
13
|
from unittest import TestCase
|
|
15
14
|
from uuid import UUID, uuid4
|
|
16
15
|
|
|
17
16
|
from eventsourcing.application import AggregateNotFoundError, Application
|
|
18
|
-
from eventsourcing.domain import Aggregate
|
|
17
|
+
from eventsourcing.domain import Aggregate, datetime_now_with_tzinfo
|
|
19
18
|
from eventsourcing.persistence import (
|
|
20
19
|
InfrastructureFactory,
|
|
20
|
+
InfrastructureFactoryError,
|
|
21
21
|
IntegrityError,
|
|
22
|
-
|
|
22
|
+
JSONTranscoder,
|
|
23
23
|
Transcoding,
|
|
24
24
|
)
|
|
25
25
|
from eventsourcing.tests.domain import BankAccount, EmailAddress
|
|
26
|
-
from eventsourcing.utils import get_topic
|
|
26
|
+
from eventsourcing.utils import EnvType, get_topic
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from datetime import datetime
|
|
30
|
+
|
|
31
|
+
TIMEIT_FACTOR = int(os.environ.get("TEST_TIMEIT_FACTOR", default="10"))
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
class ExampleApplicationTestCase(TestCase):
|
|
32
35
|
timeit_number: ClassVar[int] = TIMEIT_FACTOR
|
|
33
|
-
started_ats: ClassVar[
|
|
34
|
-
counts: ClassVar[
|
|
36
|
+
started_ats: ClassVar[dict[type[TestCase], datetime]] = {}
|
|
37
|
+
counts: ClassVar[dict[type[TestCase], int]] = {}
|
|
35
38
|
expected_factory_topic: str
|
|
36
39
|
|
|
37
|
-
def test_example_application(self):
|
|
40
|
+
def test_example_application(self) -> None:
|
|
38
41
|
app = BankAccounts(env={"IS_SNAPSHOTTING_ENABLED": "y"})
|
|
39
|
-
max_notification_id = app.recorder.max_notification_id()
|
|
40
42
|
|
|
41
43
|
self.assertEqual(get_topic(type(app.factory)), self.expected_factory_topic)
|
|
42
44
|
|
|
@@ -75,38 +77,38 @@ class ExampleApplicationTestCase(TestCase):
|
|
|
75
77
|
)
|
|
76
78
|
|
|
77
79
|
sleep(1) # Added to make eventsourcing-axon tests work, perhaps not necessary.
|
|
78
|
-
section = app.notification_log[
|
|
79
|
-
f"{max_notification_id + 1},{max_notification_id + 10}"
|
|
80
|
-
]
|
|
80
|
+
section = app.notification_log["1,10"]
|
|
81
81
|
self.assertEqual(len(section.items), 4)
|
|
82
82
|
|
|
83
83
|
# Take snapshot (specify version).
|
|
84
84
|
app.take_snapshot(account_id, version=Aggregate.INITIAL_VERSION + 1)
|
|
85
85
|
|
|
86
|
+
assert app.snapshots is not None # for mypy
|
|
86
87
|
snapshots = list(app.snapshots.get(account_id))
|
|
87
88
|
self.assertEqual(len(snapshots), 1)
|
|
88
89
|
self.assertEqual(snapshots[0].originator_version, Aggregate.INITIAL_VERSION + 1)
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
from_snapshot1: BankAccount = app.repository.get(
|
|
91
92
|
account_id, version=Aggregate.INITIAL_VERSION + 2
|
|
92
93
|
)
|
|
93
|
-
self.assertIsInstance(
|
|
94
|
-
self.assertEqual(
|
|
95
|
-
self.assertEqual(
|
|
94
|
+
self.assertIsInstance(from_snapshot1, BankAccount)
|
|
95
|
+
self.assertEqual(from_snapshot1.version, Aggregate.INITIAL_VERSION + 2)
|
|
96
|
+
self.assertEqual(from_snapshot1.balance, Decimal("35.00"))
|
|
96
97
|
|
|
97
98
|
# Take snapshot (don't specify version).
|
|
98
99
|
app.take_snapshot(account_id)
|
|
100
|
+
assert app.snapshots is not None # for mypy
|
|
99
101
|
snapshots = list(app.snapshots.get(account_id))
|
|
100
102
|
self.assertEqual(len(snapshots), 2)
|
|
101
103
|
self.assertEqual(snapshots[0].originator_version, Aggregate.INITIAL_VERSION + 1)
|
|
102
104
|
self.assertEqual(snapshots[1].originator_version, Aggregate.INITIAL_VERSION + 3)
|
|
103
105
|
|
|
104
|
-
|
|
105
|
-
self.assertIsInstance(
|
|
106
|
-
self.assertEqual(
|
|
107
|
-
self.assertEqual(
|
|
106
|
+
from_snapshot2: BankAccount = app.repository.get(account_id)
|
|
107
|
+
self.assertIsInstance(from_snapshot2, BankAccount)
|
|
108
|
+
self.assertEqual(from_snapshot2.version, Aggregate.INITIAL_VERSION + 3)
|
|
109
|
+
self.assertEqual(from_snapshot2.balance, Decimal("65.00"))
|
|
108
110
|
|
|
109
|
-
def test__put_performance(self):
|
|
111
|
+
def test__put_performance(self) -> None:
|
|
110
112
|
app = BankAccounts()
|
|
111
113
|
|
|
112
114
|
# Open an account.
|
|
@@ -116,7 +118,7 @@ class ExampleApplicationTestCase(TestCase):
|
|
|
116
118
|
)
|
|
117
119
|
account = app.get_account(account_id)
|
|
118
120
|
|
|
119
|
-
def put():
|
|
121
|
+
def put() -> None:
|
|
120
122
|
# Credit the account.
|
|
121
123
|
account.append_transaction(Decimal("10.00"))
|
|
122
124
|
app.save(account)
|
|
@@ -128,14 +130,14 @@ class ExampleApplicationTestCase(TestCase):
|
|
|
128
130
|
duration = timeit(put, number=self.timeit_number)
|
|
129
131
|
self.print_time("store events", duration)
|
|
130
132
|
|
|
131
|
-
def test__get_performance_with_snapshotting_enabled(self):
|
|
133
|
+
def test__get_performance_with_snapshotting_enabled(self) -> None:
|
|
132
134
|
print()
|
|
133
135
|
self._test_get_performance(is_snapshotting_enabled=True)
|
|
134
136
|
|
|
135
|
-
def test__get_performance_without_snapshotting_enabled(self):
|
|
137
|
+
def test__get_performance_without_snapshotting_enabled(self) -> None:
|
|
136
138
|
self._test_get_performance(is_snapshotting_enabled=False)
|
|
137
139
|
|
|
138
|
-
def _test_get_performance(self, *, is_snapshotting_enabled: bool):
|
|
140
|
+
def _test_get_performance(self, *, is_snapshotting_enabled: bool) -> None:
|
|
139
141
|
app = BankAccounts(
|
|
140
142
|
env={"IS_SNAPSHOTTING_ENABLED": "y" if is_snapshotting_enabled else "n"}
|
|
141
143
|
)
|
|
@@ -146,7 +148,7 @@ class ExampleApplicationTestCase(TestCase):
|
|
|
146
148
|
email_address="alice@example.com",
|
|
147
149
|
)
|
|
148
150
|
|
|
149
|
-
def read():
|
|
151
|
+
def read() -> None:
|
|
150
152
|
# Get the account.
|
|
151
153
|
app.get_account(account_id)
|
|
152
154
|
|
|
@@ -161,10 +163,10 @@ class ExampleApplicationTestCase(TestCase):
|
|
|
161
163
|
test_label = "get without snapshotting"
|
|
162
164
|
self.print_time(test_label, duration)
|
|
163
165
|
|
|
164
|
-
def print_time(self, test_label, duration):
|
|
166
|
+
def print_time(self, test_label: str, duration: float) -> None:
|
|
165
167
|
cls = type(self)
|
|
166
168
|
if cls not in self.started_ats:
|
|
167
|
-
self.started_ats[cls] =
|
|
169
|
+
self.started_ats[cls] = datetime_now_with_tzinfo()
|
|
168
170
|
print(f"{cls.__name__: <29} timeit number: {cls.timeit_number}")
|
|
169
171
|
self.counts[cls] = 1
|
|
170
172
|
else:
|
|
@@ -179,8 +181,8 @@ class ExampleApplicationTestCase(TestCase):
|
|
|
179
181
|
)
|
|
180
182
|
|
|
181
183
|
if self.counts[cls] == 3:
|
|
182
|
-
|
|
183
|
-
print(f"{cls.__name__: <29} timeit duration: {
|
|
184
|
+
cls_duration = datetime_now_with_tzinfo() - cls.started_ats[cls]
|
|
185
|
+
print(f"{cls.__name__: <29} timeit duration: {cls_duration}")
|
|
184
186
|
sys.stdout.flush()
|
|
185
187
|
|
|
186
188
|
|
|
@@ -198,11 +200,11 @@ class EmailAddressAsStr(Transcoding):
|
|
|
198
200
|
class BankAccounts(Application):
|
|
199
201
|
is_snapshotting_enabled = True
|
|
200
202
|
|
|
201
|
-
def register_transcodings(self, transcoder:
|
|
203
|
+
def register_transcodings(self, transcoder: JSONTranscoder) -> None:
|
|
202
204
|
super().register_transcodings(transcoder)
|
|
203
205
|
transcoder.register(EmailAddressAsStr())
|
|
204
206
|
|
|
205
|
-
def open_account(self, full_name, email_address):
|
|
207
|
+
def open_account(self, full_name: str, email_address: str) -> UUID:
|
|
206
208
|
account = BankAccount.open(
|
|
207
209
|
full_name=full_name,
|
|
208
210
|
email_address=email_address,
|
|
@@ -221,7 +223,7 @@ class BankAccounts(Application):
|
|
|
221
223
|
|
|
222
224
|
def get_account(self, account_id: UUID) -> BankAccount:
|
|
223
225
|
try:
|
|
224
|
-
aggregate = self.repository.get(account_id)
|
|
226
|
+
aggregate: BankAccount = self.repository.get(account_id)
|
|
225
227
|
except AggregateNotFoundError:
|
|
226
228
|
raise self.AccountNotFoundError(account_id) from None
|
|
227
229
|
else:
|
|
@@ -233,7 +235,7 @@ class BankAccounts(Application):
|
|
|
233
235
|
|
|
234
236
|
|
|
235
237
|
class ApplicationTestCase(TestCase):
|
|
236
|
-
def test_name(self):
|
|
238
|
+
def test_name(self) -> None:
|
|
237
239
|
self.assertEqual(Application.name, "Application")
|
|
238
240
|
|
|
239
241
|
class MyApplication1(Application):
|
|
@@ -246,7 +248,7 @@ class ApplicationTestCase(TestCase):
|
|
|
246
248
|
|
|
247
249
|
self.assertEqual(MyApplication2.name, "MyBoundedContext")
|
|
248
250
|
|
|
249
|
-
def test_resolve_persistence_topics(self):
|
|
251
|
+
def test_resolve_persistence_topics(self) -> None:
|
|
250
252
|
# None specified.
|
|
251
253
|
app = Application()
|
|
252
254
|
self.assertIsInstance(app.factory, InfrastructureFactory)
|
|
@@ -264,7 +266,7 @@ class ApplicationTestCase(TestCase):
|
|
|
264
266
|
self.assertIsInstance(app.factory, InfrastructureFactory)
|
|
265
267
|
|
|
266
268
|
# Check exceptions.
|
|
267
|
-
with self.assertRaises(
|
|
269
|
+
with self.assertRaises(InfrastructureFactoryError) as cm:
|
|
268
270
|
Application(env={"PERSISTENCE_MODULE": "eventsourcing.application"})
|
|
269
271
|
self.assertEqual(
|
|
270
272
|
cm.exception.args[0],
|
|
@@ -272,17 +274,18 @@ class ApplicationTestCase(TestCase):
|
|
|
272
274
|
"'eventsourcing.application', expected 1.",
|
|
273
275
|
)
|
|
274
276
|
|
|
275
|
-
with self.assertRaises(
|
|
277
|
+
with self.assertRaises(InfrastructureFactoryError) as cm:
|
|
276
278
|
Application(
|
|
277
279
|
env={"PERSISTENCE_MODULE": "eventsourcing.application:Application"}
|
|
278
280
|
)
|
|
279
281
|
self.assertEqual(
|
|
282
|
+
"Topic 'eventsourcing.application:Application' didn't "
|
|
283
|
+
"resolve to a persistence module or infrastructure factory class: "
|
|
284
|
+
"<class 'eventsourcing.application.Application'>",
|
|
280
285
|
cm.exception.args[0],
|
|
281
|
-
"Not an infrastructure factory class or module: "
|
|
282
|
-
"eventsourcing.application:Application",
|
|
283
286
|
)
|
|
284
287
|
|
|
285
|
-
def test_save_returns_recording_event(self):
|
|
288
|
+
def test_save_returns_recording_event(self) -> None:
|
|
286
289
|
app = Application()
|
|
287
290
|
|
|
288
291
|
recordings = app.save()
|
|
@@ -291,22 +294,22 @@ class ApplicationTestCase(TestCase):
|
|
|
291
294
|
recordings = app.save(None)
|
|
292
295
|
self.assertEqual(recordings, [])
|
|
293
296
|
|
|
294
|
-
max_id = app.recorder.max_notification_id()
|
|
295
|
-
|
|
296
297
|
recordings = app.save(Aggregate())
|
|
297
298
|
self.assertEqual(len(recordings), 1)
|
|
298
|
-
self.assertEqual(recordings[0].notification.id, 1
|
|
299
|
+
self.assertEqual(recordings[0].notification.id, 1)
|
|
299
300
|
|
|
300
301
|
recordings = app.save(Aggregate())
|
|
301
302
|
self.assertEqual(len(recordings), 1)
|
|
302
|
-
self.assertEqual(recordings[0].notification.id, 2
|
|
303
|
+
self.assertEqual(recordings[0].notification.id, 2)
|
|
303
304
|
|
|
304
305
|
recordings = app.save(Aggregate(), Aggregate())
|
|
305
306
|
self.assertEqual(len(recordings), 2)
|
|
306
|
-
self.assertEqual(recordings[0].notification.id, 3
|
|
307
|
-
self.assertEqual(recordings[1].notification.id, 4
|
|
307
|
+
self.assertEqual(recordings[0].notification.id, 3)
|
|
308
|
+
self.assertEqual(recordings[1].notification.id, 4)
|
|
308
309
|
|
|
309
|
-
def test_take_snapshot_raises_assertion_error_if_snapshotting_not_enabled(
|
|
310
|
+
def test_take_snapshot_raises_assertion_error_if_snapshotting_not_enabled(
|
|
311
|
+
self,
|
|
312
|
+
) -> None:
|
|
310
313
|
app = Application()
|
|
311
314
|
with self.assertRaises(AssertionError) as cm:
|
|
312
315
|
app.take_snapshot(uuid4())
|
|
@@ -319,12 +322,13 @@ class ApplicationTestCase(TestCase):
|
|
|
319
322
|
"application class.",
|
|
320
323
|
)
|
|
321
324
|
|
|
322
|
-
def test_application_with_cached_aggregates_and_fastforward(self):
|
|
325
|
+
def test_application_with_cached_aggregates_and_fastforward(self) -> None:
|
|
323
326
|
app = Application(env={"AGGREGATE_CACHE_MAXSIZE": "10"})
|
|
324
327
|
|
|
325
328
|
aggregate = Aggregate()
|
|
326
329
|
app.save(aggregate)
|
|
327
330
|
# Should not put the aggregate in the cache.
|
|
331
|
+
assert app.repository.cache is not None # for mypy
|
|
328
332
|
with self.assertRaises(KeyError):
|
|
329
333
|
self.assertEqual(aggregate, app.repository.cache.get(aggregate.id))
|
|
330
334
|
|
|
@@ -344,111 +348,171 @@ class ApplicationTestCase(TestCase):
|
|
|
344
348
|
app.repository.get(aggregate.id)
|
|
345
349
|
self.assertEqual(aggregate, app.repository.cache.get(aggregate.id))
|
|
346
350
|
|
|
347
|
-
def
|
|
348
|
-
|
|
351
|
+
def test_check_aggregate_fastforwarding_nonblocking(self) -> None:
|
|
352
|
+
self._check_aggregate_fastforwarding_during_contention(
|
|
349
353
|
env={
|
|
350
354
|
"AGGREGATE_CACHE_MAXSIZE": "10",
|
|
351
355
|
"AGGREGATE_CACHE_FASTFORWARD_SKIPPING": "y",
|
|
352
356
|
}
|
|
353
357
|
)
|
|
354
358
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
359
|
+
def test_check_aggregate_fastforwarding_blocking(self) -> None:
|
|
360
|
+
self._check_aggregate_fastforwarding_during_contention(
|
|
361
|
+
env={"AGGREGATE_CACHE_MAXSIZE": "10"}
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
def _check_aggregate_fastforwarding_during_contention(self, env: EnvType) -> None:
|
|
365
|
+
app = Application(env=env)
|
|
366
|
+
|
|
367
|
+
self.assertEqual(len(app.repository._fastforward_locks_inuse), 0)
|
|
368
|
+
|
|
369
|
+
# Create one aggregate.
|
|
370
|
+
original_aggregate = Aggregate()
|
|
371
|
+
app.save(original_aggregate)
|
|
372
|
+
obj_ids = set()
|
|
373
|
+
|
|
374
|
+
# Prime the cache.
|
|
375
|
+
app.repository.get(original_aggregate.id)
|
|
376
|
+
|
|
377
|
+
# Remember the aggregate ID.
|
|
378
|
+
aggregate_id = original_aggregate.id
|
|
358
379
|
|
|
359
380
|
stopped = Event()
|
|
381
|
+
errors: list[BaseException] = []
|
|
382
|
+
successful_thread_ids = set()
|
|
360
383
|
|
|
361
|
-
|
|
362
|
-
def trigger_save_get_check():
|
|
384
|
+
def trigger_save_get_check() -> None:
|
|
363
385
|
while not stopped.is_set():
|
|
364
386
|
try:
|
|
365
|
-
|
|
387
|
+
# Get the aggregate.
|
|
388
|
+
aggregate: Aggregate = app.repository.get(aggregate_id)
|
|
389
|
+
original_version = aggregate.version
|
|
390
|
+
|
|
391
|
+
# Try to record a new event.
|
|
366
392
|
aggregate.trigger_event(Aggregate.Event)
|
|
367
|
-
|
|
393
|
+
# Give other threads a chance.
|
|
368
394
|
try:
|
|
369
395
|
app.save(aggregate)
|
|
370
396
|
except IntegrityError:
|
|
397
|
+
# Start again if we didn't record a new event.
|
|
398
|
+
# print("Got integrity error")
|
|
399
|
+
sleep(0.001)
|
|
371
400
|
continue
|
|
372
|
-
cached_version = app.repository.get(aggregate_id).version
|
|
373
|
-
if saved_version > cached_version:
|
|
374
|
-
print(f"Skipped fast-forwarding at version {saved_version}")
|
|
375
|
-
stopped.set()
|
|
376
|
-
if aggregate.version % 1000 == 0:
|
|
377
|
-
print("Version:", aggregate.version, get_ident())
|
|
378
|
-
sleep(0.00)
|
|
379
|
-
except BaseException:
|
|
380
|
-
print(traceback.format_exc())
|
|
381
|
-
raise
|
|
382
401
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
402
|
+
# Get the aggregate from the cache.
|
|
403
|
+
assert app.repository.cache is not None
|
|
404
|
+
cached: Any = app.repository.cache.get(aggregate_id)
|
|
405
|
+
obj_ids.add(id(cached))
|
|
386
406
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
executor.shutdown()
|
|
407
|
+
if len(obj_ids) > 1:
|
|
408
|
+
stopped.set()
|
|
409
|
+
continue
|
|
391
410
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
env={
|
|
395
|
-
"AGGREGATE_CACHE_MAXSIZE": "10",
|
|
396
|
-
}
|
|
397
|
-
)
|
|
411
|
+
# Fast-forward the cached aggregate.
|
|
412
|
+
fastforwarded: Aggregate = app.repository.get(aggregate_id)
|
|
398
413
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
414
|
+
# Check cached aggregate was fast-forwarded with recorded event.
|
|
415
|
+
if fastforwarded.version < original_version:
|
|
416
|
+
try:
|
|
417
|
+
self.fail(
|
|
418
|
+
f"Failed to fast-forward at version {original_version}"
|
|
419
|
+
)
|
|
420
|
+
except AssertionError as e:
|
|
421
|
+
errors.append(e)
|
|
422
|
+
stopped.set()
|
|
423
|
+
continue
|
|
402
424
|
|
|
403
|
-
|
|
425
|
+
# Monitor number of threads getting involved.
|
|
426
|
+
thread_id = get_ident()
|
|
427
|
+
successful_thread_ids.add(thread_id)
|
|
404
428
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
aggregate = app.repository.get(aggregate_id)
|
|
410
|
-
aggregate.trigger_event(Aggregate.Event)
|
|
411
|
-
saved_version = aggregate.version
|
|
412
|
-
try:
|
|
413
|
-
app.save(aggregate)
|
|
414
|
-
except IntegrityError:
|
|
415
|
-
continue
|
|
416
|
-
cached_version = app.repository.get(aggregate_id).version
|
|
417
|
-
if saved_version > cached_version:
|
|
418
|
-
print(f"Skipped fast-forwarding at version {saved_version}")
|
|
429
|
+
# print("Version:", aggregate.version, thread_id)
|
|
430
|
+
|
|
431
|
+
# See if we have done enough.
|
|
432
|
+
if len(successful_thread_ids) > 10 and aggregate.version >= 25:
|
|
419
433
|
stopped.set()
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
sleep(0.
|
|
423
|
-
|
|
434
|
+
continue
|
|
435
|
+
|
|
436
|
+
sleep(0.0001)
|
|
437
|
+
# sleep(0.001)
|
|
438
|
+
except BaseException as e:
|
|
439
|
+
errors.append(e)
|
|
440
|
+
stopped.set()
|
|
424
441
|
print(traceback.format_exc())
|
|
425
442
|
raise
|
|
426
443
|
|
|
427
444
|
executor = ThreadPoolExecutor(max_workers=100)
|
|
445
|
+
futures = []
|
|
428
446
|
for _ in range(100):
|
|
429
|
-
executor.submit(trigger_save_get_check)
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
447
|
+
f = executor.submit(trigger_save_get_check)
|
|
448
|
+
futures.append(f)
|
|
449
|
+
|
|
450
|
+
# Run for three seconds.
|
|
451
|
+
stopped.wait(timeout=10)
|
|
452
|
+
for f in futures:
|
|
453
|
+
f.result()
|
|
454
|
+
# print("Got all results, shutting down executor")
|
|
435
455
|
executor.shutdown()
|
|
436
456
|
|
|
437
|
-
|
|
457
|
+
try:
|
|
458
|
+
if errors:
|
|
459
|
+
raise errors[0]
|
|
460
|
+
if len(obj_ids) > 1:
|
|
461
|
+
self.fail(f"More than one instance used in the cache: {len(obj_ids)}")
|
|
462
|
+
if len(successful_thread_ids) < 3:
|
|
463
|
+
self.fail("Insufficient sharing across contentious threads")
|
|
464
|
+
|
|
465
|
+
final_aggregate: Aggregate = app.repository.get(aggregate_id)
|
|
466
|
+
# print("Final aggregate version:", final_aggregate.version)
|
|
467
|
+
if final_aggregate.version < 25:
|
|
468
|
+
self.fail(f"Insufficient version increment: {final_aggregate.version}")
|
|
469
|
+
|
|
470
|
+
self.assertEqual(len(app.repository._fastforward_locks_inuse), 0)
|
|
471
|
+
|
|
472
|
+
finally:
|
|
473
|
+
# print("Closing application")
|
|
474
|
+
app.close()
|
|
475
|
+
|
|
476
|
+
def test_application_with_cached_aggregates_not_fastforward(self) -> None:
|
|
438
477
|
app = Application(
|
|
439
478
|
env={
|
|
440
479
|
"AGGREGATE_CACHE_MAXSIZE": "10",
|
|
441
480
|
"AGGREGATE_CACHE_FASTFORWARD": "f",
|
|
442
481
|
}
|
|
443
482
|
)
|
|
444
|
-
|
|
445
|
-
app.save(
|
|
446
|
-
|
|
447
|
-
self.assertEqual(aggregate, app.repository.cache.get(aggregate.id))
|
|
448
|
-
app.repository.get(aggregate.id)
|
|
449
|
-
self.assertEqual(aggregate, app.repository.cache.get(aggregate.id))
|
|
483
|
+
aggregate1 = Aggregate()
|
|
484
|
+
app.save(aggregate1)
|
|
485
|
+
aggregate_id = aggregate1.id
|
|
450
486
|
|
|
451
|
-
|
|
487
|
+
# Should put the aggregate in the cache.
|
|
488
|
+
assert app.repository.cache is not None # for mypy
|
|
489
|
+
self.assertEqual(aggregate1, app.repository.cache.get(aggregate_id))
|
|
490
|
+
app.repository.get(aggregate_id)
|
|
491
|
+
self.assertEqual(aggregate1, app.repository.cache.get(aggregate_id))
|
|
492
|
+
|
|
493
|
+
aggregate2 = Aggregate()
|
|
494
|
+
aggregate2._id = aggregate_id
|
|
495
|
+
aggregate2.trigger_event(Aggregate.Event)
|
|
496
|
+
|
|
497
|
+
# This will replace object in cache.
|
|
498
|
+
app.save(aggregate2)
|
|
499
|
+
|
|
500
|
+
self.assertEqual(aggregate2.version, aggregate1.version + 1)
|
|
501
|
+
aggregate3: Aggregate = app.repository.get(aggregate_id)
|
|
502
|
+
self.assertEqual(aggregate3.version, aggregate3.version)
|
|
503
|
+
self.assertEqual(id(aggregate3.version), id(aggregate3.version))
|
|
504
|
+
|
|
505
|
+
# This will mess things up because the cache has a stale aggregate.
|
|
506
|
+
aggregate3.trigger_event(Aggregate.Event)
|
|
507
|
+
app.events.put(aggregate3.collect_events())
|
|
508
|
+
|
|
509
|
+
# And so using the aggregate to record new events will cause an IntegrityError.
|
|
510
|
+
aggregate4: Aggregate = app.repository.get(aggregate_id)
|
|
511
|
+
aggregate4.trigger_event(Aggregate.Event)
|
|
512
|
+
with self.assertRaises(IntegrityError):
|
|
513
|
+
app.save(aggregate4)
|
|
514
|
+
|
|
515
|
+
def test_application_with_deepcopy_from_cache_arg(self) -> None:
|
|
452
516
|
app = Application(
|
|
453
517
|
env={
|
|
454
518
|
"AGGREGATE_CACHE_MAXSIZE": "10",
|
|
@@ -457,14 +521,15 @@ class ApplicationTestCase(TestCase):
|
|
|
457
521
|
aggregate = Aggregate()
|
|
458
522
|
app.save(aggregate)
|
|
459
523
|
self.assertEqual(aggregate.version, 1)
|
|
460
|
-
|
|
461
|
-
|
|
524
|
+
reconstructed: Aggregate = app.repository.get(aggregate.id)
|
|
525
|
+
reconstructed.version = 101
|
|
526
|
+
assert app.repository.cache is not None # for mypy
|
|
462
527
|
self.assertEqual(app.repository.cache.get(aggregate.id).version, 1)
|
|
463
|
-
|
|
464
|
-
|
|
528
|
+
cached: Aggregate = app.repository.get(aggregate.id, deepcopy_from_cache=False)
|
|
529
|
+
cached.version = 101
|
|
465
530
|
self.assertEqual(app.repository.cache.get(aggregate.id).version, 101)
|
|
466
531
|
|
|
467
|
-
def test_application_with_deepcopy_from_cache_attribute(self):
|
|
532
|
+
def test_application_with_deepcopy_from_cache_attribute(self) -> None:
|
|
468
533
|
app = Application(
|
|
469
534
|
env={
|
|
470
535
|
"AGGREGATE_CACHE_MAXSIZE": "10",
|
|
@@ -473,15 +538,16 @@ class ApplicationTestCase(TestCase):
|
|
|
473
538
|
aggregate = Aggregate()
|
|
474
539
|
app.save(aggregate)
|
|
475
540
|
self.assertEqual(aggregate.version, 1)
|
|
476
|
-
|
|
477
|
-
|
|
541
|
+
reconstructed: Aggregate = app.repository.get(aggregate.id)
|
|
542
|
+
reconstructed.version = 101
|
|
543
|
+
assert app.repository.cache is not None # for mypy
|
|
478
544
|
self.assertEqual(app.repository.cache.get(aggregate.id).version, 1)
|
|
479
545
|
app.repository.deepcopy_from_cache = False
|
|
480
|
-
|
|
481
|
-
|
|
546
|
+
cached: Aggregate = app.repository.get(aggregate.id)
|
|
547
|
+
cached.version = 101
|
|
482
548
|
self.assertEqual(app.repository.cache.get(aggregate.id).version, 101)
|
|
483
549
|
|
|
484
|
-
def test_application_log(self):
|
|
550
|
+
def test_application_log(self) -> None:
|
|
485
551
|
# Check the old 'log' attribute presents the 'notification log' object.
|
|
486
552
|
app = Application()
|
|
487
553
|
|
|
@@ -491,6 +557,6 @@ class ApplicationTestCase(TestCase):
|
|
|
491
557
|
|
|
492
558
|
self.assertEqual(1, len(w))
|
|
493
559
|
self.assertIs(w[-1].category, DeprecationWarning)
|
|
494
|
-
self.
|
|
495
|
-
"'log' is deprecated, use 'notifications' instead", w[-1].message
|
|
560
|
+
self.assertIn(
|
|
561
|
+
"'log' is deprecated, use 'notifications' instead", str(w[-1].message)
|
|
496
562
|
)
|