slick-queue-py 2.1.0__tar.gz → 2.3.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.
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/API_DIFFERENCES.md +25 -7
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/CHANGELOG.md +74 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/PKG-INFO +43 -4
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/README.md +42 -3
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/slick_queue_py.egg-info/SOURCES.txt +1 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/slick_queue_py.py +164 -13
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/test_items_per_slot.py +3 -4
- slick_queue_py-2.3.0/tests/test_segment_lifetime.py +410 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/test_traits.py +8 -12
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/.github/FUNDING.yml +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/.github/workflows/ci.yml +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/.github/workflows/publish-conda.yml +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/.github/workflows/publish-pypi.yml +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/.github/workflows/publish-release.yml +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/.github/workflows/release.yml +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/.gitignore +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/.pypirc.template +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/BUILDING.md +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/CMakeLists.txt +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/LICENSE +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/MANIFEST.in +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/atomic_ops.py +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/atomic_ops_ext.cpp +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/conda.recipe/meta.yaml +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/pyproject.toml +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/requirements.txt +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/setup.cfg +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/setup.py +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/cleanup_shm.py +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/cpp_consumer.cpp +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/cpp_multi_producer.cpp +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/cpp_producer.cpp +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/cpp_read_last_tester.cpp +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/cpp_work_stealing_consumer.cpp +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/run_test.py +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/test_atomic_cursor.py +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/test_atomic_ops.py +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/test_interop.py +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/test_local_mode.py +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/test_loss_count.py +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/test_modern_format.py +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/test_multi_producer.py +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/test_queue.py +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/test_read_last.py +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/test_read_last_interop.py +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/test_reset_detection.py +0 -0
- {slick_queue_py-2.1.0 → slick_queue_py-2.3.0}/tests/test_wrap_invariants.py +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This document explains the intentional API differences between the C++ `slick::queue<T>` and Python `SlickQueue` implementations.
|
|
4
4
|
|
|
5
|
-
Both track the same version line; this document describes slick-queue v2.
|
|
5
|
+
Both track the same version line; this document describes slick-queue v2.3.0.
|
|
6
6
|
|
|
7
7
|
## Core Compatibility
|
|
8
8
|
|
|
@@ -227,27 +227,44 @@ q[idx][:4] = struct.pack("<i", 42) # Manual packing
|
|
|
227
227
|
{
|
|
228
228
|
slick::queue<T> q(size, "name");
|
|
229
229
|
// ... use queue ...
|
|
230
|
-
|
|
230
|
+
q.remove_shm(); // Coordinator, once no peer will attach again; q's mapping stays valid
|
|
231
|
+
} // Destructor unmaps; it never removes the segment
|
|
232
|
+
|
|
233
|
+
slick::shm::shared_memory::remove("name"); // By name, without a queue - e.g. at startup
|
|
231
234
|
```
|
|
232
235
|
|
|
233
236
|
**Python:**
|
|
234
237
|
```python
|
|
235
238
|
# Option 1: Manual cleanup
|
|
236
|
-
q = SlickQueue(name='name', size=size, element_size=elem_size
|
|
239
|
+
q = SlickQueue(name='name', size=size, element_size=elem_size)
|
|
237
240
|
try:
|
|
238
241
|
# ... use queue ...
|
|
239
242
|
finally:
|
|
240
243
|
q.close()
|
|
241
|
-
q.unlink() # Only
|
|
244
|
+
q.unlink() # Only from the process that coordinates the queue's lifetime
|
|
242
245
|
|
|
243
|
-
# Option 2: Context manager
|
|
244
|
-
with SlickQueue(name='name', size=size, element_size=elem_size
|
|
246
|
+
# Option 2: Context manager - closes, never unlinks
|
|
247
|
+
with SlickQueue(name='name', size=size, element_size=elem_size) as q:
|
|
245
248
|
# ... use queue ...
|
|
246
|
-
pass
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
SlickQueue.remove('name') # By name, without attaching - e.g. at startup
|
|
247
252
|
```
|
|
248
253
|
|
|
249
254
|
**Rationale:** Python's garbage collection is non-deterministic, so explicit cleanup or context managers are needed.
|
|
250
255
|
|
|
256
|
+
Neither side deletes a segment on its own, matching C++ v2.3.0: not the C++ destructor, and not
|
|
257
|
+
Python's `close()`, context manager or process exit. Python keeps its segments out of
|
|
258
|
+
`multiprocessing`'s resource tracker for this, since the tracker would otherwise unlink them when
|
|
259
|
+
the process exits.
|
|
260
|
+
|
|
261
|
+
| C++ | Python |
|
|
262
|
+
|-----|--------|
|
|
263
|
+
| `remove_shm()` | `remove_shm()` - same contract, unlinks by name on every call |
|
|
264
|
+
| - | `unlink()` - unlinks once, valid after `close()`; a repeat is a no-op |
|
|
265
|
+
| `slick::shm::shared_memory::remove(name)` | `SlickQueue.remove(name)` - also accepts a `/`-prefixed name |
|
|
266
|
+
| `shm_name()` - `""` for a local queue | `get_shm_name()` - `None` for a local queue, `/`-prefixed on POSIX |
|
|
267
|
+
|
|
251
268
|
---
|
|
252
269
|
|
|
253
270
|
## Interoperability Examples
|
|
@@ -339,6 +356,7 @@ int main() {
|
|
|
339
356
|
| Element access | Pointer | `memoryview` | Python memory safety |
|
|
340
357
|
| Type safety | Templates | `struct` module | Language difference |
|
|
341
358
|
| Cleanup | RAII (destructor) | Explicit or context manager | Python GC is non-deterministic |
|
|
359
|
+
| Segment removal | `remove_shm()` | `remove_shm()`, `unlink()`, `SlickQueue.remove()` | Neither side unlinks on its own |
|
|
342
360
|
| `read()` data | `T*` into the ring | `bytes` copy | No safe raw pointers in Python |
|
|
343
361
|
| Overwritten-in-flight data | Caller sees a volatile pointer | Copy may hold a newer record's bytes | Detect with `loss_count()` |
|
|
344
362
|
| Feature configuration | `Traits` template parameter | `traits=` argument | Python has no templates |
|
|
@@ -1,5 +1,79 @@
|
|
|
1
1
|
# Changelogs
|
|
2
2
|
|
|
3
|
+
## [v2.3.0] - 2026-10-17
|
|
4
|
+
|
|
5
|
+
Tracks C++ slick-queue v2.3.0: no queue removes its shared-memory segment on its
|
|
6
|
+
own any more, and `remove_shm()` is how the coordinating process does. The
|
|
7
|
+
version moves from 2.1.0 to 2.3.0 to stay on the C++ number. C++ v2.2.0 added
|
|
8
|
+
only `shm_name()`, which Python already had as `get_shm_name()`. The
|
|
9
|
+
shared-memory layout is unchanged, so 2.1.0 peers in either language
|
|
10
|
+
interoperate as before.
|
|
11
|
+
|
|
12
|
+
### Segment lifetime (behavior change)
|
|
13
|
+
|
|
14
|
+
- On POSIX, a segment is no longer unlinked when the process that created it
|
|
15
|
+
exits. `SharedMemory` registered every segment with `multiprocessing`'s
|
|
16
|
+
resource tracker, which unlinks what is registered when the process exits, so
|
|
17
|
+
a creator that exited first stranded every peer still attached on an orphaned
|
|
18
|
+
mapping, while its restart silently created a fresh segment under the same
|
|
19
|
+
name that those peers never saw. C++ v2.3.0 stopped its destructor doing the
|
|
20
|
+
same. A name now lives until `unlink()`, `remove_shm()` or
|
|
21
|
+
`SlickQueue.remove(name)`, and a restarted process reattaches to the existing
|
|
22
|
+
segment and keeps its contents. Windows is unaffected: a section dies with its
|
|
23
|
+
last handle.
|
|
24
|
+
- The queue sends the resource tracker nothing at all: `track=False` on Python
|
|
25
|
+
3.13+, and on older POSIX Pythons a `SharedMemory` subclass that maps the
|
|
26
|
+
segment without registering it. With no register/unregister pair there is
|
|
27
|
+
nothing to race between threads, or between `multiprocessing` children, which
|
|
28
|
+
share their parent's tracker.
|
|
29
|
+
- Migration: code that relied on the cleanup at exit now leaves segments in
|
|
30
|
+
`/dev/shm`, and its next run attaches to them - a creator joins the existing
|
|
31
|
+
segment instead of initializing it, the contents are the old run's, and a
|
|
32
|
+
creator with a different geometry raises `ValueError`. Call `SlickQueue.remove(name)` at startup for a clean start, or
|
|
33
|
+
`unlink()` / `remove_shm()` at shutdown once no peer will attach again.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `remove_shm() -> bool`, matching C++: unlinks the segment's name so the next
|
|
38
|
+
queue created with it starts fresh, leaving open mappings - this queue's own
|
|
39
|
+
included - valid. It unlinks by name on every call, and leaves `unlink()`
|
|
40
|
+
nothing to do afterwards, so a later `unlink()` cannot delete a replacement
|
|
41
|
+
created under the name. Returns `False` for a local-memory queue or when the
|
|
42
|
+
name is already gone, and always `True` for a shared queue on Windows.
|
|
43
|
+
- `SlickQueue.remove(name) -> bool`, matching C++
|
|
44
|
+
`slick::shm::shared_memory::remove()`: unlinks a segment by name without
|
|
45
|
+
attaching to it. Accepts the `/`-prefixed value of `get_shm_name()`, and is
|
|
46
|
+
always `True` on Windows.
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- `unlink()` was a silent no-op once `close()` had run, because `close()` drops
|
|
51
|
+
the `SharedMemory` object it went through. The documented `shared_memory`
|
|
52
|
+
idiom - close every handle, then unlink the name once - therefore leaked the
|
|
53
|
+
segment on POSIX. The name is now kept alongside the mapping, so `unlink()`
|
|
54
|
+
works after `close()`, and repeating it is a no-op, so it cannot delete a
|
|
55
|
+
segment a later run has created under the same name.
|
|
56
|
+
- Before Python 3.13 on POSIX, a process that only attached to a queue unlinked
|
|
57
|
+
its segment when it exited, because attaching registered the name with that
|
|
58
|
+
process's resource tracker too.
|
|
59
|
+
|
|
60
|
+
### Tests
|
|
61
|
+
|
|
62
|
+
- New `tests/test_segment_lifetime.py` with 12 tests, registered with CTest:
|
|
63
|
+
`unlink()` after `close()` and repeated, `close()` and the context manager
|
|
64
|
+
keeping the segment, `remove_shm()` on local and shared queues, `unlink()`
|
|
65
|
+
after `remove_shm()` sparing a replacement segment, `remove()` by name, a
|
|
66
|
+
creator's and an attacher's exit in a fresh interpreter, no resource-tracker
|
|
67
|
+
traffic at all, a child forked while another thread is constructing a queue,
|
|
68
|
+
and spawned children attaching at once.
|
|
69
|
+
- `test_traits.py` and `test_items_per_slot.py` open their hand-built raw
|
|
70
|
+
segments through the library's opener, so those handles and the queue no
|
|
71
|
+
longer both claim a name in the resource tracker.
|
|
72
|
+
|
|
73
|
+
Checked against C++ slick-queue v2.3.0 with the interop tests on Windows and on
|
|
74
|
+
Linux, run twice on Linux so a segment left behind would fail the second run,
|
|
75
|
+
and on Linux under Python 3.12 (the subclass) and 3.14 (`track=False`).
|
|
76
|
+
|
|
3
77
|
## [v2.1.0] - 2026-09-24
|
|
4
78
|
|
|
5
79
|
Tracks C++ slick-queue v2.1.0. Adds `items_per_slot`, so one control slot can
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: slick-queue-py
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Lock-free MPMC queue with C++ interoperability via shared memory
|
|
5
5
|
Home-page: https://github.com/SlickQuant/slick-queue-py
|
|
6
6
|
Author: Slick Quant
|
|
@@ -600,11 +600,47 @@ Get memoryview for writing to reserved slot.
|
|
|
600
600
|
|
|
601
601
|
#### `close()`
|
|
602
602
|
|
|
603
|
-
Close the shared memory connection.
|
|
603
|
+
Close the shared memory connection. Never deletes the segment.
|
|
604
604
|
|
|
605
605
|
#### `unlink()`
|
|
606
606
|
|
|
607
|
-
Delete the shared memory segment
|
|
607
|
+
Delete the shared memory segment this queue is attached to. Valid before or after `close()`
|
|
608
|
+
(close every handle, then unlink once), and a no-op when repeated, so it cannot delete a segment
|
|
609
|
+
a later run has since created under the same name. A no-op for local memory mode.
|
|
610
|
+
|
|
611
|
+
#### `remove_shm() -> bool`
|
|
612
|
+
|
|
613
|
+
Unlink the segment's name so the next queue created with it starts fresh, matching C++
|
|
614
|
+
`remove_shm()`. Mappings already open, including this queue's own, stay valid. Unlike `unlink()`
|
|
615
|
+
it unlinks by name on every call. Returns `False` for a local-memory queue or if the name is
|
|
616
|
+
already gone, and always `True` for a shared queue on Windows.
|
|
617
|
+
|
|
618
|
+
#### `SlickQueue.remove(name) -> bool`
|
|
619
|
+
|
|
620
|
+
Unlink a segment by name without attaching to it, matching C++
|
|
621
|
+
`slick::shm::shared_memory::remove()`. Typically called at startup to clear a segment a previous
|
|
622
|
+
run left behind. Accepts the `/`-prefixed value of `get_shm_name()`. Always `True` on Windows.
|
|
623
|
+
|
|
624
|
+
#### `get_shm_name() -> Optional[str]`
|
|
625
|
+
|
|
626
|
+
The segment name to pass to C++ (`/`-prefixed on POSIX), or `None` for a local-memory queue.
|
|
627
|
+
|
|
628
|
+
#### Segment lifetime
|
|
629
|
+
|
|
630
|
+
No queue deletes its segment on its own: not `close()`, not the context manager, and not process
|
|
631
|
+
exit, matching C++ slick-queue v2.3.0. On POSIX a name outlives every mapping until it is
|
|
632
|
+
unlinked, so a departing process that unlinked it while a peer was still attached would leave that
|
|
633
|
+
peer reading an orphaned mapping while a restarted process silently created a fresh segment under
|
|
634
|
+
the same name. The process that coordinates the queue's lifetime calls `unlink()`, `remove_shm()`
|
|
635
|
+
or `SlickQueue.remove(name)` once no peer will attach again; until then a restarted process
|
|
636
|
+
reattaches to the existing segment and keeps its contents. On Windows a segment is freed when its
|
|
637
|
+
last handle closes.
|
|
638
|
+
|
|
639
|
+
The queue keeps its segments out of `multiprocessing`'s resource tracker, which would otherwise
|
|
640
|
+
unlink them when the process exits - before Python 3.13, even in a process that only attached. It
|
|
641
|
+
sends the tracker nothing at all: `track=False` on Python 3.13+, and on older POSIX Pythons a
|
|
642
|
+
`SharedMemory` subclass that maps the segment without registering it, so there is no
|
|
643
|
+
register/unregister pair to race between threads or between processes sharing one tracker.
|
|
608
644
|
|
|
609
645
|
### AtomicCursor
|
|
610
646
|
|
|
@@ -920,6 +956,9 @@ python tests/test_reset_detection.py
|
|
|
920
956
|
|
|
921
957
|
# Wrapping-producer record/size invariants
|
|
922
958
|
python tests/test_wrap_invariants.py
|
|
959
|
+
|
|
960
|
+
# Segment lifetime: unlink(), remove_shm(), remove(), process exit
|
|
961
|
+
python tests/test_segment_lifetime.py
|
|
923
962
|
```
|
|
924
963
|
|
|
925
964
|
Or run everything with pytest:
|
|
@@ -963,7 +1002,7 @@ The interop tests verify:
|
|
|
963
1002
|
|
|
964
1003
|
1. **Buffer Cleanup Warning**: You may see a `BufferError: cannot close exported pointers exist` warning during garbage collection. This is a **harmless warning** caused by Python's ctypes creating internal buffer references that persist beyond explicit cleanup. It occurs during program exit and **does not affect functionality, performance, or correctness**. The queue works perfectly despite this warning.
|
|
965
1004
|
|
|
966
|
-
2. **UserWarning**: On Linux you may see `UserWarning: resource_tracker: There appear to be 4 leaked shared_memory objects to clean up at shutdown`.
|
|
1005
|
+
2. **UserWarning**: On Linux you may see `UserWarning: resource_tracker: There appear to be 4 leaked shared_memory objects to clean up at shutdown`. `SlickQueue` keeps its own segments out of the resource tracker (see [Segment lifetime](#segment-lifetime)), so the warning comes from a `multiprocessing.shared_memory.SharedMemory` your code opened directly, such as one holding an `AtomicCursor`. The tracker unlinks those segments at exit; pass `track=False` (Python 3.13+) to keep them.
|
|
967
1006
|
|
|
968
1007
|
## Architecture
|
|
969
1008
|
|
|
@@ -564,11 +564,47 @@ Get memoryview for writing to reserved slot.
|
|
|
564
564
|
|
|
565
565
|
#### `close()`
|
|
566
566
|
|
|
567
|
-
Close the shared memory connection.
|
|
567
|
+
Close the shared memory connection. Never deletes the segment.
|
|
568
568
|
|
|
569
569
|
#### `unlink()`
|
|
570
570
|
|
|
571
|
-
Delete the shared memory segment
|
|
571
|
+
Delete the shared memory segment this queue is attached to. Valid before or after `close()`
|
|
572
|
+
(close every handle, then unlink once), and a no-op when repeated, so it cannot delete a segment
|
|
573
|
+
a later run has since created under the same name. A no-op for local memory mode.
|
|
574
|
+
|
|
575
|
+
#### `remove_shm() -> bool`
|
|
576
|
+
|
|
577
|
+
Unlink the segment's name so the next queue created with it starts fresh, matching C++
|
|
578
|
+
`remove_shm()`. Mappings already open, including this queue's own, stay valid. Unlike `unlink()`
|
|
579
|
+
it unlinks by name on every call. Returns `False` for a local-memory queue or if the name is
|
|
580
|
+
already gone, and always `True` for a shared queue on Windows.
|
|
581
|
+
|
|
582
|
+
#### `SlickQueue.remove(name) -> bool`
|
|
583
|
+
|
|
584
|
+
Unlink a segment by name without attaching to it, matching C++
|
|
585
|
+
`slick::shm::shared_memory::remove()`. Typically called at startup to clear a segment a previous
|
|
586
|
+
run left behind. Accepts the `/`-prefixed value of `get_shm_name()`. Always `True` on Windows.
|
|
587
|
+
|
|
588
|
+
#### `get_shm_name() -> Optional[str]`
|
|
589
|
+
|
|
590
|
+
The segment name to pass to C++ (`/`-prefixed on POSIX), or `None` for a local-memory queue.
|
|
591
|
+
|
|
592
|
+
#### Segment lifetime
|
|
593
|
+
|
|
594
|
+
No queue deletes its segment on its own: not `close()`, not the context manager, and not process
|
|
595
|
+
exit, matching C++ slick-queue v2.3.0. On POSIX a name outlives every mapping until it is
|
|
596
|
+
unlinked, so a departing process that unlinked it while a peer was still attached would leave that
|
|
597
|
+
peer reading an orphaned mapping while a restarted process silently created a fresh segment under
|
|
598
|
+
the same name. The process that coordinates the queue's lifetime calls `unlink()`, `remove_shm()`
|
|
599
|
+
or `SlickQueue.remove(name)` once no peer will attach again; until then a restarted process
|
|
600
|
+
reattaches to the existing segment and keeps its contents. On Windows a segment is freed when its
|
|
601
|
+
last handle closes.
|
|
602
|
+
|
|
603
|
+
The queue keeps its segments out of `multiprocessing`'s resource tracker, which would otherwise
|
|
604
|
+
unlink them when the process exits - before Python 3.13, even in a process that only attached. It
|
|
605
|
+
sends the tracker nothing at all: `track=False` on Python 3.13+, and on older POSIX Pythons a
|
|
606
|
+
`SharedMemory` subclass that maps the segment without registering it, so there is no
|
|
607
|
+
register/unregister pair to race between threads or between processes sharing one tracker.
|
|
572
608
|
|
|
573
609
|
### AtomicCursor
|
|
574
610
|
|
|
@@ -884,6 +920,9 @@ python tests/test_reset_detection.py
|
|
|
884
920
|
|
|
885
921
|
# Wrapping-producer record/size invariants
|
|
886
922
|
python tests/test_wrap_invariants.py
|
|
923
|
+
|
|
924
|
+
# Segment lifetime: unlink(), remove_shm(), remove(), process exit
|
|
925
|
+
python tests/test_segment_lifetime.py
|
|
887
926
|
```
|
|
888
927
|
|
|
889
928
|
Or run everything with pytest:
|
|
@@ -927,7 +966,7 @@ The interop tests verify:
|
|
|
927
966
|
|
|
928
967
|
1. **Buffer Cleanup Warning**: You may see a `BufferError: cannot close exported pointers exist` warning during garbage collection. This is a **harmless warning** caused by Python's ctypes creating internal buffer references that persist beyond explicit cleanup. It occurs during program exit and **does not affect functionality, performance, or correctness**. The queue works perfectly despite this warning.
|
|
929
968
|
|
|
930
|
-
2. **UserWarning**: On Linux you may see `UserWarning: resource_tracker: There appear to be 4 leaked shared_memory objects to clean up at shutdown`.
|
|
969
|
+
2. **UserWarning**: On Linux you may see `UserWarning: resource_tracker: There appear to be 4 leaked shared_memory objects to clean up at shutdown`. `SlickQueue` keeps its own segments out of the resource tracker (see [Segment lifetime](#segment-lifetime)), so the warning comes from a `multiprocessing.shared_memory.SharedMemory` your code opened directly, such as one holding an `AtomicCursor`. The tracker unlinks those segments at exit; pass `track=False` (Python 3.13+) to keep them.
|
|
931
970
|
|
|
932
971
|
## Architecture
|
|
933
972
|
|
|
@@ -29,8 +29,11 @@ Supported on Python 3.8+ (uses multiprocessing.shared_memory).
|
|
|
29
29
|
"""
|
|
30
30
|
from __future__ import annotations
|
|
31
31
|
|
|
32
|
-
__version__ = '2.
|
|
32
|
+
__version__ = '2.3.0'
|
|
33
33
|
|
|
34
|
+
import inspect
|
|
35
|
+
import mmap
|
|
36
|
+
import os
|
|
34
37
|
import struct
|
|
35
38
|
import sys
|
|
36
39
|
import time
|
|
@@ -40,6 +43,81 @@ from atomic_ops import AtomicReservedInfo, AtomicUInt64, AtomicCursor, check_pla
|
|
|
40
43
|
# Use Python's built-in shared memory (available in Python 3.8+)
|
|
41
44
|
from multiprocessing.shared_memory import SharedMemory
|
|
42
45
|
|
|
46
|
+
# Python >= 3.13 supports SharedMemory(track=False), which keeps a segment out of
|
|
47
|
+
# multiprocessing's resource tracker.
|
|
48
|
+
_SHM_SUPPORTS_TRACK = 'track' in inspect.signature(SharedMemory.__init__).parameters
|
|
49
|
+
|
|
50
|
+
if not _SHM_SUPPORTS_TRACK and sys.platform != 'win32':
|
|
51
|
+
import _posixshmem
|
|
52
|
+
|
|
53
|
+
class _UntrackedSharedMemory(SharedMemory):
|
|
54
|
+
"""``SharedMemory(track=False)`` for POSIX Python < 3.13, where that
|
|
55
|
+
argument does not exist.
|
|
56
|
+
|
|
57
|
+
The base class touches the resource tracker in exactly two places, and
|
|
58
|
+
only those are replaced: ``__init__``, which registers the name, and
|
|
59
|
+
``unlink()``, which unregisters it. This ``__init__`` sets the same
|
|
60
|
+
attributes the base one does, which ``close()``, ``buf``, ``name``,
|
|
61
|
+
``size``, ``__del__`` and ``__reduce__`` rely on.
|
|
62
|
+
|
|
63
|
+
Sending the tracker nothing at all is what makes this safe. Registering
|
|
64
|
+
and then retracting at once needed a lock against other threads, which a
|
|
65
|
+
fork could leave held in the child, and still raced against other
|
|
66
|
+
processes sharing the tracker - multiprocessing children share their
|
|
67
|
+
parent's - which answers an UNREGISTER for a name it no longer holds by
|
|
68
|
+
printing a KeyError traceback.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, name: str, create: bool = False, size: int = 0):
|
|
72
|
+
flags = os.O_RDWR
|
|
73
|
+
if create:
|
|
74
|
+
if size <= 0:
|
|
75
|
+
raise ValueError("'size' must be a positive number different from zero")
|
|
76
|
+
flags |= os.O_CREAT | os.O_EXCL
|
|
77
|
+
self._name = '/' + name
|
|
78
|
+
self._fd = _posixshmem.shm_open(self._name, flags, mode=self._mode)
|
|
79
|
+
try:
|
|
80
|
+
if create:
|
|
81
|
+
os.ftruncate(self._fd, size)
|
|
82
|
+
size = os.fstat(self._fd).st_size
|
|
83
|
+
self._mmap = mmap.mmap(self._fd, size)
|
|
84
|
+
except OSError:
|
|
85
|
+
os.close(self._fd)
|
|
86
|
+
self._fd = -1
|
|
87
|
+
if create: # only a segment this call made is ours to delete
|
|
88
|
+
try:
|
|
89
|
+
_posixshmem.shm_unlink(self._name)
|
|
90
|
+
except OSError:
|
|
91
|
+
pass
|
|
92
|
+
raise
|
|
93
|
+
self._size = size
|
|
94
|
+
self._buf = memoryview(self._mmap)
|
|
95
|
+
|
|
96
|
+
def unlink(self) -> None:
|
|
97
|
+
if self._name:
|
|
98
|
+
_posixshmem.shm_unlink(self._name)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _open_shm(name: str, create: bool, size: int = 0) -> SharedMemory:
|
|
102
|
+
"""Create or attach to a segment without registering it with
|
|
103
|
+
multiprocessing's resource tracker.
|
|
104
|
+
|
|
105
|
+
A registered name is unlinked by the tracker when the process exits, which on
|
|
106
|
+
POSIX is the lifetime C++ slick-queue v2.3.0 dropped: a peer still attached
|
|
107
|
+
keeps reading the orphaned mapping while a restarted process silently
|
|
108
|
+
creates a fresh segment under the same name, so the two never meet again.
|
|
109
|
+
Python before 3.13 registers on attach as well as on create, so any peer's
|
|
110
|
+
exit removed the segment. The name now outlives every process until
|
|
111
|
+
remove_shm(), unlink() or remove() is called. Windows has no tracker; a
|
|
112
|
+
section there dies with its last handle.
|
|
113
|
+
"""
|
|
114
|
+
if _SHM_SUPPORTS_TRACK:
|
|
115
|
+
return SharedMemory(name=name, create=create, size=size, track=False)
|
|
116
|
+
if sys.platform == 'win32':
|
|
117
|
+
return SharedMemory(name=name, create=create, size=size)
|
|
118
|
+
return _UntrackedSharedMemory(name, create, size)
|
|
119
|
+
|
|
120
|
+
|
|
43
121
|
# Layout constants
|
|
44
122
|
# Shared memory header layout (64 bytes total):
|
|
45
123
|
# Offset 0-7: std::atomic<reserved_info> (8 bytes)
|
|
@@ -318,6 +396,9 @@ class SlickQueue:
|
|
|
318
396
|
|
|
319
397
|
self.use_shm = name is not None
|
|
320
398
|
self._shm: Optional[SharedMemory] = None
|
|
399
|
+
# Tracker-form name of the mapping. Outlives close(), which drops _shm, so
|
|
400
|
+
# unlink() still knows what to unlink - see unlink().
|
|
401
|
+
self._shm_name: Optional[str] = None
|
|
321
402
|
self._local_buf: Optional[bytearray] = None
|
|
322
403
|
self.size = None
|
|
323
404
|
self._own = False
|
|
@@ -344,11 +425,10 @@ class SlickQueue:
|
|
|
344
425
|
raise ValueError("size and element_size required when creating")
|
|
345
426
|
total = self._layout_data_offset() + self.element_size * self.size
|
|
346
427
|
try:
|
|
347
|
-
self._shm =
|
|
348
|
-
# print(f"**** create new shm {self.name}")
|
|
428
|
+
self._shm = _open_shm(self.name, create=True, size=total)
|
|
349
429
|
except FileExistsError:
|
|
350
|
-
|
|
351
|
-
|
|
430
|
+
self._shm = _open_shm(self.name, create=False)
|
|
431
|
+
self._shm_name = self._shm._name
|
|
352
432
|
|
|
353
433
|
# Use CAS on init_state to determine ownership (matches C++ queue.h:618-648)
|
|
354
434
|
buf = self._shm.buf
|
|
@@ -428,7 +508,8 @@ class SlickQueue:
|
|
|
428
508
|
raise ValueError("element_size must be provided when opening existing shared memory")
|
|
429
509
|
|
|
430
510
|
# Open existing shared memory (size parameter not needed/ignored)
|
|
431
|
-
self._shm =
|
|
511
|
+
self._shm = _open_shm(self.name, create=False)
|
|
512
|
+
self._shm_name = self._shm._name
|
|
432
513
|
buf = self._shm.buf
|
|
433
514
|
|
|
434
515
|
# Wait for initialization to complete (matches C++ queue.h:558-562)
|
|
@@ -1199,18 +1280,88 @@ class SlickQueue:
|
|
|
1199
1280
|
pass
|
|
1200
1281
|
|
|
1201
1282
|
def unlink(self) -> None:
|
|
1202
|
-
"""Unlink (delete) the shared memory segment.
|
|
1283
|
+
"""Unlink (delete) the shared memory segment this queue is attached to.
|
|
1284
|
+
|
|
1285
|
+
Only applicable for shared memory mode; a no-op for local memory mode. No
|
|
1286
|
+
queue ever does this on its own - not close(), not the context manager and
|
|
1287
|
+
not process exit - so call it from the process that coordinates the queue's
|
|
1288
|
+
lifetime, once no peer will attach again.
|
|
1289
|
+
|
|
1290
|
+
Valid after :meth:`close`, which is the documented ``shared_memory`` idiom -
|
|
1291
|
+
close every handle, then unlink the name once. Deleting a name never touches
|
|
1292
|
+
mappings that already exist, open or closed. Unlinking twice is a no-op, so a
|
|
1293
|
+
repeat cannot delete a segment a later run has since created under the same
|
|
1294
|
+
name; :meth:`remove_shm` is the C++-style call that unlinks on every call.
|
|
1295
|
+
"""
|
|
1296
|
+
prefixed = self._shm_name
|
|
1297
|
+
if prefixed is None:
|
|
1298
|
+
return
|
|
1299
|
+
self._shm_name = None # unlink once, however many times this is called
|
|
1300
|
+
SlickQueue.remove(prefixed)
|
|
1301
|
+
|
|
1302
|
+
def remove_shm(self) -> bool:
|
|
1303
|
+
"""Unlink this queue's segment name so the next queue created with it starts
|
|
1304
|
+
fresh. Matches C++ ``remove_shm()``.
|
|
1305
|
+
|
|
1306
|
+
Mappings that are already open, including this queue's own, stay valid until
|
|
1307
|
+
they are closed. Unlike :meth:`unlink` it unlinks by name on every call, so
|
|
1308
|
+
it deletes whatever holds the name at the time.
|
|
1309
|
+
|
|
1310
|
+
After this call :meth:`unlink` is a no-op, whatever the result: a name
|
|
1311
|
+
removed here, or one already gone, can be taken by a replacement segment,
|
|
1312
|
+
which the pending unlink() would otherwise delete.
|
|
1203
1313
|
|
|
1204
|
-
|
|
1314
|
+
Returns:
|
|
1315
|
+
True if the name was removed; False for a local-memory queue or if
|
|
1316
|
+
removal failed (on POSIX, typically because the name is already gone).
|
|
1317
|
+
Always True for a shared queue on Windows, where the segment is freed
|
|
1318
|
+
when its last handle closes and there is no name to remove.
|
|
1205
1319
|
"""
|
|
1206
1320
|
if not self.use_shm:
|
|
1207
|
-
return
|
|
1321
|
+
return False
|
|
1322
|
+
self._shm_name = None # unlink() is done with this name - see above
|
|
1323
|
+
return SlickQueue.remove(self.name)
|
|
1324
|
+
|
|
1325
|
+
@staticmethod
|
|
1326
|
+
def remove(name: str) -> bool:
|
|
1327
|
+
"""Unlink a shared memory segment by name without attaching to it. Matches
|
|
1328
|
+
C++ ``slick::shm::shared_memory::remove()``.
|
|
1329
|
+
|
|
1330
|
+
Typically called once at startup to clear a segment a previous run left
|
|
1331
|
+
behind, which a creator would otherwise reattach to with its old contents::
|
|
1332
|
+
|
|
1333
|
+
SlickQueue.remove("my_queue")
|
|
1334
|
+
q = SlickQueue(name="my_queue", size=1024, element_size=64)
|
|
1208
1335
|
|
|
1336
|
+
A leading ``/`` is accepted, so the value of :meth:`get_shm_name` can be
|
|
1337
|
+
passed back.
|
|
1338
|
+
|
|
1339
|
+
Returns:
|
|
1340
|
+
True if the segment was removed (always True on Windows).
|
|
1341
|
+
|
|
1342
|
+
Warning:
|
|
1343
|
+
Only call this when no process will attach again. On POSIX the name is
|
|
1344
|
+
unlinked immediately, so the next creator gets a fresh segment while any
|
|
1345
|
+
process still mapped to the old one keeps reading the orphaned copy. On
|
|
1346
|
+
Windows this is a no-op - a section disappears once its last handle
|
|
1347
|
+
closes.
|
|
1348
|
+
"""
|
|
1349
|
+
if name.startswith('/'):
|
|
1350
|
+
name = name[1:]
|
|
1351
|
+
if sys.platform == 'win32':
|
|
1352
|
+
return True
|
|
1353
|
+
# Straight to shm_unlink rather than SharedMemory.unlink(), which before
|
|
1354
|
+
# Python 3.13 also retracts a tracker registration this module never keeps -
|
|
1355
|
+
# see _open_shm().
|
|
1209
1356
|
try:
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1357
|
+
from _posixshmem import shm_unlink
|
|
1358
|
+
except ImportError: # pragma: no cover - POSIX always has it
|
|
1359
|
+
return False
|
|
1360
|
+
try:
|
|
1361
|
+
shm_unlink('/' + name)
|
|
1362
|
+
except OSError: # not there, or not ours to unlink
|
|
1363
|
+
return False
|
|
1364
|
+
return True
|
|
1214
1365
|
|
|
1215
1366
|
def __enter__(self):
|
|
1216
1367
|
"""Context manager entry."""
|
|
@@ -11,8 +11,6 @@ from pathlib import Path
|
|
|
11
11
|
|
|
12
12
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
13
13
|
|
|
14
|
-
from multiprocessing.shared_memory import SharedMemory
|
|
15
|
-
|
|
16
14
|
from atomic_ops import AtomicCursor
|
|
17
15
|
from slick_queue_py import (
|
|
18
16
|
SlickQueue,
|
|
@@ -21,6 +19,7 @@ from slick_queue_py import (
|
|
|
21
19
|
SLOT_SIZE,
|
|
22
20
|
HEADER_MAGIC_OFFSET,
|
|
23
21
|
ITEMS_PER_SLOT_OFFSET,
|
|
22
|
+
_open_shm,
|
|
24
23
|
)
|
|
25
24
|
|
|
26
25
|
SLQ1 = 0x534C5131
|
|
@@ -37,7 +36,7 @@ def _marker(q):
|
|
|
37
36
|
|
|
38
37
|
|
|
39
38
|
def _set_raw_marker(q, marker):
|
|
40
|
-
raw =
|
|
39
|
+
raw = _open_shm(q._shm.name, create=False)
|
|
41
40
|
try:
|
|
42
41
|
struct.pack_into("<I", raw.buf, HEADER_MAGIC_OFFSET, marker)
|
|
43
42
|
finally:
|
|
@@ -364,7 +363,7 @@ def test_zero_in_header_reads_as_one():
|
|
|
364
363
|
name = _unique("slq_ips_legacy")
|
|
365
364
|
creator = SlickQueue(name=name, size=8, element_size=8)
|
|
366
365
|
try:
|
|
367
|
-
raw =
|
|
366
|
+
raw = _open_shm(creator._shm.name, create=False)
|
|
368
367
|
try:
|
|
369
368
|
struct.pack_into("<I", raw.buf, ITEMS_PER_SLOT_OFFSET, 0)
|
|
370
369
|
finally:
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for shared-memory segment lifetime (matching C++ slick-queue v2.3.0).
|
|
3
|
+
|
|
4
|
+
No queue unlinks its segment on its own - not close(), not the context manager and
|
|
5
|
+
not process exit through multiprocessing's resource tracker. The name lives until
|
|
6
|
+
unlink(), remove_shm() or SlickQueue.remove() is called.
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import signal
|
|
10
|
+
import struct
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
import warnings
|
|
17
|
+
from multiprocessing import resource_tracker
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
ROOT = Path(__file__).parent.parent
|
|
21
|
+
sys.path.insert(0, str(ROOT))
|
|
22
|
+
|
|
23
|
+
from slick_queue_py import SlickQueue
|
|
24
|
+
|
|
25
|
+
IS_WINDOWS = sys.platform == 'win32'
|
|
26
|
+
|
|
27
|
+
# A fresh interpreter, so it has its own resource tracker - a multiprocessing child
|
|
28
|
+
# would share the parent's, and the parent's exit would be what unlinked the name.
|
|
29
|
+
CHILD = r"""
|
|
30
|
+
import struct, sys
|
|
31
|
+
sys.path.insert(0, sys.argv[1])
|
|
32
|
+
from slick_queue_py import SlickQueue
|
|
33
|
+
name, mode, value = sys.argv[2], sys.argv[3], int(sys.argv[4])
|
|
34
|
+
if mode == 'create':
|
|
35
|
+
q = SlickQueue(name=name, size=16, element_size=8)
|
|
36
|
+
assert q._own, "child expected to create the segment"
|
|
37
|
+
else:
|
|
38
|
+
q = SlickQueue(name=name, element_size=8)
|
|
39
|
+
idx = q.reserve()
|
|
40
|
+
q[idx][:8] = struct.pack('<Q', value)
|
|
41
|
+
q.publish(idx)
|
|
42
|
+
q.close()
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _unique(prefix):
|
|
47
|
+
return f"{prefix}_{os.getpid()}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _run_child(name, mode, value):
|
|
51
|
+
result = subprocess.run(
|
|
52
|
+
[sys.executable, "-c", CHILD, str(ROOT), name, mode, str(value)],
|
|
53
|
+
capture_output=True, text=True, timeout=60)
|
|
54
|
+
assert result.returncode == 0, f"child failed:\n{result.stdout}\n{result.stderr}"
|
|
55
|
+
# The tracker warns about every registered name it cleans up at shutdown
|
|
56
|
+
assert "resource_tracker" not in result.stderr, result.stderr
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _values(q):
|
|
61
|
+
"""Every value published so far, from the start of the queue."""
|
|
62
|
+
out = []
|
|
63
|
+
cursor = 0
|
|
64
|
+
while True:
|
|
65
|
+
data, size, cursor = q.read(cursor)
|
|
66
|
+
if data is None:
|
|
67
|
+
return out
|
|
68
|
+
out.append(struct.unpack('<Q', data[:8])[0])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _expect_gone(name):
|
|
72
|
+
try:
|
|
73
|
+
q = SlickQueue(name=name, element_size=8)
|
|
74
|
+
except FileNotFoundError:
|
|
75
|
+
return
|
|
76
|
+
q.close()
|
|
77
|
+
raise AssertionError(f"segment {name!r} still exists")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_unlink_after_close_removes_segment():
|
|
81
|
+
"""unlink() after close() was a silent no-op: close() dropped the SharedMemory
|
|
82
|
+
object unlink() went through, so the documented idiom leaked the segment."""
|
|
83
|
+
name = _unique("lt_unlink_after_close")
|
|
84
|
+
SlickQueue.remove(name)
|
|
85
|
+
q = SlickQueue(name=name, size=16, element_size=8)
|
|
86
|
+
q.close()
|
|
87
|
+
q.unlink()
|
|
88
|
+
_expect_gone(name)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_unlink_twice_is_noop():
|
|
92
|
+
"""A repeated unlink() must not delete a segment a later run created under the
|
|
93
|
+
same name."""
|
|
94
|
+
name = _unique("lt_unlink_twice")
|
|
95
|
+
SlickQueue.remove(name)
|
|
96
|
+
first = SlickQueue(name=name, size=16, element_size=8)
|
|
97
|
+
first.close()
|
|
98
|
+
first.unlink()
|
|
99
|
+
|
|
100
|
+
second = SlickQueue(name=name, size=16, element_size=8)
|
|
101
|
+
try:
|
|
102
|
+
assert second._own
|
|
103
|
+
first.unlink()
|
|
104
|
+
reopened = SlickQueue(name=name, element_size=8) # still there
|
|
105
|
+
reopened.close()
|
|
106
|
+
finally:
|
|
107
|
+
second.close()
|
|
108
|
+
second.unlink()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_close_and_context_manager_keep_segment():
|
|
112
|
+
name = _unique("lt_close_keeps")
|
|
113
|
+
SlickQueue.remove(name)
|
|
114
|
+
keeper = SlickQueue(name=name, size=16, element_size=8) # holds it on Windows
|
|
115
|
+
try:
|
|
116
|
+
with SlickQueue(name=name, element_size=8) as q:
|
|
117
|
+
idx = q.reserve()
|
|
118
|
+
q[idx][:8] = struct.pack('<Q', 7)
|
|
119
|
+
q.publish(idx)
|
|
120
|
+
q.close()
|
|
121
|
+
reopened = SlickQueue(name=name, element_size=8)
|
|
122
|
+
try:
|
|
123
|
+
assert _values(reopened) == [7]
|
|
124
|
+
finally:
|
|
125
|
+
reopened.close()
|
|
126
|
+
finally:
|
|
127
|
+
keeper.close()
|
|
128
|
+
keeper.unlink()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_remove_shm_local_queue_returns_false():
|
|
132
|
+
q = SlickQueue(size=16, element_size=8)
|
|
133
|
+
try:
|
|
134
|
+
assert q.remove_shm() is False
|
|
135
|
+
finally:
|
|
136
|
+
q.close()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_remove_shm_keeps_open_mapping_valid():
|
|
140
|
+
name = _unique("lt_remove_shm")
|
|
141
|
+
SlickQueue.remove(name)
|
|
142
|
+
q = SlickQueue(name=name, size=16, element_size=8)
|
|
143
|
+
try:
|
|
144
|
+
idx = q.reserve()
|
|
145
|
+
q[idx][:8] = struct.pack('<Q', 42)
|
|
146
|
+
q.publish(idx)
|
|
147
|
+
|
|
148
|
+
assert q.remove_shm() is True
|
|
149
|
+
# The mapping outlives its name
|
|
150
|
+
assert _values(q) == [42]
|
|
151
|
+
idx = q.reserve()
|
|
152
|
+
q[idx][:8] = struct.pack('<Q', 43)
|
|
153
|
+
q.publish(idx)
|
|
154
|
+
assert _values(q) == [42, 43]
|
|
155
|
+
|
|
156
|
+
if not IS_WINDOWS:
|
|
157
|
+
# The next creator starts fresh rather than reattaching
|
|
158
|
+
assert q.remove_shm() is False # already gone
|
|
159
|
+
fresh = SlickQueue(name=name, size=16, element_size=8)
|
|
160
|
+
try:
|
|
161
|
+
assert fresh._own
|
|
162
|
+
assert _values(fresh) == []
|
|
163
|
+
finally:
|
|
164
|
+
fresh.close()
|
|
165
|
+
fresh.unlink()
|
|
166
|
+
finally:
|
|
167
|
+
q.close()
|
|
168
|
+
q.unlink()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_remove_by_name():
|
|
172
|
+
name = _unique("lt_remove_static")
|
|
173
|
+
SlickQueue.remove(name)
|
|
174
|
+
q = SlickQueue(name=name, size=16, element_size=8)
|
|
175
|
+
shm_name = q.get_shm_name()
|
|
176
|
+
q.close()
|
|
177
|
+
# get_shm_name()'s value, '/'-prefixed on POSIX, is accepted back
|
|
178
|
+
assert SlickQueue.remove(shm_name) is True
|
|
179
|
+
_expect_gone(name)
|
|
180
|
+
# Windows has no name to remove, so it always reports success
|
|
181
|
+
assert SlickQueue.remove(name) is IS_WINDOWS
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_creator_exit_keeps_segment():
|
|
185
|
+
"""A creator's exit used to unlink the name on POSIX through the resource
|
|
186
|
+
tracker, stranding any peer on an orphaned mapping. On Windows a section dies
|
|
187
|
+
with its last handle, which no one else holds here."""
|
|
188
|
+
if IS_WINDOWS:
|
|
189
|
+
return
|
|
190
|
+
name = _unique("lt_creator_exit")
|
|
191
|
+
SlickQueue.remove(name)
|
|
192
|
+
_run_child(name, 'create', 11)
|
|
193
|
+
q = SlickQueue(name=name, element_size=8)
|
|
194
|
+
try:
|
|
195
|
+
assert _values(q) == [11]
|
|
196
|
+
# A restarted creator reattaches and keeps the contents
|
|
197
|
+
again = SlickQueue(name=name, size=16, element_size=8)
|
|
198
|
+
try:
|
|
199
|
+
assert not again._own
|
|
200
|
+
finally:
|
|
201
|
+
again.close()
|
|
202
|
+
finally:
|
|
203
|
+
q.close()
|
|
204
|
+
q.unlink()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_attacher_exit_keeps_segment():
|
|
208
|
+
"""Before Python 3.13 attaching registered the name with the attacher's resource
|
|
209
|
+
tracker too, so any peer's exit unlinked the segment on POSIX."""
|
|
210
|
+
name = _unique("lt_attacher_exit")
|
|
211
|
+
SlickQueue.remove(name)
|
|
212
|
+
q = SlickQueue(name=name, size=16, element_size=8)
|
|
213
|
+
try:
|
|
214
|
+
_run_child(name, 'open', 21)
|
|
215
|
+
_run_child(name, 'open', 22)
|
|
216
|
+
reopened = SlickQueue(name=name, element_size=8)
|
|
217
|
+
try:
|
|
218
|
+
assert _values(reopened) == [21, 22]
|
|
219
|
+
finally:
|
|
220
|
+
reopened.close()
|
|
221
|
+
finally:
|
|
222
|
+
q.close()
|
|
223
|
+
q.unlink()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_unlink_after_remove_shm_spares_replacement():
|
|
227
|
+
"""Regression: remove_shm() left unlink()'s pending name in place, so once the
|
|
228
|
+
old segment was removed and a replacement created under the same name, the old
|
|
229
|
+
queue's unlink() deleted the replacement. Also when remove_shm() finds the name
|
|
230
|
+
already gone, since a replacement can take it just the same."""
|
|
231
|
+
name = _unique("lt_rm_unlink")
|
|
232
|
+
for gone_first in (False, True):
|
|
233
|
+
SlickQueue.remove(name)
|
|
234
|
+
old = SlickQueue(name=name, size=16, element_size=8)
|
|
235
|
+
try:
|
|
236
|
+
if gone_first:
|
|
237
|
+
SlickQueue.remove(name) # someone else got there first
|
|
238
|
+
assert old.remove_shm() is IS_WINDOWS
|
|
239
|
+
else:
|
|
240
|
+
assert old.remove_shm() is True
|
|
241
|
+
replacement = SlickQueue(name=name, size=16, element_size=8)
|
|
242
|
+
try:
|
|
243
|
+
if not IS_WINDOWS:
|
|
244
|
+
assert replacement._own
|
|
245
|
+
old.unlink()
|
|
246
|
+
SlickQueue(name=name, element_size=8).close() # still there
|
|
247
|
+
finally:
|
|
248
|
+
replacement.close()
|
|
249
|
+
replacement.unlink()
|
|
250
|
+
finally:
|
|
251
|
+
old.close()
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_never_talks_to_resource_tracker():
|
|
255
|
+
"""No REGISTER or UNREGISTER for a queue's segment reaches multiprocessing's
|
|
256
|
+
resource tracker - not on create, attach, close or unlink. Before Python 3.13
|
|
257
|
+
the library registered each name and then retracted it at once, which raced
|
|
258
|
+
between processes sharing one tracker and needed a lock that a fork could
|
|
259
|
+
leave held (see the two tests below)."""
|
|
260
|
+
name = _unique("lt_no_tracker")
|
|
261
|
+
SlickQueue.remove(name)
|
|
262
|
+
calls = []
|
|
263
|
+
orig_register, orig_unregister = resource_tracker.register, resource_tracker.unregister
|
|
264
|
+
resource_tracker.register = lambda n, rtype: calls.append(("register", n))
|
|
265
|
+
resource_tracker.unregister = lambda n, rtype: calls.append(("unregister", n))
|
|
266
|
+
try:
|
|
267
|
+
creator = SlickQueue(name=name, size=16, element_size=8)
|
|
268
|
+
opener = SlickQueue(name=name, element_size=8)
|
|
269
|
+
second = SlickQueue(name=name, size=16, element_size=8) # attaches
|
|
270
|
+
for q in (second, opener, creator):
|
|
271
|
+
q.close()
|
|
272
|
+
creator.unlink()
|
|
273
|
+
finally:
|
|
274
|
+
resource_tracker.register, resource_tracker.unregister = orig_register, orig_unregister
|
|
275
|
+
SlickQueue.remove(name)
|
|
276
|
+
assert calls == [], calls
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_fork_during_construction_does_not_deadlock():
|
|
280
|
+
"""A child forked while another thread is constructing a queue can still
|
|
281
|
+
construct one. Python < 3.13 held a module lock around SharedMemory() for the
|
|
282
|
+
tracker fallback, and a fork inside that window handed the child a copy of
|
|
283
|
+
the lock that nothing would ever release, so its next construction hung."""
|
|
284
|
+
if not hasattr(os, 'fork'):
|
|
285
|
+
return
|
|
286
|
+
name = _unique("lt_fork")
|
|
287
|
+
SlickQueue.remove(name)
|
|
288
|
+
keeper = SlickQueue(name=name, size=16, element_size=8)
|
|
289
|
+
stop = threading.Event()
|
|
290
|
+
|
|
291
|
+
def churn():
|
|
292
|
+
while not stop.is_set():
|
|
293
|
+
SlickQueue(name=name, element_size=8).close()
|
|
294
|
+
|
|
295
|
+
thread = threading.Thread(target=churn, daemon=True)
|
|
296
|
+
thread.start()
|
|
297
|
+
try:
|
|
298
|
+
with warnings.catch_warnings():
|
|
299
|
+
# Python 3.12+ warns about forking a multi-threaded process
|
|
300
|
+
warnings.simplefilter("ignore", DeprecationWarning)
|
|
301
|
+
for attempt in range(20):
|
|
302
|
+
pid = os.fork()
|
|
303
|
+
if pid == 0:
|
|
304
|
+
try:
|
|
305
|
+
SlickQueue(name=name, element_size=8).close()
|
|
306
|
+
except BaseException:
|
|
307
|
+
os._exit(1)
|
|
308
|
+
os._exit(0)
|
|
309
|
+
deadline = time.monotonic() + 5
|
|
310
|
+
while True:
|
|
311
|
+
done, status = os.waitpid(pid, os.WNOHANG)
|
|
312
|
+
if done:
|
|
313
|
+
break
|
|
314
|
+
if time.monotonic() > deadline:
|
|
315
|
+
os.kill(pid, signal.SIGKILL)
|
|
316
|
+
os.waitpid(pid, 0)
|
|
317
|
+
raise AssertionError(
|
|
318
|
+
f"forked child {attempt} hung constructing a queue")
|
|
319
|
+
time.sleep(0.01)
|
|
320
|
+
assert os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0, \
|
|
321
|
+
f"forked child {attempt} failed (status {status})"
|
|
322
|
+
finally:
|
|
323
|
+
stop.set()
|
|
324
|
+
thread.join()
|
|
325
|
+
keeper.close()
|
|
326
|
+
keeper.unlink()
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
RELATED_PROCESSES = r"""
|
|
330
|
+
import multiprocessing
|
|
331
|
+
import sys
|
|
332
|
+
sys.path.insert(0, sys.argv[1])
|
|
333
|
+
from slick_queue_py import SlickQueue
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def churn(name, n):
|
|
337
|
+
for _ in range(n):
|
|
338
|
+
SlickQueue(name=name, element_size=8).close()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
if __name__ == '__main__':
|
|
342
|
+
name = sys.argv[2]
|
|
343
|
+
SlickQueue.remove(name)
|
|
344
|
+
q = SlickQueue(name=name, size=16, element_size=8)
|
|
345
|
+
ctx = multiprocessing.get_context('spawn')
|
|
346
|
+
procs = [ctx.Process(target=churn, args=(name, 200)) for _ in range(4)]
|
|
347
|
+
for p in procs:
|
|
348
|
+
p.start()
|
|
349
|
+
for p in procs:
|
|
350
|
+
p.join()
|
|
351
|
+
q.close()
|
|
352
|
+
q.unlink()
|
|
353
|
+
# Every worker must succeed: max() let a killed one's negative code hide
|
|
354
|
+
# behind any worker that exited 0
|
|
355
|
+
codes = [p.exitcode for p in procs]
|
|
356
|
+
if codes != [0] * len(procs):
|
|
357
|
+
sys.exit(f"worker exit codes: {codes}")
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def test_related_processes_do_not_race_in_tracker():
|
|
362
|
+
"""multiprocessing children share their parent's resource tracker, so before
|
|
363
|
+
Python 3.13 two of them attaching at once could send REGISTER, REGISTER,
|
|
364
|
+
UNREGISTER, UNREGISTER and make the tracker print a KeyError traceback. The
|
|
365
|
+
tracker writes to the stderr it inherited, so the scenario runs in a fresh
|
|
366
|
+
interpreter whose stderr this test captures. A spawned child re-imports the
|
|
367
|
+
script by path, hence a file rather than -c."""
|
|
368
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
369
|
+
script = Path(tmp) / "related_processes.py"
|
|
370
|
+
script.write_text(RELATED_PROCESSES)
|
|
371
|
+
result = subprocess.run(
|
|
372
|
+
[sys.executable, str(script), str(ROOT), _unique("lt_related")],
|
|
373
|
+
capture_output=True, text=True, timeout=120)
|
|
374
|
+
assert result.returncode == 0, f"script failed:\n{result.stdout}\n{result.stderr}"
|
|
375
|
+
assert "KeyError" not in result.stderr, result.stderr
|
|
376
|
+
assert "resource_tracker" not in result.stderr, result.stderr
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def run_all_tests():
|
|
380
|
+
tests = [(name, obj) for name, obj in sorted(globals().items())
|
|
381
|
+
if name.startswith("test_") and callable(obj)]
|
|
382
|
+
|
|
383
|
+
print("=" * 70)
|
|
384
|
+
print("Segment lifetime tests")
|
|
385
|
+
print("=" * 70)
|
|
386
|
+
|
|
387
|
+
passed = 0
|
|
388
|
+
failed = 0
|
|
389
|
+
for name, test_func in tests:
|
|
390
|
+
try:
|
|
391
|
+
print(f"\n[TEST] {name}...", end=" ")
|
|
392
|
+
test_func()
|
|
393
|
+
print("[PASSED]")
|
|
394
|
+
passed += 1
|
|
395
|
+
except Exception as e:
|
|
396
|
+
print("[FAILED]")
|
|
397
|
+
print(f" Error: {e}")
|
|
398
|
+
import traceback
|
|
399
|
+
traceback.print_exc()
|
|
400
|
+
failed += 1
|
|
401
|
+
|
|
402
|
+
print("\n" + "=" * 70)
|
|
403
|
+
print(f"Results: {passed} passed, {failed} failed")
|
|
404
|
+
print("=" * 70)
|
|
405
|
+
return failed == 0
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
if __name__ == '__main__':
|
|
409
|
+
success = run_all_tests()
|
|
410
|
+
sys.exit(0 if success else 1)
|
|
@@ -9,8 +9,6 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
11
11
|
|
|
12
|
-
from multiprocessing.shared_memory import SharedMemory
|
|
13
|
-
|
|
14
12
|
from slick_queue_py import (
|
|
15
13
|
SlickQueue,
|
|
16
14
|
QueueTraits,
|
|
@@ -24,6 +22,7 @@ from slick_queue_py import (
|
|
|
24
22
|
INIT_STATE_OFFSET,
|
|
25
23
|
INIT_STATE_READY,
|
|
26
24
|
K_INVALID_INDEX,
|
|
25
|
+
_open_shm,
|
|
27
26
|
)
|
|
28
27
|
|
|
29
28
|
|
|
@@ -475,9 +474,12 @@ def test_differently_configured_queues_coexist():
|
|
|
475
474
|
|
|
476
475
|
|
|
477
476
|
def _make_raw_segment(name, size, element_size, magic):
|
|
478
|
-
"""Build a segment by hand so a marker the library never writes can be tested.
|
|
477
|
+
"""Build a segment by hand so a marker the library never writes can be tested.
|
|
478
|
+
|
|
479
|
+
Through the library's own opener, so this handle and the queue that attaches to
|
|
480
|
+
the same name do not both claim it in the resource tracker before Python 3.13."""
|
|
479
481
|
total = HEADER_SIZE + SLOT_SIZE * size + element_size * size
|
|
480
|
-
shm =
|
|
482
|
+
shm = _open_shm(name, create=True, size=total)
|
|
481
483
|
buf = shm.buf
|
|
482
484
|
buf[:HEADER_SIZE] = bytes(HEADER_SIZE)
|
|
483
485
|
struct.pack_into("<I I", buf, SIZE_OFFSET, size, element_size)
|
|
@@ -505,10 +507,7 @@ def test_attach_rejects_segment_without_marker():
|
|
|
505
507
|
raise AssertionError("expected RuntimeError for an unmarked segment")
|
|
506
508
|
finally:
|
|
507
509
|
shm.close()
|
|
508
|
-
|
|
509
|
-
shm.unlink()
|
|
510
|
-
except Exception:
|
|
511
|
-
pass
|
|
510
|
+
SlickQueue.remove(name)
|
|
512
511
|
|
|
513
512
|
|
|
514
513
|
def test_attach_rejects_unknown_feature_bits():
|
|
@@ -526,10 +525,7 @@ def test_attach_rejects_unknown_feature_bits():
|
|
|
526
525
|
raise AssertionError("expected RuntimeError for unknown feature bits")
|
|
527
526
|
finally:
|
|
528
527
|
shm.close()
|
|
529
|
-
|
|
530
|
-
shm.unlink()
|
|
531
|
-
except Exception:
|
|
532
|
-
pass
|
|
528
|
+
SlickQueue.remove(name)
|
|
533
529
|
|
|
534
530
|
|
|
535
531
|
# ------------------------------------------------------------------ reserve()
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|