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