slick-stream-buffer-py 0.1.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_stream_buffer_py-0.1.0 → slick_stream_buffer_py-2.0.0}/API_DIFFERENCES.md +53 -10
- {slick_stream_buffer_py-0.1.0 → slick_stream_buffer_py-2.0.0}/PKG-INFO +93 -10
- {slick_stream_buffer_py-0.1.0 → slick_stream_buffer_py-2.0.0}/README.md +92 -9
- {slick_stream_buffer_py-0.1.0 → slick_stream_buffer_py-2.0.0}/slick_stream_buffer_py.py +380 -131
- {slick_stream_buffer_py-0.1.0 → slick_stream_buffer_py-2.0.0}/ssb_atomic_ops.py +2 -2
- {slick_stream_buffer_py-0.1.0 → slick_stream_buffer_py-2.0.0}/tests/test_interop.py +49 -3
- {slick_stream_buffer_py-0.1.0 → slick_stream_buffer_py-2.0.0}/tests/test_local_mode.py +171 -36
- slick_stream_buffer_py-2.0.0/tests/test_shm_mode.py +735 -0
- slick_stream_buffer_py-0.1.0/tests/test_shm_mode.py +0 -372
- {slick_stream_buffer_py-0.1.0 → slick_stream_buffer_py-2.0.0}/LICENSE +0 -0
- {slick_stream_buffer_py-0.1.0 → slick_stream_buffer_py-2.0.0}/MANIFEST.in +0 -0
- {slick_stream_buffer_py-0.1.0 → slick_stream_buffer_py-2.0.0}/pyproject.toml +0 -0
- {slick_stream_buffer_py-0.1.0 → slick_stream_buffer_py-2.0.0}/requirements.txt +0 -0
- {slick_stream_buffer_py-0.1.0 → slick_stream_buffer_py-2.0.0}/setup.cfg +0 -0
- {slick_stream_buffer_py-0.1.0 → slick_stream_buffer_py-2.0.0}/setup.py +0 -0
- {slick_stream_buffer_py-0.1.0 → slick_stream_buffer_py-2.0.0}/slick_stream_buffer_py.egg-info/SOURCES.txt +0 -0
- {slick_stream_buffer_py-0.1.0 → slick_stream_buffer_py-2.0.0}/ssb_atomic_ops_ext.cpp +0 -0
- {slick_stream_buffer_py-0.1.0 → slick_stream_buffer_py-2.0.0}/tests/test_atomic_ops.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# API Differences: Python vs C++
|
|
2
2
|
|
|
3
3
|
The Python implementation is binary-compatible with the C++ `slick::SlickStreamBuffer`
|
|
4
|
-
at the shared memory level — every byte, offset, and
|
|
5
|
-
*language-level* API differs in the following ways.
|
|
4
|
+
(`slick/stream_buffer.hpp` v2.0.0) at the shared memory level — every byte, offset, and
|
|
5
|
+
memory ordering matches. The *language-level* API differs in the following ways.
|
|
6
6
|
|
|
7
7
|
## Construction
|
|
8
8
|
|
|
@@ -11,9 +11,16 @@ at the shared memory level — every byte, offset, and memory ordering matches.
|
|
|
11
11
|
| `stream_buffer buf(capacity, control_size)` | `SlickStreamBuffer(capacity=..., control_size=...)` |
|
|
12
12
|
| `stream_buffer buf(capacity, control_size, "name")` | `SlickStreamBuffer(capacity=..., control_size=..., name="name")` |
|
|
13
13
|
| `stream_buffer buf("name")` | `SlickStreamBuffer(name="name")` |
|
|
14
|
+
| `stream_buffer::remove("name")` | `SlickStreamBuffer.remove("name")` (static, returns `bool`) |
|
|
14
15
|
| throws `std::invalid_argument` (bad sizes) | raises `ValueError` |
|
|
16
|
+
| throws `std::length_error` (geometry past the address space) | raises `ValueError` |
|
|
15
17
|
| throws `std::runtime_error` (shm failures) | raises `RuntimeError` (or `FileNotFoundError` when opening a missing segment) |
|
|
16
18
|
|
|
19
|
+
Both implementations take initialization authority from the OS, not from the in-band
|
|
20
|
+
init-state slot: C++ uses `slick::shm`'s `is_creator()`, Python uses whether
|
|
21
|
+
`SharedMemory(create=True)` succeeded. Both reduce to `O_CREAT|O_EXCL` on POSIX and
|
|
22
|
+
`ERROR_ALREADY_EXISTS` on Windows, so neither ever initializes a segment it did not create.
|
|
23
|
+
|
|
17
24
|
## Consumer cursor is passed by value
|
|
18
25
|
|
|
19
26
|
C++ updates the cursor through a reference; Python has no by-reference ints, so the
|
|
@@ -34,6 +41,31 @@ data, length, cursor = buf.read(cursor) # updated cursor returned
|
|
|
34
41
|
`read()` returns `(None, 0, cursor)` where C++ returns `{nullptr, 0}`.
|
|
35
42
|
`read_last()` returns `(None, 0)` / `(bytes, length)`.
|
|
36
43
|
|
|
44
|
+
## Read traits are a runtime argument, not a template parameter
|
|
45
|
+
|
|
46
|
+
C++ configures `read()` with a template parameter, so the flags are compile-time constants
|
|
47
|
+
and a misconfiguration cannot cross a translation unit. Python has no equivalent, so the
|
|
48
|
+
same traits class is passed as an ordinary second argument:
|
|
49
|
+
|
|
50
|
+
```cpp
|
|
51
|
+
// C++
|
|
52
|
+
struct counting : slick::read_traits { static constexpr bool count_loss = true; };
|
|
53
|
+
auto [data, len] = buf.read<counting>(cursor);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# Python
|
|
58
|
+
from slick_stream_buffer_py import counting_read_traits
|
|
59
|
+
data, length, cursor = buf.read(cursor, counting_read_traits)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The flags and their defaults are identical (`detect_reset = True`, `count_loss = False`),
|
|
63
|
+
and subclassing `read_traits` works the same way as deriving from `slick::read_traits`.
|
|
64
|
+
Python also exports `ReadTraits` / `CountingReadTraits` as PEP 8 spellings of the same two
|
|
65
|
+
classes. The performance argument for making `count_loss` opt-in is a C++ one (~20% of
|
|
66
|
+
`read()` on MSVC); in Python the cost is two attribute lookups per call, but the *semantics*
|
|
67
|
+
match so that `loss_count()` means the same thing on both sides.
|
|
68
|
+
|
|
37
69
|
## Zero-copy vs copies
|
|
38
70
|
|
|
39
71
|
| Operation | C++ | Python |
|
|
@@ -52,11 +84,12 @@ Python API safe by default.
|
|
|
52
84
|
`prepare()`/`data()` memoryviews are invalidated by the next `prepare()` (which may
|
|
53
85
|
relocate the region) — same semantics as C++ flat_buffer-style invalidation.
|
|
54
86
|
|
|
55
|
-
## prepare() overflow
|
|
87
|
+
## prepare() / consume() overflow
|
|
56
88
|
|
|
57
|
-
C++ throws `std::length_error` when `size() + n > capacity()`; Python raises
|
|
58
|
-
`
|
|
59
|
-
|
|
89
|
+
C++ throws `std::length_error` when `size() + n > capacity()`; Python raises `ValueError`.
|
|
90
|
+
`consume()` behaves the same on both sides — a message of 4 GiB or more is rejected before
|
|
91
|
+
any state moves, so nothing is published and the bytes stay readable — but C++ throws
|
|
92
|
+
`std::length_error` and Python raises `ValueError`.
|
|
60
93
|
|
|
61
94
|
## consume() return value
|
|
62
95
|
|
|
@@ -66,9 +99,10 @@ published (`data is None`), matching C++ `operator bool`.
|
|
|
66
99
|
|
|
67
100
|
## loss_count()
|
|
68
101
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
both implementations, not
|
|
102
|
+
Identical semantics on both sides: losses are counted only for reads made with a traits
|
|
103
|
+
whose `count_loss` is true, so `loss_count()` reads 0 under the defaults. Records are still
|
|
104
|
+
skipped correctly either way. The counter is per-instance in both implementations, not
|
|
105
|
+
shared through the segment.
|
|
72
106
|
|
|
73
107
|
## Memory orderings
|
|
74
108
|
|
|
@@ -84,10 +118,19 @@ release/acquire — strictly stronger, therefore still correct.
|
|
|
84
118
|
Python adds explicit lifecycle management that C++ handles via RAII:
|
|
85
119
|
|
|
86
120
|
- `close()` — detach from the segment (does not delete it)
|
|
87
|
-
- `unlink()` — delete the segment (call once, from the owner,
|
|
121
|
+
- `unlink()` — delete the segment this instance is attached to (call once, from the owner,
|
|
122
|
+
after all users closed - including this instance, so `close()` then `unlink()` is fine)
|
|
88
123
|
- context manager support: `with SlickStreamBuffer(...) as buf: ...` (closes on exit)
|
|
89
124
|
- `get_shm_name()` — the exact segment name to pass to C++ (`/`-prefixed on POSIX)
|
|
90
125
|
|
|
126
|
+
`remove(name)` is the one lifecycle call both sides share, with the same contract: unlink a
|
|
127
|
+
segment by name without attaching to it, returning `True` on success and, on Windows, always
|
|
128
|
+
`True` because a section dies with its last handle. Python routes it through
|
|
129
|
+
`_posixshmem.shm_unlink`, then drops the name from `multiprocessing`'s resource tracker if -
|
|
130
|
+
and only if - this process is what registered it. A name registered elsewhere stays that
|
|
131
|
+
owner's to account for: the tracker answers an unregister for a name it never saw by raising
|
|
132
|
+
`KeyError` and printing a traceback from its own process.
|
|
133
|
+
|
|
91
134
|
On POSIX with Python >= 3.13, attaching to an existing segment uses
|
|
92
135
|
`SharedMemory(track=False)` so Python's resource tracker never unlinks a segment the
|
|
93
136
|
process does not own. On older Pythons the tracker may log a harmless warning at exit
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: slick-stream-buffer-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Lock-free SPMC byte stream buffer with C++ interoperability via shared memory
|
|
5
5
|
Home-page: https://github.com/SlickQuant/slick-stream-buffer-py
|
|
6
6
|
Author: Slick Quant
|
|
@@ -43,6 +43,10 @@ a lock-free single-producer multi-consumer (SPMC) byte stream buffer with shared
|
|
|
43
43
|
consumer (or vice versa) communicate seamlessly through the same shared memory segment, using
|
|
44
44
|
the same `std::atomic` synchronization primitives.
|
|
45
45
|
|
|
46
|
+
Tracks C++ `slick/stream_buffer.hpp` **v2.0.0** (header magic `'SSB1'`); the two carry the same
|
|
47
|
+
version number. The shared memory format is unchanged from v1.x, so a segment written by an
|
|
48
|
+
older process is still readable.
|
|
49
|
+
|
|
46
50
|
## Features
|
|
47
51
|
|
|
48
52
|
- **Byte stream, message records**: the producer writes raw bytes (`prepare`/`commit`) and
|
|
@@ -51,10 +55,13 @@ the same `std::atomic` synchronization primitives.
|
|
|
51
55
|
- **Lock-free SPMC**: one producer, any number of consumers, no locks anywhere
|
|
52
56
|
- **Binary-compatible with C++**: identical 64-byte header, 32-byte control records, and
|
|
53
57
|
acquire/release memory orderings (`slick/stream_buffer.hpp`, header magic `'SSB1'`)
|
|
58
|
+
- **Never touches a segment it does not own**: only the process that actually creates a
|
|
59
|
+
segment initializes it, and an existing one is validated (magic, geometry, mapped size)
|
|
60
|
+
before a byte is written
|
|
54
61
|
- **Shared memory IPC**: built on `multiprocessing.shared_memory`, matching the C++
|
|
55
62
|
slick-shm naming conventions on Windows, Linux, and macOS
|
|
56
|
-
- **Lossy by design**: slow consumers skip overwritten data
|
|
57
|
-
|
|
63
|
+
- **Lossy by design**: slow consumers skip overwritten data instead of blocking the producer,
|
|
64
|
+
and count it when you ask them to (see [Read traits](#read-traits))
|
|
58
65
|
- **Local memory mode**: same API without shared memory for single-process use
|
|
59
66
|
- **No runtime dependencies**: Python 3.8+ standard library plus a small bundled C++
|
|
60
67
|
extension for the atomics
|
|
@@ -108,6 +115,33 @@ while True:
|
|
|
108
115
|
handle_package(data, length)
|
|
109
116
|
```
|
|
110
117
|
|
|
118
|
+
### Read traits
|
|
119
|
+
|
|
120
|
+
`read()` takes a per-call configuration, mirroring the C++ template parameter:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
class read_traits:
|
|
124
|
+
detect_reset = True # resync a cursor that outlived a reset()
|
|
125
|
+
count_loss = False # accumulate loss_count()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Ordinary callers write `buf.read(cursor)` and never name a traits class. Loss counting is
|
|
129
|
+
opt-in - in C++ it measured ~20% of `read()` even in a run that lost nothing - so subclass, or
|
|
130
|
+
use the bundled `counting_read_traits`, when you need it:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from slick_stream_buffer_py import counting_read_traits
|
|
134
|
+
|
|
135
|
+
data, length, cursor = stream.read(cursor, counting_read_traits)
|
|
136
|
+
print(stream.loss_count())
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
So `loss_count()` stays 0 unless you read through traits that ask for it - records are still
|
|
140
|
+
skipped correctly, only the counter is silent. `detect_reset` is on by default; turn it off
|
|
141
|
+
if you never call `reset()`.
|
|
142
|
+
|
|
143
|
+
`ReadTraits` / `CountingReadTraits` are PEP 8 aliases of the same two classes.
|
|
144
|
+
|
|
111
145
|
### Local memory mode (single process)
|
|
112
146
|
|
|
113
147
|
```python
|
|
@@ -143,6 +177,44 @@ Shared memory naming: use `get_shm_name()` to obtain the exact name to pass to C
|
|
|
143
177
|
(on POSIX it includes the leading `/` that `shm_open()` requires; on Windows it is the
|
|
144
178
|
raw name).
|
|
145
179
|
|
|
180
|
+
### Shared-memory lifecycle
|
|
181
|
+
|
|
182
|
+
Only the process that actually creates a segment initializes it - `SharedMemory(create=True)`
|
|
183
|
+
is the OS-backed answer (`O_CREAT|O_EXCL` on POSIX, `ERROR_ALREADY_EXISTS` on Windows), so a
|
|
184
|
+
name that collides with another application's segment is never overwritten. Constructing with
|
|
185
|
+
a geometry over a live segment attaches to it instead: `own_buffer` is then `False` and the
|
|
186
|
+
existing cursors are left alone.
|
|
187
|
+
|
|
188
|
+
A crashed creator can leave a segment behind. On POSIX the name survives the process, so:
|
|
189
|
+
|
|
190
|
+
- if it died **part-way through initialization**, the segment is stuck in the `INITIALIZING`
|
|
191
|
+
state and every later creator and opener fails construction after a 2-second wait;
|
|
192
|
+
- if it died **after initialization**, the segment is intact and gets attached with the dead
|
|
193
|
+
producer's cursors still in it.
|
|
194
|
+
|
|
195
|
+
Neither is recovered automatically: nothing portable can distinguish a dead creator from a
|
|
196
|
+
slow one - a PID stamped in the header means nothing across PID namespaces and is unreliable
|
|
197
|
+
under PID reuse - so taking a segment over is an explicit decision. The wedged-segment error
|
|
198
|
+
names the step. Call `remove()` once at startup, when the application knows no other process
|
|
199
|
+
is using the segment:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
SlickStreamBuffer.remove("my_stream") # clear anything a previous run left
|
|
203
|
+
buf = SlickStreamBuffer(capacity=1 << 20, control_size=4096, name="my_stream")
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
On POSIX `remove()` unlinks immediately: the next creator gets a fresh segment, while any
|
|
207
|
+
process still mapped to the old one keeps reading the orphaned copy. On Windows it is a no-op
|
|
208
|
+
returning `True` - a section there disappears once the last handle closes.
|
|
209
|
+
|
|
210
|
+
One Python-only caveat: on POSIX before Python 3.13, `multiprocessing`'s resource tracker
|
|
211
|
+
unlinks every segment the process mapped when it exits, including ones it only attached to.
|
|
212
|
+
Python 3.13+ attaches with `SharedMemory(track=False)`, which is why 3.13 or newer is
|
|
213
|
+
recommended when several processes share a segment. `remove()` unlinks the segment either way,
|
|
214
|
+
and retracts the tracker's registration only when this process is the one that registered it -
|
|
215
|
+
a name registered by some other process, or by a `SharedMemory` you built yourself, stays that
|
|
216
|
+
owner's to account for. See [API_DIFFERENCES.md](API_DIFFERENCES.md).
|
|
217
|
+
|
|
146
218
|
### Shared memory layout
|
|
147
219
|
|
|
148
220
|
```
|
|
@@ -172,12 +244,13 @@ Total segment size: `64 + 32 * control_size + capacity`.
|
|
|
172
244
|
| `consume(n) -> PublishedRecord` | producer | publish the first n readable bytes as ONE record (clamped) |
|
|
173
245
|
| `discard()` | producer | drop unconsumed + prepared bytes without publishing |
|
|
174
246
|
| `data() -> memoryview` / `size()` | producer | the committed-but-unconsumed region |
|
|
175
|
-
| `read(cursor) -> (data, length, cursor)` | consumer | next record, or `(None, 0, cursor)`; skips lost records |
|
|
247
|
+
| `read(cursor, traits=read_traits) -> (data, length, cursor)` | consumer | next record, or `(None, 0, cursor)`; skips lost records |
|
|
176
248
|
| `read_last() -> (data, length)` | consumer | newest record without a cursor |
|
|
177
249
|
| `initial_reading_index()` | consumer | cursor for a late joiner (skips history) |
|
|
178
|
-
| `loss_count()` | consumer | records skipped by this instance
|
|
250
|
+
| `loss_count()` | consumer | records skipped by this instance, counting only reads made with `count_loss` traits |
|
|
179
251
|
| `reset()` | owner | clear all state (not thread-safe) |
|
|
180
|
-
| `close()` / `unlink()` | lifecycle | detach / delete
|
|
252
|
+
| `close()` / `unlink()` | lifecycle | detach / delete this instance's segment |
|
|
253
|
+
| `SlickStreamBuffer.remove(name)` | lifecycle | unlink a segment by name - the stale-segment recovery step |
|
|
181
254
|
|
|
182
255
|
See [API_DIFFERENCES.md](API_DIFFERENCES.md) for the exact deviations from the C++ API
|
|
183
256
|
(cursor passed by value, bytes copies vs pointers, exceptions).
|
|
@@ -187,10 +260,13 @@ See [API_DIFFERENCES.md](API_DIFFERENCES.md) for the exact deviations from the C
|
|
|
187
260
|
- Producer methods (`prepare`/`commit`/`consume`/`discard`/`data`/`size`/`reset`) must be
|
|
188
261
|
called from a single thread.
|
|
189
262
|
- The buffer is lossy: if the producer outruns a consumer by more than the control ring or
|
|
190
|
-
data ring size, the consumer skips ahead
|
|
263
|
+
data ring size, the consumer skips ahead. The loss is counted only for reads made with
|
|
264
|
+
traits whose `count_loss` is true (see [Read traits](#read-traits)).
|
|
191
265
|
- `prepare()` may relocate the readable region: memoryviews previously returned by `data()`
|
|
192
266
|
or `prepare()` are invalidated.
|
|
193
|
-
- A single message (one `consume()` call) is limited to < 4 GiB
|
|
267
|
+
- A single message (one `consume()` call) is limited to < 4 GiB: `consume()` raises
|
|
268
|
+
`ValueError` rather than publishing a truncated record length. It raises before touching any
|
|
269
|
+
state, so the bytes stay readable and can go out as several smaller records.
|
|
194
270
|
|
|
195
271
|
## Building and Testing
|
|
196
272
|
|
|
@@ -203,7 +279,7 @@ python tests/test_atomic_ops.py
|
|
|
203
279
|
python tests/test_local_mode.py
|
|
204
280
|
python tests/test_shm_mode.py
|
|
205
281
|
|
|
206
|
-
# 3. C++ interop tests (requires CMake + a C++20 compiler)
|
|
282
|
+
# 3. C++ interop tests (requires CMake 3.21+ and a C++20 compiler)
|
|
207
283
|
cmake -S . -B build
|
|
208
284
|
cmake --build build --config Debug
|
|
209
285
|
cd build && ctest -C Debug --output-on-failure
|
|
@@ -220,7 +296,14 @@ cmake -S . -B build \
|
|
|
220
296
|
-DFETCHCONTENT_SOURCE_DIR_SLICK-SHM=/path/to/slick-shm
|
|
221
297
|
```
|
|
222
298
|
|
|
223
|
-
|
|
299
|
+
The 4 GiB record-limit test needs a 4 GiB ring, so it is skipped unless you opt in:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
SSB_PY_TEST_4GIB=1 python tests/test_local_mode.py
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
If a failed test run leaves segments behind: `python tests/cleanup_shm.py`, or
|
|
306
|
+
`SlickStreamBuffer.remove(name)` for one you know by name.
|
|
224
307
|
|
|
225
308
|
## Related Projects
|
|
226
309
|
|
|
@@ -7,6 +7,10 @@ a lock-free single-producer multi-consumer (SPMC) byte stream buffer with shared
|
|
|
7
7
|
consumer (or vice versa) communicate seamlessly through the same shared memory segment, using
|
|
8
8
|
the same `std::atomic` synchronization primitives.
|
|
9
9
|
|
|
10
|
+
Tracks C++ `slick/stream_buffer.hpp` **v2.0.0** (header magic `'SSB1'`); the two carry the same
|
|
11
|
+
version number. The shared memory format is unchanged from v1.x, so a segment written by an
|
|
12
|
+
older process is still readable.
|
|
13
|
+
|
|
10
14
|
## Features
|
|
11
15
|
|
|
12
16
|
- **Byte stream, message records**: the producer writes raw bytes (`prepare`/`commit`) and
|
|
@@ -15,10 +19,13 @@ the same `std::atomic` synchronization primitives.
|
|
|
15
19
|
- **Lock-free SPMC**: one producer, any number of consumers, no locks anywhere
|
|
16
20
|
- **Binary-compatible with C++**: identical 64-byte header, 32-byte control records, and
|
|
17
21
|
acquire/release memory orderings (`slick/stream_buffer.hpp`, header magic `'SSB1'`)
|
|
22
|
+
- **Never touches a segment it does not own**: only the process that actually creates a
|
|
23
|
+
segment initializes it, and an existing one is validated (magic, geometry, mapped size)
|
|
24
|
+
before a byte is written
|
|
18
25
|
- **Shared memory IPC**: built on `multiprocessing.shared_memory`, matching the C++
|
|
19
26
|
slick-shm naming conventions on Windows, Linux, and macOS
|
|
20
|
-
- **Lossy by design**: slow consumers skip overwritten data
|
|
21
|
-
|
|
27
|
+
- **Lossy by design**: slow consumers skip overwritten data instead of blocking the producer,
|
|
28
|
+
and count it when you ask them to (see [Read traits](#read-traits))
|
|
22
29
|
- **Local memory mode**: same API without shared memory for single-process use
|
|
23
30
|
- **No runtime dependencies**: Python 3.8+ standard library plus a small bundled C++
|
|
24
31
|
extension for the atomics
|
|
@@ -72,6 +79,33 @@ while True:
|
|
|
72
79
|
handle_package(data, length)
|
|
73
80
|
```
|
|
74
81
|
|
|
82
|
+
### Read traits
|
|
83
|
+
|
|
84
|
+
`read()` takes a per-call configuration, mirroring the C++ template parameter:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
class read_traits:
|
|
88
|
+
detect_reset = True # resync a cursor that outlived a reset()
|
|
89
|
+
count_loss = False # accumulate loss_count()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Ordinary callers write `buf.read(cursor)` and never name a traits class. Loss counting is
|
|
93
|
+
opt-in - in C++ it measured ~20% of `read()` even in a run that lost nothing - so subclass, or
|
|
94
|
+
use the bundled `counting_read_traits`, when you need it:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from slick_stream_buffer_py import counting_read_traits
|
|
98
|
+
|
|
99
|
+
data, length, cursor = stream.read(cursor, counting_read_traits)
|
|
100
|
+
print(stream.loss_count())
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
So `loss_count()` stays 0 unless you read through traits that ask for it - records are still
|
|
104
|
+
skipped correctly, only the counter is silent. `detect_reset` is on by default; turn it off
|
|
105
|
+
if you never call `reset()`.
|
|
106
|
+
|
|
107
|
+
`ReadTraits` / `CountingReadTraits` are PEP 8 aliases of the same two classes.
|
|
108
|
+
|
|
75
109
|
### Local memory mode (single process)
|
|
76
110
|
|
|
77
111
|
```python
|
|
@@ -107,6 +141,44 @@ Shared memory naming: use `get_shm_name()` to obtain the exact name to pass to C
|
|
|
107
141
|
(on POSIX it includes the leading `/` that `shm_open()` requires; on Windows it is the
|
|
108
142
|
raw name).
|
|
109
143
|
|
|
144
|
+
### Shared-memory lifecycle
|
|
145
|
+
|
|
146
|
+
Only the process that actually creates a segment initializes it - `SharedMemory(create=True)`
|
|
147
|
+
is the OS-backed answer (`O_CREAT|O_EXCL` on POSIX, `ERROR_ALREADY_EXISTS` on Windows), so a
|
|
148
|
+
name that collides with another application's segment is never overwritten. Constructing with
|
|
149
|
+
a geometry over a live segment attaches to it instead: `own_buffer` is then `False` and the
|
|
150
|
+
existing cursors are left alone.
|
|
151
|
+
|
|
152
|
+
A crashed creator can leave a segment behind. On POSIX the name survives the process, so:
|
|
153
|
+
|
|
154
|
+
- if it died **part-way through initialization**, the segment is stuck in the `INITIALIZING`
|
|
155
|
+
state and every later creator and opener fails construction after a 2-second wait;
|
|
156
|
+
- if it died **after initialization**, the segment is intact and gets attached with the dead
|
|
157
|
+
producer's cursors still in it.
|
|
158
|
+
|
|
159
|
+
Neither is recovered automatically: nothing portable can distinguish a dead creator from a
|
|
160
|
+
slow one - a PID stamped in the header means nothing across PID namespaces and is unreliable
|
|
161
|
+
under PID reuse - so taking a segment over is an explicit decision. The wedged-segment error
|
|
162
|
+
names the step. Call `remove()` once at startup, when the application knows no other process
|
|
163
|
+
is using the segment:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
SlickStreamBuffer.remove("my_stream") # clear anything a previous run left
|
|
167
|
+
buf = SlickStreamBuffer(capacity=1 << 20, control_size=4096, name="my_stream")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
On POSIX `remove()` unlinks immediately: the next creator gets a fresh segment, while any
|
|
171
|
+
process still mapped to the old one keeps reading the orphaned copy. On Windows it is a no-op
|
|
172
|
+
returning `True` - a section there disappears once the last handle closes.
|
|
173
|
+
|
|
174
|
+
One Python-only caveat: on POSIX before Python 3.13, `multiprocessing`'s resource tracker
|
|
175
|
+
unlinks every segment the process mapped when it exits, including ones it only attached to.
|
|
176
|
+
Python 3.13+ attaches with `SharedMemory(track=False)`, which is why 3.13 or newer is
|
|
177
|
+
recommended when several processes share a segment. `remove()` unlinks the segment either way,
|
|
178
|
+
and retracts the tracker's registration only when this process is the one that registered it -
|
|
179
|
+
a name registered by some other process, or by a `SharedMemory` you built yourself, stays that
|
|
180
|
+
owner's to account for. See [API_DIFFERENCES.md](API_DIFFERENCES.md).
|
|
181
|
+
|
|
110
182
|
### Shared memory layout
|
|
111
183
|
|
|
112
184
|
```
|
|
@@ -136,12 +208,13 @@ Total segment size: `64 + 32 * control_size + capacity`.
|
|
|
136
208
|
| `consume(n) -> PublishedRecord` | producer | publish the first n readable bytes as ONE record (clamped) |
|
|
137
209
|
| `discard()` | producer | drop unconsumed + prepared bytes without publishing |
|
|
138
210
|
| `data() -> memoryview` / `size()` | producer | the committed-but-unconsumed region |
|
|
139
|
-
| `read(cursor) -> (data, length, cursor)` | consumer | next record, or `(None, 0, cursor)`; skips lost records |
|
|
211
|
+
| `read(cursor, traits=read_traits) -> (data, length, cursor)` | consumer | next record, or `(None, 0, cursor)`; skips lost records |
|
|
140
212
|
| `read_last() -> (data, length)` | consumer | newest record without a cursor |
|
|
141
213
|
| `initial_reading_index()` | consumer | cursor for a late joiner (skips history) |
|
|
142
|
-
| `loss_count()` | consumer | records skipped by this instance
|
|
214
|
+
| `loss_count()` | consumer | records skipped by this instance, counting only reads made with `count_loss` traits |
|
|
143
215
|
| `reset()` | owner | clear all state (not thread-safe) |
|
|
144
|
-
| `close()` / `unlink()` | lifecycle | detach / delete
|
|
216
|
+
| `close()` / `unlink()` | lifecycle | detach / delete this instance's segment |
|
|
217
|
+
| `SlickStreamBuffer.remove(name)` | lifecycle | unlink a segment by name - the stale-segment recovery step |
|
|
145
218
|
|
|
146
219
|
See [API_DIFFERENCES.md](API_DIFFERENCES.md) for the exact deviations from the C++ API
|
|
147
220
|
(cursor passed by value, bytes copies vs pointers, exceptions).
|
|
@@ -151,10 +224,13 @@ See [API_DIFFERENCES.md](API_DIFFERENCES.md) for the exact deviations from the C
|
|
|
151
224
|
- Producer methods (`prepare`/`commit`/`consume`/`discard`/`data`/`size`/`reset`) must be
|
|
152
225
|
called from a single thread.
|
|
153
226
|
- The buffer is lossy: if the producer outruns a consumer by more than the control ring or
|
|
154
|
-
data ring size, the consumer skips ahead
|
|
227
|
+
data ring size, the consumer skips ahead. The loss is counted only for reads made with
|
|
228
|
+
traits whose `count_loss` is true (see [Read traits](#read-traits)).
|
|
155
229
|
- `prepare()` may relocate the readable region: memoryviews previously returned by `data()`
|
|
156
230
|
or `prepare()` are invalidated.
|
|
157
|
-
- A single message (one `consume()` call) is limited to < 4 GiB
|
|
231
|
+
- A single message (one `consume()` call) is limited to < 4 GiB: `consume()` raises
|
|
232
|
+
`ValueError` rather than publishing a truncated record length. It raises before touching any
|
|
233
|
+
state, so the bytes stay readable and can go out as several smaller records.
|
|
158
234
|
|
|
159
235
|
## Building and Testing
|
|
160
236
|
|
|
@@ -167,7 +243,7 @@ python tests/test_atomic_ops.py
|
|
|
167
243
|
python tests/test_local_mode.py
|
|
168
244
|
python tests/test_shm_mode.py
|
|
169
245
|
|
|
170
|
-
# 3. C++ interop tests (requires CMake + a C++20 compiler)
|
|
246
|
+
# 3. C++ interop tests (requires CMake 3.21+ and a C++20 compiler)
|
|
171
247
|
cmake -S . -B build
|
|
172
248
|
cmake --build build --config Debug
|
|
173
249
|
cd build && ctest -C Debug --output-on-failure
|
|
@@ -184,7 +260,14 @@ cmake -S . -B build \
|
|
|
184
260
|
-DFETCHCONTENT_SOURCE_DIR_SLICK-SHM=/path/to/slick-shm
|
|
185
261
|
```
|
|
186
262
|
|
|
187
|
-
|
|
263
|
+
The 4 GiB record-limit test needs a 4 GiB ring, so it is skipped unless you opt in:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
SSB_PY_TEST_4GIB=1 python tests/test_local_mode.py
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
If a failed test run leaves segments behind: `python tests/cleanup_shm.py`, or
|
|
270
|
+
`SlickStreamBuffer.remove(name)` for one you know by name.
|
|
188
271
|
|
|
189
272
|
## Related Projects
|
|
190
273
|
|