slick-queue-py 2.3.0__tar.gz → 2.4.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.
Files changed (50) hide show
  1. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/API_DIFFERENCES.md +37 -2
  2. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/CHANGELOG.md +54 -0
  3. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/PKG-INFO +84 -2
  4. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/README.md +83 -1
  5. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/slick_queue_py.egg-info/SOURCES.txt +3 -0
  6. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/slick_queue_py.py +578 -213
  7. slick_queue_py-2.4.0/tests/cpp_spsc_tester.cpp +136 -0
  8. slick_queue_py-2.4.0/tests/test_spsc.py +715 -0
  9. slick_queue_py-2.4.0/tests/test_spsc_interop.py +138 -0
  10. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/.github/FUNDING.yml +0 -0
  11. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/.github/workflows/ci.yml +0 -0
  12. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/.github/workflows/publish-conda.yml +0 -0
  13. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/.github/workflows/publish-pypi.yml +0 -0
  14. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/.github/workflows/publish-release.yml +0 -0
  15. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/.github/workflows/release.yml +0 -0
  16. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/.gitignore +0 -0
  17. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/.pypirc.template +0 -0
  18. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/BUILDING.md +0 -0
  19. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/CMakeLists.txt +0 -0
  20. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/LICENSE +0 -0
  21. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/MANIFEST.in +0 -0
  22. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/atomic_ops.py +0 -0
  23. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/atomic_ops_ext.cpp +0 -0
  24. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/conda.recipe/meta.yaml +0 -0
  25. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/pyproject.toml +0 -0
  26. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/requirements.txt +0 -0
  27. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/setup.cfg +0 -0
  28. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/setup.py +0 -0
  29. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/cleanup_shm.py +0 -0
  30. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/cpp_consumer.cpp +0 -0
  31. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/cpp_multi_producer.cpp +0 -0
  32. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/cpp_producer.cpp +0 -0
  33. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/cpp_read_last_tester.cpp +0 -0
  34. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/cpp_work_stealing_consumer.cpp +0 -0
  35. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/run_test.py +0 -0
  36. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_atomic_cursor.py +0 -0
  37. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_atomic_ops.py +0 -0
  38. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_interop.py +0 -0
  39. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_items_per_slot.py +0 -0
  40. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_local_mode.py +0 -0
  41. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_loss_count.py +0 -0
  42. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_modern_format.py +0 -0
  43. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_multi_producer.py +0 -0
  44. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_queue.py +0 -0
  45. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_read_last.py +0 -0
  46. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_read_last_interop.py +0 -0
  47. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_reset_detection.py +0 -0
  48. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_segment_lifetime.py +0 -0
  49. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_traits.py +0 -0
  50. {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_wrap_invariants.py +0 -0
@@ -1,8 +1,8 @@
1
1
  # C++ / Python API Differences
2
2
 
3
- This document explains the intentional API differences between the C++ `slick::queue<T>` and Python `SlickQueue` implementations.
3
+ This document explains the intentional API differences between the C++ `slick::queue<T>` and Python `SlickQueue` implementations, and between C++ `slick::spsc_queue<T>` and Python `SpscQueue` (section 8).
4
4
 
5
- Both track the same version line; this document describes slick-queue v2.3.0.
5
+ Both track the same version line; this document describes slick-queue v2.4.0.
6
6
 
7
7
  ## Core Compatibility
8
8
 
@@ -267,6 +267,39 @@ the process exits.
267
267
 
268
268
  ---
269
269
 
270
+ ### 8. SpscQueue: Held Slot vs. Copy
271
+
272
+ **C++:**
273
+ ```cpp
274
+ slick::spsc_queue<uint64_t> q("ticks");
275
+ if (uint64_t* v = q.read()) { /* slot held until the next read(), read_last() or release() */ }
276
+ uint64_t dropped = 0;
277
+ uint64_t* last = q.read_last(dropped);
278
+ ```
279
+
280
+ **Python:**
281
+ ```python
282
+ q = SpscQueue(name='ticks', element_size=8)
283
+ data = q.read() # bytes copy; the slot is already back with the producer
284
+ data, dropped = q.read_last()
285
+ ```
286
+
287
+ **Rationale:** Python copies the element anyway, so once `read()` returns there is nothing
288
+ left to hold, and it hands the slot back at once. A Python consumer therefore gets the full
289
+ `size` as capacity, even at size 1, and has no `release()`. A C++ consumer that exits while
290
+ holding a slot still has that element delivered again to whoever attaches next, Python
291
+ included.
292
+
293
+ | C++ | Python |
294
+ |-----|--------|
295
+ | `try_reserve()` returns `invalid_index` | Same, as `SpscQueue.invalid_index` |
296
+ | `read()` returns `T*`, `nullptr` when empty | `bytes`, `None` when empty |
297
+ | `read_last(dropped)` with an out-parameter | `read_last()` returns `(data, dropped)` |
298
+ | `release()` | Not needed: nothing is held |
299
+ | `spsc_queue(name)` adopts `size`, and `T` gives the element size | `SpscQueue(name=..., element_size=...)` adopts `size`; `element_size` is always required |
300
+
301
+ ---
302
+
270
303
  ## Interoperability Examples
271
304
 
272
305
  ### Example 1: C++ Producer → Python Consumer
@@ -363,5 +396,7 @@ int main() {
363
396
  | Misconfigured traits | Compile error (concept / `static_assert`) | `TypeError` / `RuntimeError` at runtime | No compile step |
364
397
  | Default loss detection | Off in Release | Always on | +0.2% here, and the only overrun signal a caller has |
365
398
  | Layout marker | `'SLQ1'` / `'SLQ0'` | Identical, and validated against C++ peers | Shared contract |
399
+ | SPSC `read()` | `T*` held until the next read | `bytes` copy, slot freed at once | Python copies anyway |
400
+ | SPSC `read_last()` dropped count | Out-parameter | Second item of the returned tuple | Python has no pass-by-reference |
366
401
 
367
402
  **Bottom Line:** The Python API is designed to be Pythonic while maintaining **100% binary compatibility** with C++. All atomic operations and memory layouts are identical, enabling seamless C++/Python interoperability.
@@ -1,5 +1,59 @@
1
1
  # Changelogs
2
2
 
3
+ ## [v2.4.0] - 2026-11-09
4
+
5
+ Tracks C++ slick-queue v2.4.0, which added `slick::spsc_queue`. The `SlickQueue`
6
+ shared-memory layout is unchanged, so 2.3.0 peers in either language interoperate
7
+ with it as before.
8
+
9
+ ### Added
10
+
11
+ - `SpscQueue`, a port of C++ `slick::spsc_queue<T>`: a lossless single-producer
12
+ single-consumer ring in local or shared memory. `try_reserve(n)` returns
13
+ `SpscQueue.invalid_index` when the ring is full and `reserve(n)` waits until the
14
+ consumer frees room. `publish()` never moves the tail backwards, so publishing an
15
+ earlier reservation after a later one is a no-op.
16
+ - `read()` returns the next element as `bytes`. `read_last()` returns
17
+ `(data, dropped)`, the newest element and how many unread ones it skipped, from
18
+ one load of the producer's tail however large the backlog.
19
+ - Both reads copy the element and hand its slot back before they return. C++ holds
20
+ the slot until the consumer's next call, so a Python consumer gets the full `size`
21
+ as capacity, nothing it read is delivered again after it reattaches, and there is
22
+ no `release()`.
23
+ - `SpscQueueTraits` with one setting, `enable_cpu_relax`, which yields the GIL while
24
+ `reserve()` waits. `q.traits` is a read-only snapshot, as for `SlickQueue`.
25
+ - The C++ shared-memory layout: a 384-byte header with the tail at offset 128 and the
26
+ head at 256, then the data array. Size, element size, the `'SLS0'` marker and init
27
+ state keep the `SlickQueue` offsets. Both cursors live in the segment, so a
28
+ reattached peer resumes where the previous one stopped.
29
+ - A creator that claims a segment left uninitialized, whose own creator died before
30
+ claiming it, checks the mapping holds its geometry first. If it doesn't, it raises
31
+ and hands the claim back, instead of writing a header for a ring the mapping can't
32
+ hold. Every attacher checks the mapping size too.
33
+ - Using an `SpscQueue` after `close()` raises `RuntimeError("queue is closed")`.
34
+
35
+ ### Changed
36
+
37
+ - `SlickQueue` attaching to an `SpscQueue` segment now raises "holds a
38
+ slick::spsc_queue" instead of the generic missing-marker error, matching C++, and
39
+ `SpscQueue` names a `SlickQueue` segment the same way.
40
+ - Name handling, opening and closing the segment, `unlink()`, `remove_shm()`,
41
+ `remove()` and the context manager moved into a base class both queues share.
42
+ `SlickQueue` behaves as before.
43
+ - `close()` still never raises, but no longer prints the exception it swallows.
44
+
45
+ ### Tests
46
+
47
+ - `tests/test_spsc.py` covers the local and shared-memory API: wraps, a full ring,
48
+ out-of-order publish, element sizes other than 8, `read_last()` accounting under a
49
+ concurrent producer, reset, reattach, a too-small leftover segment, use after
50
+ `close()` and the cross-type marker checks.
51
+ - `tests/test_spsc_interop.py` runs against C++ through the new `cpp_spsc_tester`:
52
+ data in both directions, attaching to a C++-created segment, `read_last()` from
53
+ each side, and each language rejecting the other queue type.
54
+
55
+ Checked against C++ slick-queue v2.4.0 with the interop tests on Windows.
56
+
3
57
  ## [v2.3.0] - 2026-10-17
4
58
 
5
59
  Tracks C++ slick-queue v2.3.0: no queue removes its shared-memory segment on its
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slick-queue-py
3
- Version: 2.3.0
3
+ Version: 2.4.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
@@ -36,7 +36,7 @@ Dynamic: requires-python
36
36
 
37
37
  # slick-queue-py
38
38
 
39
- Python implementation of SlickQueue - a lock-free multi-producer multi-consumer (MPMC) queue with C++ interoperability through shared memory.
39
+ Python implementation of SlickQueue - a lock-free multi-producer multi-consumer (MPMC) queue, plus a lossless single-producer single-consumer (SPSC) queue, with C++ interoperability through shared memory.
40
40
 
41
41
  This is the Python binding for the [SlickQueue C++ library](https://github.com/SlickQuant/slick-queue). The Python implementation maintains exact binary compatibility with the C++ version, enabling seamless interprocess communication between Python and C++ applications.
42
42
 
@@ -50,6 +50,8 @@ This is the Python binding for the [SlickQueue C++ library](https://github.com/S
50
50
  - **Local Memory Mode**: In-process queue using local memory (no shared memory overhead)
51
51
  - **Shared Memory Mode**: Inter-process queue for interprocess communication
52
52
  - **Lock-Free Multi-Producer Multi-Consumer**: True MPMC support using atomic operations
53
+ - **Lossless SPSC Queue**: `SpscQueue` ports C++ `slick::spsc_queue`. It never overwrites an
54
+ unread element and reports a full ring to the producer instead. See [SpscQueue](#spscqueue)
53
55
  - **C++/Python Interoperability**: Python and C++ processes can share the same queue
54
56
  - **Cross-Platform**: Windows and Linux/macOS support (x86-64)
55
57
  - **Memory Layout Compatible**: Exact binary compatibility with C++ `slick::queue<T>`
@@ -642,6 +644,51 @@ sends the tracker nothing at all: `track=False` on Python 3.13+, and on older PO
642
644
  `SharedMemory` subclass that maps the segment without registering it, so there is no
643
645
  register/unregister pair to race between threads or between processes sharing one tracker.
644
646
 
647
+ ### SpscQueue
648
+
649
+ A lossless single-producer single-consumer ring, compatible with C++ `slick::spsc_queue<T>`. One
650
+ thread or process produces and one consumes. Unlike `SlickQueue` it never overwrites an unread
651
+ element: `try_reserve()` reports a full ring instead.
652
+
653
+ ```python
654
+ import struct
655
+ from slick_queue_py import SpscQueue
656
+
657
+ q = SpscQueue(name='ticks', size=1024, element_size=8) # create, or open with the same geometry
658
+ peer = SpscQueue(name='ticks', element_size=8) # attach, adopting the segment's size
659
+ local = SpscQueue(size=1024, element_size=8) # local memory
660
+
661
+ # Producer
662
+ i = q.try_reserve() # SpscQueue.invalid_index when full; reserve() waits instead
663
+ if i != SpscQueue.invalid_index:
664
+ q[i][:8] = struct.pack('<Q', 42)
665
+ q.publish(i)
666
+
667
+ # Consumer
668
+ data = peer.read() # next element as bytes, or None
669
+ data, dropped = peer.read_last() # newest element, and how many unread ones it skipped
670
+ ```
671
+
672
+ - `reserve(n)` and `try_reserve(n)` reserve `n` separate elements. Write element `k` through
673
+ `q[i + k]`, because a batch may wrap around the end of the ring. Publish in reservation order.
674
+ Publishing a later batch first also exposes the earlier one, and publishing the earlier one
675
+ afterwards is a no-op.
676
+ - `read()` and `read_last()` copy the element and hand its slot back before they return. C++ holds
677
+ the slot until the consumer's next call, so here the full `size` is always usable and there is no
678
+ `release()`.
679
+ - `read_last()` loads the producer's tail and moves the read cursor there, so its cost doesn't grow
680
+ with the backlog. It returns `(None, 0)` if nothing was published since the last read. Note
681
+ that the second item counts skipped elements, where `SlickQueue.read_last()` returns a size.
682
+ - Both cursors live in the segment, and a peer that reattaches resumes where the previous one
683
+ stopped. Each object caches its own side's cursor, so keep one live object per side. After
684
+ another process has produced or consumed, attach a fresh object rather than reusing an old one.
685
+ - `reset()` empties the queue. It isn't thread-safe, and a peer in another process must reattach.
686
+ - `close()`, `unlink()`, `remove_shm()`, `SpscQueue.remove(name)`, `get_shm_name()` and the
687
+ context manager behave as they do for `SlickQueue`. Using the queue after `close()` raises
688
+ `RuntimeError("queue is closed")`.
689
+ - `traits=` takes an `SpscQueueTraits` subclass. Its one setting is `enable_cpu_relax` (default
690
+ `True`), which yields the GIL while `reserve()` waits. `q.traits` is a read-only snapshot.
691
+
645
692
  ### AtomicCursor
646
693
 
647
694
  The `AtomicCursor` class enables multi-consumer work-stealing patterns by providing an atomic read cursor that multiple consumers can coordinate through. Works in both local mode (multi-threading) and shared memory mode (multi-process).
@@ -817,6 +864,34 @@ Bits 2-3 of the nibble are reserved and must be 0; a marker that sets one is rej
817
864
  newer than this build understands. Segments created before slick-queue v1.4.0 carry no
818
865
  marker at all and are rejected at attach time.
819
866
 
867
+ ### SpscQueue layout
868
+
869
+ `SpscQueue` matches C++ `slick::spsc_queue<T>`. Each cursor sits on its own 128-byte line on every
870
+ platform:
871
+
872
+ ```
873
+ Offset | Size | Content
874
+ -------|---------------|------------------
875
+ 0 | 8 bytes | reserved (0)
876
+ 8 | 4 bytes | uint32_t size (queue capacity)
877
+ 12 | 4 bytes | uint32_t element_size
878
+ 16 | 8 bytes | reserved (0)
879
+ 24 | 4 bytes | uint32_t header_magic - 'SLS0' (0x534C5330)
880
+ 28 | 20 bytes | reserved (0)
881
+ 48 | 4 bytes | uint32_t init_state (atomic)
882
+ 52 | 76 bytes | padding (to 128)
883
+ 128 | 8 bytes | uint64_t tail (atomic), written by the producer
884
+ 136 | 120 bytes | padding
885
+ 256 | 8 bytes | uint64_t head (atomic), written by the consumer
886
+ 264 | 120 bytes | padding (to 384)
887
+ 384 | elem*size | data array
888
+ ```
889
+
890
+ `size`, `element_size`, the marker and `init_state` sit at the same offsets as in a `SlickQueue`
891
+ segment. Each queue type therefore turns the other's segment away at attach time, with *"holds a
892
+ slick::spsc_queue"* or *"holds a slick::queue"*, instead of timing out. The marker's low nibble is
893
+ reserved for future layout features and must be 0. The C++ side requires `alignof(T) <= 128`.
894
+
820
895
  ## Platform Support
821
896
 
822
897
  ### Fully Supported (Lock-Free)
@@ -959,6 +1034,9 @@ python tests/test_wrap_invariants.py
959
1034
 
960
1035
  # Segment lifetime: unlink(), remove_shm(), remove(), process exit
961
1036
  python tests/test_segment_lifetime.py
1037
+
1038
+ # SpscQueue, local and shared memory
1039
+ python tests/test_spsc.py
962
1040
  ```
963
1041
 
964
1042
  Or run everything with pytest:
@@ -987,6 +1065,9 @@ python tests/test_interop.py --test cpp_producer_python_consumer
987
1065
  python tests/test_interop.py --test multi_producer_interop
988
1066
  python tests/test_interop.py --test stress_interop
989
1067
  python tests/test_interop.py --test cpp_shm_creation
1068
+
1069
+ # SpscQueue against C++ slick::spsc_queue (uses cpp_spsc_tester)
1070
+ python tests/test_spsc_interop.py
990
1071
  ```
991
1072
 
992
1073
  The interop tests verify:
@@ -1052,6 +1133,7 @@ queue so consumers keep up, and use `loss_count()` to detect when they have not.
1052
1133
  | read(atomic cursor) multi-consumer | ✅ | ✅ |
1053
1134
  | Feature traits | Template parameter | `traits=` argument |
1054
1135
  | Shared layout marker | `'SLQ1'` / `'SLQ0'` | Same, and validated against C++ peers |
1136
+ | Lossless SPSC queue | `slick::spsc_queue<T>` | `SpscQueue`, same `'SLS0'` layout |
1055
1137
 
1056
1138
  ## Contributing
1057
1139
 
@@ -1,6 +1,6 @@
1
1
  # slick-queue-py
2
2
 
3
- Python implementation of SlickQueue - a lock-free multi-producer multi-consumer (MPMC) queue with C++ interoperability through shared memory.
3
+ Python implementation of SlickQueue - a lock-free multi-producer multi-consumer (MPMC) queue, plus a lossless single-producer single-consumer (SPSC) queue, with C++ interoperability through shared memory.
4
4
 
5
5
  This is the Python binding for the [SlickQueue C++ library](https://github.com/SlickQuant/slick-queue). The Python implementation maintains exact binary compatibility with the C++ version, enabling seamless interprocess communication between Python and C++ applications.
6
6
 
@@ -14,6 +14,8 @@ This is the Python binding for the [SlickQueue C++ library](https://github.com/S
14
14
  - **Local Memory Mode**: In-process queue using local memory (no shared memory overhead)
15
15
  - **Shared Memory Mode**: Inter-process queue for interprocess communication
16
16
  - **Lock-Free Multi-Producer Multi-Consumer**: True MPMC support using atomic operations
17
+ - **Lossless SPSC Queue**: `SpscQueue` ports C++ `slick::spsc_queue`. It never overwrites an
18
+ unread element and reports a full ring to the producer instead. See [SpscQueue](#spscqueue)
17
19
  - **C++/Python Interoperability**: Python and C++ processes can share the same queue
18
20
  - **Cross-Platform**: Windows and Linux/macOS support (x86-64)
19
21
  - **Memory Layout Compatible**: Exact binary compatibility with C++ `slick::queue<T>`
@@ -606,6 +608,51 @@ sends the tracker nothing at all: `track=False` on Python 3.13+, and on older PO
606
608
  `SharedMemory` subclass that maps the segment without registering it, so there is no
607
609
  register/unregister pair to race between threads or between processes sharing one tracker.
608
610
 
611
+ ### SpscQueue
612
+
613
+ A lossless single-producer single-consumer ring, compatible with C++ `slick::spsc_queue<T>`. One
614
+ thread or process produces and one consumes. Unlike `SlickQueue` it never overwrites an unread
615
+ element: `try_reserve()` reports a full ring instead.
616
+
617
+ ```python
618
+ import struct
619
+ from slick_queue_py import SpscQueue
620
+
621
+ q = SpscQueue(name='ticks', size=1024, element_size=8) # create, or open with the same geometry
622
+ peer = SpscQueue(name='ticks', element_size=8) # attach, adopting the segment's size
623
+ local = SpscQueue(size=1024, element_size=8) # local memory
624
+
625
+ # Producer
626
+ i = q.try_reserve() # SpscQueue.invalid_index when full; reserve() waits instead
627
+ if i != SpscQueue.invalid_index:
628
+ q[i][:8] = struct.pack('<Q', 42)
629
+ q.publish(i)
630
+
631
+ # Consumer
632
+ data = peer.read() # next element as bytes, or None
633
+ data, dropped = peer.read_last() # newest element, and how many unread ones it skipped
634
+ ```
635
+
636
+ - `reserve(n)` and `try_reserve(n)` reserve `n` separate elements. Write element `k` through
637
+ `q[i + k]`, because a batch may wrap around the end of the ring. Publish in reservation order.
638
+ Publishing a later batch first also exposes the earlier one, and publishing the earlier one
639
+ afterwards is a no-op.
640
+ - `read()` and `read_last()` copy the element and hand its slot back before they return. C++ holds
641
+ the slot until the consumer's next call, so here the full `size` is always usable and there is no
642
+ `release()`.
643
+ - `read_last()` loads the producer's tail and moves the read cursor there, so its cost doesn't grow
644
+ with the backlog. It returns `(None, 0)` if nothing was published since the last read. Note
645
+ that the second item counts skipped elements, where `SlickQueue.read_last()` returns a size.
646
+ - Both cursors live in the segment, and a peer that reattaches resumes where the previous one
647
+ stopped. Each object caches its own side's cursor, so keep one live object per side. After
648
+ another process has produced or consumed, attach a fresh object rather than reusing an old one.
649
+ - `reset()` empties the queue. It isn't thread-safe, and a peer in another process must reattach.
650
+ - `close()`, `unlink()`, `remove_shm()`, `SpscQueue.remove(name)`, `get_shm_name()` and the
651
+ context manager behave as they do for `SlickQueue`. Using the queue after `close()` raises
652
+ `RuntimeError("queue is closed")`.
653
+ - `traits=` takes an `SpscQueueTraits` subclass. Its one setting is `enable_cpu_relax` (default
654
+ `True`), which yields the GIL while `reserve()` waits. `q.traits` is a read-only snapshot.
655
+
609
656
  ### AtomicCursor
610
657
 
611
658
  The `AtomicCursor` class enables multi-consumer work-stealing patterns by providing an atomic read cursor that multiple consumers can coordinate through. Works in both local mode (multi-threading) and shared memory mode (multi-process).
@@ -781,6 +828,34 @@ Bits 2-3 of the nibble are reserved and must be 0; a marker that sets one is rej
781
828
  newer than this build understands. Segments created before slick-queue v1.4.0 carry no
782
829
  marker at all and are rejected at attach time.
783
830
 
831
+ ### SpscQueue layout
832
+
833
+ `SpscQueue` matches C++ `slick::spsc_queue<T>`. Each cursor sits on its own 128-byte line on every
834
+ platform:
835
+
836
+ ```
837
+ Offset | Size | Content
838
+ -------|---------------|------------------
839
+ 0 | 8 bytes | reserved (0)
840
+ 8 | 4 bytes | uint32_t size (queue capacity)
841
+ 12 | 4 bytes | uint32_t element_size
842
+ 16 | 8 bytes | reserved (0)
843
+ 24 | 4 bytes | uint32_t header_magic - 'SLS0' (0x534C5330)
844
+ 28 | 20 bytes | reserved (0)
845
+ 48 | 4 bytes | uint32_t init_state (atomic)
846
+ 52 | 76 bytes | padding (to 128)
847
+ 128 | 8 bytes | uint64_t tail (atomic), written by the producer
848
+ 136 | 120 bytes | padding
849
+ 256 | 8 bytes | uint64_t head (atomic), written by the consumer
850
+ 264 | 120 bytes | padding (to 384)
851
+ 384 | elem*size | data array
852
+ ```
853
+
854
+ `size`, `element_size`, the marker and `init_state` sit at the same offsets as in a `SlickQueue`
855
+ segment. Each queue type therefore turns the other's segment away at attach time, with *"holds a
856
+ slick::spsc_queue"* or *"holds a slick::queue"*, instead of timing out. The marker's low nibble is
857
+ reserved for future layout features and must be 0. The C++ side requires `alignof(T) <= 128`.
858
+
784
859
  ## Platform Support
785
860
 
786
861
  ### Fully Supported (Lock-Free)
@@ -923,6 +998,9 @@ python tests/test_wrap_invariants.py
923
998
 
924
999
  # Segment lifetime: unlink(), remove_shm(), remove(), process exit
925
1000
  python tests/test_segment_lifetime.py
1001
+
1002
+ # SpscQueue, local and shared memory
1003
+ python tests/test_spsc.py
926
1004
  ```
927
1005
 
928
1006
  Or run everything with pytest:
@@ -951,6 +1029,9 @@ python tests/test_interop.py --test cpp_producer_python_consumer
951
1029
  python tests/test_interop.py --test multi_producer_interop
952
1030
  python tests/test_interop.py --test stress_interop
953
1031
  python tests/test_interop.py --test cpp_shm_creation
1032
+
1033
+ # SpscQueue against C++ slick::spsc_queue (uses cpp_spsc_tester)
1034
+ python tests/test_spsc_interop.py
954
1035
  ```
955
1036
 
956
1037
  The interop tests verify:
@@ -1016,6 +1097,7 @@ queue so consumers keep up, and use `loss_count()` to detect when they have not.
1016
1097
  | read(atomic cursor) multi-consumer | ✅ | ✅ |
1017
1098
  | Feature traits | Template parameter | `traits=` argument |
1018
1099
  | Shared layout marker | `'SLQ1'` / `'SLQ0'` | Same, and validated against C++ peers |
1100
+ | Lossless SPSC queue | `slick::spsc_queue<T>` | `SpscQueue`, same `'SLS0'` layout |
1019
1101
 
1020
1102
  ## Contributing
1021
1103
 
@@ -25,6 +25,7 @@ tests/cpp_consumer.cpp
25
25
  tests/cpp_multi_producer.cpp
26
26
  tests/cpp_producer.cpp
27
27
  tests/cpp_read_last_tester.cpp
28
+ tests/cpp_spsc_tester.cpp
28
29
  tests/cpp_work_stealing_consumer.cpp
29
30
  tests/run_test.py
30
31
  tests/test_atomic_cursor.py
@@ -40,5 +41,7 @@ tests/test_read_last.py
40
41
  tests/test_read_last_interop.py
41
42
  tests/test_reset_detection.py
42
43
  tests/test_segment_lifetime.py
44
+ tests/test_spsc.py
45
+ tests/test_spsc_interop.py
43
46
  tests/test_traits.py
44
47
  tests/test_wrap_invariants.py