slick-queue-py 1.2.0__tar.gz → 2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/API_DIFFERENCES.md +113 -13
  2. slick_queue_py-2.1.0/CHANGELOG.md +276 -0
  3. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/PKG-INFO +226 -21
  4. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/README.md +225 -20
  5. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/slick_queue_py.egg-info/SOURCES.txt +5 -1
  6. slick_queue_py-2.1.0/slick_queue_py.py +1232 -0
  7. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/cpp_consumer.cpp +2 -2
  8. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/cpp_multi_producer.cpp +2 -2
  9. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/cpp_producer.cpp +2 -2
  10. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/cpp_read_last_tester.cpp +3 -3
  11. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/cpp_work_stealing_consumer.cpp +2 -2
  12. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/test_interop.py +33 -10
  13. slick_queue_py-2.1.0/tests/test_items_per_slot.py +485 -0
  14. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/test_modern_format.py +17 -7
  15. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/test_multi_producer.py +56 -10
  16. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/test_read_last_interop.py +3 -3
  17. slick_queue_py-2.1.0/tests/test_reset_detection.py +272 -0
  18. slick_queue_py-2.1.0/tests/test_traits.py +582 -0
  19. slick_queue_py-2.1.0/tests/test_wrap_invariants.py +406 -0
  20. slick_queue_py-1.2.0/CHANGELOG.md +0 -97
  21. slick_queue_py-1.2.0/slick_queue_py.py +0 -770
  22. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/.github/FUNDING.yml +0 -0
  23. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/.github/workflows/ci.yml +0 -0
  24. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/.github/workflows/publish-conda.yml +0 -0
  25. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/.github/workflows/publish-pypi.yml +0 -0
  26. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/.github/workflows/publish-release.yml +0 -0
  27. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/.github/workflows/release.yml +0 -0
  28. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/.gitignore +0 -0
  29. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/.pypirc.template +0 -0
  30. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/BUILDING.md +0 -0
  31. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/CMakeLists.txt +0 -0
  32. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/LICENSE +0 -0
  33. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/MANIFEST.in +0 -0
  34. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/atomic_ops.py +0 -0
  35. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/atomic_ops_ext.cpp +0 -0
  36. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/conda.recipe/meta.yaml +0 -0
  37. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/pyproject.toml +0 -0
  38. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/requirements.txt +0 -0
  39. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/setup.cfg +0 -0
  40. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/setup.py +0 -0
  41. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/cleanup_shm.py +0 -0
  42. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/run_test.py +0 -0
  43. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/test_atomic_cursor.py +0 -0
  44. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/test_atomic_ops.py +0 -0
  45. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/test_local_mode.py +0 -0
  46. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/test_loss_count.py +0 -0
  47. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/test_queue.py +0 -0
  48. {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/test_read_last.py +0 -0
@@ -1,6 +1,8 @@
1
1
  # C++ / Python API Differences
2
2
 
3
- This document explains the intentional API differences between the C++ `slick::SlickQueue<T>` and Python `SlickQueue` implementations.
3
+ This document explains the intentional API differences between the C++ `slick::queue<T>` and Python `SlickQueue` implementations.
4
+
5
+ Both track the same version line; this document describes slick-queue v2.1.0.
4
6
 
5
7
  ## Core Compatibility
6
8
 
@@ -17,13 +19,16 @@ Both implementations:
17
19
  **C++:**
18
20
  ```cpp
19
21
  // Create new queue in shared memory
20
- slick::SlickQueue<uint8_t> q(queue_size, queue_name);
22
+ slick::queue<uint8_t> q(queue_size, queue_name);
21
23
 
22
24
  // Open existing queue
23
- slick::SlickQueue<uint8_t> q(queue_name);
25
+ slick::queue<uint8_t> q(queue_name);
24
26
 
25
27
  // Local memory mode
26
- slick::SlickQueue<uint8_t> q(queue_size); // No shared memory
28
+ slick::queue<uint8_t> q(queue_size); // No shared memory
29
+
30
+ // Minimum of 64 items per reservation (one control slot per 64 items)
31
+ slick::queue<uint8_t> q(queue_size, 64, queue_name);
27
32
  ```
28
33
 
29
34
  **Python:**
@@ -36,10 +41,17 @@ q = SlickQueue(name=queue_name, element_size=element_size)
36
41
 
37
42
  # Local memory mode
38
43
  q = SlickQueue(size=queue_size, element_size=element_size) # No shared memory
44
+
45
+ # Minimum of 64 items per reservation (one control slot per 64 items)
46
+ q = SlickQueue(name=queue_name, size=queue_size, element_size=1, items_per_slot=64)
39
47
  ```
40
48
 
41
49
  **Rationale:** Python uses keyword arguments for clarity and supports both shared memory and local memory modes.
42
50
 
51
+ `items_per_slot` is the one place the opener differs: C++ `queue(name)` can only adopt the
52
+ segment's value, while Python's opener also accepts `items_per_slot=` and raises `ValueError`
53
+ if the segment disagrees. Omitting it adopts the segment's value, exactly as C++ does.
54
+
43
55
  ---
44
56
 
45
57
  ### 2. read() Method - Return Value vs. Reference Parameter
@@ -81,7 +93,89 @@ if data is not None:
81
93
 
82
94
  ---
83
95
 
84
- ### 3. Element Access
96
+ ### 3. Feature Configuration (Traits)
97
+
98
+ **C++:**
99
+ ```cpp
100
+ // A template parameter - differently configured queues are different types
101
+ struct my_traits : slick::queue_traits {
102
+ static constexpr bool enable_reset_check = true;
103
+ static constexpr bool enable_read_last = false;
104
+ };
105
+
106
+ slick::queue<int, my_traits> lean(1024);
107
+ slick::queue<int> standard(1024); // default_queue_traits
108
+ ```
109
+
110
+ **Python:**
111
+ ```python
112
+ # A constructor argument - the configuration is per-instance
113
+ class MyTraits(QueueTraits):
114
+ enable_reset_check = True
115
+ enable_read_last = False
116
+
117
+ lean = SlickQueue(size=1024, element_size=4, traits=MyTraits)
118
+ standard = SlickQueue(size=1024, element_size=4) # default_queue_traits
119
+ ```
120
+
121
+ **Rationale:** Python has no templates, so the traits type is passed as a value
122
+ rather than a template argument. The consequences differ slightly:
123
+
124
+ | | C++ | Python |
125
+ |---|---|---|
126
+ | Malformed traits type | `queue_traits_type` concept, compile error | `validate_traits()`, `TypeError` at construction |
127
+ | Mutating the traits after use | Impossible - `static constexpr` members | Snapshotted at construction; `q.traits` is read-only |
128
+ | `read_last()` without `enable_read_last` | `static_assert`, compile error | `RuntimeError` at the call |
129
+ | Traits types | Two (`queue_traits`, `debug_queue_traits`) so a debug/release mismatch fails to link | One - no translation units, so nothing for a second type to protect |
130
+ | Default loss detection | Keyed off `NDEBUG` (Debug on, Release off) | Always on (+0.2%); not keyed off `__debug__`, which would zero `loss_count()` under `python -O` |
131
+ | Layout when a feature is off | Its counter is dropped from the object | No object-layout effect |
132
+
133
+ **Important:** `enable_read_last` is part of the *shared-memory* contract in both
134
+ languages, and they agree byte for byte. The creator records it in the segment's
135
+ layout marker - `'SLQ1'` when the last-published index is maintained, `'SLQ0'`
136
+ when it is not - and every attacher matches it against its own configuration,
137
+ raising `RuntimeError` (C++ `std::runtime_error`) on a mismatch in either
138
+ direction. A Python `'SLQ0'` segment is therefore rejected by a C++ peer built
139
+ with the feature on, and vice versa. The remaining traits are process-local and
140
+ mix freely on one segment, so a Python reader with `enable_reset_check` on can
141
+ share a segment with a C++ writer that has it off.
142
+
143
+ ---
144
+
145
+ ### 4. Lossy Reads: Pointer vs. Copy
146
+
147
+ **C++:**
148
+ ```cpp
149
+ auto [data, size] = queue.read(read_index); // data is a T* into the ring
150
+ process(data, size); // producer may overwrite as you read
151
+ ```
152
+
153
+ **Python:**
154
+ ```python
155
+ data, size, read_index = q.read(read_index) # data is a bytes copy
156
+ ```
157
+
158
+ **Rationale:** Python cannot hand out a raw pointer into the ring safely, so
159
+ `read()` copies. This changes what "lossy" feels like, without changing the
160
+ guarantee.
161
+
162
+ Both implementations guarantee that the cursor and the returned size describe one
163
+ and the same record - that is what the v2.0.0 seqlock validation fixed. Neither
164
+ guarantees the *contents*: a producer writes an element's data before it publishes
165
+ the slot's new index, so a consumer that has been lapped can read bytes the
166
+ producer is midway through overwriting, and no check on the slot can detect it
167
+ because the index has not changed yet.
168
+
169
+ The difference is only in how visible this is. A C++ caller holds a pointer into a
170
+ ring buffer and can see that its target is volatile. A Python caller holds what
171
+ looks like a snapshot, but under overrun those bytes may belong to a newer record
172
+ than the cursor names. Use `loss_count()` to detect it: a non-zero count means this
173
+ consumer was overrun and bytes may have been overwritten in flight. If your data
174
+ must be self-identifying, put a sequence number in the payload.
175
+
176
+ ---
177
+
178
+ ### 5. Element Access
85
179
 
86
180
  **C++:**
87
181
  ```cpp
@@ -103,12 +197,12 @@ q.publish(idx)
103
197
 
104
198
  ---
105
199
 
106
- ### 4. Type Safety
200
+ ### 6. Type Safety
107
201
 
108
202
  **C++:**
109
203
  ```cpp
110
204
  // Template-based, compile-time type safety
111
- slick::SlickQueue<int32_t> q(1024, "queue");
205
+ slick::queue<int32_t> q(1024, "queue");
112
206
  auto idx = q.reserve();
113
207
  *q[idx] = 42; // Type-safe int32_t access
114
208
  ```
@@ -126,12 +220,12 @@ q[idx][:4] = struct.pack("<i", 42) # Manual packing
126
220
 
127
221
  ---
128
222
 
129
- ### 5. Resource Management
223
+ ### 7. Resource Management
130
224
 
131
225
  **C++:**
132
226
  ```cpp
133
227
  {
134
- slick::SlickQueue<T> q(size, "name");
228
+ slick::queue<T> q(size, "name");
135
229
  // ... use queue ...
136
230
  } // Destructor automatically cleans up
137
231
  ```
@@ -162,11 +256,11 @@ with SlickQueue(name='name', size=size, element_size=elem_size, create=True) as
162
256
 
163
257
  **C++ (producer.cpp):**
164
258
  ```cpp
165
- #include <slick/queue.h>
259
+ #include <slick/queue.hpp>
166
260
  #include <cstring>
167
261
 
168
262
  int main() {
169
- slick::SlickQueue<uint8_t> queue("my_queue"); // Open existing queue
263
+ slick::queue<uint8_t> queue("my_queue"); // Open existing queue
170
264
 
171
265
  for (int i = 0; i < 100; i++) {
172
266
  auto idx = queue.reserve();
@@ -216,12 +310,12 @@ q.close()
216
310
 
217
311
  **C++ (consumer.cpp):**
218
312
  ```cpp
219
- #include <slick/queue.h>
313
+ #include <slick/queue.hpp>
220
314
  #include <iostream>
221
315
  #include <cstring>
222
316
 
223
317
  int main() {
224
- slick::SlickQueue<uint8_t> queue("my_queue"); // Open existing queue
318
+ slick::queue<uint8_t> queue("my_queue"); // Open existing queue
225
319
 
226
320
  uint64_t read_index = 0;
227
321
  for (int i = 0; i < 100; i++) {
@@ -245,5 +339,11 @@ int main() {
245
339
  | Element access | Pointer | `memoryview` | Python memory safety |
246
340
  | Type safety | Templates | `struct` module | Language difference |
247
341
  | Cleanup | RAII (destructor) | Explicit or context manager | Python GC is non-deterministic |
342
+ | `read()` data | `T*` into the ring | `bytes` copy | No safe raw pointers in Python |
343
+ | Overwritten-in-flight data | Caller sees a volatile pointer | Copy may hold a newer record's bytes | Detect with `loss_count()` |
344
+ | Feature configuration | `Traits` template parameter | `traits=` argument | Python has no templates |
345
+ | Misconfigured traits | Compile error (concept / `static_assert`) | `TypeError` / `RuntimeError` at runtime | No compile step |
346
+ | Default loss detection | Off in Release | Always on | +0.2% here, and the only overrun signal a caller has |
347
+ | Layout marker | `'SLQ1'` / `'SLQ0'` | Identical, and validated against C++ peers | Shared contract |
248
348
 
249
349
  **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.
@@ -0,0 +1,276 @@
1
+ # Changelogs
2
+
3
+ ## [v2.1.0] - 2026-09-24
4
+
5
+ Tracks C++ slick-queue v2.1.0. Adds `items_per_slot`, so one control slot can
6
+ cover several elements and a byte buffer no longer carries a 16-byte control
7
+ slot per byte. With the default of 1 the shared-memory layout is unchanged, and
8
+ 2.0.0 peers in either language interoperate as before.
9
+
10
+ ### Added
11
+
12
+ - `items_per_slot=` argument on `SlickQueue`: the minimum number of elements a
13
+ single `reserve()` consumes (a power of 2, `<= size`). `reserve(n)` rounds `n`
14
+ up to a multiple of it and one control slot covers one such unit, so the
15
+ control array holds `size // items_per_slot` slots instead of `size`. A 16M
16
+ byte buffer drops from 256 MB of control to 4 MB with `items_per_slot=64`, at
17
+ the cost that a message shorter than the minimum still takes a whole unit.
18
+ - `read()` returns the published size and advances the cursor by the whole units
19
+ the reservation consumed. The geometry is exposed as `q.items_per_slot` and
20
+ `q.slot_count`.
21
+ - Defaults to 1 when creating. Opening an existing segment by name adopts the
22
+ segment's value, as `size` is adopted; passing one explicitly raises
23
+ `ValueError` if the segment disagrees. C++'s attacher can only adopt - see
24
+ `API_DIFFERENCES.md`.
25
+ - Constants `ITEMS_PER_SLOT_OFFSET`, `HEADER_MAGIC_ITEMS_PER_SLOT` and
26
+ `HEADER_MAGIC_KNOWN_FEATURES`.
27
+
28
+ ### Shared-Memory Layout
29
+
30
+ - `items_per_slot` is stored at header offset 28, previously padding. A zero
31
+ field - every segment created by 2.0.0 and earlier - reads as 1. A creator
32
+ opening a segment with a different value raises `ValueError`.
33
+ - Layout marker feature bit 1 is set when `items_per_slot != 1` (`'SLQ3'`, or
34
+ `'SLQ2'` without `enable_read_last`). slick-queue-py 2.0.0 and C++ slick-queue
35
+ 2.0.0 reject feature bits they do not recognise, so they refuse such a segment
36
+ at attach time instead of misreading it; default segments keep
37
+ `'SLQ1'`/`'SLQ0'`. A marker whose bit disagrees with the offset-28 field
38
+ raises `RuntimeError`.
39
+ - When `items_per_slot != 1` the data array is padded to the lowest set bit of
40
+ `element_size`, matching C++, so the offset agrees with a C++ peer whose
41
+ element type is over-aligned.
42
+
43
+ ### Changed
44
+
45
+ - Local and shared-memory construction, and `reset()`, share helpers for slot
46
+ initialisation and view setup instead of repeating them per path.
47
+
48
+ ### Tests
49
+
50
+ - New `tests/test_items_per_slot.py` with 20 tests: geometry, reservation
51
+ rounding, cursor advance by unit for both read paths, multi-unit messages,
52
+ mid-buffer wrap, `read_last()` after wrap, `reset()`, loss counting in
53
+ elements, attacher adoption and mismatch on both attach paths, the legacy zero
54
+ field, the marker bit in each configuration, a marker contradicting its field,
55
+ and data-array padding.
56
+ - `test_modern_format.py` asserts the new offset and marker bit.
57
+ `test_attach_rejects_unknown_feature_bits` uses bit 2, since bit 1 now has a
58
+ meaning.
59
+
60
+ Checked against C++ built from the v2.1.0 header in both directions - byte
61
+ elements with `items_per_slot=16`, and an `alignas(32)` element with a single
62
+ control slot - and against unmodified 2.0.0 code in both languages, which
63
+ refuses an `'SLQ3'` segment and still attaches to an `'SLQ1'` one. The interop
64
+ tests in `tests/` fetch slick-queue from GitHub, so they exercise the default
65
+ layout until the v2.1.0 release is published.
66
+
67
+ ## [v2.0.0] - 2026-09-07
68
+
69
+ Tracks C++ slick-queue v2.0.0. Feature configuration moves to a `traits`
70
+ argument, `enable_read_last` becomes part of the shared-memory contract so a
71
+ mismatched peer is rejected at attach time instead of degrading silently, and
72
+ several read-path correctness bugs are fixed.
73
+
74
+ ### Breaking Changes
75
+
76
+ - **BREAKING:** `read_last()` requires `traits.enable_read_last` and raises
77
+ `RuntimeError` otherwise. The legacy reserved-cursor fallback is gone with it;
78
+ it reported reservations that were never published and truncated sizes above
79
+ 65,535.
80
+ - **BREAKING:** `enable_read_last` is part of the shared-memory contract. The
81
+ header magic carries a feature nibble - `'SLQ1'` when the last-published index
82
+ is maintained, `'SLQ0'` when it is not - and every attacher matches it against
83
+ its own configuration, raising `RuntimeError` on a mismatch in either
84
+ direction. The other traits stay process-local and mix freely on one segment.
85
+ - **BREAKING:** Segments created before slick-queue v1.4.0 carry no layout marker
86
+ and are rejected at attach time, rather than served by the reserved-cursor
87
+ fallback.
88
+ - **BREAKING:** Reset detection is opt-in via `enable_reset_check` (default off),
89
+ matching C++. `read()` no longer loads the producer's reservation counter on
90
+ every call.
91
+ - **BREAKING:** `q.traits` is a read-only snapshot, not the class passed in, so
92
+ `q.traits is MyTraits` is now False and its attributes cannot be assigned.
93
+ - **BREAKING:** The private `_last_published_valid` attribute is gone; use
94
+ `q.traits.enable_read_last`.
95
+ - `reserve(0)` raises `ValueError` instead of reserving nothing.
96
+
97
+ ### Added
98
+
99
+ - `QueueTraits`, `default_queue_traits`, `validate_traits()`, and a `traits=`
100
+ argument on `SlickQueue`. Subclass `QueueTraits` to override
101
+ `enable_read_last`, `enable_reset_check`, `enable_loss_detection` or
102
+ `enable_cpu_relax`; differently configured queues coexist in one process.
103
+ `validate_traits()` rejects a malformed traits type at construction, as the C++
104
+ `queue_traits_type` concept does, and like it cannot catch a misspelled
105
+ override.
106
+ - Constants `HEADER_MAGIC_FEATURE_MASK`, `HEADER_MAGIC_READ_LAST` and
107
+ `SLOT_SIZE_OFFSET`.
108
+ - 40 tests. `tests/test_traits.py` (24) covers traits validation, the marker each
109
+ configuration writes, both mismatch directions, unmarked and unknown-feature
110
+ segments, and mutation of a traits class after construction.
111
+ `tests/test_reset_detection.py` (9) asserts a stale reader actually rewinds
112
+ onto the new generation, across both read paths and both memory modes.
113
+ `tests/test_wrap_invariants.py` (7) pins the cursor/size invariant under a
114
+ wrapping producer; three drive the recycle deterministically, because CPython
115
+ switches threads far too rarely to hit the window by racing.
116
+
117
+ ### Fixed
118
+
119
+ - Both `read()` overloads loaded `slot.size` twice, unvalidated, so a producer
120
+ recycling the slot between the loads left the cursor describing a different
121
+ record than the caller was handed - in the shared-cursor overload the two loads
122
+ straddled the claiming CAS. Both now load it once and re-validate the slot
123
+ before committing, as `read_last()` does.
124
+ - `read_last()` could return a `(data, size)` pair describing two different
125
+ records; the slot is now validated before and after the size load.
126
+ - Reset detection never fired for the case it exists to handle. It compared the
127
+ *slot's* index against the reservation counter, but `reset()` clears the
128
+ control array before rewinding the counter, so a stale reader always landed on
129
+ a slot holding an invalid or fresh-low index - neither ahead of the counter.
130
+ The test is now on the reader's cursor, shared by both read paths through
131
+ `_was_reset()`.
132
+ - The reset branch in the shared-cursor `read()` overwrote the cursor with a
133
+ blind store, discarding other consumers' progress; it now rewinds via
134
+ compare-exchange.
135
+ - The single-consumer `read()` charged the loss counter when the overrun was
136
+ spotted rather than on commit, so a retry could count the same overrun twice.
137
+ - `reset()` did not rewind the reservation counter in local memory mode.
138
+ - Traits were re-read from the caller's class on every `publish()` and `read()`,
139
+ while the layout marker and the optional atomics were created once. Mutating
140
+ the class afterwards froze `read_last()` on an `'SLQ1'` segment, or raised
141
+ `AttributeError` from `publish()` and `reset()` because the last-published
142
+ atomic had never been created. They are snapshotted at construction now.
143
+
144
+ ### Changed
145
+
146
+ - Loss detection is gated on `enable_loss_detection`, on by default. C++ keys its
147
+ equivalent off `NDEBUG` and needs two traits types so a debug/release mismatch
148
+ fails to link instead of violating the ODR; Python has no translation units to
149
+ protect, and `__debug__` was rejected because it would zero `loss_count()`
150
+ under `python -O`. It costs +0.2% on a reader that keeps up.
151
+ - `publish()` updates the last-published index unconditionally when the feature
152
+ is on - validating the nibble once at attach time makes the old per-call
153
+ `_last_published_valid` check redundant.
154
+ - Contended CAS loops in `reserve()` and the shared-cursor `read()` back off
155
+ through `_cpu_relax()`, gated on `enable_cpu_relax`.
156
+ - C++ interop programs include the renamed `<slick/queue.hpp>` and use the
157
+ `slick::queue` alias; the old `<slick/queue.h>` path is a deprecation shim.
158
+ - `tests/CMakeLists.txt` registers `test_loss_count.py` (missed in v1.2.0) and
159
+ the three new files. `tests/test_interop.py` bounds each child-result `get()`,
160
+ which previously hung a run forever when a child was killed on a join timeout.
161
+
162
+ ### Notes
163
+
164
+ `slot.size` became a `std::atomic<uint32_t>` in C++ but stays a plain struct
165
+ field here: that change fixed C++ undefined behaviour and compiler re-loading,
166
+ neither of which exists in CPython, and the field is already an aligned 4-byte
167
+ access with an identical wire format.
168
+
169
+ The seqlock validation guarantees the cursor and the returned size describe one
170
+ and the same record. It does **not** make the returned bytes a snapshot: a
171
+ producer writes an element's data before it publishes the slot's new index, so a
172
+ lapped consumer can copy bytes mid-overwrite and no check on the slot can detect
173
+ it. C++ has the identical hazard behind the pointer it returns; `loss_count()` is
174
+ how a consumer detects it. See README.md and API_DIFFERENCES.md.
175
+
176
+ ### Compatibility
177
+
178
+ Verified against C++ slick-queue v2.0.0 binaries: all 10 interop tests pass, and
179
+ a Python-created `'SLQ0'` segment is rejected by a C++ peer built with
180
+ `enable_read_last` on, with the message the C++ side raises.
181
+
182
+ ## [v1.2.0] - 2026-07-09
183
+
184
+ ### Added
185
+ - `loss_count()`: per-instance count of items skipped due to overwrite, matching
186
+ C++ slick-queue v1.5.0 (queue.h:230-236). Counted in both the single-consumer
187
+ read (slot overwritten by a newer generation at the same position) and the
188
+ AtomicCursor work-stealing read (overrun attributed to the claiming consumer).
189
+ Cleared by `reset()`. Note: Python always counts; C++ counts only when
190
+ `SLICK_QUEUE_ENABLE_LOSS_DETECTION` is enabled (debug builds by default).
191
+ - `initial_reading_index()`: cursor for a late-joining consumer - 0 for a newly
192
+ created queue, or the current writing index of an opened queue, matching C++
193
+ slick-queue v1.5.0 (queue.h:242-244).
194
+ - `tests/test_loss_count.py`: 7 tests covering both new methods.
195
+
196
+ These are per-instance API additions only - the shared memory layout is unchanged
197
+ and remains fully compatible with C++ slick-queue v1.x ('SLQ1' format).
198
+
199
+ ## [v1.1.0] - 2026-02-04
200
+
201
+ ### Breaking Changes
202
+ - **BREAKING:** `read_last()` return signature changed from `Optional[bytes]` to `Tuple[Optional[bytes], int]`
203
+ - Now returns `(data, size)` tuple where `size` indicates the number of slots the item occupies
204
+ - Migration: Change `data = q.read_last()` to `data, size = q.read_last()`
205
+ - Provides actual slot size information instead of always returning full `element_size`
206
+
207
+ ### Added
208
+ - Modern format support with efficient `last_published_` atomic tracking
209
+ - New `last_published_` atomic field at offset 16 for O(1) `read_last()` performance
210
+ - Header magic number `0x534C5131` ('SLQ1') at offset 24 for format detection
211
+ - Init state atomic at offset 48 for CAS-based ownership detection
212
+ - Automatic format detection distinguishes modern vs legacy queues
213
+ - CAS-based queue ownership detection matching C++ implementation (queue.h:618-648)
214
+ - Helper methods `_wait_for_shared_memory_ready()` and `_detect_format_version()`
215
+ - Comprehensive test suites:
216
+ - `test_read_last.py`: 10 tests covering empty queue, single/multiple items, multi-slot publishes, concurrent publishes, reset, shared memory, wraparound, variable sizes
217
+ - `test_read_last_interop.py`: 5 tests for Python ↔ C++ interoperability
218
+ - `test_modern_format.py`: 6 tests for format compatibility and memory layout verification
219
+ - C++ test program `cpp_read_last_tester.cpp` for read_last() interoperability testing
220
+ - New constants: `SIZE_OFFSET`, `ELEMENT_SIZE_OFFSET`, `LAST_PUBLISHED_OFFSET`, `HEADER_MAGIC_OFFSET`, `HEADER_MAGIC`, `INIT_STATE_OFFSET`, `INIT_STATE_*`, `K_INVALID_INDEX`
221
+ - Documentation: `READ_LAST_INTEROP.md` comprehensive interoperability guide
222
+
223
+ ### Changed
224
+ - Updated shared memory header layout to match C++ slick-queue v1.x:
225
+ - Reserved info: 8 bytes at offset 0-7 (compact from previous 32-byte padded format)
226
+ - Size: 4 bytes at offset 8-11 (moved from offset 32)
227
+ - Element size: 4 bytes at offset 12-15 (moved from offset 36)
228
+ - Last published: 8 bytes at offset 16-23 (NEW)
229
+ - Header magic: 4 bytes at offset 24-27 (NEW)
230
+ - Init state: 4 bytes at offset 48-51 (NEW)
231
+ - `publish()` now updates `last_published_` atomic using CAS loop in modern format (queue.h:331-337)
232
+ - `reset()` now resets `last_published_` to `K_INVALID_INDEX` when in modern format
233
+ - `close()` properly releases `_atomic_last_published` wrapper
234
+ - `AtomicReservedInfo.RESERVED_INFO_FMT` simplified from 32-byte to 8-byte format
235
+ - Repository name changed from slick_queue_py to slick-queue-py
236
+ - Updated README.md with new `read_last()` API documentation
237
+ - Enhanced CMakeLists.txt with new test targets
238
+
239
+ ### Fixed
240
+ - `read_last()` now returns actual slot size instead of always returning full `element_size`
241
+ - Test queue names shortened to comply with macOS 31-character shared memory name limit
242
+
243
+ ### Performance
244
+ - O(1) `read_last()` via direct atomic load (improved from reserved_info calculation)
245
+ - Reduced header size: 8-byte reserved_info vs 32-byte padded (24 bytes saved)
246
+ - Lock-free `last_published_` tracking enables efficient concurrent access
247
+
248
+ ### Compatibility
249
+ - **Backward Compatible:** Automatically detects and supports legacy queue format
250
+ - **C++ Interop:** Full compatibility with C++ slick-queue modern format
251
+ - Python and C++ can share queues created by either language
252
+ - All existing tests continue to pass (atomic ops, cursors, local mode, multi-producer, interop)
253
+
254
+ ## [v1.0.1] - 2025-12-27
255
+
256
+ ### Fixed
257
+ - Fixed extra `/` prefix being added to shared memory names on POSIX systems (Linux/macOS)
258
+ - Fixed test failures on Linux and macOS platforms
259
+
260
+ ### Added
261
+ - macOS shared memory name length validation (31 character limit including `/` prefix)
262
+ - C++ extension support for Linux/macOS platforms with automatic fallback to native methods if unavailable
263
+ - `atomic_store_64` and `atomic_cas_64` functions to C++ extension for improved cross-platform consistency
264
+
265
+ ### Changed
266
+ - Linux/macOS now prioritize C++ extension for atomic operations, falling back to native methods (`__sync_val_compare_and_swap` or `libatomic`) if extension is not available
267
+ - Improved atomic operation reliability across all platforms
268
+ - Enhanced test suite for better cross-platform compatibility
269
+ - Publishing: Updated PyPI builds to use `cibuildwheel` for proper manylinux wheel generation
270
+ - Ignore conda publish failing for now
271
+
272
+ ## [v1.0.0] - 2025-12-26
273
+
274
+ - Initial release
275
+ - Python implementation of SlickQueue - a lock-free multi-producer multi-consumer (MPMC) queue with C++ interoperability through shared memory.
276
+ - Windows, Linux, and macOS support