py-sharedmemory 4.0.0__tar.gz → 4.1.0__tar.gz
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.
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/CHANGELOG.md +7 -0
- {py_sharedmemory-4.0.0/py_sharedmemory.egg-info → py_sharedmemory-4.1.0}/PKG-INFO +9 -3
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/README.md +8 -2
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/py_sharedmemory/__init__.py +1 -1
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/py_sharedmemory/sender.py +34 -5
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0/py_sharedmemory.egg-info}/PKG-INFO +9 -3
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/tests/conftest.py +10 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/tests/helpers.py +37 -5
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/tests/test_processes.py +48 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/tests/test_queue.py +64 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/LICENSE +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/MANIFEST.in +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/benchmarks/__init__.py +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/benchmarks/benchmark.py +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/benchmarks/plot.py +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/benchmarks/results/windows-numpy.json +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/benchmarks/results/windows-numpy.png +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/examples/basic.py +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/examples/numpy_batches.py +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/examples/worker_pool.py +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/py_sharedmemory/convert.py +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/py_sharedmemory/factory.py +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/py_sharedmemory/py.typed +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/py_sharedmemory/receiver.py +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/py_sharedmemory.egg-info/SOURCES.txt +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/py_sharedmemory.egg-info/dependency_links.txt +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/py_sharedmemory.egg-info/requires.txt +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/py_sharedmemory.egg-info/top_level.txt +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/pyproject.toml +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/setup.cfg +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/tests/__init__.py +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/tests/test_benchmark.py +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/tests/test_convert.py +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/tests/test_objects.py +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/tests/test_package.py +0 -0
- {py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/tests/test_threads.py +0 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.1.0
|
|
4
|
+
|
|
5
|
+
- Warn with a `RuntimeWarning` when leaving a `with sender:` block or exiting the
|
|
6
|
+
producing process discards unacknowledged messages, so a forgotten
|
|
7
|
+
`wait_for_all_ack()` no longer loses data silently. Explicit `close()` calls and
|
|
8
|
+
exits caused by exceptions stay silent, and closing still never blocks.
|
|
9
|
+
|
|
3
10
|
## 4.0.0
|
|
4
11
|
|
|
5
12
|
- **Breaking:** the import package is renamed from `memory` to `py_sharedmemory` to match the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: py-sharedmemory
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.1.0
|
|
4
4
|
Summary: Fast multiprocessing.Queue alternative that moves large Python objects and NumPy arrays through shared memory
|
|
5
5
|
Author: Tobias Würth
|
|
6
6
|
License-Expression: MIT
|
|
@@ -230,6 +230,11 @@ with sender:
|
|
|
230
230
|
raise TimeoutError("Consumer did not receive everything")
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
+
Closing never blocks. If a `with sender:` block or the producing process ends normally
|
|
234
|
+
while messages are still unacknowledged, they are discarded and the sender emits a
|
|
235
|
+
`RuntimeWarning`, so forgetting to drain does not lose data silently. An explicit
|
|
236
|
+
`close()` or an exception leaving the `with` block discards without a warning.
|
|
237
|
+
|
|
233
238
|
When both endpoints live in one process, drain before closing either of them. A sentinel
|
|
234
239
|
such as `None` is a simple way to tell consumers that the stream has ended.
|
|
235
240
|
|
|
@@ -249,6 +254,7 @@ copy; other views, such as strided slices, arrive as contiguous copies.
|
|
|
249
254
|
| `BrokenPipeError` | The endpoint is closed, or the producer's acknowledgement channel failed. |
|
|
250
255
|
| `RuntimeError` | A second process tries to produce, or a used sender is passed to another process. |
|
|
251
256
|
| Pickling errors | The object cannot be pickled or unpickled. Both endpoints stay usable, and failed messages are still freed. |
|
|
257
|
+
| `RuntimeWarning` (warning) | A `with sender:` block or the producing process ended normally and discarded unacknowledged messages. |
|
|
252
258
|
|
|
253
259
|
## API reference
|
|
254
260
|
|
|
@@ -265,9 +271,9 @@ for the underlying queues; it defaults to `multiprocessing.get_context()`.
|
|
|
265
271
|
| `put(obj, block=True, timeout=None)` | Copy `obj` into a new shared-memory block and send it. |
|
|
266
272
|
| `put_nowait(obj)` | `put(obj, block=False)`. |
|
|
267
273
|
| `wait_for_all_ack(timeout=None) -> bool` | Wait until every message is received and freed; `False` on timeout. |
|
|
268
|
-
| `close()` | Free unread messages and release resources. Runs automatically at normal process exit. |
|
|
274
|
+
| `close()` | Free unread messages silently and release resources. Runs automatically at normal process exit. |
|
|
269
275
|
| `closed` | Whether the sender is closed. |
|
|
270
|
-
| `with sender:` | Closes the sender on exit. |
|
|
276
|
+
| `with sender:` | Closes the sender on exit; warns if a clean exit discards unacknowledged messages. |
|
|
271
277
|
|
|
272
278
|
### `SharedMemoryReceiver`
|
|
273
279
|
|
|
@@ -180,6 +180,11 @@ with sender:
|
|
|
180
180
|
raise TimeoutError("Consumer did not receive everything")
|
|
181
181
|
```
|
|
182
182
|
|
|
183
|
+
Closing never blocks. If a `with sender:` block or the producing process ends normally
|
|
184
|
+
while messages are still unacknowledged, they are discarded and the sender emits a
|
|
185
|
+
`RuntimeWarning`, so forgetting to drain does not lose data silently. An explicit
|
|
186
|
+
`close()` or an exception leaving the `with` block discards without a warning.
|
|
187
|
+
|
|
183
188
|
When both endpoints live in one process, drain before closing either of them. A sentinel
|
|
184
189
|
such as `None` is a simple way to tell consumers that the stream has ended.
|
|
185
190
|
|
|
@@ -199,6 +204,7 @@ copy; other views, such as strided slices, arrive as contiguous copies.
|
|
|
199
204
|
| `BrokenPipeError` | The endpoint is closed, or the producer's acknowledgement channel failed. |
|
|
200
205
|
| `RuntimeError` | A second process tries to produce, or a used sender is passed to another process. |
|
|
201
206
|
| Pickling errors | The object cannot be pickled or unpickled. Both endpoints stay usable, and failed messages are still freed. |
|
|
207
|
+
| `RuntimeWarning` (warning) | A `with sender:` block or the producing process ended normally and discarded unacknowledged messages. |
|
|
202
208
|
|
|
203
209
|
## API reference
|
|
204
210
|
|
|
@@ -215,9 +221,9 @@ for the underlying queues; it defaults to `multiprocessing.get_context()`.
|
|
|
215
221
|
| `put(obj, block=True, timeout=None)` | Copy `obj` into a new shared-memory block and send it. |
|
|
216
222
|
| `put_nowait(obj)` | `put(obj, block=False)`. |
|
|
217
223
|
| `wait_for_all_ack(timeout=None) -> bool` | Wait until every message is received and freed; `False` on timeout. |
|
|
218
|
-
| `close()` | Free unread messages and release resources. Runs automatically at normal process exit. |
|
|
224
|
+
| `close()` | Free unread messages silently and release resources. Runs automatically at normal process exit. |
|
|
219
225
|
| `closed` | Whether the sender is closed. |
|
|
220
|
-
| `with sender:` | Closes the sender on exit. |
|
|
226
|
+
| `with sender:` | Closes the sender on exit; warns if a clean exit discards unacknowledged messages. |
|
|
221
227
|
|
|
222
228
|
### `SharedMemoryReceiver`
|
|
223
229
|
|
|
@@ -25,5 +25,5 @@ from .factory import create_shared_memory_pair
|
|
|
25
25
|
from .receiver import SharedMemoryReceiver
|
|
26
26
|
from .sender import SharedMemorySender
|
|
27
27
|
|
|
28
|
-
__version__ = "4.
|
|
28
|
+
__version__ = "4.1.0"
|
|
29
29
|
__all__ = ["SharedMemoryReceiver", "SharedMemorySender", "__version__", "create_shared_memory_pair"]
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import os
|
|
6
6
|
import threading
|
|
7
7
|
import time
|
|
8
|
+
import warnings
|
|
8
9
|
from multiprocessing.queues import Queue
|
|
9
10
|
from multiprocessing.shared_memory import SharedMemory
|
|
10
11
|
from multiprocessing.util import Finalize
|
|
@@ -33,7 +34,9 @@ class SharedMemorySender:
|
|
|
33
34
|
Each `put` copies one object into a new shared-memory block that this sender owns
|
|
34
35
|
until a receiver acknowledges it. The sender is thread-safe, but only one process
|
|
35
36
|
may produce: pass it to that process as a `Process` argument before the first
|
|
36
|
-
`put`. Call `wait_for_all_ack` before
|
|
37
|
+
`put`. Call `wait_for_all_ack` before closing, because closing frees unread blocks.
|
|
38
|
+
Leaving a `with` block or exiting the process normally while messages are still
|
|
39
|
+
unacknowledged emits a `RuntimeWarning`.
|
|
37
40
|
"""
|
|
38
41
|
|
|
39
42
|
def __init__(
|
|
@@ -112,8 +115,9 @@ class SharedMemorySender:
|
|
|
112
115
|
def close(self) -> None:
|
|
113
116
|
"""Free unread messages, stop the acknowledgement thread, and close the queues.
|
|
114
117
|
|
|
115
|
-
Idempotent. Unread messages are
|
|
116
|
-
Also runs automatically when the producing process
|
|
118
|
+
Idempotent. Unread messages are discarded silently, so drain with
|
|
119
|
+
`wait_for_all_ack` first. Also runs automatically when the producing process
|
|
120
|
+
exits normally, warning if that discards messages.
|
|
117
121
|
"""
|
|
118
122
|
self._check_process()
|
|
119
123
|
errors: list[Exception] = []
|
|
@@ -157,7 +161,12 @@ class SharedMemorySender:
|
|
|
157
161
|
exc: BaseException | None,
|
|
158
162
|
traceback: TracebackType | None,
|
|
159
163
|
) -> None:
|
|
160
|
-
|
|
164
|
+
try:
|
|
165
|
+
# An exception already signals the failure; only warn on a clean exit.
|
|
166
|
+
if exc_type is None:
|
|
167
|
+
self._warn_if_discarding()
|
|
168
|
+
finally:
|
|
169
|
+
self.close()
|
|
161
170
|
|
|
162
171
|
def __getstate__(self) -> dict[str, Any]:
|
|
163
172
|
if self._process_id is not None:
|
|
@@ -174,6 +183,26 @@ class SharedMemorySender:
|
|
|
174
183
|
if self._process_id is not None and self._process_id != os.getpid():
|
|
175
184
|
raise RuntimeError("An initialized sender cannot be used in another process")
|
|
176
185
|
|
|
186
|
+
def _close_at_exit(self) -> None:
|
|
187
|
+
try:
|
|
188
|
+
self._warn_if_discarding()
|
|
189
|
+
finally:
|
|
190
|
+
self.close()
|
|
191
|
+
|
|
192
|
+
def _warn_if_discarding(self) -> None:
|
|
193
|
+
# Explicit close() discards silently; implicit closes make the data loss visible.
|
|
194
|
+
self._check_process()
|
|
195
|
+
with self._condition:
|
|
196
|
+
unacknowledged = 0 if self._closed else len(self._blocks)
|
|
197
|
+
if unacknowledged:
|
|
198
|
+
noun = "message" if unacknowledged == 1 else "messages"
|
|
199
|
+
warnings.warn(
|
|
200
|
+
f"Closing the sender discards {unacknowledged} unacknowledged {noun} that "
|
|
201
|
+
"receivers may not have read; call wait_for_all_ack() first",
|
|
202
|
+
RuntimeWarning,
|
|
203
|
+
stacklevel=3,
|
|
204
|
+
)
|
|
205
|
+
|
|
177
206
|
def _ensure_open(self) -> None:
|
|
178
207
|
if self._closed:
|
|
179
208
|
raise BrokenPipeError("Sender is closed") from self._ack_error
|
|
@@ -243,7 +272,7 @@ class SharedMemorySender:
|
|
|
243
272
|
self._ack_thread = threading.Thread(
|
|
244
273
|
target=self._process_acknowledgements, name="shared-memory-acks", daemon=True
|
|
245
274
|
)
|
|
246
|
-
self._finalizer = Finalize(self, self.
|
|
275
|
+
self._finalizer = Finalize(self, self._close_at_exit, exitpriority=10)
|
|
247
276
|
self._ack_thread.start()
|
|
248
277
|
except BaseException:
|
|
249
278
|
if self._finalizer is not None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: py-sharedmemory
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.1.0
|
|
4
4
|
Summary: Fast multiprocessing.Queue alternative that moves large Python objects and NumPy arrays through shared memory
|
|
5
5
|
Author: Tobias Würth
|
|
6
6
|
License-Expression: MIT
|
|
@@ -230,6 +230,11 @@ with sender:
|
|
|
230
230
|
raise TimeoutError("Consumer did not receive everything")
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
+
Closing never blocks. If a `with sender:` block or the producing process ends normally
|
|
234
|
+
while messages are still unacknowledged, they are discarded and the sender emits a
|
|
235
|
+
`RuntimeWarning`, so forgetting to drain does not lose data silently. An explicit
|
|
236
|
+
`close()` or an exception leaving the `with` block discards without a warning.
|
|
237
|
+
|
|
233
238
|
When both endpoints live in one process, drain before closing either of them. A sentinel
|
|
234
239
|
such as `None` is a simple way to tell consumers that the stream has ended.
|
|
235
240
|
|
|
@@ -249,6 +254,7 @@ copy; other views, such as strided slices, arrive as contiguous copies.
|
|
|
249
254
|
| `BrokenPipeError` | The endpoint is closed, or the producer's acknowledgement channel failed. |
|
|
250
255
|
| `RuntimeError` | A second process tries to produce, or a used sender is passed to another process. |
|
|
251
256
|
| Pickling errors | The object cannot be pickled or unpickled. Both endpoints stay usable, and failed messages are still freed. |
|
|
257
|
+
| `RuntimeWarning` (warning) | A `with sender:` block or the producing process ended normally and discarded unacknowledged messages. |
|
|
252
258
|
|
|
253
259
|
## API reference
|
|
254
260
|
|
|
@@ -265,9 +271,9 @@ for the underlying queues; it defaults to `multiprocessing.get_context()`.
|
|
|
265
271
|
| `put(obj, block=True, timeout=None)` | Copy `obj` into a new shared-memory block and send it. |
|
|
266
272
|
| `put_nowait(obj)` | `put(obj, block=False)`. |
|
|
267
273
|
| `wait_for_all_ack(timeout=None) -> bool` | Wait until every message is received and freed; `False` on timeout. |
|
|
268
|
-
| `close()` | Free unread messages and release resources. Runs automatically at normal process exit. |
|
|
274
|
+
| `close()` | Free unread messages silently and release resources. Runs automatically at normal process exit. |
|
|
269
275
|
| `closed` | Whether the sender is closed. |
|
|
270
|
-
| `with sender:` | Closes the sender on exit. |
|
|
276
|
+
| `with sender:` | Closes the sender on exit; warns if a clean exit discards unacknowledged messages. |
|
|
271
277
|
|
|
272
278
|
### `SharedMemoryReceiver`
|
|
273
279
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import gc
|
|
1
2
|
import multiprocessing as mp
|
|
2
3
|
|
|
3
4
|
import pytest
|
|
@@ -5,6 +6,15 @@ import pytest
|
|
|
5
6
|
from py_sharedmemory import create_shared_memory_pair
|
|
6
7
|
|
|
7
8
|
|
|
9
|
+
@pytest.fixture(autouse=True)
|
|
10
|
+
def collect_garbage():
|
|
11
|
+
# Queues left in reference cycles finalize their semaphores whenever the collector
|
|
12
|
+
# runs. On Python 3.12 that raises ReentrantCallError if it interrupts this thread
|
|
13
|
+
# inside the resource tracker, so collect at a safe point after every test.
|
|
14
|
+
yield
|
|
15
|
+
gc.collect()
|
|
16
|
+
|
|
17
|
+
|
|
8
18
|
@pytest.fixture
|
|
9
19
|
def make_pair():
|
|
10
20
|
pairs = []
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import multiprocessing.shared_memory as sm
|
|
2
|
+
import warnings
|
|
2
3
|
from queue import Queue
|
|
3
4
|
|
|
4
5
|
import pytest
|
|
@@ -53,7 +54,34 @@ def consume(receiver, count, results):
|
|
|
53
54
|
results.put(values)
|
|
54
55
|
|
|
55
56
|
|
|
57
|
+
PAYLOAD_BYTES = 1_000_000
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def make_payload(index):
|
|
61
|
+
"""Alternate in-band bytes and out-of-band NumPy buffers with index-specific content."""
|
|
62
|
+
if index % 2:
|
|
63
|
+
return bytes([index % 251]) * PAYLOAD_BYTES
|
|
64
|
+
import numpy as np
|
|
65
|
+
|
|
66
|
+
return np.full(PAYLOAD_BYTES // 4, index, dtype=np.int32)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def payload_intact(index, payload):
|
|
70
|
+
if isinstance(payload, bytes):
|
|
71
|
+
return len(payload) == PAYLOAD_BYTES and payload.count(index % 251) == PAYLOAD_BYTES
|
|
72
|
+
return payload.shape == (PAYLOAD_BYTES // 4,) and bool((payload == index).all())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def verify_payloads(receiver, results):
|
|
76
|
+
with receiver:
|
|
77
|
+
while (message := receiver.get(timeout=10)) is not None:
|
|
78
|
+
index, payload = message
|
|
79
|
+
results.put((index, payload_intact(index, payload)))
|
|
80
|
+
|
|
81
|
+
|
|
56
82
|
def exit_with_unreceived_data(sender, ready_to_exit):
|
|
83
|
+
# test_process_exit_with_unread_messages_warns covers the exit warning itself.
|
|
84
|
+
warnings.filterwarnings("ignore", "Closing the sender discards", RuntimeWarning)
|
|
57
85
|
sender.put(bytearray(65_536))
|
|
58
86
|
if not ready_to_exit.wait(10):
|
|
59
87
|
raise TimeoutError("Parent did not read the metadata")
|
|
@@ -61,11 +89,15 @@ def exit_with_unreceived_data(sender, ready_to_exit):
|
|
|
61
89
|
|
|
62
90
|
|
|
63
91
|
def abandon_metadata(sender, result_pipe):
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
92
|
+
def abandon():
|
|
93
|
+
with sender:
|
|
94
|
+
for _ in range(1_000):
|
|
95
|
+
sender.put(b"x")
|
|
96
|
+
result_pipe.send(list(sender._blocks))
|
|
97
|
+
result_pipe.close()
|
|
98
|
+
|
|
99
|
+
with pytest.warns(RuntimeWarning, match="discards 1000 unacknowledged"):
|
|
100
|
+
abandon()
|
|
69
101
|
|
|
70
102
|
|
|
71
103
|
def try_foreign_sender(sender, result_pipe):
|
|
@@ -11,8 +11,10 @@ from .helpers import (
|
|
|
11
11
|
consume,
|
|
12
12
|
exit_with_unreceived_data,
|
|
13
13
|
join_process,
|
|
14
|
+
make_payload,
|
|
14
15
|
produce,
|
|
15
16
|
try_foreign_sender,
|
|
17
|
+
verify_payloads,
|
|
16
18
|
)
|
|
17
19
|
|
|
18
20
|
|
|
@@ -94,6 +96,32 @@ def test_multiple_consumer_processes(make_pair, ctx):
|
|
|
94
96
|
results.join_thread()
|
|
95
97
|
|
|
96
98
|
|
|
99
|
+
def test_large_messages_arrive_intact_across_consumer_processes(make_pair, ctx):
|
|
100
|
+
sender, receiver = make_pair(4, ctx=ctx)
|
|
101
|
+
results = ctx.Queue()
|
|
102
|
+
consumers = [ctx.Process(target=verify_payloads, args=(receiver, results)) for _ in range(3)]
|
|
103
|
+
for process in consumers:
|
|
104
|
+
process.start()
|
|
105
|
+
try:
|
|
106
|
+
for index in range(60):
|
|
107
|
+
sender.put((index, make_payload(index)), timeout=10)
|
|
108
|
+
for _ in consumers:
|
|
109
|
+
sender.put(None, timeout=10)
|
|
110
|
+
received = dict(results.get(timeout=15) for _ in range(60))
|
|
111
|
+
assert sender.wait_for_all_ack(timeout=10)
|
|
112
|
+
# Every byte of every payload is checked in the consumer that received it.
|
|
113
|
+
assert received == dict.fromkeys(range(60), True)
|
|
114
|
+
for process in consumers:
|
|
115
|
+
join_process(process)
|
|
116
|
+
finally:
|
|
117
|
+
for process in consumers:
|
|
118
|
+
if process.is_alive():
|
|
119
|
+
process.terminate()
|
|
120
|
+
process.join(5)
|
|
121
|
+
results.close()
|
|
122
|
+
results.join_thread()
|
|
123
|
+
|
|
124
|
+
|
|
97
125
|
def test_normal_process_exit_cleans_unreceived_shared_memory(make_pair, ctx):
|
|
98
126
|
sender, receiver = make_pair(1, ctx=ctx)
|
|
99
127
|
ready_to_exit = ctx.Event()
|
|
@@ -165,6 +193,26 @@ raise SystemExit(process.exitcode)
|
|
|
165
193
|
"""
|
|
166
194
|
|
|
167
195
|
|
|
196
|
+
UNREAD_AT_EXIT = """
|
|
197
|
+
from py_sharedmemory import create_shared_memory_pair
|
|
198
|
+
|
|
199
|
+
sender, receiver = create_shared_memory_pair(1)
|
|
200
|
+
sender.put(b"never received")
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_process_exit_with_unread_messages_warns():
|
|
205
|
+
result = subprocess.run(
|
|
206
|
+
[sys.executable, "-c", UNREAD_AT_EXIT],
|
|
207
|
+
cwd=Path(__file__).resolve().parents[1],
|
|
208
|
+
capture_output=True,
|
|
209
|
+
text=True,
|
|
210
|
+
timeout=30,
|
|
211
|
+
)
|
|
212
|
+
assert result.returncode == 0, result.stderr
|
|
213
|
+
assert "RuntimeWarning: Closing the sender discards 1 unacknowledged message" in result.stderr
|
|
214
|
+
|
|
215
|
+
|
|
168
216
|
@pytest.mark.skipif("fork" not in mp.get_all_start_methods(), reason="Requires fork")
|
|
169
217
|
def test_forked_producer_leaves_no_resource_tracker_entries():
|
|
170
218
|
# A fresh interpreter has no resource tracker running before the fork.
|
|
@@ -3,6 +3,7 @@ import os
|
|
|
3
3
|
import pickle
|
|
4
4
|
import threading
|
|
5
5
|
import time
|
|
6
|
+
import warnings
|
|
6
7
|
from concurrent.futures import ThreadPoolExecutor
|
|
7
8
|
from contextlib import suppress
|
|
8
9
|
from queue import Empty, Full
|
|
@@ -269,6 +270,69 @@ def test_context_managers_close_unused_endpoints(make_pair):
|
|
|
269
270
|
assert receiver.closed
|
|
270
271
|
|
|
271
272
|
|
|
273
|
+
def send_then_leave(sender, count, *, fail=False):
|
|
274
|
+
"""Leave a `with sender:` block without draining; return the abandoned block names."""
|
|
275
|
+
with sender:
|
|
276
|
+
for index in range(count):
|
|
277
|
+
sender.put(index)
|
|
278
|
+
if fail:
|
|
279
|
+
raise ValueError("producer failed")
|
|
280
|
+
return list(sender._blocks)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@pytest.mark.parametrize(
|
|
284
|
+
("count", "text"),
|
|
285
|
+
[(1, "1 unacknowledged message that"), (2, "2 unacknowledged messages that")],
|
|
286
|
+
)
|
|
287
|
+
def test_clean_with_exit_warns_about_discarded_messages(make_pair, count, text):
|
|
288
|
+
sender, _ = make_pair(0)
|
|
289
|
+
with pytest.warns(RuntimeWarning, match=f"discards {text}") as caught:
|
|
290
|
+
names = send_then_leave(sender, count)
|
|
291
|
+
assert caught[0].filename == __file__ # Points at the user's `with` statement
|
|
292
|
+
assert sender.closed
|
|
293
|
+
for name in names:
|
|
294
|
+
assert_unlinked(name)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@pytest.mark.parametrize("ending", ["drained", "exception", "close"])
|
|
298
|
+
def test_drained_failed_and_explicit_closes_do_not_warn(make_pair, ending):
|
|
299
|
+
sender, receiver = make_pair()
|
|
300
|
+
with warnings.catch_warnings():
|
|
301
|
+
warnings.simplefilter("error")
|
|
302
|
+
if ending == "drained":
|
|
303
|
+
with sender:
|
|
304
|
+
sender.put(1)
|
|
305
|
+
assert receiver.get(timeout=3) == 1
|
|
306
|
+
assert sender.wait_for_all_ack(timeout=3)
|
|
307
|
+
elif ending == "exception":
|
|
308
|
+
with pytest.raises(ValueError, match="producer failed"):
|
|
309
|
+
send_then_leave(sender, 1, fail=True)
|
|
310
|
+
else:
|
|
311
|
+
sender.put(1)
|
|
312
|
+
sender.close()
|
|
313
|
+
assert sender.closed
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def test_process_exit_cleanup_warns_about_discarded_messages(make_pair):
|
|
317
|
+
sender, _ = make_pair()
|
|
318
|
+
sender.put(1)
|
|
319
|
+
with pytest.warns(RuntimeWarning, match="discards 1 unacknowledged message"):
|
|
320
|
+
sender._finalizer()
|
|
321
|
+
assert sender.closed
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def test_warning_as_error_still_closes_the_sender(make_pair):
|
|
325
|
+
sender, _ = make_pair()
|
|
326
|
+
sender.put(1)
|
|
327
|
+
name = next(iter(sender._blocks))
|
|
328
|
+
with warnings.catch_warnings():
|
|
329
|
+
warnings.simplefilter("error", RuntimeWarning)
|
|
330
|
+
with pytest.raises(RuntimeWarning, match="unacknowledged"), sender:
|
|
331
|
+
pass
|
|
332
|
+
assert sender.closed
|
|
333
|
+
assert_unlinked(name)
|
|
334
|
+
|
|
335
|
+
|
|
272
336
|
def test_ack_channel_failure_closes_without_joining_its_own_thread():
|
|
273
337
|
class BrokenAckQueue(LocalQueue):
|
|
274
338
|
def get(self, **kwargs):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_sharedmemory-4.0.0 → py_sharedmemory-4.1.0}/py_sharedmemory.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|