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.
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/API_DIFFERENCES.md +113 -13
- slick_queue_py-2.1.0/CHANGELOG.md +276 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/PKG-INFO +226 -21
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/README.md +225 -20
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/slick_queue_py.egg-info/SOURCES.txt +5 -1
- slick_queue_py-2.1.0/slick_queue_py.py +1232 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/cpp_consumer.cpp +2 -2
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/cpp_multi_producer.cpp +2 -2
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/cpp_producer.cpp +2 -2
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/cpp_read_last_tester.cpp +3 -3
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/cpp_work_stealing_consumer.cpp +2 -2
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/test_interop.py +33 -10
- slick_queue_py-2.1.0/tests/test_items_per_slot.py +485 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/test_modern_format.py +17 -7
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/test_multi_producer.py +56 -10
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/test_read_last_interop.py +3 -3
- slick_queue_py-2.1.0/tests/test_reset_detection.py +272 -0
- slick_queue_py-2.1.0/tests/test_traits.py +582 -0
- slick_queue_py-2.1.0/tests/test_wrap_invariants.py +406 -0
- slick_queue_py-1.2.0/CHANGELOG.md +0 -97
- slick_queue_py-1.2.0/slick_queue_py.py +0 -770
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/.github/FUNDING.yml +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/.github/workflows/ci.yml +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/.github/workflows/publish-conda.yml +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/.github/workflows/publish-pypi.yml +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/.github/workflows/publish-release.yml +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/.github/workflows/release.yml +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/.gitignore +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/.pypirc.template +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/BUILDING.md +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/CMakeLists.txt +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/LICENSE +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/MANIFEST.in +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/atomic_ops.py +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/atomic_ops_ext.cpp +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/conda.recipe/meta.yaml +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/pyproject.toml +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/requirements.txt +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/setup.cfg +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/setup.py +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/cleanup_shm.py +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/run_test.py +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/test_atomic_cursor.py +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/test_atomic_ops.py +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/test_local_mode.py +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/test_loss_count.py +0 -0
- {slick_queue_py-1.2.0 → slick_queue_py-2.1.0}/tests/test_queue.py +0 -0
- {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::
|
|
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::
|
|
22
|
+
slick::queue<uint8_t> q(queue_size, queue_name);
|
|
21
23
|
|
|
22
24
|
// Open existing queue
|
|
23
|
-
slick::
|
|
25
|
+
slick::queue<uint8_t> q(queue_name);
|
|
24
26
|
|
|
25
27
|
// Local memory mode
|
|
26
|
-
slick::
|
|
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.
|
|
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
|
-
###
|
|
200
|
+
### 6. Type Safety
|
|
107
201
|
|
|
108
202
|
**C++:**
|
|
109
203
|
```cpp
|
|
110
204
|
// Template-based, compile-time type safety
|
|
111
|
-
slick::
|
|
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
|
-
###
|
|
223
|
+
### 7. Resource Management
|
|
130
224
|
|
|
131
225
|
**C++:**
|
|
132
226
|
```cpp
|
|
133
227
|
{
|
|
134
|
-
slick::
|
|
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.
|
|
259
|
+
#include <slick/queue.hpp>
|
|
166
260
|
#include <cstring>
|
|
167
261
|
|
|
168
262
|
int main() {
|
|
169
|
-
slick::
|
|
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.
|
|
313
|
+
#include <slick/queue.hpp>
|
|
220
314
|
#include <iostream>
|
|
221
315
|
#include <cstring>
|
|
222
316
|
|
|
223
317
|
int main() {
|
|
224
|
-
slick::
|
|
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
|