py-sharedmemory 4.1.0__tar.gz → 4.1.1__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.
Files changed (36) hide show
  1. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/CHANGELOG.md +12 -0
  2. {py_sharedmemory-4.1.0/py_sharedmemory.egg-info → py_sharedmemory-4.1.1}/PKG-INFO +12 -11
  3. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/README.md +11 -10
  4. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/py_sharedmemory/__init__.py +1 -1
  5. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/py_sharedmemory/convert.py +10 -1
  6. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/py_sharedmemory/factory.py +1 -2
  7. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/py_sharedmemory/sender.py +14 -1
  8. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1/py_sharedmemory.egg-info}/PKG-INFO +12 -11
  9. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/tests/test_convert.py +9 -0
  10. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/tests/test_processes.py +29 -0
  11. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/tests/test_queue.py +37 -1
  12. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/LICENSE +0 -0
  13. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/MANIFEST.in +0 -0
  14. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/benchmarks/__init__.py +0 -0
  15. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/benchmarks/benchmark.py +0 -0
  16. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/benchmarks/plot.py +0 -0
  17. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/benchmarks/results/windows-numpy.json +0 -0
  18. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/benchmarks/results/windows-numpy.png +0 -0
  19. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/examples/basic.py +0 -0
  20. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/examples/numpy_batches.py +0 -0
  21. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/examples/worker_pool.py +0 -0
  22. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/py_sharedmemory/py.typed +0 -0
  23. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/py_sharedmemory/receiver.py +0 -0
  24. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/py_sharedmemory.egg-info/SOURCES.txt +0 -0
  25. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/py_sharedmemory.egg-info/dependency_links.txt +0 -0
  26. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/py_sharedmemory.egg-info/requires.txt +0 -0
  27. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/py_sharedmemory.egg-info/top_level.txt +0 -0
  28. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/pyproject.toml +0 -0
  29. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/setup.cfg +0 -0
  30. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/tests/__init__.py +0 -0
  31. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/tests/conftest.py +0 -0
  32. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/tests/helpers.py +0 -0
  33. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/tests/test_benchmark.py +0 -0
  34. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/tests/test_objects.py +0 -0
  35. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/tests/test_package.py +0 -0
  36. {py_sharedmemory-4.1.0 → py_sharedmemory-4.1.1}/tests/test_threads.py +0 -0
@@ -1,11 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.1.1
4
+
5
+ - Unread messages no longer hold file descriptors on Linux and macOS: the sender unmaps
6
+ each block right after writing it. Unbounded pairs previously failed with
7
+ `Too many open files` after about 120 unread messages under macOS's default limit.
8
+ - Cap outstanding messages at the metadata queue's limit (32,767 on macOS), so a blocking
9
+ `put` on an unbounded pair waits instead of raising `queue.Full`.
10
+ - Explain a freed block in the receiver's `FileNotFoundError`, which happens when the
11
+ sender closes before a message is read.
12
+
3
13
  ## 4.1.0
4
14
 
5
15
  - Warn with a `RuntimeWarning` when leaving a `with sender:` block or exiting the
6
16
  producing process discards unacknowledged messages, so a forgotten
7
17
  `wait_for_all_ack()` no longer loses data silently. Explicit `close()` calls and
8
18
  exits caused by exceptions stay silent, and closing still never blocks.
19
+ - Collect garbage between tests to avoid Python 3.12 resource-tracker reentrancy errors,
20
+ and add a stress test that checks every byte across consumer processes.
9
21
 
10
22
  ## 4.0.0
11
23
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py-sharedmemory
3
- Version: 4.1.0
3
+ Version: 4.1.1
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
@@ -190,9 +190,7 @@ consumer = ctx.Process(target=consume, args=(receiver,))
190
190
 
191
191
  `capacity` counts messages that were sent but not yet received and freed. When it is
192
192
  reached, `put()` blocks, so a fast producer cannot exhaust memory. Shared-memory use is
193
- at most about `capacity × message size`. `capacity=0` or `None` means unbounded; prefer
194
- a limit, because unread messages also hold open files on Linux and macOS (see the
195
- platform notes below).
193
+ at most about `capacity × message size`. `capacity=0` or `None` means unbounded.
196
194
 
197
195
  ### Timeouts and non-blocking calls
198
196
 
@@ -296,13 +294,9 @@ with `OSError: [Errno 28] No space left on device` or crash with `SIGBUS`.
296
294
  **Linux:** outside containers, `/dev/shm` usually defaults to half of physical RAM.
297
295
 
298
296
  **macOS:** fully supported and tested on Apple Silicon. The default start method is
299
- `spawn`, so guard the entry point with `if __name__ == "__main__":`.
300
-
301
- **Open-file limit (Linux and macOS):** each unread message keeps two file descriptors
302
- open in the producer until it is received. macOS allows 256 per process by default and
303
- Linux usually 1,024, so an unbounded pair fails with `OSError: [Errno 24] Too many open
304
- files` after roughly 120 or 500 unread messages. Set a `capacity` well below that, or
305
- raise the limit with `ulimit -n`. The sender stays usable after this error.
297
+ `spawn`, so guard the entry point with `if __name__ == "__main__":`. An unbounded pair
298
+ holds at most 32,767 unread messages, the limit of macOS semaphores; beyond that `put`
299
+ waits or raises `queue.Full` like a full bounded pair.
306
300
 
307
301
  **Windows:** shared memory is backed by the page file, and each block disappears once
308
302
  the last handle closes, so nothing can leak after a crash.
@@ -310,6 +304,13 @@ the last handle closes, so nothing can leak after a crash.
310
304
  **"resource_tracker: There appear to be N leaked shared_memory objects":** fixed in
311
305
  3.0.1. Upgrade if you see this warning with the `fork` start method.
312
306
 
307
+ **`FileNotFoundError: Shared-memory block was freed before it was received`:** the
308
+ producer closed or exited before this message was read. Call `wait_for_all_ack()` before
309
+ closing the sender; the sender also warns when this happens.
310
+
311
+ **`OSError: [Errno 24] Too many open files` with many unread messages:** fixed in 4.1.1,
312
+ where unread messages no longer hold file descriptors.
313
+
313
314
  **`RuntimeError: A shared-memory pair supports one producing process`:** a second
314
315
  process called `put`. Create a separate pair for each producer process.
315
316
 
@@ -140,9 +140,7 @@ consumer = ctx.Process(target=consume, args=(receiver,))
140
140
 
141
141
  `capacity` counts messages that were sent but not yet received and freed. When it is
142
142
  reached, `put()` blocks, so a fast producer cannot exhaust memory. Shared-memory use is
143
- at most about `capacity × message size`. `capacity=0` or `None` means unbounded; prefer
144
- a limit, because unread messages also hold open files on Linux and macOS (see the
145
- platform notes below).
143
+ at most about `capacity × message size`. `capacity=0` or `None` means unbounded.
146
144
 
147
145
  ### Timeouts and non-blocking calls
148
146
 
@@ -246,13 +244,9 @@ with `OSError: [Errno 28] No space left on device` or crash with `SIGBUS`.
246
244
  **Linux:** outside containers, `/dev/shm` usually defaults to half of physical RAM.
247
245
 
248
246
  **macOS:** fully supported and tested on Apple Silicon. The default start method is
249
- `spawn`, so guard the entry point with `if __name__ == "__main__":`.
250
-
251
- **Open-file limit (Linux and macOS):** each unread message keeps two file descriptors
252
- open in the producer until it is received. macOS allows 256 per process by default and
253
- Linux usually 1,024, so an unbounded pair fails with `OSError: [Errno 24] Too many open
254
- files` after roughly 120 or 500 unread messages. Set a `capacity` well below that, or
255
- raise the limit with `ulimit -n`. The sender stays usable after this error.
247
+ `spawn`, so guard the entry point with `if __name__ == "__main__":`. An unbounded pair
248
+ holds at most 32,767 unread messages, the limit of macOS semaphores; beyond that `put`
249
+ waits or raises `queue.Full` like a full bounded pair.
256
250
 
257
251
  **Windows:** shared memory is backed by the page file, and each block disappears once
258
252
  the last handle closes, so nothing can leak after a crash.
@@ -260,6 +254,13 @@ the last handle closes, so nothing can leak after a crash.
260
254
  **"resource_tracker: There appear to be N leaked shared_memory objects":** fixed in
261
255
  3.0.1. Upgrade if you see this warning with the `fork` start method.
262
256
 
257
+ **`FileNotFoundError: Shared-memory block was freed before it was received`:** the
258
+ producer closed or exited before this message was read. Call `wait_for_all_ack()` before
259
+ closing the sender; the sender also warns when this happens.
260
+
261
+ **`OSError: [Errno 24] Too many open files` with many unread messages:** fixed in 4.1.1,
262
+ where unread messages no longer hold file descriptors.
263
+
263
264
  **`RuntimeError: A shared-memory pair supports one producing process`:** a second
264
265
  process called `put`. Create a separate pair for each producer process.
265
266
 
@@ -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.1.0"
28
+ __version__ = "4.1.1"
29
29
  __all__ = ["SharedMemoryReceiver", "SharedMemorySender", "__version__", "create_shared_memory_pair"]
@@ -1,5 +1,6 @@
1
1
  """Serialize protocol-5 pickle data through a single shared-memory block."""
2
2
 
3
+ import errno
3
4
  import pickle
4
5
  import sys
5
6
  from contextlib import suppress
@@ -60,7 +61,15 @@ def serialize(data: Any) -> tuple[SharedMemory, SharedMemoryMessage]:
60
61
  def deserialize(message: SharedMemoryMessage) -> Any:
61
62
  """Unpickle with owned local buffers and always close the shared mapping."""
62
63
  _validate_message(message)
63
- allocation = SharedMemory(name=message.name, **_ATTACH_OPTIONS)
64
+ try:
65
+ allocation = SharedMemory(name=message.name, **_ATTACH_OPTIONS)
66
+ except FileNotFoundError as error:
67
+ raise FileNotFoundError(
68
+ errno.ENOENT,
69
+ "Shared-memory block was freed before it was received; the sender probably "
70
+ "closed without calling wait_for_all_ack()",
71
+ message.name,
72
+ ) from error
64
73
  try:
65
74
  if message.size > allocation.size:
66
75
  raise ValueError("Shared-memory block is smaller than its metadata")
@@ -21,8 +21,7 @@ def create_shared_memory_pair(
21
21
 
22
22
  Args:
23
23
  capacity: Maximum messages sent but not yet received and freed; `put` blocks
24
- when it is reached. `0` or `None` means unbounded, but on Linux and macOS
25
- every unread message also holds two open file descriptors.
24
+ when it is reached. `0` or `None` means unbounded.
26
25
  ctx: Multiprocessing context for the underlying queues, such as
27
26
  `multiprocessing.get_context("spawn")`. Defaults to the default context;
28
27
  use the same context to start the worker processes.
@@ -3,11 +3,13 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
+ import sys
6
7
  import threading
7
8
  import time
8
9
  import warnings
9
10
  from multiprocessing.queues import Queue
10
11
  from multiprocessing.shared_memory import SharedMemory
12
+ from multiprocessing.synchronize import SEM_VALUE_MAX # type: ignore[attr-defined] # not in typeshed
11
13
  from multiprocessing.util import Finalize
12
14
  from queue import Empty, Full
13
15
  from types import TracebackType
@@ -18,6 +20,14 @@ from .convert import SharedMemoryMessage, release_shared_memory, serialize
18
20
  if TYPE_CHECKING:
19
21
  from multiprocessing.sharedctypes import Synchronized
20
22
 
23
+ # A POSIX block lives on by name until it is unlinked, so the sender can drop its mapping
24
+ # and file descriptors right after writing. Windows frees a block with its last handle.
25
+ _UNMAP_AFTER_WRITE = sys.platform != "win32"
26
+
27
+ # The metadata queue holds at most SEM_VALUE_MAX unread items (32,767 on macOS); beyond
28
+ # that its put would raise queue.Full even for a blocking put on an unbounded sender.
29
+ _MAX_OUTSTANDING = SEM_VALUE_MAX
30
+
21
31
 
22
32
  def _validate_capacity(capacity: int | None) -> None:
23
33
  if capacity is None:
@@ -215,7 +225,8 @@ class SharedMemorySender:
215
225
  with self._condition:
216
226
  self._ensure_open()
217
227
  self._start_ack_thread()
218
- while self.capacity and self._pending_count >= self.capacity:
228
+ limit = min(self.capacity or _MAX_OUTSTANDING, _MAX_OUTSTANDING)
229
+ while self._pending_count >= limit:
219
230
  if not block:
220
231
  raise Full
221
232
  remaining = None if deadline is None else deadline - time.monotonic()
@@ -235,6 +246,8 @@ class SharedMemorySender:
235
246
  raise
236
247
  try:
237
248
  self._ensure_open()
249
+ if _UNMAP_AFTER_WRITE:
250
+ allocation.close()
238
251
  self._data_queue.put_nowait(message)
239
252
  except BaseException:
240
253
  self._release_message(allocation.name)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py-sharedmemory
3
- Version: 4.1.0
3
+ Version: 4.1.1
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
@@ -190,9 +190,7 @@ consumer = ctx.Process(target=consume, args=(receiver,))
190
190
 
191
191
  `capacity` counts messages that were sent but not yet received and freed. When it is
192
192
  reached, `put()` blocks, so a fast producer cannot exhaust memory. Shared-memory use is
193
- at most about `capacity × message size`. `capacity=0` or `None` means unbounded; prefer
194
- a limit, because unread messages also hold open files on Linux and macOS (see the
195
- platform notes below).
193
+ at most about `capacity × message size`. `capacity=0` or `None` means unbounded.
196
194
 
197
195
  ### Timeouts and non-blocking calls
198
196
 
@@ -296,13 +294,9 @@ with `OSError: [Errno 28] No space left on device` or crash with `SIGBUS`.
296
294
  **Linux:** outside containers, `/dev/shm` usually defaults to half of physical RAM.
297
295
 
298
296
  **macOS:** fully supported and tested on Apple Silicon. The default start method is
299
- `spawn`, so guard the entry point with `if __name__ == "__main__":`.
300
-
301
- **Open-file limit (Linux and macOS):** each unread message keeps two file descriptors
302
- open in the producer until it is received. macOS allows 256 per process by default and
303
- Linux usually 1,024, so an unbounded pair fails with `OSError: [Errno 24] Too many open
304
- files` after roughly 120 or 500 unread messages. Set a `capacity` well below that, or
305
- raise the limit with `ulimit -n`. The sender stays usable after this error.
297
+ `spawn`, so guard the entry point with `if __name__ == "__main__":`. An unbounded pair
298
+ holds at most 32,767 unread messages, the limit of macOS semaphores; beyond that `put`
299
+ waits or raises `queue.Full` like a full bounded pair.
306
300
 
307
301
  **Windows:** shared memory is backed by the page file, and each block disappears once
308
302
  the last handle closes, so nothing can leak after a crash.
@@ -310,6 +304,13 @@ the last handle closes, so nothing can leak after a crash.
310
304
  **"resource_tracker: There appear to be N leaked shared_memory objects":** fixed in
311
305
  3.0.1. Upgrade if you see this warning with the `fork` start method.
312
306
 
307
+ **`FileNotFoundError: Shared-memory block was freed before it was received`:** the
308
+ producer closed or exited before this message was read. Call `wait_for_all_ack()` before
309
+ closing the sender; the sender also warns when this happens.
310
+
311
+ **`OSError: [Errno 24] Too many open files` with many unread messages:** fixed in 4.1.1,
312
+ where unread messages no longer hold file descriptors.
313
+
313
314
  **`RuntimeError: A shared-memory pair supports one producing process`:** a second
314
315
  process called `put`. Create a separate pair for each producer process.
315
316
 
@@ -242,6 +242,15 @@ def test_cleanup_tolerates_already_unlinked_blocks():
242
242
  assert_unlinked(info.name)
243
243
 
244
244
 
245
+ def test_freed_block_reports_a_clear_error():
246
+ handle, info = serialize(42)
247
+ release_shared_memory(handle)
248
+ with pytest.raises(FileNotFoundError, match="freed before it was received") as caught:
249
+ deserialize(info)
250
+ assert caught.value.filename == info.name
251
+ assert isinstance(caught.value.__cause__, FileNotFoundError)
252
+
253
+
245
254
  def test_cleanup_ignores_names_that_are_already_gone():
246
255
  # Windows never raises here because unlink is a no-op, so simulate POSIX.
247
256
  handle = Mock()
@@ -193,6 +193,35 @@ raise SystemExit(process.exitcode)
193
193
  """
194
194
 
195
195
 
196
+ LOW_FILE_LIMIT = """
197
+ import resource
198
+
199
+ from py_sharedmemory import create_shared_memory_pair
200
+
201
+ # macOS defaults to 256 open files; unread messages must not hold descriptors.
202
+ resource.setrlimit(resource.RLIMIT_NOFILE, (256, 256))
203
+ sender, receiver = create_shared_memory_pair(0)
204
+ for index in range(1_000):
205
+ sender.put(index)
206
+ assert [receiver.get(timeout=10) for _ in range(1_000)] == list(range(1_000))
207
+ assert sender.wait_for_all_ack(timeout=10)
208
+ sender.close()
209
+ receiver.close()
210
+ """
211
+
212
+
213
+ @pytest.mark.skipif(sys.platform == "win32", reason="POSIX file-descriptor limits")
214
+ def test_unread_messages_hold_no_file_descriptors():
215
+ result = subprocess.run(
216
+ [sys.executable, "-c", LOW_FILE_LIMIT],
217
+ cwd=Path(__file__).resolve().parents[1],
218
+ capture_output=True,
219
+ text=True,
220
+ timeout=60,
221
+ )
222
+ assert result.returncode == 0, result.stderr
223
+
224
+
196
225
  UNREAD_AT_EXIT = """
197
226
  from py_sharedmemory import create_shared_memory_pair
198
227
 
@@ -1,11 +1,13 @@
1
1
  import multiprocessing as mp
2
2
  import os
3
3
  import pickle
4
+ import sys
4
5
  import threading
5
6
  import time
6
7
  import warnings
7
8
  from concurrent.futures import ThreadPoolExecutor
8
9
  from contextlib import suppress
10
+ from multiprocessing.shared_memory import SharedMemory
9
11
  from queue import Empty, Full
10
12
  from unittest.mock import Mock
11
13
 
@@ -93,6 +95,38 @@ def test_full_nonblocking_and_immediate_timeouts(make_pair, kwargs):
93
95
  assert receiver.get(timeout=3) == 3
94
96
 
95
97
 
98
+ @pytest.mark.parametrize("capacity", [0, None, 5])
99
+ def test_outstanding_messages_never_exceed_the_metadata_queue_limit(
100
+ make_pair, monkeypatch, capacity
101
+ ):
102
+ # macOS limits a multiprocessing queue to 32,767 unread items; simulate a tiny limit.
103
+ monkeypatch.setattr(sender_module, "_MAX_OUTSTANDING", 2)
104
+ sender, receiver = make_pair(capacity)
105
+ sender.put(1)
106
+ sender.put(2)
107
+ with pytest.raises(Full):
108
+ sender.put_nowait(3)
109
+ with pytest.raises(Full):
110
+ sender.put(3, timeout=0.05)
111
+ assert receiver.get(timeout=3) == 1
112
+ sender.put(3, timeout=3)
113
+ assert [receiver.get(timeout=3) for _ in range(2)] == [2, 3]
114
+ assert sender.wait_for_all_ack(timeout=3)
115
+
116
+
117
+ @pytest.mark.parametrize("unmap", [False, True])
118
+ def test_sender_unmaps_blocks_only_where_names_keep_them_alive(make_pair, monkeypatch, unmap):
119
+ monkeypatch.setattr(sender_module, "_UNMAP_AFTER_WRITE", unmap)
120
+ sender, receiver = make_pair()
121
+ sender.put(42)
122
+ allocation = next(iter(sender._blocks.values()))
123
+ assert (allocation.buf is None) is unmap
124
+ if not (unmap and sys.platform == "win32"):
125
+ # Windows frees an unmapped block at once, which is why it never unmaps.
126
+ assert receiver.get(timeout=3) == 42
127
+ assert sender.wait_for_all_ack(timeout=3)
128
+
129
+
96
130
  def test_put_timeout_waits_for_capacity(make_pair):
97
131
  sender, _ = make_pair()
98
132
  sender.put(1)
@@ -204,7 +238,9 @@ def test_failed_decode_is_acknowledged_and_next_message_succeeds(make_pair):
204
238
  sender, receiver = make_pair()
205
239
  sender.put(42)
206
240
  name = next(iter(sender._blocks))
207
- sender._blocks[name].buf[0] = ord("!")
241
+ corrupt = SharedMemory(name=name)
242
+ corrupt.buf[0] = ord("!")
243
+ corrupt.close()
208
244
  with pytest.raises(pickle.UnpicklingError):
209
245
  receiver.get(timeout=3)
210
246
  assert sender.wait_for_all_ack(timeout=3)
File without changes