slick-stream-buffer-multiplexer-py 0.1.0__tar.gz → 2.2.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.
Files changed (22) hide show
  1. slick_stream_buffer_multiplexer_py-2.2.0/API_DIFFERENCES.md +184 -0
  2. slick_stream_buffer_multiplexer_py-2.2.0/PKG-INFO +414 -0
  3. slick_stream_buffer_multiplexer_py-2.2.0/README.md +377 -0
  4. {slick_stream_buffer_multiplexer_py-0.1.0 → slick_stream_buffer_multiplexer_py-2.2.0}/pyproject.toml +2 -2
  5. {slick_stream_buffer_multiplexer_py-0.1.0 → slick_stream_buffer_multiplexer_py-2.2.0}/requirements.txt +2 -2
  6. {slick_stream_buffer_multiplexer_py-0.1.0 → slick_stream_buffer_multiplexer_py-2.2.0}/setup.py +2 -2
  7. {slick_stream_buffer_multiplexer_py-0.1.0 → slick_stream_buffer_multiplexer_py-2.2.0}/slick_stream_buffer_multiplexer_py.egg-info/SOURCES.txt +1 -0
  8. slick_stream_buffer_multiplexer_py-2.2.0/slick_stream_buffer_multiplexer_py.py +933 -0
  9. {slick_stream_buffer_multiplexer_py-0.1.0 → slick_stream_buffer_multiplexer_py-2.2.0}/tests/test_interop.py +197 -7
  10. {slick_stream_buffer_multiplexer_py-0.1.0 → slick_stream_buffer_multiplexer_py-2.2.0}/tests/test_multiplexer.py +120 -0
  11. slick_stream_buffer_multiplexer_py-2.2.0/tests/test_multiplexer_mpmc.py +460 -0
  12. slick_stream_buffer_multiplexer_py-2.2.0/tests/test_multiplexer_queue.py +572 -0
  13. slick_stream_buffer_multiplexer_py-2.2.0/tests/test_multiplexer_shm.py +569 -0
  14. slick_stream_buffer_multiplexer_py-0.1.0/API_DIFFERENCES.md +0 -78
  15. slick_stream_buffer_multiplexer_py-0.1.0/PKG-INFO +0 -216
  16. slick_stream_buffer_multiplexer_py-0.1.0/README.md +0 -179
  17. slick_stream_buffer_multiplexer_py-0.1.0/slick_stream_buffer_multiplexer_py.py +0 -407
  18. slick_stream_buffer_multiplexer_py-0.1.0/tests/test_multiplexer_mpmc.py +0 -236
  19. slick_stream_buffer_multiplexer_py-0.1.0/tests/test_multiplexer_shm.py +0 -292
  20. {slick_stream_buffer_multiplexer_py-0.1.0 → slick_stream_buffer_multiplexer_py-2.2.0}/LICENSE +0 -0
  21. {slick_stream_buffer_multiplexer_py-0.1.0 → slick_stream_buffer_multiplexer_py-2.2.0}/MANIFEST.in +0 -0
  22. {slick_stream_buffer_multiplexer_py-0.1.0 → slick_stream_buffer_multiplexer_py-2.2.0}/setup.cfg +0 -0
@@ -0,0 +1,184 @@
1
+ # API Differences: Python vs C++
2
+
3
+ The Python implementation is binary-compatible with the C++
4
+ `slick::stream_buffer_multiplexer` at the shared memory level — the shared record queue
5
+ ('SLQ1', or 'SLQ0' with `enable_read_last` off; 16-byte elements), every stream buffer
6
+ producer segment ('SSB1') and every queue producer segment (a slick-queue with 1-byte elements)
7
+ match byte-for-byte. Peers must agree on `enable_read_last`, the one trait recorded in
8
+ the queue's header (see [Traits](#traits-are-a-constructor-argument-not-a-template-parameter)).
9
+ The *language-level* API differs in the following ways.
10
+
11
+ ## Construction
12
+
13
+ | C++ | Python |
14
+ |---|---|
15
+ | `stream_buffer_multiplexer mux(size)` | `StreamBufferMultiplexer(shared_queue_size=size)` |
16
+ | `stream_buffer_multiplexer mux(size, "name")` | `StreamBufferMultiplexer(shared_queue_size=size, name="name")` |
17
+ | `stream_buffer_multiplexer mux("name")` | `StreamBufferMultiplexer(name="name")` |
18
+ | `add_producer(id, cap, ctl)` | `add_producer(id, capacity=cap, control_size=ctl)` |
19
+ | `add_producer(id, cap, ctl, "name")` | `add_producer(id, capacity=cap, control_size=ctl, name="name")` |
20
+ | `add_producer(id, "name")` | `add_producer(id, name="name")` |
21
+ | `add_queue_producer(id, size)` | `add_queue_producer(id, size=size)` |
22
+ | `add_queue_producer(id, size, items_per_slot)` | `add_queue_producer(id, size=size, items_per_slot=ips)` |
23
+ | `add_queue_producer(id, size, "name")` / `(id, size, items_per_slot, "name")` | `add_queue_producer(id, size=size, [items_per_slot=ips,] name="name")` |
24
+ | `add_queue_producer(id, "name")` | `add_queue_producer(id, name="name")` (an explicit `items_per_slot=` must match the segment) |
25
+ | `add_queue_producer(id, std::shared_ptr<queue_type>)` | `add_queue_producer(id, queue=q)` |
26
+ | throws `std::invalid_argument` (duplicate id, bad sizes) | raises `ValueError` |
27
+ | `uint32_t producer_id` (range enforced by the type) | raises `ValueError` outside `0..0xFFFFFFFF`, `TypeError` for a non-integer |
28
+ | throws `std::runtime_error` (shm failures) | raises `RuntimeError`, `ValueError` (queue geometry mismatch), or `FileNotFoundError` (missing segment) |
29
+
30
+ ## Consumer cursor is passed by value
31
+
32
+ ```cpp
33
+ // C++
34
+ uint64_t cursor = 0;
35
+ auto rec = mux.read(cursor); // cursor updated in place
36
+ ```
37
+
38
+ ```python
39
+ # Python
40
+ cursor = 0
41
+ rec, cursor = mux.read(cursor) # updated cursor returned
42
+ ```
43
+
44
+ For the work-stealing overload, pass an `AtomicCursor` (from slick-queue-py); the second
45
+ return value is then the claimed shared-queue index (`-1` when nothing was available):
46
+
47
+ ```python
48
+ rec, idx = mux.read(shared_cursor)
49
+ ```
50
+
51
+ ## Zero-copy vs copies
52
+
53
+ Same policy as slick-stream-buffer-py: `prepare()` and `data()` return zero-copy
54
+ memoryviews into the producer's ring; `read()` results (`MultiplexRecord.data`) and
55
+ `consume()` results are **bytes copies**, safe by default under the lossy overwrite
56
+ semantics.
57
+
58
+ ## Producer buffer accessors
59
+
60
+ C++ exposes both `stream_buffer()` (reference) and `stream_buffer_ptr()`
61
+ (`std::shared_ptr`); Python has no owning/non-owning distinction, so there is a single
62
+ `producer.stream_buffer` property returning the underlying `SlickStreamBuffer`.
63
+ Similarly, `get_producer_buffer()` and `find_producer()` both return the same
64
+ `ProducerBuffer` object (or `None`).
65
+
66
+ ## Queue producers
67
+
68
+ C++ `producer_queue::operator[](index)` returns a `uint8_t*` to the reservation; Python's
69
+ `ProducerQueue[index]` returns a writable `memoryview` from `index` to the end of the ring's data
70
+ array, of which the reserved `n` bytes are the first `n` — write them as
71
+ `pq[index][:n] = payload`. (A reservation never splits across the end of the ring, so they are
72
+ always contiguous.) `SlickQueue`'s own `[]` is one element wide, so it cannot stand in for this.
73
+
74
+ C++ `queue_type` is `slick::queue<uint8_t, Traits>`, so adopting a queue with different traits
75
+ fails to compile. Python builds a created queue with the multiplexer's traits too, but accepts an
76
+ adopted `SlickQueue` whatever its traits — only `enable_read_last` reaches shared memory, and
77
+ slick-queue-py checks that against the segment — and requires only `element_size == 1`
78
+ (`ValueError` otherwise, `TypeError` for something that is not a `SlickQueue`).
79
+
80
+ `queue` is the single accessor for the underlying `SlickQueue`, as `stream_buffer` is for
81
+ `ProducerBuffer` (C++ has both `queue()` and `queue_ptr()`). `size`, `items_per_slot`,
82
+ `own_buffer` and `use_shm` are properties, matching `SlickQueue`'s attributes.
83
+
84
+ C++ `slick::producer_kind::stream_buffer` / `::queue` is `ProducerKind.STREAM_BUFFER` /
85
+ `ProducerKind.QUEUE`, on each handle as `.kind`.
86
+
87
+ The payload-retention check (a queue message is skipped once the reservation counter is more than
88
+ one ring past it) runs after `read()` has copied the bytes, so in Python it covers the copy handed
89
+ back; in C++, which hands back a pointer, a producer can still overwrite the bytes after the check.
90
+
91
+ ## Producer lookup
92
+
93
+ C++ uses one dense vector of both kinds for producer ids < 4096 with a hash-map fallback;
94
+ Python uses one plain dict of both kinds for all ids (already O(1) — the dense-path
95
+ micro-optimization is meaningless in Python), and each handle dereferences its own records where
96
+ C++ branches on the kind.
97
+
98
+ `get_producer_buffers()` and `get_producer_queues()` return `const` map references in C++;
99
+ Python returns cached read-only `types.MappingProxyType`s over the same dicts — mutation raises,
100
+ later registrations still show through, and the call allocates nothing. Iteration order is
101
+ insertion order, which C++'s `unordered_map` does not promise.
102
+
103
+ ## Traits are a constructor argument, not a template parameter
104
+
105
+ C++ spells the configuration `basic_stream_buffer_multiplexer<Traits = default_queue_traits>`;
106
+ Python passes the same `QueueTraits` class as a `traits=` keyword argument, matching how
107
+ slick-queue-py does it:
108
+
109
+ ```cpp
110
+ // C++
111
+ slick::basic_stream_buffer_multiplexer<slick::debug_queue_traits> mux(1024);
112
+ ```
113
+
114
+ ```python
115
+ # Python
116
+ mux = StreamBufferMultiplexer(shared_queue_size=1024, traits=MyTraits)
117
+ ```
118
+
119
+ `mux.traits` returns the shared queue's frozen snapshot (C++ spells this
120
+ `using traits = Traits`). One behavioural difference, inherited from slick-queue-py:
121
+ C++'s `default_queue_traits` follows `NDEBUG`, so loss counting is off in release builds;
122
+ Python has no debug/release split and defaults `enable_loss_detection` to on.
123
+
124
+ That difference is harmless across processes because only one trait reaches shared memory:
125
+ `enable_read_last`. The queue's creator records it in the layout marker (`'SLQ1'` on, `'SLQ0'`
126
+ off) and every attacher in either language must be configured the same way, or construction fails
127
+ with "Shared memory feature mismatch" (`RuntimeError` / `std::runtime_error`). Both sides default
128
+ it to on; a peer that turns it off needs its counterparts to do the same:
129
+
130
+ ```cpp
131
+ // C++
132
+ struct no_read_last_traits : slick::default_queue_traits {
133
+ static constexpr bool enable_read_last = false;
134
+ };
135
+ slick::basic_stream_buffer_multiplexer<no_read_last_traits> mux("mux_records");
136
+ ```
137
+
138
+ ```python
139
+ # Python
140
+ class NoReadLast(QueueTraits):
141
+ enable_read_last = False
142
+
143
+ mux = StreamBufferMultiplexer(name="mux_records", traits=NoReadLast)
144
+ ```
145
+
146
+ ## loss_count()
147
+
148
+ Same semantics on both sides: `traits.enable_loss_detection` switches the shared queue's
149
+ wrap-loss term and the multiplexer-level term together, and both read 0 when it is off. Only
150
+ the multiplexer-level term is filtered by producer registration. Records are skipped correctly
151
+ either way. All counters are per-instance, not shared through the segments.
152
+
153
+ `ProducerBuffer.loss_count()` is likewise identical: the multiplexer dereferences through
154
+ traits with `count_loss` off, so that counter only moves for reads the caller makes directly
155
+ through `producer.stream_buffer.read(cursor, traits)`.
156
+
157
+ `ProducerQueue.loss_count()` is the inner queue's own counter on both sides, and the multiplexer's
158
+ lapped dereferences reach it on both sides. It moves by default in Python only because
159
+ slick-queue-py's default traits count; in a C++ release build of the default configuration it
160
+ reads 0.
161
+
162
+ ## Producer handles are not movable in C++
163
+
164
+ C++ deletes the copy and move operations of `producer_buffer` and `producer_queue`, because the
165
+ multiplexer keeps a raw pointer to the same object its `shared_ptr` names, and moving one out
166
+ would leave both pointing at an emptied object. Python has no moves and rebinding a name copies a
167
+ reference, so there is nothing to delete — `ProducerBuffer` and `ProducerQueue` have no
168
+ equivalent restriction.
169
+
170
+ ## Lifecycle
171
+
172
+ Python adds explicit lifecycle management that C++ handles via RAII:
173
+
174
+ - `close()` — detach from all producer segments, of both kinds, and the queue segment; the
175
+ registration tables are kept, so a later `unlink()` still reaches every producer
176
+ - `unlink()` — delete the queue segment and all registered producers' segments, of both kinds;
177
+ valid before or after `close()` (close every handle, then unlink once), and a no-op when repeated
178
+ - context manager support: `with StreamBufferMultiplexer(...) as mux: ...`
179
+ - `get_shm_name()` — on the multiplexer (queue segment) and on each `ProducerBuffer` and
180
+ `ProducerQueue`, the exact names to pass to C++ (`/`-prefixed on POSIX)
181
+
182
+ `remove(name)` is the one lifecycle call both sides share, with the same contract: unlink a
183
+ segment by name without attaching to it, returning `True` on success and, on Windows, always
184
+ `True` because a section dies with its last handle.
@@ -0,0 +1,414 @@
1
+ Metadata-Version: 2.4
2
+ Name: slick-stream-buffer-multiplexer-py
3
+ Version: 2.2.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>=2.1.0
32
+ Requires-Dist: slick-stream-buffer-py>=2.0.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 across processes, fanning into the same shared memory segments — provided
45
+ every peer of a shared queue agrees on `enable_read_last` (see
46
+ [`enable_read_last` must match on every peer](#enable_read_last-must-match-on-every-peer)).
47
+
48
+ Tracks C++ `slick/stream_buffer_multiplexer.hpp` **v2.2.0**, and requires slick-queue-py >=
49
+ 2.1.0 and slick-stream-buffer-py >= 2.0.0. The shared-memory formats are unchanged
50
+ (`'SLQ1'` / `'SSB1'`), so segments written by v0.1.0 or by a C++ v1.x process are still
51
+ readable with the default traits.
52
+
53
+ ## Concept
54
+
55
+ Each producer owns its own byte ring, of one of two kinds, freely mixed under one multiplexer and
56
+ one `producer_id` space:
57
+
58
+ - a [slick-stream-buffer](https://github.com/SlickQuant/slick-stream-buffer) (an
59
+ independently-sized byte ring with per-producer lap/loss detection), written with
60
+ `prepare`/`commit`/`consume` from one thread;
61
+ - a byte [slick-queue](https://github.com/SlickQuant/slick-queue) (`element_size=1`, the C++
62
+ `slick::queue<uint8_t>`), written with `reserve`/`publish`, which several threads may do at once.
63
+
64
+ Publishing through either additionally publishes a small 16-byte `{sequence, producer_id}` record
65
+ into one shared slick-queue, which acts as the lock-free MPMC fan-in / global ordering point.
66
+ Consumers read from the shared queue and dereference each record back into the matching
67
+ producer's ring, getting `(data, length, producer_id)` whichever kind published it.
68
+
69
+ ```
70
+ Producer A ──prepare/commit/consume──▶ [StreamBuffer A] ──┐
71
+ Producer B ──prepare/commit/consume──▶ [StreamBuffer B] ──┼─▶ {sequence, producer_id}
72
+ Producer C ──reserve/write/publish───▶ [byte SlickQueue C]┘ 16-byte records
73
+ │
74
+ [shared SlickQueue]
75
+ │
76
+ consumers read(cursor) and dereference
77
+ into the ring of producer_id
78
+ ```
79
+
80
+ Producers and the shared queue each independently choose local memory or shared memory (IPC).
81
+ A cross-process consumer only registers the producer ids whose shared memory it can access;
82
+ records referencing other producer ids are silently skipped (not counted as loss).
83
+
84
+ ## Requirements
85
+
86
+ - Python 3.8+
87
+ - [slick-queue-py](https://github.com/SlickQuant/slick-queue-py) >= 2.1.0 (queue producers need
88
+ its `items_per_slot`)
89
+ - [slick-stream-buffer-py](https://github.com/SlickQuant/slick-stream-buffer-py) >= 2.0.0
90
+
91
+ Both dependencies bundle a small C++ extension for `std::atomic` operations; this package itself
92
+ is pure Python.
93
+
94
+ ## Installation
95
+
96
+ ```bash
97
+ pip install slick-queue-py slick-stream-buffer-py
98
+ pip install -e .
99
+ ```
100
+
101
+ ## Quick Start
102
+
103
+ ### Producers (any process, any language)
104
+
105
+ ```python
106
+ from slick_stream_buffer_multiplexer_py import StreamBufferMultiplexer
107
+
108
+ # create the shared record queue (the fan-in ordering point)
109
+ mux = StreamBufferMultiplexer(shared_queue_size=1 << 16, name="mux_records")
110
+
111
+ # each producer gets its own independently-sized stream buffer
112
+ feed_a = mux.add_producer(0, capacity=1 << 26, control_size=1 << 16, name="feed_a")
113
+ feed_b = mux.add_producer(1, capacity=1 << 20, control_size=1 << 10, name="feed_b")
114
+
115
+ mv = feed_a.prepare(64 * 1024) # contiguous writable memoryview (zero-copy)
116
+ n = sock.recv_into(mv) # write network bytes directly into the ring
117
+ feed_a.commit(n)
118
+ feed_a.consume(n) # publish as ONE message + fan into the shared queue
119
+ ```
120
+
121
+ ### Consumer (opens everything created elsewhere — Python or C++)
122
+
123
+ ```python
124
+ mux = StreamBufferMultiplexer(name="mux_records") # open the shared record queue
125
+ mux.add_producer(0, name="feed_a") # register the producers to follow
126
+ mux.add_producer(1, name="feed_b") # (geometry read from each header)
127
+
128
+ cursor = mux.initial_reading_index() # or 0 to read history
129
+ while True:
130
+ rec, cursor = mux.read(cursor)
131
+ if not rec:
132
+ continue
133
+ handle_message(rec.producer_id, rec.data, rec.length)
134
+ ```
135
+
136
+ ### Queue producers
137
+
138
+ A producer can be a byte slick-queue instead. Register it with `add_queue_producer()` and publish
139
+ through the `ProducerQueue` it returns, which has slick-queue's own producer interface:
140
+
141
+ ```python
142
+ # 1 MB byte queue, minimum 64 bytes per reservation (both must be powers of 2)
143
+ fills = mux.add_queue_producer(2, size=1 << 20, items_per_slot=64, name="fills")
144
+
145
+ index = fills.reserve(len(message)) # len(message) contiguous bytes; never blocks
146
+ fills[index][:len(message)] = message # fills[index] is a writable view from index onward
147
+ fills.publish(index, len(message)) # publish as ONE message + fan into the shared queue
148
+ ```
149
+
150
+ One message is the bytes of one `reserve()`/`publish()` pair, and consumers get it exactly as they
151
+ get a stream buffer message. What differs is the producer side:
152
+
153
+ - **Several threads may publish through one `ProducerQueue`.** slick-queue is MPMC, and so is the
154
+ fan-in. Each thread's own messages stay in order.
155
+ - **Give a large queue an `items_per_slot`.** It is the minimum number of bytes one `reserve()`
156
+ consumes, and slick-queue keeps one 16-byte control slot per unit of it. Without it there is a
157
+ slot per byte - 16 MB of control for 1 MB of data. See
158
+ [slick-queue-py](https://github.com/SlickQuant/slick-queue-py#byte-buffers-and-items_per_slot).
159
+ - **A message is limited to the queue's size.** `reserve()` raises `RuntimeError` for a larger
160
+ one and `ValueError` for 0, as `SlickQueue.reserve()` does.
161
+
162
+ The forms mirror the C++ overloads: `add_queue_producer(id, size=...[, items_per_slot=...])` for
163
+ local memory, add `name=...` to create a segment, `add_queue_producer(id, name=...)` to open one
164
+ (its size and `items_per_slot` come from the segment), or `add_queue_producer(id, queue=q)` to
165
+ adopt a byte `SlickQueue` you already have. Only what is published through the handle is
166
+ multiplexed: a message put on an adopted queue directly reaches whoever reads that queue, but
167
+ `mux.read()` never returns it.
168
+
169
+ `producer_id` is one namespace across both kinds: an id registered with `add_producer()` cannot be
170
+ registered again with `add_queue_producer()`, or the other way round. `has_producer()` and
171
+ `producer_count()` cover both; `get_producer_queue()`, `find_producer_queue()` and
172
+ `get_producer_queues()` are the queue counterparts of the stream buffer accessors, and each kind's
173
+ accessors return `None` for the other kind's ids.
174
+
175
+ ### Work-stealing consumers (each message to exactly one consumer)
176
+
177
+ ```python
178
+ from slick_stream_buffer_multiplexer_py import AtomicCursor
179
+ from multiprocessing.shared_memory import SharedMemory
180
+
181
+ cursor_shm = SharedMemory(name="mux_cursor", create=True, size=8)
182
+ shared_cursor = AtomicCursor(cursor_shm.buf, 0)
183
+
184
+ rec, idx = mux.read(shared_cursor) # atomically claims the next message
185
+ ```
186
+
187
+ ## Traits
188
+
189
+ Feature configuration is a `traits` constructor argument — and it is slick-queue-py's
190
+ `QueueTraits`, the traits of the shared record queue the multiplexer is built on, not a traits
191
+ type of its own:
192
+
193
+ ```python
194
+ from slick_stream_buffer_multiplexer_py import StreamBufferMultiplexer, QueueTraits
195
+
196
+ class Silent(QueueTraits):
197
+ enable_loss_detection = False # both loss terms go quiet together
198
+
199
+ mux = StreamBufferMultiplexer(shared_queue_size=1024, traits=Silent)
200
+ ```
201
+
202
+ Every knob the multiplexer has beyond that queue is a passthrough, and its one real tunable
203
+ asks the same question `enable_loss_detection` already asks — so a separate flag would only let
204
+ the two disagree. That matters because `loss_count()` **sums** the two counters: a
205
+ half-configured pair would return a partial total that reads like a complete one. One flag, one
206
+ number. `mux.traits` is the queue's own frozen snapshot, not a second copy.
207
+
208
+ Unlike C++ — where `default_queue_traits` follows `NDEBUG` — slick-queue-py defaults
209
+ `enable_loss_detection` to **on**, so the counters work out of the box.
210
+
211
+ ### `enable_read_last` must match on every peer
212
+
213
+ The other traits are local to one instance and may differ freely between processes, but
214
+ `enable_read_last` is part of the shared record queue's header protocol. The queue's creator
215
+ stamps it into the segment's layout marker — `'SLQ1'` when the last-published index is maintained,
216
+ `'SLQ0'` when it is not — and every attacher, Python or C++, checks the marker against its own
217
+ traits. A peer that disagrees is refused at construction (`RuntimeError` in Python,
218
+ `std::runtime_error` in C++, both reading *"Shared memory feature mismatch"*) rather than
219
+ misreading the header: a Python queue created with `enable_read_last = False` cannot be opened by a
220
+ C++ `slick::stream_buffer_multiplexer`, whose default traits have it on.
221
+
222
+ Both languages default it to on, so only turning it off needs coordinating — do it identically on
223
+ every peer of the queue:
224
+
225
+ ```python
226
+ class NoReadLast(QueueTraits):
227
+ enable_read_last = False
228
+
229
+ mux = StreamBufferMultiplexer(shared_queue_size=1 << 16, name="mux_records", traits=NoReadLast)
230
+ ```
231
+
232
+ ```cpp
233
+ struct no_read_last_traits : slick::default_queue_traits {
234
+ static constexpr bool enable_read_last = false;
235
+ };
236
+ slick::basic_stream_buffer_multiplexer<no_read_last_traits> mux("mux_records");
237
+ ```
238
+
239
+ The multiplexer itself never calls `read_last()` (`initial_reading_index()` reads the reservation
240
+ counter instead), so turning it off only drops one CAS from each `consume()`'s publish into the
241
+ shared queue. Stream buffer producer segments (`'SSB1'`) carry no such flag. Queue producers are
242
+ built with the multiplexer's traits, as in C++, so their segments carry the same marker and the
243
+ same rule applies to them.
244
+
245
+ ## Three Loss Counters
246
+
247
+ | Counter | Meaning |
248
+ |---|---|
249
+ | `mux.loss_count()` | shared-queue wrap loss **plus** multiplexer-level loss (registered producers lapped before dereference); both terms switched by `traits.enable_loss_detection` |
250
+ | `ProducerBuffer.loss_count()` | that producer's own inner-ring loss — **only** for reads the caller makes through `producer.stream_buffer.read(cursor, traits)` with counting traits |
251
+ | `ProducerQueue.loss_count()` | the inner queue's own counter — a diagnostic of that queue, **not** a record count (see below) |
252
+ | unregistered producer ids | **never counted** — silently skipped |
253
+
254
+ Only the multiplexer-level term is filtered by registration. The wrap-loss term counts entries
255
+ the shared queue dropped by wrapping, and a lapped slot has already been overwritten, so nothing
256
+ is left to say which producer it named — an instance registering a subset of producers therefore
257
+ sees wrap loss for producers it ignores, making the total an upper bound on what it missed. Size
258
+ the shared queue so it does not wrap and that term goes to 0.
259
+
260
+ `producer.loss_count()` stays at 0 under multiplexer traffic by design: the multiplexer
261
+ dereferences through traits with `count_loss` off, because `read()`'s counter measures how far
262
+ a *sequential scan* jumped, which for a jump-read is a distance every later lapped dereference
263
+ would add all over again. The multiplexer counts one loss per lapped record instead.
264
+
265
+ `ProducerQueue.loss_count()` cannot be kept clean the same way: slick-queue configures loss
266
+ detection per queue, not per call, so with counting traits (slick-queue-py's default) every
267
+ dereference that finds its record lapped adds slick-queue's "how far has this slot moved on"
268
+ distance to it - and each later lapped dereference adds its own again. Ten one-byte messages
269
+ through a 4-byte queue lose 6 records, which `mux.loss_count()` reports as 6 while that queue's
270
+ own counter reads 32. Read loss off the multiplexer.
271
+
272
+ **A queue message is lost once its bytes may be overwritten, not only its slot.** Messages differ
273
+ in length, so a later reservation can cover an older message's bytes without landing on its
274
+ control slot: in an 8-byte queue, `"ijkl"` at index 8 overwrites `"AB"` at 0 and `"CD"` at 2 while
275
+ `"CD"`'s slot still names it. `read()` therefore also skips (and counts) any queue message the
276
+ reservation counter has moved more than one ring past. `read()` copies the bytes before it checks,
277
+ so the check covers the copy you get back. It is conservative, as in C++: a reservation that does
278
+ not fit before the end of the ring skips the rest of the lap, so the counter also passes indices
279
+ nothing wrote, and a message lying in that skipped tail is reported lost although its bytes are
280
+ intact.
281
+
282
+ `mux.get_producer_buffers()` returns every registered stream buffer producer keyed by
283
+ `producer_id`, and `mux.get_producer_queues()` every queue producer — read-only live views of the
284
+ registration tables, cheap enough for a monitoring loop:
285
+
286
+ ```python
287
+ inner_loss = sum(p.loss_count() for p in mux.get_producer_buffers().values())
288
+ queues = {pid: p.size for pid, p in mux.get_producer_queues().items()}
289
+ ```
290
+
291
+ ## Shutdown
292
+
293
+ Close every handle, then unlink once — the usual `shared_memory` idiom:
294
+
295
+ ```python
296
+ mux.close() # or leave a `with StreamBufferMultiplexer(...) as mux:` block
297
+ mux.unlink() # deletes the record queue and every registered producer's segment, both kinds
298
+ ```
299
+
300
+ `close()` keeps the registration table so `unlink()` can still reach each producer's segment, and
301
+ `unlink()` runs once: repeating it never deletes a segment a later run has created under the same
302
+ name. On Windows `unlink()` is a no-op — a section disappears once its last handle closes.
303
+
304
+ ## Stale-segment recovery
305
+
306
+ `StreamBufferMultiplexer.remove(name)` unlinks a segment by name — it works for every kind this
307
+ class creates, a shared record queue's, a producer stream buffer's and a producer queue's, since
308
+ all are plain shared memory segments underneath. Use it when a previous run died mid-initialization and left a
309
+ segment wedged, which construction over that name reports by raising:
310
+
311
+ ```python
312
+ StreamBufferMultiplexer.remove("md_records") # clear what a dead run left
313
+ mux = StreamBufferMultiplexer(shared_queue_size=1024, name="md_records")
314
+ ```
315
+
316
+ Only call it when no process is using the segment. On Windows it is a no-op returning `True` —
317
+ a section there disappears once the last handle closes.
318
+
319
+ ## C++ Interoperability
320
+
321
+ The C++ side composes the identical primitives — either language can create any segment:
322
+
323
+ ```cpp
324
+ #include <slick/stream_buffer_multiplexer.hpp>
325
+
326
+ slick::stream_buffer_multiplexer mux("mux_records"); // open queue created by Python
327
+ auto feed = mux.add_producer(0, "feed_a"); // open Python's producer segment
328
+ auto fills = mux.add_queue_producer(2, "fills"); // and its queue producer's
329
+
330
+ uint64_t cursor = mux.initial_reading_index();
331
+ for (;;) {
332
+ auto rec = mux.read(cursor);
333
+ if (!rec) continue;
334
+ handle_message(rec.producer_id, rec.data, rec.length);
335
+ }
336
+ ```
337
+
338
+ Use `get_shm_name()` on the multiplexer (queue segment) and each producer to get the exact names
339
+ to pass to C++ (POSIX names include the required leading `/`). Each side must register an id as
340
+ the kind it is: the record carries only `{sequence, producer_id}`, so the kind behind an id is part
341
+ of the same convention as its name. A consumer on C++ 2.1 or earlier, or on this package before
342
+ 2.2.0, has no queue producers; it skips their records as unregistered, without counting them as
343
+ loss.
344
+
345
+ ### Binary layout
346
+
347
+ The multiplexer adds no shared-memory structures of its own:
348
+
349
+ - The shared queue is a standard slick-queue segment (`'SLQ1'`, or `'SLQ0'` when created with
350
+ `enable_read_last` off; `element_size=16`) whose elements are
351
+ `uint64 sequence | uint32 producer_id | uint32 pad0`; `sequence` is a stream buffer's record
352
+ sequence or a queue producer's reservation index
353
+ - Each stream buffer producer is a standard slick-stream-buffer segment (`'SSB1'`)
354
+ - Each queue producer is a standard slick-queue segment with `element_size=1`, marked like any
355
+ slick-queue segment: `'SLQ1'` by default, with the `items_per_slot` bit added (`'SLQ3'`) when
356
+ `items_per_slot` is not 1, and the read-last bit cleared (`'SLQ0'` / `'SLQ2'`) when created
357
+ with `enable_read_last` off
358
+
359
+ ## Caveats (same as C++)
360
+
361
+ - `add_producer()` and `add_queue_producer()` are single-threaded setup: call them before
362
+ producer/consumer threads start.
363
+ - A `producer_id` must fit the record's `uint32` field: both registration calls raise
364
+ `ValueError` for one outside `0..0xFFFFFFFF` (and `TypeError` for a non-integer), before any
365
+ segment is created.
366
+ - Each `ProducerBuffer`'s `prepare/commit/consume/...` must be called from a single thread. A
367
+ `ProducerQueue` has no such limit.
368
+ - Lossy semantics: slow consumers skip overwritten data (see the loss counters above).
369
+ Records are skipped correctly whatever the traits say; only the counters go quiet.
370
+ - A single message (one `consume()` call) is limited to < 4 GiB: `consume()` raises
371
+ `ValueError` rather than truncating one that is not, before any state moves — so nothing is
372
+ published to either the producer's ring or the shared record queue.
373
+
374
+ See [API_DIFFERENCES.md](API_DIFFERENCES.md) for exact deviations from the C++ API.
375
+
376
+ ## Building and Testing
377
+
378
+ ```bash
379
+ # Pure-Python tests (sibling repos slick-queue-py / slick-stream-buffer-py are
380
+ # picked up automatically from ../ if not pip-installed)
381
+ python tests/test_multiplexer.py
382
+ python tests/test_multiplexer_queue.py
383
+ python tests/test_multiplexer_mpmc.py
384
+ python tests/test_multiplexer_shm.py
385
+
386
+ # C++ interop tests (requires CMake + a C++20 compiler)
387
+ cmake -S . -B build
388
+ cmake --build build --config Debug
389
+ cd build && ctest -C Debug --output-on-failure
390
+ ```
391
+
392
+ The interop tests fetch slick-stream-buffer-multiplexer (which pulls slick-stream-buffer,
393
+ slick-queue, and slick-shm) from GitHub and build real C++ producer/consumer binaries against
394
+ the actual header. To build against local checkouts instead (no network):
395
+
396
+ ```bash
397
+ cmake -S . -B build \
398
+ -DFETCHCONTENT_SOURCE_DIR_SLICK-STREAM-BUFFER-MULTIPLEXER=/path/to/slick-stream-buffer-multiplexer \
399
+ -DFETCHCONTENT_SOURCE_DIR_SLICK-STREAM-BUFFER=/path/to/slick-stream-buffer \
400
+ -DFETCHCONTENT_SOURCE_DIR_SLICK-QUEUE=/path/to/slick-queue \
401
+ -DFETCHCONTENT_SOURCE_DIR_SLICK-SHM=/path/to/slick-shm
402
+ ```
403
+
404
+ If a failed test run leaves segments behind: `python tests/cleanup_shm.py`
405
+
406
+ ## Related Projects
407
+
408
+ - [slick-stream-buffer-multiplexer](https://github.com/SlickQuant/slick-stream-buffer-multiplexer) — the C++ implementation
409
+ - [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
410
+ - [slick-queue-py](https://github.com/SlickQuant/slick-queue-py) / [slick-queue](https://github.com/SlickQuant/slick-queue) — the shared MPMC record queue
411
+
412
+ ## License
413
+
414
+ MIT