slick-stream-buffer-multiplexer-py 0.1.0__py3-none-any.whl
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_multiplexer_py-0.1.0.dist-info/METADATA +216 -0
- slick_stream_buffer_multiplexer_py-0.1.0.dist-info/RECORD +6 -0
- slick_stream_buffer_multiplexer_py-0.1.0.dist-info/WHEEL +5 -0
- slick_stream_buffer_multiplexer_py-0.1.0.dist-info/licenses/LICENSE +21 -0
- slick_stream_buffer_multiplexer_py-0.1.0.dist-info/top_level.txt +1 -0
- slick_stream_buffer_multiplexer_py.py +407 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: slick-stream-buffer-multiplexer-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lock-free MPMC byte stream multiplexer with C++ interoperability via shared memory
|
|
5
|
+
Home-page: https://github.com/SlickQuant/slick-stream-buffer-multiplexer-py
|
|
6
|
+
Author: Slick Quant
|
|
7
|
+
Author-email: Slick Quant <slickquant@slickquant.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/SlickQuant/slick-stream-buffer-multiplexer-py
|
|
10
|
+
Project-URL: Documentation, https://github.com/SlickQuant/slick-stream-buffer-multiplexer-py#readme
|
|
11
|
+
Project-URL: Repository, https://github.com/SlickQuant/slick-stream-buffer-multiplexer-py
|
|
12
|
+
Project-URL: Bug Tracker, https://github.com/SlickQuant/slick-stream-buffer-multiplexer-py/issues
|
|
13
|
+
Keywords: stream-buffer,multiplexer,lock-free,atomic,shared-memory,ipc,multiprocessing,mpmc
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
26
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
27
|
+
Classifier: Operating System :: MacOS
|
|
28
|
+
Requires-Python: >=3.8
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: slick-queue-py>=1.2.0
|
|
32
|
+
Requires-Dist: slick-stream-buffer-py>=0.1.0
|
|
33
|
+
Dynamic: author
|
|
34
|
+
Dynamic: home-page
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
Dynamic: requires-python
|
|
37
|
+
|
|
38
|
+
# slick-stream-buffer-multiplexer-py
|
|
39
|
+
|
|
40
|
+
A Python implementation of [slick-stream-buffer-multiplexer](https://github.com/SlickQuant/slick-stream-buffer-multiplexer) —
|
|
41
|
+
a lock-free multi-producer multi-consumer (MPMC) byte stream multiplexer with shared memory support.
|
|
42
|
+
|
|
43
|
+
**Maintains exact binary compatibility with the C++ version**: Python and C++ producers and
|
|
44
|
+
consumers can be mixed freely across processes, fanning into the same shared memory segments.
|
|
45
|
+
|
|
46
|
+
## Concept
|
|
47
|
+
|
|
48
|
+
Each producer owns its own [slick-stream-buffer](https://github.com/SlickQuant/slick-stream-buffer)
|
|
49
|
+
(an independently-sized byte ring with per-producer lap/loss detection). `consume()` additionally
|
|
50
|
+
publishes a small 16-byte `{sequence, producer_id}` record into one shared
|
|
51
|
+
[slick-queue](https://github.com/SlickQuant/slick-queue), which acts as the lock-free MPMC
|
|
52
|
+
fan-in / global ordering point. Consumers read from the shared queue and dereference each record
|
|
53
|
+
back into the matching producer's buffer.
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
Producer A ──prepare/commit/consume──▶ [StreamBuffer A] ──┐
|
|
57
|
+
Producer B ──prepare/commit/consume──▶ [StreamBuffer B] ──┼─▶ {sequence, producer_id}
|
|
58
|
+
Producer N ──prepare/commit/consume──▶ [StreamBuffer N] ──┘ 16-byte records
|
|
59
|
+
│
|
|
60
|
+
[shared SlickQueue]
|
|
61
|
+
│
|
|
62
|
+
consumers read(cursor) and dereference
|
|
63
|
+
into StreamBuffer[producer_id]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Producers and the shared queue each independently choose local memory or shared memory (IPC).
|
|
67
|
+
A cross-process consumer only registers the producer ids whose shared memory it can access;
|
|
68
|
+
records referencing other producer ids are silently skipped (not counted as loss).
|
|
69
|
+
|
|
70
|
+
## Requirements
|
|
71
|
+
|
|
72
|
+
- Python 3.8+
|
|
73
|
+
- [slick-queue-py](https://github.com/SlickQuant/slick-queue-py) >= 1.2.0
|
|
74
|
+
- [slick-stream-buffer-py](https://github.com/SlickQuant/slick-stream-buffer-py) >= 0.1.0
|
|
75
|
+
|
|
76
|
+
Both dependencies bundle a small C++ extension for `std::atomic` operations; this package itself
|
|
77
|
+
is pure Python.
|
|
78
|
+
|
|
79
|
+
## Installation
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install slick-queue-py slick-stream-buffer-py
|
|
83
|
+
pip install -e .
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Quick Start
|
|
87
|
+
|
|
88
|
+
### Producers (any process, any language)
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from slick_stream_buffer_multiplexer_py import StreamBufferMultiplexer
|
|
92
|
+
|
|
93
|
+
# create the shared record queue (the fan-in ordering point)
|
|
94
|
+
mux = StreamBufferMultiplexer(shared_queue_size=1 << 16, name="mux_records")
|
|
95
|
+
|
|
96
|
+
# each producer gets its own independently-sized stream buffer
|
|
97
|
+
feed_a = mux.add_producer(0, capacity=1 << 26, control_size=1 << 16, name="feed_a")
|
|
98
|
+
feed_b = mux.add_producer(1, capacity=1 << 20, control_size=1 << 10, name="feed_b")
|
|
99
|
+
|
|
100
|
+
mv = feed_a.prepare(64 * 1024) # contiguous writable memoryview (zero-copy)
|
|
101
|
+
n = sock.recv_into(mv) # write network bytes directly into the ring
|
|
102
|
+
feed_a.commit(n)
|
|
103
|
+
feed_a.consume(n) # publish as ONE message + fan into the shared queue
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Consumer (opens everything created elsewhere — Python or C++)
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
mux = StreamBufferMultiplexer(name="mux_records") # open the shared record queue
|
|
110
|
+
mux.add_producer(0, name="feed_a") # register the producers to follow
|
|
111
|
+
mux.add_producer(1, name="feed_b") # (geometry read from each header)
|
|
112
|
+
|
|
113
|
+
cursor = mux.initial_reading_index() # or 0 to read history
|
|
114
|
+
while True:
|
|
115
|
+
rec, cursor = mux.read(cursor)
|
|
116
|
+
if not rec:
|
|
117
|
+
continue
|
|
118
|
+
handle_message(rec.producer_id, rec.data, rec.length)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Work-stealing consumers (each message to exactly one consumer)
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from slick_stream_buffer_multiplexer_py import AtomicCursor
|
|
125
|
+
from multiprocessing.shared_memory import SharedMemory
|
|
126
|
+
|
|
127
|
+
cursor_shm = SharedMemory(name="mux_cursor", create=True, size=8)
|
|
128
|
+
shared_cursor = AtomicCursor(cursor_shm.buf, 0)
|
|
129
|
+
|
|
130
|
+
rec, idx = mux.read(shared_cursor) # atomically claims the next message
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Three Loss Counters
|
|
134
|
+
|
|
135
|
+
| Counter | Meaning |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `mux.loss_count()` | shared-queue wrap loss **plus** multiplexer-level loss (registered producers lapped before dereference) |
|
|
138
|
+
| `producer.loss_count()` | that producer's own inner-ring loss |
|
|
139
|
+
| unregistered producer ids | **never counted** — silently skipped |
|
|
140
|
+
|
|
141
|
+
## C++ Interoperability
|
|
142
|
+
|
|
143
|
+
The C++ side composes the identical primitives — either language can create any segment:
|
|
144
|
+
|
|
145
|
+
```cpp
|
|
146
|
+
#include <slick/stream_buffer_multiplexer.hpp>
|
|
147
|
+
|
|
148
|
+
slick::stream_buffer_multiplexer mux("mux_records"); // open queue created by Python
|
|
149
|
+
auto feed = mux.add_producer(0, "feed_a"); // open Python's producer segment
|
|
150
|
+
|
|
151
|
+
uint64_t cursor = mux.initial_reading_index();
|
|
152
|
+
for (;;) {
|
|
153
|
+
auto rec = mux.read(cursor);
|
|
154
|
+
if (!rec) continue;
|
|
155
|
+
handle_message(rec.producer_id, rec.data, rec.length);
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Use `get_shm_name()` on the multiplexer (queue segment) and each producer buffer to get the
|
|
160
|
+
exact names to pass to C++ (POSIX names include the required leading `/`).
|
|
161
|
+
|
|
162
|
+
### Binary layout
|
|
163
|
+
|
|
164
|
+
The multiplexer adds no shared-memory structures of its own:
|
|
165
|
+
|
|
166
|
+
- The shared queue is a standard slick-queue segment (`'SLQ1'`, `element_size=16`) whose
|
|
167
|
+
elements are `uint64 sequence | uint32 producer_id | uint32 pad0`
|
|
168
|
+
- Each producer buffer is a standard slick-stream-buffer segment (`'SSB1'`)
|
|
169
|
+
|
|
170
|
+
## Caveats (same as C++)
|
|
171
|
+
|
|
172
|
+
- `add_producer()` is single-threaded setup: call it before producer/consumer threads start.
|
|
173
|
+
- Each producer's `prepare/commit/consume/...` must be called from a single thread.
|
|
174
|
+
- Lossy semantics: slow consumers skip overwritten data (see the loss counters above).
|
|
175
|
+
- A single message (one `consume()` call) is limited to < 4 GiB.
|
|
176
|
+
|
|
177
|
+
See [API_DIFFERENCES.md](API_DIFFERENCES.md) for exact deviations from the C++ API.
|
|
178
|
+
|
|
179
|
+
## Building and Testing
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# Pure-Python tests (sibling repos slick-queue-py / slick-stream-buffer-py are
|
|
183
|
+
# picked up automatically from ../ if not pip-installed)
|
|
184
|
+
python tests/test_multiplexer.py
|
|
185
|
+
python tests/test_multiplexer_mpmc.py
|
|
186
|
+
python tests/test_multiplexer_shm.py
|
|
187
|
+
|
|
188
|
+
# C++ interop tests (requires CMake + a C++20 compiler)
|
|
189
|
+
cmake -S . -B build
|
|
190
|
+
cmake --build build --config Debug
|
|
191
|
+
cd build && ctest -C Debug --output-on-failure
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The interop tests fetch slick-stream-buffer-multiplexer (which pulls slick-stream-buffer,
|
|
195
|
+
slick-queue, and slick-shm) from GitHub and build real C++ producer/consumer binaries against
|
|
196
|
+
the actual header. To build against local checkouts instead (no network):
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
cmake -S . -B build \
|
|
200
|
+
-DFETCHCONTENT_SOURCE_DIR_SLICK-STREAM-BUFFER-MULTIPLEXER=/path/to/slick-stream-buffer-multiplexer \
|
|
201
|
+
-DFETCHCONTENT_SOURCE_DIR_SLICK-STREAM-BUFFER=/path/to/slick-stream-buffer \
|
|
202
|
+
-DFETCHCONTENT_SOURCE_DIR_SLICK-QUEUE=/path/to/slick-queue \
|
|
203
|
+
-DFETCHCONTENT_SOURCE_DIR_SLICK-SHM=/path/to/slick-shm
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
If a failed test run leaves segments behind: `python tests/cleanup_shm.py`
|
|
207
|
+
|
|
208
|
+
## Related Projects
|
|
209
|
+
|
|
210
|
+
- [slick-stream-buffer-multiplexer](https://github.com/SlickQuant/slick-stream-buffer-multiplexer) — the C++ implementation
|
|
211
|
+
- [slick-stream-buffer-py](https://github.com/SlickQuant/slick-stream-buffer-py) / [slick-stream-buffer](https://github.com/SlickQuant/slick-stream-buffer) — the per-producer SPMC byte stream buffer
|
|
212
|
+
- [slick-queue-py](https://github.com/SlickQuant/slick-queue-py) / [slick-queue](https://github.com/SlickQuant/slick-queue) — the shared MPMC record queue
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
MIT
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
slick_stream_buffer_multiplexer_py.py,sha256=alaL4mZ3Fl3NyMnoQOYiTikEyNvHos_KmnQr1CZe9Gg,16823
|
|
2
|
+
slick_stream_buffer_multiplexer_py-0.1.0.dist-info/licenses/LICENSE,sha256=6kgljIbXmMwPF6yv8Foa-s6D9F7CanDaBZF4Jjv_N6M,1068
|
|
3
|
+
slick_stream_buffer_multiplexer_py-0.1.0.dist-info/METADATA,sha256=BMfEobHK3KIf_uSzeiGqL9PJr_tGki1ZVsDn0cq_55c,9227
|
|
4
|
+
slick_stream_buffer_multiplexer_py-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
5
|
+
slick_stream_buffer_multiplexer_py-0.1.0.dist-info/top_level.txt,sha256=mwlFHSOpYq-PbIRIpL84oLK6JxFe145iIPEocRydAmE,35
|
|
6
|
+
slick_stream_buffer_multiplexer_py-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Slick Quant
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
slick_stream_buffer_multiplexer_py
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"""
|
|
2
|
+
slick-stream-buffer-multiplexer-py: A lock-free multi-producer multi-consumer
|
|
3
|
+
byte stream multiplexer, binary-compatible with the C++
|
|
4
|
+
slick::stream_buffer_multiplexer (slick/stream_buffer_multiplexer.hpp v1.0.x).
|
|
5
|
+
|
|
6
|
+
Each producer owns its own SlickStreamBuffer (an independently-sized data ring
|
|
7
|
+
with its own control ring). consume() additionally publishes a small 16-byte
|
|
8
|
+
{sequence, producer_id} record into one shared SlickQueue, which acts as the
|
|
9
|
+
lock-free MPMC fan-in / global ordering point.
|
|
10
|
+
|
|
11
|
+
Consumers call read(cursor), which dequeues the next shared-queue record and
|
|
12
|
+
dereferences it back into the matching producer's stream buffer, returning a
|
|
13
|
+
(data, length, producer_id) message.
|
|
14
|
+
|
|
15
|
+
Producers and the shared queue each independently choose local memory or
|
|
16
|
+
shared memory (IPC): see add_producer() and the constructor. A cross-process
|
|
17
|
+
consumer only needs to register (via add_producer) the producer ids whose
|
|
18
|
+
shared memory it has access to; records referencing other producer ids are
|
|
19
|
+
silently skipped (not counted as loss - see loss_count()).
|
|
20
|
+
|
|
21
|
+
Binary compatibility: the multiplexer layer adds no shared-memory structures
|
|
22
|
+
of its own. The shared queue is a standard slick-queue segment ('SLQ1',
|
|
23
|
+
element_size=16) whose elements hold:
|
|
24
|
+
|
|
25
|
+
Offset 0-7 (8 bytes): uint64 sequence - record seq in the producer's buffer
|
|
26
|
+
Offset 8-11 (4 bytes): uint32 producer_id
|
|
27
|
+
Offset 12-15 (4 bytes): uint32 pad0
|
|
28
|
+
|
|
29
|
+
Each producer buffer is a standard slick-stream-buffer segment ('SSB1').
|
|
30
|
+
A C++ slick::stream_buffer_multiplexer and this class interoperate through
|
|
31
|
+
the same segments in any combination of producer/consumer roles.
|
|
32
|
+
|
|
33
|
+
Caveats (same as C++):
|
|
34
|
+
- add_producer() is single-threaded setup: call it before any producer or
|
|
35
|
+
consumer threads start.
|
|
36
|
+
- Each ProducerBuffer's producer-side methods (prepare/commit/consume/...)
|
|
37
|
+
must be called from a single thread, same as SlickStreamBuffer.
|
|
38
|
+
- Three independent loss counters exist: the shared queue's own
|
|
39
|
+
queue-wrap loss, this multiplexer's loss_count() (which adds
|
|
40
|
+
multiplexer-level loss: shared-queue entries whose producer_id is registered
|
|
41
|
+
on this instance but were lapped by that producer's own ring before being
|
|
42
|
+
dereferenced), and each ProducerBuffer's own loss_count() (inner ring loss).
|
|
43
|
+
Entries whose producer_id is unregistered on this instance are never counted
|
|
44
|
+
as loss.
|
|
45
|
+
- A single message (one consume() call) is limited to < 4 GiB.
|
|
46
|
+
"""
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
__version__ = '0.1.0'
|
|
50
|
+
|
|
51
|
+
import struct
|
|
52
|
+
from typing import Dict, Optional, Tuple, Union
|
|
53
|
+
|
|
54
|
+
from slick_queue_py import SlickQueue
|
|
55
|
+
from slick_stream_buffer_py import SlickStreamBuffer, PublishedRecord
|
|
56
|
+
from atomic_ops import AtomicCursor # from slick-queue-py
|
|
57
|
+
|
|
58
|
+
# Shared-queue element (matches stream_buffer_multiplexer.hpp:80-85):
|
|
59
|
+
# uint64 sequence @0 | uint32 producer_id @8 | uint32 pad0 @12
|
|
60
|
+
RECORD_FMT = "<QII"
|
|
61
|
+
RECORD_SIZE = struct.calcsize(RECORD_FMT)
|
|
62
|
+
assert RECORD_SIZE == 16, "record must be 16 bytes"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class MultiplexRecord:
|
|
66
|
+
"""A dereferenced message as returned by read(). Mirrors C++
|
|
67
|
+
multiplex_record (stream_buffer_multiplexer.hpp:88-93).
|
|
68
|
+
|
|
69
|
+
Evaluates to False if no message was available. Unlike C++, `data` is a
|
|
70
|
+
bytes copy, not a pointer into the producer's ring.
|
|
71
|
+
"""
|
|
72
|
+
__slots__ = ('data', 'length', 'producer_id')
|
|
73
|
+
|
|
74
|
+
def __init__(self, data: Optional[bytes] = None, length: int = 0,
|
|
75
|
+
producer_id: int = 0):
|
|
76
|
+
self.data = data
|
|
77
|
+
self.length = length
|
|
78
|
+
self.producer_id = producer_id
|
|
79
|
+
|
|
80
|
+
def __bool__(self) -> bool:
|
|
81
|
+
return self.data is not None
|
|
82
|
+
|
|
83
|
+
def __repr__(self) -> str:
|
|
84
|
+
return (f"MultiplexRecord(producer_id={self.producer_id}, "
|
|
85
|
+
f"length={self.length})")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ProducerBuffer:
|
|
89
|
+
"""Per-producer handle: wraps one SlickStreamBuffer and publishes into the
|
|
90
|
+
shared record queue. Mirrors C++ producer_buffer
|
|
91
|
+
(stream_buffer_multiplexer.hpp:96-168).
|
|
92
|
+
|
|
93
|
+
Created only by StreamBufferMultiplexer.add_producer().
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, producer_id: int, shared_queue: SlickQueue,
|
|
97
|
+
buffer: SlickStreamBuffer):
|
|
98
|
+
self._producer_id = producer_id
|
|
99
|
+
self._shared_queue = shared_queue
|
|
100
|
+
self._buffer = buffer
|
|
101
|
+
|
|
102
|
+
# ------------------------------------------------------------------
|
|
103
|
+
# Producer side (single thread only) - forwards to the inner stream buffer
|
|
104
|
+
# ------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
def prepare(self, n: int) -> memoryview:
|
|
107
|
+
"""Get a contiguous writable region of n bytes (zero-copy memoryview)."""
|
|
108
|
+
return self._buffer.prepare(n)
|
|
109
|
+
|
|
110
|
+
def commit(self, n: int) -> None:
|
|
111
|
+
"""Move n prepared bytes into the readable region (clamped)."""
|
|
112
|
+
self._buffer.commit(n)
|
|
113
|
+
|
|
114
|
+
def consume(self, n: int) -> PublishedRecord:
|
|
115
|
+
"""Publish the first n committed bytes as one message, and fan it into
|
|
116
|
+
the shared record queue so multiplexer consumers can find it.
|
|
117
|
+
(Matches C++ stream_buffer_multiplexer.hpp:116-127.)
|
|
118
|
+
"""
|
|
119
|
+
rec = self._buffer.consume(n)
|
|
120
|
+
if rec:
|
|
121
|
+
idx = self._shared_queue.reserve()
|
|
122
|
+
struct.pack_into(RECORD_FMT, self._shared_queue[idx], 0,
|
|
123
|
+
rec.sequence, self._producer_id, 0)
|
|
124
|
+
self._shared_queue.publish(idx)
|
|
125
|
+
return rec
|
|
126
|
+
|
|
127
|
+
def discard(self) -> None:
|
|
128
|
+
"""Drop unconsumed + prepared bytes without publishing."""
|
|
129
|
+
self._buffer.discard()
|
|
130
|
+
|
|
131
|
+
def data(self) -> memoryview:
|
|
132
|
+
"""Read-only view of the committed-but-unconsumed region."""
|
|
133
|
+
return self._buffer.data()
|
|
134
|
+
|
|
135
|
+
def size(self) -> int:
|
|
136
|
+
"""Number of committed-but-unconsumed bytes."""
|
|
137
|
+
return self._buffer.size()
|
|
138
|
+
|
|
139
|
+
# ------------------------------------------------------------------
|
|
140
|
+
# Shared accessors
|
|
141
|
+
# ------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def capacity(self) -> int:
|
|
145
|
+
return self._buffer.capacity
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def control_size(self) -> int:
|
|
149
|
+
return self._buffer.control_size
|
|
150
|
+
|
|
151
|
+
def loss_count(self) -> int:
|
|
152
|
+
"""THIS producer's inner ring loss only (not multiplexer-level loss)."""
|
|
153
|
+
return self._buffer.loss_count()
|
|
154
|
+
|
|
155
|
+
def initial_reading_index(self) -> int:
|
|
156
|
+
return self._buffer.initial_reading_index()
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def own_buffer(self) -> bool:
|
|
160
|
+
return self._buffer.own_buffer
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def use_shm(self) -> bool:
|
|
164
|
+
return self._buffer.use_shm
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def producer_id(self) -> int:
|
|
168
|
+
return self._producer_id
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def stream_buffer(self) -> SlickStreamBuffer:
|
|
172
|
+
"""The underlying SlickStreamBuffer (C++ exposes both stream_buffer()
|
|
173
|
+
and stream_buffer_ptr(); Python needs only one accessor)."""
|
|
174
|
+
return self._buffer
|
|
175
|
+
|
|
176
|
+
def get_shm_name(self) -> Optional[str]:
|
|
177
|
+
"""The producer segment's name for C++ interop (see
|
|
178
|
+
SlickStreamBuffer.get_shm_name())."""
|
|
179
|
+
return self._buffer.get_shm_name()
|
|
180
|
+
|
|
181
|
+
def close(self) -> None:
|
|
182
|
+
"""Close the underlying stream buffer connection."""
|
|
183
|
+
self._buffer.close()
|
|
184
|
+
|
|
185
|
+
def unlink(self) -> None:
|
|
186
|
+
"""Unlink (delete) the underlying shared memory segment, if any."""
|
|
187
|
+
self._buffer.unlink()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class StreamBufferMultiplexer:
|
|
191
|
+
"""A lock-free MPMC byte stream multiplexer compatible with C++
|
|
192
|
+
slick::stream_buffer_multiplexer.
|
|
193
|
+
|
|
194
|
+
Construction modes (mirroring the three C++ constructors,
|
|
195
|
+
stream_buffer_multiplexer.hpp:175-184):
|
|
196
|
+
|
|
197
|
+
- ``StreamBufferMultiplexer(shared_queue_size=1024)``
|
|
198
|
+
local-memory shared record queue
|
|
199
|
+
- ``StreamBufferMultiplexer(shared_queue_size=1024, name="mux_q")``
|
|
200
|
+
shared-memory record queue (create or open)
|
|
201
|
+
- ``StreamBufferMultiplexer(name="mux_q")``
|
|
202
|
+
open an existing shared-memory record queue (geometry self-described)
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
shared_queue_size: Shared record queue capacity (must be a power of 2).
|
|
206
|
+
name: Shared memory name for the record queue. If None, local memory.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
def __init__(self, *, shared_queue_size: Optional[int] = None,
|
|
210
|
+
name: Optional[str] = None):
|
|
211
|
+
if shared_queue_size is not None and (
|
|
212
|
+
shared_queue_size == 0 or shared_queue_size & (shared_queue_size - 1)):
|
|
213
|
+
raise ValueError("shared_queue_size must be a power of two")
|
|
214
|
+
if shared_queue_size is not None:
|
|
215
|
+
if name is not None:
|
|
216
|
+
self._shared_queue = SlickQueue(
|
|
217
|
+
name=name, size=shared_queue_size, element_size=RECORD_SIZE)
|
|
218
|
+
else:
|
|
219
|
+
self._shared_queue = SlickQueue(
|
|
220
|
+
size=shared_queue_size, element_size=RECORD_SIZE)
|
|
221
|
+
else:
|
|
222
|
+
if name is None:
|
|
223
|
+
raise ValueError("shared_queue_size or name must be provided")
|
|
224
|
+
self._shared_queue = SlickQueue(name=name, element_size=RECORD_SIZE)
|
|
225
|
+
|
|
226
|
+
self._producers: Dict[int, ProducerBuffer] = {}
|
|
227
|
+
# multiplexer-level loss (registered producer lapped before dereference);
|
|
228
|
+
# per-instance, like the C++ loss_count_ member. Always counted in Python.
|
|
229
|
+
self._loss_count = 0
|
|
230
|
+
|
|
231
|
+
# ------------------------------------------------------------------
|
|
232
|
+
# Producer registration (single-threaded setup)
|
|
233
|
+
# ------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
def add_producer(self, producer_id: int, *, capacity: Optional[int] = None,
|
|
236
|
+
control_size: Optional[int] = None,
|
|
237
|
+
name: Optional[str] = None) -> ProducerBuffer:
|
|
238
|
+
"""Register a producer, mirroring the three C++ overloads
|
|
239
|
+
(stream_buffer_multiplexer.hpp:194-206):
|
|
240
|
+
|
|
241
|
+
- ``add_producer(id, capacity=..., control_size=...)`` local memory
|
|
242
|
+
- ``add_producer(id, capacity=..., control_size=..., name=...)`` shm creator
|
|
243
|
+
- ``add_producer(id, name=...)`` open existing shm (geometry self-described)
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
ValueError: if producer_id is already registered, or sizes invalid.
|
|
247
|
+
"""
|
|
248
|
+
if self.has_producer(producer_id):
|
|
249
|
+
raise ValueError(f"producer_id {producer_id} already registered")
|
|
250
|
+
buffer = SlickStreamBuffer(capacity=capacity, control_size=control_size,
|
|
251
|
+
name=name)
|
|
252
|
+
producer = ProducerBuffer(producer_id, self._shared_queue, buffer)
|
|
253
|
+
self._producers[producer_id] = producer
|
|
254
|
+
return producer
|
|
255
|
+
|
|
256
|
+
def has_producer(self, producer_id: int) -> bool:
|
|
257
|
+
return producer_id in self._producers
|
|
258
|
+
|
|
259
|
+
def get_producer_buffer(self, producer_id: int) -> Optional[ProducerBuffer]:
|
|
260
|
+
"""The registered producer, or None if producer_id is unregistered."""
|
|
261
|
+
return self._producers.get(producer_id)
|
|
262
|
+
|
|
263
|
+
def find_producer(self, producer_id: int) -> Optional[ProducerBuffer]:
|
|
264
|
+
"""Alias of get_producer_buffer() (C++ has both; Python has no
|
|
265
|
+
owning/non-owning pointer distinction)."""
|
|
266
|
+
return self._producers.get(producer_id)
|
|
267
|
+
|
|
268
|
+
def producer_count(self) -> int:
|
|
269
|
+
"""Number of registered producers on this multiplexer instance."""
|
|
270
|
+
return len(self._producers)
|
|
271
|
+
|
|
272
|
+
# ------------------------------------------------------------------
|
|
273
|
+
# Consumer side
|
|
274
|
+
# ------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
def read(self, cursor: Union[int, AtomicCursor]) -> Tuple[MultiplexRecord, int]:
|
|
277
|
+
"""Read the next message in global order.
|
|
278
|
+
|
|
279
|
+
Two modes, mirroring the two C++ overloads
|
|
280
|
+
(stream_buffer_multiplexer.hpp:237-248):
|
|
281
|
+
|
|
282
|
+
- int cursor (per-consumer): ``rec, cursor = mux.read(cursor)`` -
|
|
283
|
+
the updated cursor is returned (Python has no by-ref ints)
|
|
284
|
+
- AtomicCursor (work-stealing across consumers): ``rec, idx = mux.read(cursor)`` -
|
|
285
|
+
the cursor advances atomically inside the shared queue; idx is the
|
|
286
|
+
claimed shared-queue index (-1 when no message was available)
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
(record, cursor_or_index) - record is falsy when no message is
|
|
290
|
+
available. Records from unregistered producer ids are silently
|
|
291
|
+
skipped (not loss); records lapped by their producer's ring are
|
|
292
|
+
skipped and counted in loss_count().
|
|
293
|
+
"""
|
|
294
|
+
if isinstance(cursor, AtomicCursor):
|
|
295
|
+
return self._read_atomic(cursor)
|
|
296
|
+
return self._read_int(cursor)
|
|
297
|
+
|
|
298
|
+
def loss_count(self) -> int:
|
|
299
|
+
"""Shared-queue wrap loss plus multiplexer-level loss (shared-queue
|
|
300
|
+
entries whose producer_id is registered on this instance but were
|
|
301
|
+
lapped by that producer's own ring before being dereferenced).
|
|
302
|
+
Entries whose producer_id is unregistered are never counted.
|
|
303
|
+
(Matches C++ stream_buffer_multiplexer.hpp:255-261; Python always
|
|
304
|
+
counts, C++ only when the loss-detection macros are enabled.)
|
|
305
|
+
"""
|
|
306
|
+
return self._shared_queue.loss_count() + self._loss_count
|
|
307
|
+
|
|
308
|
+
def initial_reading_index(self) -> int:
|
|
309
|
+
"""Initial reading cursor for a late-joining consumer
|
|
310
|
+
(see SlickQueue.initial_reading_index())."""
|
|
311
|
+
return self._shared_queue.initial_reading_index()
|
|
312
|
+
|
|
313
|
+
# ------------------------------------------------------------------
|
|
314
|
+
# Lifecycle
|
|
315
|
+
# ------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
@property
|
|
318
|
+
def shared_queue(self) -> SlickQueue:
|
|
319
|
+
"""The underlying shared record queue."""
|
|
320
|
+
return self._shared_queue
|
|
321
|
+
|
|
322
|
+
def get_shm_name(self) -> Optional[str]:
|
|
323
|
+
"""The shared record queue's segment name for C++ interop."""
|
|
324
|
+
return self._shared_queue.get_shm_name()
|
|
325
|
+
|
|
326
|
+
def close(self) -> None:
|
|
327
|
+
"""Close all producer buffers and the shared record queue."""
|
|
328
|
+
for producer in self._producers.values():
|
|
329
|
+
try:
|
|
330
|
+
producer.close()
|
|
331
|
+
except Exception:
|
|
332
|
+
pass
|
|
333
|
+
self._producers = {}
|
|
334
|
+
try:
|
|
335
|
+
self._shared_queue.close()
|
|
336
|
+
except Exception:
|
|
337
|
+
pass
|
|
338
|
+
|
|
339
|
+
def unlink(self) -> None:
|
|
340
|
+
"""Unlink (delete) the shared record queue segment and all registered
|
|
341
|
+
producers' segments. Call once, after all users have closed."""
|
|
342
|
+
for producer in self._producers.values():
|
|
343
|
+
try:
|
|
344
|
+
producer.unlink()
|
|
345
|
+
except Exception:
|
|
346
|
+
pass
|
|
347
|
+
try:
|
|
348
|
+
self._shared_queue.unlink()
|
|
349
|
+
except Exception:
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
def __enter__(self):
|
|
353
|
+
return self
|
|
354
|
+
|
|
355
|
+
def __exit__(self, exc_type, exc_val, exc_tb): # noqa: U100
|
|
356
|
+
self.close()
|
|
357
|
+
return False
|
|
358
|
+
|
|
359
|
+
# ------------------------------------------------------------------
|
|
360
|
+
# Internal
|
|
361
|
+
# ------------------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
def _dereference(self, producer: ProducerBuffer, sequence: int,
|
|
364
|
+
producer_id: int) -> MultiplexRecord:
|
|
365
|
+
"""Dereference a shared-queue record into the matching producer's
|
|
366
|
+
stream buffer. Falsy if the record was lapped before being
|
|
367
|
+
dereferenced. (Matches C++ stream_buffer_multiplexer.hpp:315-322.)"""
|
|
368
|
+
data, length, new_cursor = producer.stream_buffer.read(sequence)
|
|
369
|
+
if data is not None and new_cursor == sequence + 1:
|
|
370
|
+
return MultiplexRecord(data, length, producer_id)
|
|
371
|
+
return MultiplexRecord()
|
|
372
|
+
|
|
373
|
+
def _read_int(self, cursor: int) -> Tuple[MultiplexRecord, int]:
|
|
374
|
+
"""read_impl with a per-consumer int cursor
|
|
375
|
+
(stream_buffer_multiplexer.hpp:324-345)."""
|
|
376
|
+
while True:
|
|
377
|
+
data, _, cursor = self._shared_queue.read(cursor)
|
|
378
|
+
if data is None:
|
|
379
|
+
return MultiplexRecord(), cursor
|
|
380
|
+
sequence, producer_id, _ = struct.unpack(RECORD_FMT, data)
|
|
381
|
+
producer = self._producers.get(producer_id)
|
|
382
|
+
if producer is None:
|
|
383
|
+
continue # not registered on this instance: ignored, not loss
|
|
384
|
+
result = self._dereference(producer, sequence, producer_id)
|
|
385
|
+
if result:
|
|
386
|
+
return result, cursor
|
|
387
|
+
self._loss_count += 1
|
|
388
|
+
|
|
389
|
+
def _read_atomic(self, cursor: AtomicCursor) -> Tuple[MultiplexRecord, int]:
|
|
390
|
+
"""read_impl with a shared atomic work-stealing cursor
|
|
391
|
+
(stream_buffer_multiplexer.hpp:246-248, 324-345)."""
|
|
392
|
+
while True:
|
|
393
|
+
data, _, idx = self._shared_queue.read(cursor)
|
|
394
|
+
if data is None:
|
|
395
|
+
return MultiplexRecord(), -1
|
|
396
|
+
sequence, producer_id, _ = struct.unpack(RECORD_FMT, data)
|
|
397
|
+
producer = self._producers.get(producer_id)
|
|
398
|
+
if producer is None:
|
|
399
|
+
continue
|
|
400
|
+
result = self._dereference(producer, sequence, producer_id)
|
|
401
|
+
if result:
|
|
402
|
+
return result, idx
|
|
403
|
+
self._loss_count += 1
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
__all__ = ["StreamBufferMultiplexer", "ProducerBuffer", "MultiplexRecord",
|
|
407
|
+
"AtomicCursor", "RECORD_SIZE", "__version__"]
|