eventsourcing 9.2.21__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.

Files changed (145) hide show
  1. eventsourcing/__init__.py +1 -1
  2. eventsourcing/application.py +137 -132
  3. eventsourcing/cipher.py +17 -12
  4. eventsourcing/compressor.py +2 -0
  5. eventsourcing/dispatch.py +30 -56
  6. eventsourcing/domain.py +221 -227
  7. eventsourcing/examples/__init__.py +0 -0
  8. eventsourcing/examples/aggregate1/__init__.py +0 -0
  9. eventsourcing/examples/aggregate1/application.py +27 -0
  10. eventsourcing/examples/aggregate1/domainmodel.py +16 -0
  11. eventsourcing/examples/aggregate1/test_application.py +37 -0
  12. eventsourcing/examples/aggregate2/__init__.py +0 -0
  13. eventsourcing/examples/aggregate2/application.py +27 -0
  14. eventsourcing/examples/aggregate2/domainmodel.py +22 -0
  15. eventsourcing/examples/aggregate2/test_application.py +37 -0
  16. eventsourcing/examples/aggregate3/__init__.py +0 -0
  17. eventsourcing/examples/aggregate3/application.py +27 -0
  18. eventsourcing/examples/aggregate3/domainmodel.py +38 -0
  19. eventsourcing/examples/aggregate3/test_application.py +37 -0
  20. eventsourcing/examples/aggregate4/__init__.py +0 -0
  21. eventsourcing/examples/aggregate4/application.py +27 -0
  22. eventsourcing/examples/aggregate4/domainmodel.py +114 -0
  23. eventsourcing/examples/aggregate4/test_application.py +38 -0
  24. eventsourcing/examples/aggregate5/__init__.py +0 -0
  25. eventsourcing/examples/aggregate5/application.py +27 -0
  26. eventsourcing/examples/aggregate5/domainmodel.py +131 -0
  27. eventsourcing/examples/aggregate5/test_application.py +38 -0
  28. eventsourcing/examples/aggregate6/__init__.py +0 -0
  29. eventsourcing/examples/aggregate6/application.py +30 -0
  30. eventsourcing/examples/aggregate6/domainmodel.py +123 -0
  31. eventsourcing/examples/aggregate6/test_application.py +38 -0
  32. eventsourcing/examples/aggregate6a/__init__.py +0 -0
  33. eventsourcing/examples/aggregate6a/application.py +40 -0
  34. eventsourcing/examples/aggregate6a/domainmodel.py +149 -0
  35. eventsourcing/examples/aggregate6a/test_application.py +45 -0
  36. eventsourcing/examples/aggregate7/__init__.py +0 -0
  37. eventsourcing/examples/aggregate7/application.py +48 -0
  38. eventsourcing/examples/aggregate7/domainmodel.py +144 -0
  39. eventsourcing/examples/aggregate7/persistence.py +57 -0
  40. eventsourcing/examples/aggregate7/test_application.py +38 -0
  41. eventsourcing/examples/aggregate7/test_compression_and_encryption.py +45 -0
  42. eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +67 -0
  43. eventsourcing/examples/aggregate7a/__init__.py +0 -0
  44. eventsourcing/examples/aggregate7a/application.py +56 -0
  45. eventsourcing/examples/aggregate7a/domainmodel.py +170 -0
  46. eventsourcing/examples/aggregate7a/test_application.py +46 -0
  47. eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +45 -0
  48. eventsourcing/examples/aggregate8/__init__.py +0 -0
  49. eventsourcing/examples/aggregate8/application.py +47 -0
  50. eventsourcing/examples/aggregate8/domainmodel.py +65 -0
  51. eventsourcing/examples/aggregate8/persistence.py +57 -0
  52. eventsourcing/examples/aggregate8/test_application.py +37 -0
  53. eventsourcing/examples/aggregate8/test_compression_and_encryption.py +44 -0
  54. eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +38 -0
  55. eventsourcing/examples/bankaccounts/__init__.py +0 -0
  56. eventsourcing/examples/bankaccounts/application.py +70 -0
  57. eventsourcing/examples/bankaccounts/domainmodel.py +56 -0
  58. eventsourcing/examples/bankaccounts/test.py +173 -0
  59. eventsourcing/examples/cargoshipping/__init__.py +0 -0
  60. eventsourcing/examples/cargoshipping/application.py +126 -0
  61. eventsourcing/examples/cargoshipping/domainmodel.py +330 -0
  62. eventsourcing/examples/cargoshipping/interface.py +143 -0
  63. eventsourcing/examples/cargoshipping/test.py +231 -0
  64. eventsourcing/examples/contentmanagement/__init__.py +0 -0
  65. eventsourcing/examples/contentmanagement/application.py +118 -0
  66. eventsourcing/examples/contentmanagement/domainmodel.py +69 -0
  67. eventsourcing/examples/contentmanagement/test.py +180 -0
  68. eventsourcing/examples/contentmanagement/utils.py +26 -0
  69. eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
  70. eventsourcing/examples/contentmanagementsystem/application.py +54 -0
  71. eventsourcing/examples/contentmanagementsystem/postgres.py +17 -0
  72. eventsourcing/examples/contentmanagementsystem/sqlite.py +17 -0
  73. eventsourcing/examples/contentmanagementsystem/system.py +14 -0
  74. eventsourcing/examples/contentmanagementsystem/test_system.py +180 -0
  75. eventsourcing/examples/searchablecontent/__init__.py +0 -0
  76. eventsourcing/examples/searchablecontent/application.py +45 -0
  77. eventsourcing/examples/searchablecontent/persistence.py +23 -0
  78. eventsourcing/examples/searchablecontent/postgres.py +118 -0
  79. eventsourcing/examples/searchablecontent/sqlite.py +136 -0
  80. eventsourcing/examples/searchablecontent/test_application.py +110 -0
  81. eventsourcing/examples/searchablecontent/test_recorder.py +68 -0
  82. eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
  83. eventsourcing/examples/searchabletimestamps/application.py +32 -0
  84. eventsourcing/examples/searchabletimestamps/persistence.py +20 -0
  85. eventsourcing/examples/searchabletimestamps/postgres.py +110 -0
  86. eventsourcing/examples/searchabletimestamps/sqlite.py +99 -0
  87. eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +94 -0
  88. eventsourcing/examples/test_invoice.py +176 -0
  89. eventsourcing/examples/test_parking_lot.py +206 -0
  90. eventsourcing/interface.py +4 -2
  91. eventsourcing/persistence.py +88 -82
  92. eventsourcing/popo.py +32 -31
  93. eventsourcing/postgres.py +388 -593
  94. eventsourcing/sqlite.py +100 -102
  95. eventsourcing/system.py +66 -71
  96. eventsourcing/tests/application.py +20 -32
  97. eventsourcing/tests/application_tests/__init__.py +0 -0
  98. eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +55 -0
  99. eventsourcing/tests/application_tests/test_application_with_popo.py +22 -0
  100. eventsourcing/tests/application_tests/test_application_with_postgres.py +75 -0
  101. eventsourcing/tests/application_tests/test_application_with_sqlite.py +72 -0
  102. eventsourcing/tests/application_tests/test_cache.py +134 -0
  103. eventsourcing/tests/application_tests/test_event_sourced_log.py +162 -0
  104. eventsourcing/tests/application_tests/test_notificationlog.py +232 -0
  105. eventsourcing/tests/application_tests/test_notificationlogreader.py +126 -0
  106. eventsourcing/tests/application_tests/test_processapplication.py +110 -0
  107. eventsourcing/tests/application_tests/test_processingpolicy.py +109 -0
  108. eventsourcing/tests/application_tests/test_repository.py +504 -0
  109. eventsourcing/tests/application_tests/test_snapshotting.py +68 -0
  110. eventsourcing/tests/application_tests/test_upcasting.py +459 -0
  111. eventsourcing/tests/docs_tests/__init__.py +0 -0
  112. eventsourcing/tests/docs_tests/test_docs.py +293 -0
  113. eventsourcing/tests/domain.py +1 -1
  114. eventsourcing/tests/domain_tests/__init__.py +0 -0
  115. eventsourcing/tests/domain_tests/test_aggregate.py +1180 -0
  116. eventsourcing/tests/domain_tests/test_aggregate_decorators.py +1604 -0
  117. eventsourcing/tests/domain_tests/test_domainevent.py +80 -0
  118. eventsourcing/tests/interface_tests/__init__.py +0 -0
  119. eventsourcing/tests/interface_tests/test_remotenotificationlog.py +258 -0
  120. eventsourcing/tests/persistence.py +52 -50
  121. eventsourcing/tests/persistence_tests/__init__.py +0 -0
  122. eventsourcing/tests/persistence_tests/test_aes.py +93 -0
  123. eventsourcing/tests/persistence_tests/test_connection_pool.py +722 -0
  124. eventsourcing/tests/persistence_tests/test_eventstore.py +72 -0
  125. eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +21 -0
  126. eventsourcing/tests/persistence_tests/test_mapper.py +113 -0
  127. eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +69 -0
  128. eventsourcing/tests/persistence_tests/test_popo.py +124 -0
  129. eventsourcing/tests/persistence_tests/test_postgres.py +1119 -0
  130. eventsourcing/tests/persistence_tests/test_sqlite.py +348 -0
  131. eventsourcing/tests/persistence_tests/test_transcoder.py +44 -0
  132. eventsourcing/tests/postgres_utils.py +7 -7
  133. eventsourcing/tests/system_tests/__init__.py +0 -0
  134. eventsourcing/tests/system_tests/test_runner.py +935 -0
  135. eventsourcing/tests/system_tests/test_system.py +284 -0
  136. eventsourcing/tests/utils_tests/__init__.py +0 -0
  137. eventsourcing/tests/utils_tests/test_utils.py +226 -0
  138. eventsourcing/utils.py +49 -50
  139. {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/METADATA +30 -33
  140. eventsourcing-9.3.0.dist-info/RECORD +145 -0
  141. {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/WHEEL +1 -2
  142. eventsourcing-9.2.21.dist-info/RECORD +0 -25
  143. eventsourcing-9.2.21.dist-info/top_level.txt +0 -1
  144. {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/AUTHORS +0 -0
  145. {eventsourcing-9.2.21.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)