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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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__"]