eventsourcing 9.2.22__py3-none-any.whl → 9.3.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 +1 -1
- eventsourcing/application.py +116 -135
- eventsourcing/cipher.py +15 -12
- eventsourcing/dispatch.py +31 -91
- eventsourcing/domain.py +220 -226
- eventsourcing/examples/__init__.py +0 -0
- eventsourcing/examples/aggregate1/__init__.py +0 -0
- eventsourcing/examples/aggregate1/application.py +27 -0
- eventsourcing/examples/aggregate1/domainmodel.py +16 -0
- eventsourcing/examples/aggregate1/test_application.py +37 -0
- eventsourcing/examples/aggregate2/__init__.py +0 -0
- eventsourcing/examples/aggregate2/application.py +27 -0
- eventsourcing/examples/aggregate2/domainmodel.py +22 -0
- eventsourcing/examples/aggregate2/test_application.py +37 -0
- eventsourcing/examples/aggregate3/__init__.py +0 -0
- eventsourcing/examples/aggregate3/application.py +27 -0
- eventsourcing/examples/aggregate3/domainmodel.py +38 -0
- eventsourcing/examples/aggregate3/test_application.py +37 -0
- eventsourcing/examples/aggregate4/__init__.py +0 -0
- eventsourcing/examples/aggregate4/application.py +27 -0
- eventsourcing/examples/aggregate4/domainmodel.py +114 -0
- eventsourcing/examples/aggregate4/test_application.py +38 -0
- eventsourcing/examples/aggregate5/__init__.py +0 -0
- eventsourcing/examples/aggregate5/application.py +27 -0
- eventsourcing/examples/aggregate5/domainmodel.py +131 -0
- eventsourcing/examples/aggregate5/test_application.py +38 -0
- eventsourcing/examples/aggregate6/__init__.py +0 -0
- eventsourcing/examples/aggregate6/application.py +30 -0
- eventsourcing/examples/aggregate6/domainmodel.py +123 -0
- eventsourcing/examples/aggregate6/test_application.py +38 -0
- eventsourcing/examples/aggregate6a/__init__.py +0 -0
- eventsourcing/examples/aggregate6a/application.py +40 -0
- eventsourcing/examples/aggregate6a/domainmodel.py +149 -0
- eventsourcing/examples/aggregate6a/test_application.py +45 -0
- eventsourcing/examples/aggregate7/__init__.py +0 -0
- eventsourcing/examples/aggregate7/application.py +48 -0
- eventsourcing/examples/aggregate7/domainmodel.py +144 -0
- eventsourcing/examples/aggregate7/persistence.py +57 -0
- eventsourcing/examples/aggregate7/test_application.py +38 -0
- eventsourcing/examples/aggregate7/test_compression_and_encryption.py +45 -0
- eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +67 -0
- eventsourcing/examples/aggregate7a/__init__.py +0 -0
- eventsourcing/examples/aggregate7a/application.py +56 -0
- eventsourcing/examples/aggregate7a/domainmodel.py +170 -0
- eventsourcing/examples/aggregate7a/test_application.py +46 -0
- eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +45 -0
- eventsourcing/examples/aggregate8/__init__.py +0 -0
- eventsourcing/examples/aggregate8/application.py +47 -0
- eventsourcing/examples/aggregate8/domainmodel.py +65 -0
- eventsourcing/examples/aggregate8/persistence.py +57 -0
- eventsourcing/examples/aggregate8/test_application.py +37 -0
- eventsourcing/examples/aggregate8/test_compression_and_encryption.py +44 -0
- eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +38 -0
- eventsourcing/examples/bankaccounts/__init__.py +0 -0
- eventsourcing/examples/bankaccounts/application.py +70 -0
- eventsourcing/examples/bankaccounts/domainmodel.py +56 -0
- eventsourcing/examples/bankaccounts/test.py +173 -0
- eventsourcing/examples/cargoshipping/__init__.py +0 -0
- eventsourcing/examples/cargoshipping/application.py +126 -0
- eventsourcing/examples/cargoshipping/domainmodel.py +330 -0
- eventsourcing/examples/cargoshipping/interface.py +143 -0
- eventsourcing/examples/cargoshipping/test.py +231 -0
- eventsourcing/examples/contentmanagement/__init__.py +0 -0
- eventsourcing/examples/contentmanagement/application.py +118 -0
- eventsourcing/examples/contentmanagement/domainmodel.py +69 -0
- eventsourcing/examples/contentmanagement/test.py +180 -0
- eventsourcing/examples/contentmanagement/utils.py +26 -0
- eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
- eventsourcing/examples/contentmanagementsystem/application.py +54 -0
- eventsourcing/examples/contentmanagementsystem/postgres.py +17 -0
- eventsourcing/examples/contentmanagementsystem/sqlite.py +17 -0
- eventsourcing/examples/contentmanagementsystem/system.py +14 -0
- eventsourcing/examples/contentmanagementsystem/test_system.py +180 -0
- eventsourcing/examples/searchablecontent/__init__.py +0 -0
- eventsourcing/examples/searchablecontent/application.py +45 -0
- eventsourcing/examples/searchablecontent/persistence.py +23 -0
- eventsourcing/examples/searchablecontent/postgres.py +118 -0
- eventsourcing/examples/searchablecontent/sqlite.py +136 -0
- eventsourcing/examples/searchablecontent/test_application.py +110 -0
- eventsourcing/examples/searchablecontent/test_recorder.py +68 -0
- eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
- eventsourcing/examples/searchabletimestamps/application.py +32 -0
- eventsourcing/examples/searchabletimestamps/persistence.py +20 -0
- eventsourcing/examples/searchabletimestamps/postgres.py +110 -0
- eventsourcing/examples/searchabletimestamps/sqlite.py +99 -0
- eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +94 -0
- eventsourcing/examples/test_invoice.py +176 -0
- eventsourcing/examples/test_parking_lot.py +206 -0
- eventsourcing/interface.py +2 -2
- eventsourcing/persistence.py +85 -81
- eventsourcing/popo.py +30 -31
- eventsourcing/postgres.py +379 -590
- eventsourcing/sqlite.py +91 -99
- eventsourcing/system.py +52 -57
- eventsourcing/tests/application.py +20 -32
- eventsourcing/tests/application_tests/__init__.py +0 -0
- eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +55 -0
- eventsourcing/tests/application_tests/test_application_with_popo.py +22 -0
- eventsourcing/tests/application_tests/test_application_with_postgres.py +75 -0
- eventsourcing/tests/application_tests/test_application_with_sqlite.py +72 -0
- eventsourcing/tests/application_tests/test_cache.py +134 -0
- eventsourcing/tests/application_tests/test_event_sourced_log.py +162 -0
- eventsourcing/tests/application_tests/test_notificationlog.py +232 -0
- eventsourcing/tests/application_tests/test_notificationlogreader.py +126 -0
- eventsourcing/tests/application_tests/test_processapplication.py +110 -0
- eventsourcing/tests/application_tests/test_processingpolicy.py +109 -0
- eventsourcing/tests/application_tests/test_repository.py +504 -0
- eventsourcing/tests/application_tests/test_snapshotting.py +68 -0
- eventsourcing/tests/application_tests/test_upcasting.py +459 -0
- eventsourcing/tests/docs_tests/__init__.py +0 -0
- eventsourcing/tests/docs_tests/test_docs.py +293 -0
- eventsourcing/tests/domain.py +1 -1
- eventsourcing/tests/domain_tests/__init__.py +0 -0
- eventsourcing/tests/domain_tests/test_aggregate.py +1180 -0
- eventsourcing/tests/domain_tests/test_aggregate_decorators.py +1604 -0
- eventsourcing/tests/domain_tests/test_domainevent.py +80 -0
- eventsourcing/tests/interface_tests/__init__.py +0 -0
- eventsourcing/tests/interface_tests/test_remotenotificationlog.py +258 -0
- eventsourcing/tests/persistence.py +52 -50
- eventsourcing/tests/persistence_tests/__init__.py +0 -0
- eventsourcing/tests/persistence_tests/test_aes.py +93 -0
- eventsourcing/tests/persistence_tests/test_connection_pool.py +722 -0
- eventsourcing/tests/persistence_tests/test_eventstore.py +72 -0
- eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +21 -0
- eventsourcing/tests/persistence_tests/test_mapper.py +113 -0
- eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +69 -0
- eventsourcing/tests/persistence_tests/test_popo.py +124 -0
- eventsourcing/tests/persistence_tests/test_postgres.py +1119 -0
- eventsourcing/tests/persistence_tests/test_sqlite.py +348 -0
- eventsourcing/tests/persistence_tests/test_transcoder.py +44 -0
- eventsourcing/tests/postgres_utils.py +7 -7
- eventsourcing/tests/system_tests/__init__.py +0 -0
- eventsourcing/tests/system_tests/test_runner.py +935 -0
- eventsourcing/tests/system_tests/test_system.py +284 -0
- eventsourcing/tests/utils_tests/__init__.py +0 -0
- eventsourcing/tests/utils_tests/test_utils.py +226 -0
- eventsourcing/utils.py +47 -50
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/METADATA +29 -79
- eventsourcing-9.3.0.dist-info/RECORD +145 -0
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/WHEEL +1 -2
- eventsourcing-9.2.22.dist-info/RECORD +0 -25
- eventsourcing-9.2.22.dist-info/top_level.txt +0 -1
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import sys
|
|
5
|
+
from threading import Event, Lock, Thread
|
|
6
|
+
from time import sleep, time
|
|
7
|
+
from typing import Any, ClassVar, List
|
|
8
|
+
from unittest import TestCase
|
|
9
|
+
|
|
10
|
+
from eventsourcing.persistence import (
|
|
11
|
+
Connection,
|
|
12
|
+
ConnectionNotFromPoolError,
|
|
13
|
+
ConnectionPool,
|
|
14
|
+
ConnectionPoolClosedError,
|
|
15
|
+
ConnectionUnavailableError,
|
|
16
|
+
Cursor,
|
|
17
|
+
PersistenceError,
|
|
18
|
+
ProgrammingError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DummyCursor(Cursor):
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self._closed = False
|
|
25
|
+
self._results = None
|
|
26
|
+
|
|
27
|
+
def execute(self, statement: str | bytes, _: Any = None):
|
|
28
|
+
if self._closed:
|
|
29
|
+
raise PersistenceError
|
|
30
|
+
assert statement == "SELECT 1"
|
|
31
|
+
self._results = [[1]]
|
|
32
|
+
|
|
33
|
+
def fetchall(self):
|
|
34
|
+
if self._closed:
|
|
35
|
+
raise PersistenceError
|
|
36
|
+
if self._results is None:
|
|
37
|
+
raise ProgrammingError
|
|
38
|
+
return self._results
|
|
39
|
+
|
|
40
|
+
def fetchone(self):
|
|
41
|
+
if self._closed:
|
|
42
|
+
raise PersistenceError
|
|
43
|
+
if self._results is None:
|
|
44
|
+
raise ProgrammingError
|
|
45
|
+
return self._results[0]
|
|
46
|
+
|
|
47
|
+
def close(self):
|
|
48
|
+
self._closed = True
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DummyConnection(Connection):
|
|
52
|
+
def __init__(self, max_age: float | None = None):
|
|
53
|
+
super().__init__(max_age=max_age)
|
|
54
|
+
self._cursors: List[DummyCursor] = []
|
|
55
|
+
self._closed_on_server = False
|
|
56
|
+
|
|
57
|
+
def commit(self):
|
|
58
|
+
if self.closed:
|
|
59
|
+
msg = "Closed"
|
|
60
|
+
raise PersistenceError(msg)
|
|
61
|
+
|
|
62
|
+
def rollback(self):
|
|
63
|
+
if self.closed:
|
|
64
|
+
msg = "Closed"
|
|
65
|
+
raise PersistenceError(msg)
|
|
66
|
+
|
|
67
|
+
def cursor(self):
|
|
68
|
+
curs = DummyCursor()
|
|
69
|
+
self._cursors.append(curs)
|
|
70
|
+
if self._closed or self._closed_on_server:
|
|
71
|
+
curs.close()
|
|
72
|
+
return curs
|
|
73
|
+
|
|
74
|
+
def _close(self):
|
|
75
|
+
for curs in self._cursors:
|
|
76
|
+
curs.close()
|
|
77
|
+
super()._close()
|
|
78
|
+
|
|
79
|
+
def close_on_server(self):
|
|
80
|
+
self._closed_on_server = True
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class DummyConnectionPool(ConnectionPool):
|
|
84
|
+
def _create_connection(self) -> Connection:
|
|
85
|
+
return DummyConnection(max_age=self.max_age)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestConnection(TestCase):
|
|
89
|
+
def tearDown(self) -> None:
|
|
90
|
+
sys.stdout.flush()
|
|
91
|
+
|
|
92
|
+
def test_commit_rollback_close(self):
|
|
93
|
+
conn = DummyConnection()
|
|
94
|
+
self.assertFalse(conn.closed)
|
|
95
|
+
self.assertFalse(conn.closing)
|
|
96
|
+
self.assertTrue(conn.in_use.locked())
|
|
97
|
+
conn.commit()
|
|
98
|
+
conn.rollback()
|
|
99
|
+
conn.close()
|
|
100
|
+
self.assertTrue(conn.closed)
|
|
101
|
+
self.assertFalse(conn.closing)
|
|
102
|
+
|
|
103
|
+
with self.assertRaises(PersistenceError):
|
|
104
|
+
conn.commit()
|
|
105
|
+
|
|
106
|
+
with self.assertRaises(PersistenceError):
|
|
107
|
+
conn.rollback()
|
|
108
|
+
|
|
109
|
+
def test_max_age(self):
|
|
110
|
+
conn = DummyConnection(max_age=0)
|
|
111
|
+
sleep(0.01)
|
|
112
|
+
self.assertTrue(conn.closing)
|
|
113
|
+
self.assertFalse(conn.closed)
|
|
114
|
+
conn.in_use.release()
|
|
115
|
+
sleep(0.01)
|
|
116
|
+
self.assertTrue(conn.closed)
|
|
117
|
+
|
|
118
|
+
def test_close_on_server(self):
|
|
119
|
+
conn = DummyConnection()
|
|
120
|
+
conn.close_on_server()
|
|
121
|
+
self.assertFalse(conn.closing)
|
|
122
|
+
self.assertFalse(conn.closed)
|
|
123
|
+
with self.assertRaises(PersistenceError):
|
|
124
|
+
conn.cursor().execute("SELECT 1")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestConnectionPool(TestCase):
|
|
128
|
+
ProgrammingError = ProgrammingError
|
|
129
|
+
PersistenceError = PersistenceError
|
|
130
|
+
allowed_connecting_time = 0
|
|
131
|
+
expected_result_from_select_1: ClassVar[List[List[int]]] = [[1]]
|
|
132
|
+
|
|
133
|
+
def create_pool(
|
|
134
|
+
self,
|
|
135
|
+
pool_size=1,
|
|
136
|
+
max_overflow=0,
|
|
137
|
+
max_age=None,
|
|
138
|
+
pre_ping=False,
|
|
139
|
+
mutually_exclusive_read_write=False,
|
|
140
|
+
):
|
|
141
|
+
return DummyConnectionPool(
|
|
142
|
+
pool_size=pool_size,
|
|
143
|
+
max_overflow=max_overflow,
|
|
144
|
+
max_age=max_age,
|
|
145
|
+
pre_ping=pre_ping,
|
|
146
|
+
mutually_exclusive_read_write=mutually_exclusive_read_write,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def close_connection_on_server(self, *connections):
|
|
150
|
+
for conn in connections:
|
|
151
|
+
assert isinstance(conn, DummyConnection)
|
|
152
|
+
conn.close_on_server()
|
|
153
|
+
|
|
154
|
+
def test_get_and_put(self):
|
|
155
|
+
pool = self.create_pool(pool_size=2, max_overflow=2)
|
|
156
|
+
|
|
157
|
+
self.assertEqual(pool.num_in_use, 0)
|
|
158
|
+
self.assertEqual(pool.num_in_pool, 0)
|
|
159
|
+
|
|
160
|
+
conn1 = pool.get_connection()
|
|
161
|
+
self.assertEqual(pool.num_in_use, 1)
|
|
162
|
+
self.assertEqual(pool.num_in_pool, 0)
|
|
163
|
+
|
|
164
|
+
conn2 = pool.get_connection()
|
|
165
|
+
self.assertEqual(pool.num_in_use, 2)
|
|
166
|
+
self.assertEqual(pool.num_in_pool, 0)
|
|
167
|
+
|
|
168
|
+
conn3 = pool.get_connection()
|
|
169
|
+
self.assertEqual(pool.num_in_use, 3)
|
|
170
|
+
self.assertEqual(pool.num_in_pool, 0)
|
|
171
|
+
|
|
172
|
+
conn4 = pool.get_connection()
|
|
173
|
+
self.assertEqual(pool.num_in_use, 4)
|
|
174
|
+
self.assertEqual(pool.num_in_pool, 0)
|
|
175
|
+
|
|
176
|
+
with self.assertRaises(ConnectionUnavailableError):
|
|
177
|
+
pool.get_connection(timeout=0)
|
|
178
|
+
self.assertEqual(pool.num_in_use, 4)
|
|
179
|
+
self.assertEqual(pool.num_in_pool, 0)
|
|
180
|
+
|
|
181
|
+
pool.put_connection(conn1)
|
|
182
|
+
self.assertEqual(pool.num_in_use, 3)
|
|
183
|
+
self.assertEqual(pool.num_in_pool, 1)
|
|
184
|
+
self.assertFalse(conn1.closed)
|
|
185
|
+
|
|
186
|
+
conn5 = pool.get_connection()
|
|
187
|
+
self.assertEqual(pool.num_in_use, 4)
|
|
188
|
+
self.assertEqual(pool.num_in_pool, 0)
|
|
189
|
+
|
|
190
|
+
with self.assertRaises(ConnectionUnavailableError):
|
|
191
|
+
pool.get_connection(timeout=0)
|
|
192
|
+
|
|
193
|
+
pool.put_connection(conn2)
|
|
194
|
+
self.assertEqual(pool.num_in_use, 3)
|
|
195
|
+
self.assertEqual(pool.num_in_pool, 1)
|
|
196
|
+
self.assertFalse(conn2.closed)
|
|
197
|
+
|
|
198
|
+
pool.put_connection(conn3)
|
|
199
|
+
self.assertEqual(pool.num_in_use, 2)
|
|
200
|
+
self.assertEqual(pool.num_in_pool, 2)
|
|
201
|
+
self.assertFalse(conn3.closed)
|
|
202
|
+
|
|
203
|
+
pool.put_connection(conn4)
|
|
204
|
+
self.assertEqual(pool.num_in_use, 1)
|
|
205
|
+
self.assertEqual(pool.num_in_pool, 2)
|
|
206
|
+
self.assertTrue(conn4.closed)
|
|
207
|
+
|
|
208
|
+
pool.put_connection(conn5)
|
|
209
|
+
self.assertEqual(pool.num_in_use, 0)
|
|
210
|
+
self.assertEqual(pool.num_in_pool, 2)
|
|
211
|
+
self.assertTrue(conn5.closed)
|
|
212
|
+
|
|
213
|
+
# Do it all again.
|
|
214
|
+
conn6 = pool.get_connection()
|
|
215
|
+
self.assertEqual(pool.num_in_use, 1)
|
|
216
|
+
self.assertEqual(pool.num_in_pool, 1)
|
|
217
|
+
|
|
218
|
+
conn7 = pool.get_connection()
|
|
219
|
+
self.assertEqual(pool.num_in_use, 2)
|
|
220
|
+
self.assertEqual(pool.num_in_pool, 0)
|
|
221
|
+
|
|
222
|
+
conn8 = pool.get_connection()
|
|
223
|
+
self.assertEqual(pool.num_in_use, 3)
|
|
224
|
+
self.assertEqual(pool.num_in_pool, 0)
|
|
225
|
+
|
|
226
|
+
conn9 = pool.get_connection()
|
|
227
|
+
self.assertEqual(pool.num_in_use, 4)
|
|
228
|
+
self.assertEqual(pool.num_in_pool, 0)
|
|
229
|
+
|
|
230
|
+
with self.assertRaises(ConnectionUnavailableError):
|
|
231
|
+
pool.get_connection(timeout=0)
|
|
232
|
+
self.assertEqual(pool.num_in_use, 4)
|
|
233
|
+
self.assertEqual(pool.num_in_pool, 0)
|
|
234
|
+
|
|
235
|
+
pool.put_connection(conn6)
|
|
236
|
+
self.assertEqual(pool.num_in_use, 3)
|
|
237
|
+
self.assertEqual(pool.num_in_pool, 1)
|
|
238
|
+
self.assertFalse(conn6.closed)
|
|
239
|
+
|
|
240
|
+
conn10 = pool.get_connection()
|
|
241
|
+
self.assertEqual(pool.num_in_use, 4)
|
|
242
|
+
self.assertEqual(pool.num_in_pool, 0)
|
|
243
|
+
|
|
244
|
+
with self.assertRaises(ConnectionUnavailableError):
|
|
245
|
+
pool.get_connection(timeout=0)
|
|
246
|
+
|
|
247
|
+
pool.put_connection(conn7)
|
|
248
|
+
self.assertEqual(pool.num_in_use, 3)
|
|
249
|
+
self.assertEqual(pool.num_in_pool, 1)
|
|
250
|
+
self.assertFalse(conn7.closed)
|
|
251
|
+
|
|
252
|
+
pool.put_connection(conn8)
|
|
253
|
+
self.assertEqual(pool.num_in_use, 2)
|
|
254
|
+
self.assertEqual(pool.num_in_pool, 2)
|
|
255
|
+
self.assertFalse(conn8.closed)
|
|
256
|
+
|
|
257
|
+
pool.put_connection(conn9)
|
|
258
|
+
self.assertEqual(pool.num_in_use, 1)
|
|
259
|
+
self.assertEqual(pool.num_in_pool, 2)
|
|
260
|
+
self.assertTrue(conn9.closed)
|
|
261
|
+
|
|
262
|
+
pool.put_connection(conn10)
|
|
263
|
+
self.assertEqual(pool.num_in_use, 0)
|
|
264
|
+
self.assertEqual(pool.num_in_pool, 2)
|
|
265
|
+
self.assertTrue(conn10.closed)
|
|
266
|
+
|
|
267
|
+
def test_connection_not_from_pool(self):
|
|
268
|
+
pool = self.create_pool()
|
|
269
|
+
with self.assertRaises(ConnectionNotFromPoolError):
|
|
270
|
+
pool.put_connection(pool._create_connection())
|
|
271
|
+
|
|
272
|
+
def test_close_before_returning(self):
|
|
273
|
+
pool = self.create_pool()
|
|
274
|
+
self.assertEqual(pool.num_in_use, 0)
|
|
275
|
+
self.assertEqual(pool.num_in_pool, 0)
|
|
276
|
+
|
|
277
|
+
conn1 = pool.get_connection()
|
|
278
|
+
self.assertEqual(pool.num_in_use, 1)
|
|
279
|
+
self.assertEqual(pool.num_in_pool, 0)
|
|
280
|
+
|
|
281
|
+
conn1.close()
|
|
282
|
+
pool.put_connection(conn1)
|
|
283
|
+
self.assertEqual(pool.num_in_use, 0)
|
|
284
|
+
self.assertEqual(pool.num_in_pool, 0)
|
|
285
|
+
|
|
286
|
+
def test_close_after_returning(self):
|
|
287
|
+
pool = self.create_pool()
|
|
288
|
+
conn1 = pool.get_connection()
|
|
289
|
+
pool.put_connection(conn1)
|
|
290
|
+
conn1.close()
|
|
291
|
+
self.assertEqual(pool.num_in_use, 0)
|
|
292
|
+
self.assertEqual(pool.num_in_pool, 1)
|
|
293
|
+
conn1 = pool.get_connection()
|
|
294
|
+
self.assertFalse(conn1.closed)
|
|
295
|
+
|
|
296
|
+
def test_close_on_server_after_returning_without_pre_ping(self):
|
|
297
|
+
pool = self.create_pool()
|
|
298
|
+
|
|
299
|
+
conn1 = pool.get_connection()
|
|
300
|
+
curs = conn1.cursor()
|
|
301
|
+
with self.assertRaises(self.ProgrammingError):
|
|
302
|
+
self.assertEqual(curs.fetchall(), None)
|
|
303
|
+
curs.execute("SELECT 1")
|
|
304
|
+
self.assertEqual(curs.fetchall(), self.expected_result_from_select_1)
|
|
305
|
+
|
|
306
|
+
pool.put_connection(conn1)
|
|
307
|
+
self.close_connection_on_server(conn1)
|
|
308
|
+
self.assertEqual(pool.num_in_use, 0)
|
|
309
|
+
self.assertEqual(pool.num_in_pool, 1)
|
|
310
|
+
|
|
311
|
+
conn1 = pool.get_connection()
|
|
312
|
+
self.assertFalse(conn1.closed)
|
|
313
|
+
|
|
314
|
+
with self.assertRaises(self.PersistenceError):
|
|
315
|
+
conn1.cursor().execute("SELECT 1")
|
|
316
|
+
|
|
317
|
+
def test_close_on_server_after_returning_with_pre_ping(self):
|
|
318
|
+
pool = self.create_pool(pre_ping=True)
|
|
319
|
+
|
|
320
|
+
conn1 = pool.get_connection()
|
|
321
|
+
pool.put_connection(conn1)
|
|
322
|
+
self.close_connection_on_server(conn1)
|
|
323
|
+
self.assertEqual(pool.num_in_use, 0)
|
|
324
|
+
self.assertEqual(pool.num_in_pool, 1)
|
|
325
|
+
|
|
326
|
+
conn2 = pool.get_connection()
|
|
327
|
+
self.assertFalse(conn2.closed)
|
|
328
|
+
|
|
329
|
+
curs = conn2.cursor()
|
|
330
|
+
curs.execute("SELECT 1")
|
|
331
|
+
self.assertEqual(curs.fetchall(), self.expected_result_from_select_1)
|
|
332
|
+
|
|
333
|
+
def test_max_age(self):
|
|
334
|
+
pool = self.create_pool(max_age=0.2)
|
|
335
|
+
|
|
336
|
+
# Timer fires after conn returned to pool.
|
|
337
|
+
conn1 = pool.get_connection()
|
|
338
|
+
self.assertFalse(conn1.closed)
|
|
339
|
+
self.assertFalse(conn1.closing)
|
|
340
|
+
pool.put_connection(conn1)
|
|
341
|
+
self.assertEqual(pool.num_in_pool, 1)
|
|
342
|
+
sleep(0.3)
|
|
343
|
+
self.assertTrue(conn1.closed)
|
|
344
|
+
self.assertTrue(conn1.closing)
|
|
345
|
+
|
|
346
|
+
# Pool returns a new connection.
|
|
347
|
+
conn2 = pool.get_connection()
|
|
348
|
+
self.assertEqual(pool.num_in_pool, 0)
|
|
349
|
+
self.assertFalse(conn2.closed)
|
|
350
|
+
self.assertFalse(conn2.closing)
|
|
351
|
+
self.assertNotEqual(id(conn1), id(conn2))
|
|
352
|
+
self.assertEqual(pool.num_in_pool, 0)
|
|
353
|
+
|
|
354
|
+
# Timer fires before conn returned to pool.
|
|
355
|
+
sleep(0.3)
|
|
356
|
+
self.assertFalse(conn2.closed)
|
|
357
|
+
self.assertTrue(conn2.closing)
|
|
358
|
+
self.assertEqual(pool.num_in_pool, 0)
|
|
359
|
+
pool.put_connection(conn2)
|
|
360
|
+
self.assertEqual(pool.num_in_pool, 0)
|
|
361
|
+
sleep(0.1)
|
|
362
|
+
self.assertTrue(conn1.closed)
|
|
363
|
+
|
|
364
|
+
# Pool returns another new connection.
|
|
365
|
+
conn3 = pool.get_connection()
|
|
366
|
+
self.assertFalse(conn3.closed)
|
|
367
|
+
self.assertFalse(conn3.closing)
|
|
368
|
+
self.assertNotEqual(id(conn2), id(conn3))
|
|
369
|
+
pool.put_connection(conn3)
|
|
370
|
+
|
|
371
|
+
def test_get_with_timeout(self):
|
|
372
|
+
pool = self.create_pool()
|
|
373
|
+
|
|
374
|
+
# Get a connection.
|
|
375
|
+
conn1 = pool.get_connection()
|
|
376
|
+
|
|
377
|
+
# Check request for a second connection times out immediately.
|
|
378
|
+
started = time()
|
|
379
|
+
with self.assertRaises(ConnectionUnavailableError):
|
|
380
|
+
pool.get_connection(timeout=0)
|
|
381
|
+
ended = time()
|
|
382
|
+
self.assertLess(ended - started, 0.1)
|
|
383
|
+
|
|
384
|
+
# Check request for a second connection times out after delay.
|
|
385
|
+
started = time()
|
|
386
|
+
with self.assertRaises(ConnectionUnavailableError):
|
|
387
|
+
pool.get_connection(timeout=0.1)
|
|
388
|
+
ended = time()
|
|
389
|
+
self.assertGreater(ended - started, 0.1)
|
|
390
|
+
|
|
391
|
+
# Check request for second connection is kept waiting
|
|
392
|
+
# but doesn't timeout if first connection is returned.
|
|
393
|
+
getting_conn2 = Event()
|
|
394
|
+
got_conn2 = Event()
|
|
395
|
+
|
|
396
|
+
def put_conn1():
|
|
397
|
+
getting_conn2.wait()
|
|
398
|
+
sleep(0.05)
|
|
399
|
+
pool.put_connection(conn1)
|
|
400
|
+
|
|
401
|
+
def get_conn2():
|
|
402
|
+
getting_conn2.set()
|
|
403
|
+
pool.get_connection(timeout=0.1)
|
|
404
|
+
got_conn2.set()
|
|
405
|
+
|
|
406
|
+
thread1 = Thread(target=put_conn1, daemon=True)
|
|
407
|
+
thread2 = Thread(target=get_conn2, daemon=True)
|
|
408
|
+
thread1.start()
|
|
409
|
+
thread2.start()
|
|
410
|
+
self.assertTrue(got_conn2.wait(timeout=0.3))
|
|
411
|
+
|
|
412
|
+
def test_close_pool(self):
|
|
413
|
+
# Get three connections and return one of them.
|
|
414
|
+
pool = self.create_pool(pool_size=2, max_overflow=1)
|
|
415
|
+
conn1 = pool.get_connection()
|
|
416
|
+
conn2 = pool.get_connection()
|
|
417
|
+
conn3 = pool.get_connection()
|
|
418
|
+
pool.put_connection(conn1)
|
|
419
|
+
|
|
420
|
+
# Close pool.
|
|
421
|
+
pool.close()
|
|
422
|
+
|
|
423
|
+
# All connections are closed (returned and those in use).
|
|
424
|
+
self.assertTrue(conn1.closed)
|
|
425
|
+
self.assertTrue(conn2.closed)
|
|
426
|
+
self.assertTrue(conn3.closed)
|
|
427
|
+
|
|
428
|
+
# Raises error when putting connection after pool closed.
|
|
429
|
+
with self.assertRaises(ConnectionPoolClosedError):
|
|
430
|
+
pool.put_connection(conn2)
|
|
431
|
+
|
|
432
|
+
with self.assertRaises(ConnectionPoolClosedError):
|
|
433
|
+
pool.put_connection(conn3)
|
|
434
|
+
|
|
435
|
+
# Raises error when getting connection after pool closed.
|
|
436
|
+
with self.assertRaises(ConnectionPoolClosedError):
|
|
437
|
+
pool.get_connection()
|
|
438
|
+
|
|
439
|
+
# Can call close() twice.
|
|
440
|
+
pool.close()
|
|
441
|
+
|
|
442
|
+
self.assertTrue(pool.closed)
|
|
443
|
+
|
|
444
|
+
def test_fairness(self):
|
|
445
|
+
pool_size = 1
|
|
446
|
+
num_threads = 5
|
|
447
|
+
num_gets = 5
|
|
448
|
+
|
|
449
|
+
# Pre-initialise pool.
|
|
450
|
+
pool = self.create_pool(pool_size=pool_size, max_overflow=0, pre_ping=False)
|
|
451
|
+
connections = [pool.get_connection() for _ in range(pool_size)]
|
|
452
|
+
self.assertEqual(pool.num_in_use, pool_size)
|
|
453
|
+
for conn in connections:
|
|
454
|
+
pool.put_connection(conn)
|
|
455
|
+
|
|
456
|
+
is_stopped = Event()
|
|
457
|
+
hold_connection = 0.1
|
|
458
|
+
wait_after_connection = 0.01
|
|
459
|
+
conn_sequence = []
|
|
460
|
+
|
|
461
|
+
deadline = num_threads * num_gets * hold_connection * 10
|
|
462
|
+
|
|
463
|
+
class WorkerThread(Thread):
|
|
464
|
+
def __init__(self, name):
|
|
465
|
+
super().__init__(daemon=True)
|
|
466
|
+
self.name = name
|
|
467
|
+
|
|
468
|
+
def run(self):
|
|
469
|
+
for _ in range(num_gets):
|
|
470
|
+
if is_stopped.is_set():
|
|
471
|
+
break
|
|
472
|
+
try:
|
|
473
|
+
conn = pool.get_connection(timeout=deadline)
|
|
474
|
+
conn_sequence.append(self.name)
|
|
475
|
+
sleep(hold_connection)
|
|
476
|
+
pool.put_connection(conn)
|
|
477
|
+
sleep(wait_after_connection)
|
|
478
|
+
except BaseException:
|
|
479
|
+
is_stopped.set()
|
|
480
|
+
raise
|
|
481
|
+
|
|
482
|
+
names = [str(i) for i in range(num_threads)]
|
|
483
|
+
threads = []
|
|
484
|
+
for name in names:
|
|
485
|
+
thread = WorkerThread(name)
|
|
486
|
+
threads.append(thread)
|
|
487
|
+
sleep(wait_after_connection)
|
|
488
|
+
thread.start()
|
|
489
|
+
|
|
490
|
+
for thread in threads:
|
|
491
|
+
thread.join(timeout=deadline)
|
|
492
|
+
self.assertFalse(is_stopped.is_set())
|
|
493
|
+
|
|
494
|
+
expected_sequence = names * num_gets
|
|
495
|
+
self.assertEqual(expected_sequence, conn_sequence)
|
|
496
|
+
|
|
497
|
+
def test_reader_writer(self):
|
|
498
|
+
self._test_reader_writer_with_mutually_exclusive_read_write()
|
|
499
|
+
self._test_reader_writer_without_mutually_exclusive_read_write()
|
|
500
|
+
|
|
501
|
+
def _test_reader_writer_with_mutually_exclusive_read_write(self):
|
|
502
|
+
pool = self.create_pool(pool_size=3, mutually_exclusive_read_write=True)
|
|
503
|
+
self.assertTrue(pool._mutually_exclusive_read_write)
|
|
504
|
+
|
|
505
|
+
self.assertEqual(0, pool._num_writers)
|
|
506
|
+
self.assertEqual(0, pool._num_readers)
|
|
507
|
+
|
|
508
|
+
# Get writer.
|
|
509
|
+
writer_conn = pool.get_connection(is_writer=True, timeout=0)
|
|
510
|
+
self.assertTrue(writer_conn.is_writer)
|
|
511
|
+
|
|
512
|
+
self.assertEqual(1, pool._num_writers)
|
|
513
|
+
self.assertEqual(0, pool._num_readers)
|
|
514
|
+
|
|
515
|
+
# Return writer.
|
|
516
|
+
pool.put_connection(writer_conn)
|
|
517
|
+
|
|
518
|
+
self.assertEqual(0, pool._num_writers)
|
|
519
|
+
self.assertEqual(0, pool._num_readers)
|
|
520
|
+
|
|
521
|
+
# Get two readers.
|
|
522
|
+
reader_conn1 = pool.get_connection(is_writer=False)
|
|
523
|
+
reader_conn2 = pool.get_connection(is_writer=False)
|
|
524
|
+
|
|
525
|
+
self.assertFalse(reader_conn1.is_writer)
|
|
526
|
+
self.assertFalse(reader_conn2.is_writer)
|
|
527
|
+
|
|
528
|
+
self.assertEqual(0, pool._num_writers)
|
|
529
|
+
self.assertEqual(2, pool._num_readers)
|
|
530
|
+
|
|
531
|
+
# Fail to get writer.
|
|
532
|
+
with self.assertRaises(ConnectionUnavailableError) as cm:
|
|
533
|
+
pool.get_connection(is_writer=True, timeout=0)
|
|
534
|
+
self.assertEqual(cm.exception.args[0], "Timed out waiting for return of reader")
|
|
535
|
+
|
|
536
|
+
self.assertEqual(0, pool._num_writers)
|
|
537
|
+
self.assertEqual(2, pool._num_readers)
|
|
538
|
+
|
|
539
|
+
# Return readers to pool.
|
|
540
|
+
pool.put_connection(reader_conn1)
|
|
541
|
+
pool.put_connection(reader_conn2)
|
|
542
|
+
|
|
543
|
+
self.assertEqual(0, pool._num_writers)
|
|
544
|
+
self.assertEqual(0, pool._num_readers)
|
|
545
|
+
|
|
546
|
+
# Get writer.
|
|
547
|
+
writer_conn = pool.get_connection(is_writer=True, timeout=0)
|
|
548
|
+
|
|
549
|
+
self.assertEqual(1, pool._num_writers)
|
|
550
|
+
self.assertEqual(0, pool._num_readers)
|
|
551
|
+
|
|
552
|
+
# Fail to get reader.
|
|
553
|
+
with self.assertRaises(ConnectionUnavailableError) as cm:
|
|
554
|
+
pool.get_connection(is_writer=False, timeout=0)
|
|
555
|
+
self.assertEqual(cm.exception.args[0], "Timed out waiting for return of writer")
|
|
556
|
+
|
|
557
|
+
# Fail to get writer.
|
|
558
|
+
with self.assertRaises(ConnectionUnavailableError) as cm:
|
|
559
|
+
pool.get_connection(is_writer=True, timeout=0)
|
|
560
|
+
self.assertEqual(cm.exception.args[0], "Timed out waiting for return of writer")
|
|
561
|
+
|
|
562
|
+
self.assertEqual(1, pool._num_writers)
|
|
563
|
+
self.assertEqual(0, pool._num_readers)
|
|
564
|
+
|
|
565
|
+
# Return writer.
|
|
566
|
+
pool.put_connection(writer_conn)
|
|
567
|
+
|
|
568
|
+
self.assertEqual(0, pool._num_writers)
|
|
569
|
+
self.assertEqual(0, pool._num_readers)
|
|
570
|
+
|
|
571
|
+
# Get and put another writer.
|
|
572
|
+
writer_conn = pool.get_connection(is_writer=True)
|
|
573
|
+
pool.put_connection(writer_conn)
|
|
574
|
+
|
|
575
|
+
self.assertEqual(0, pool._num_writers)
|
|
576
|
+
self.assertEqual(0, pool._num_readers)
|
|
577
|
+
|
|
578
|
+
# Get two readers.
|
|
579
|
+
reader_conn1 = pool.get_connection(is_writer=False)
|
|
580
|
+
reader_conn2 = pool.get_connection(is_writer=False)
|
|
581
|
+
|
|
582
|
+
self.assertEqual(0, pool._num_writers)
|
|
583
|
+
self.assertEqual(2, pool._num_readers)
|
|
584
|
+
|
|
585
|
+
pool.put_connection(reader_conn1)
|
|
586
|
+
pool.put_connection(reader_conn2)
|
|
587
|
+
|
|
588
|
+
self.assertEqual(0, pool._num_writers)
|
|
589
|
+
self.assertEqual(0, pool._num_readers)
|
|
590
|
+
|
|
591
|
+
def _test_reader_writer_without_mutually_exclusive_read_write(self):
|
|
592
|
+
pool = self.create_pool(pool_size=3, mutually_exclusive_read_write=False)
|
|
593
|
+
self.assertFalse(pool._mutually_exclusive_read_write)
|
|
594
|
+
|
|
595
|
+
self.assertEqual(0, pool._num_writers)
|
|
596
|
+
self.assertEqual(0, pool._num_readers)
|
|
597
|
+
|
|
598
|
+
# Get writer.
|
|
599
|
+
writer_conn = pool.get_connection(is_writer=True, timeout=0)
|
|
600
|
+
self.assertTrue(writer_conn.is_writer)
|
|
601
|
+
|
|
602
|
+
self.assertEqual(1, pool._num_writers)
|
|
603
|
+
self.assertEqual(0, pool._num_readers)
|
|
604
|
+
|
|
605
|
+
# Get two readers.
|
|
606
|
+
reader_conn1 = pool.get_connection(is_writer=False)
|
|
607
|
+
reader_conn2 = pool.get_connection(is_writer=False)
|
|
608
|
+
|
|
609
|
+
self.assertFalse(reader_conn1.is_writer)
|
|
610
|
+
self.assertFalse(reader_conn2.is_writer)
|
|
611
|
+
|
|
612
|
+
self.assertEqual(1, pool._num_writers)
|
|
613
|
+
self.assertEqual(2, pool._num_readers)
|
|
614
|
+
|
|
615
|
+
# Fail to get another writer.
|
|
616
|
+
with self.assertRaises(ConnectionUnavailableError) as cm:
|
|
617
|
+
pool.get_connection(is_writer=True, timeout=0)
|
|
618
|
+
self.assertEqual(cm.exception.args[0], "Timed out waiting for return of writer")
|
|
619
|
+
|
|
620
|
+
self.assertEqual(1, pool._num_writers)
|
|
621
|
+
self.assertEqual(2, pool._num_readers)
|
|
622
|
+
|
|
623
|
+
# Return writer.
|
|
624
|
+
pool.put_connection(writer_conn)
|
|
625
|
+
|
|
626
|
+
self.assertEqual(0, pool._num_writers)
|
|
627
|
+
self.assertEqual(2, pool._num_readers)
|
|
628
|
+
|
|
629
|
+
# Return readers to pool.
|
|
630
|
+
pool.put_connection(reader_conn1)
|
|
631
|
+
pool.put_connection(reader_conn2)
|
|
632
|
+
|
|
633
|
+
self.assertEqual(0, pool._num_writers)
|
|
634
|
+
self.assertEqual(0, pool._num_readers)
|
|
635
|
+
|
|
636
|
+
# Get two readers.
|
|
637
|
+
pool.get_connection(is_writer=False)
|
|
638
|
+
pool.get_connection(is_writer=False)
|
|
639
|
+
|
|
640
|
+
self.assertEqual(0, pool._num_writers)
|
|
641
|
+
self.assertEqual(2, pool._num_readers)
|
|
642
|
+
|
|
643
|
+
# Get another writer.
|
|
644
|
+
writer_conn = pool.get_connection(is_writer=True, timeout=0)
|
|
645
|
+
|
|
646
|
+
self.assertEqual(1, pool._num_writers)
|
|
647
|
+
self.assertEqual(2, pool._num_readers)
|
|
648
|
+
|
|
649
|
+
# Fail to get another writer.
|
|
650
|
+
with self.assertRaises(ConnectionUnavailableError) as cm:
|
|
651
|
+
pool.get_connection(is_writer=True, timeout=0)
|
|
652
|
+
self.assertEqual(cm.exception.args[0], "Timed out waiting for return of writer")
|
|
653
|
+
|
|
654
|
+
self.assertEqual(1, pool._num_writers)
|
|
655
|
+
self.assertEqual(2, pool._num_readers)
|
|
656
|
+
|
|
657
|
+
# Return writer.
|
|
658
|
+
pool.put_connection(writer_conn)
|
|
659
|
+
|
|
660
|
+
self.assertEqual(0, pool._num_writers)
|
|
661
|
+
self.assertEqual(2, pool._num_readers)
|
|
662
|
+
|
|
663
|
+
# Get and put another writer.
|
|
664
|
+
writer_conn = pool.get_connection(is_writer=True)
|
|
665
|
+
pool.put_connection(writer_conn)
|
|
666
|
+
|
|
667
|
+
self.assertEqual(0, pool._num_writers)
|
|
668
|
+
self.assertEqual(2, pool._num_readers)
|
|
669
|
+
|
|
670
|
+
def test_semaphore_timeout_branch(self):
|
|
671
|
+
# This test exercises unusual path where waiting for
|
|
672
|
+
# the semaphore times out. This only happens when
|
|
673
|
+
# the timeouts are very short and there are a lot
|
|
674
|
+
# of threads (regardless of whether the pool is
|
|
675
|
+
# exhausted) because Python just can't process
|
|
676
|
+
# everything in time. It would also happen if
|
|
677
|
+
# 'is_writer' is either True or False for a
|
|
678
|
+
# connection request and this request waits
|
|
679
|
+
# for return of reader/writer whilst holding
|
|
680
|
+
# the semaphore, and a later request uses a
|
|
681
|
+
# timeout that is less than that of the
|
|
682
|
+
# blocked request. But anyway, the semaphore
|
|
683
|
+
# request needs to be acquired with a timeout
|
|
684
|
+
# and so the failure to get the semaphore lock
|
|
685
|
+
# needs a branch in the code, and this branch
|
|
686
|
+
# needs to be covered with a test.
|
|
687
|
+
|
|
688
|
+
# Create a pool.
|
|
689
|
+
pool = self.create_pool(pool_size=3)
|
|
690
|
+
|
|
691
|
+
# Get a writer connection (blocks subsequent writers).
|
|
692
|
+
pool.get_connection(is_writer=True)
|
|
693
|
+
|
|
694
|
+
# Block on waiting for a writer connection (holds the semaphore).
|
|
695
|
+
class WriterThread(Thread):
|
|
696
|
+
def run(self):
|
|
697
|
+
with contextlib.suppress(ConnectionUnavailableError):
|
|
698
|
+
pool.get_connection(timeout=0.1, is_writer=True)
|
|
699
|
+
|
|
700
|
+
thread = WriterThread()
|
|
701
|
+
thread.start()
|
|
702
|
+
|
|
703
|
+
# Wait for semaphore value to be zero.
|
|
704
|
+
while pool._get_semaphore._value != 0:
|
|
705
|
+
sleep(0.001)
|
|
706
|
+
|
|
707
|
+
# With a zero timeout, fail to get semaphore.
|
|
708
|
+
with self.assertRaises(ConnectionUnavailableError) as cm:
|
|
709
|
+
pool.get_connection(timeout=0)
|
|
710
|
+
self.assertEqual(
|
|
711
|
+
cm.exception.args[0], "Timed out waiting for connection pool semaphore"
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
_print = print
|
|
716
|
+
|
|
717
|
+
print_lock = Lock()
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def print(*args): # noqa: A001
|
|
721
|
+
with print_lock:
|
|
722
|
+
_print(*args)
|