Skip to content

Commit 36e6819

Browse files
committed
Release v2.1.0: segments outlive their creator; add remove_shm()
1 parent 5d37c25 commit 36e6819

7 files changed

Lines changed: 533 additions & 119 deletions

File tree

‎API_DIFFERENCES.md‎

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# API Differences: Python vs C++
22

33
The Python implementation is binary-compatible with the C++ `slick::SlickStreamBuffer`
4-
(`slick/stream_buffer.hpp` v2.0.0) at the shared memory level — every byte, offset, and
4+
(`slick/stream_buffer.hpp` v2.1.0) at the shared memory level — every byte, offset, and
55
memory ordering matches. The *language-level* API differs in the following ways.
66

77
## Construction
@@ -119,19 +119,28 @@ Python adds explicit lifecycle management that C++ handles via RAII:
119119

120120
- `close()` — detach from the segment (does not delete it)
121121
- `unlink()` — delete the segment this instance is attached to (call once, from the owner,
122-
after all users closed - including this instance, so `close()` then `unlink()` is fine)
122+
after all users closed - including this instance, so `close()` then `unlink()` is fine).
123+
A repeat is a no-op, so it cannot delete a segment a later run created under the same name
123124
- context manager support: `with SlickStreamBuffer(...) as buf: ...` (closes on exit)
124-
- `get_shm_name()` — the exact segment name to pass to C++ (`/`-prefixed on POSIX)
125+
- `get_shm_name()` — the exact segment name to pass to C++ (`/`-prefixed on POSIX); C++
126+
`shm_name()` returns `""` for a local-memory buffer where Python returns `None`
127+
128+
Neither side deletes a segment on its own, matching C++ v2.1.0: not the C++ destructor, and not
129+
Python's `close()`, context manager or process exit. `remove_shm()` is shared with the same
130+
contract: unlink this buffer's segment name, on every call, leaving open mappings valid, and
131+
return `False` for a local-memory buffer.
125132

126133
`remove(name)` is the one lifecycle call both sides share, with the same contract: unlink a
127134
segment by name without attaching to it, returning `True` on success and, on Windows, always
128135
`True` because a section dies with its last handle. Python routes it through
129-
`_posixshmem.shm_unlink`, then drops the name from `multiprocessing`'s resource tracker if -
130-
and only if - this process is what registered it. A name registered elsewhere stays that
131-
owner's to account for: the tracker answers an unregister for a name it never saw by raising
132-
`KeyError` and printing a traceback from its own process.
133-
134-
On POSIX with Python >= 3.13, attaching to an existing segment uses
135-
`SharedMemory(track=False)` so Python's resource tracker never unlinks a segment the
136-
process does not own. On older Pythons the tracker may log a harmless warning at exit
137-
(see `tests/run_test.py`, which filters it).
136+
`_posixshmem.shm_unlink` and never sends the resource tracker an unregister: the tracker answers
137+
one for a name it does not hold by raising `KeyError` and printing a traceback from its own
138+
process.
139+
140+
Python keeps every segment it creates or attaches to out of `multiprocessing`'s resource tracker,
141+
which would otherwise unlink it when the process exits, and sends the tracker nothing at all:
142+
`SharedMemory(track=False)` on Python 3.13+, and on older POSIX Pythons a `SharedMemory` subclass
143+
that maps the segment without registering it. Registering and retracting at once instead would
144+
race between processes sharing one tracker - `multiprocessing` children share their parent's -
145+
and need a lock that a fork could leave held. A `SharedMemory` you open yourself is still tracked
146+
unless you pass `track=False` (Python 3.13+).

‎CHANGELOG.md‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,63 @@
11
# Changelogs
22

3+
## [v2.1.0] - 2026-10-17
4+
5+
Matches C++ [slick-stream-buffer v2.1.0](https://github.com/SlickQuant/slick-stream-buffer/blob/main/CHANGELOG.md):
6+
no buffer removes its shared-memory segment on its own any more, and `remove_shm()` is how the
7+
coordinating process does. The shared memory format is unchanged (`SSB1`), so 2.0 and 2.1
8+
processes in either language can share a segment.
9+
10+
### Segment lifetime (behavior change)
11+
- On POSIX, a segment is no longer unlinked when the process that created it exits.
12+
`SharedMemory(create=True)` registered it with `multiprocessing`'s resource tracker, which
13+
unlinks what is registered when the process exits, so a producer that exited first stranded
14+
every consumer still attached on an orphaned mapping, while its restart silently created a
15+
fresh segment under the same name that those consumers never saw. C++ v2.1.0 stopped its
16+
destructor doing the same. A name now lives until `unlink()`, `remove_shm()` or
17+
`remove(name)`, and a restarted process reattaches to the existing segment and keeps its
18+
contents. Windows is unaffected: a section dies with its last handle.
19+
- The buffer sends the resource tracker nothing at all: `track=False` on Python 3.13+, and on
20+
older POSIX Pythons a `SharedMemory` subclass that maps the segment without registering it.
21+
The 2.0.0 bookkeeping of which names this process had registered is gone, and `remove()` no
22+
longer contacts the tracker.
23+
- Migration: code that relied on the cleanup at exit now leaves segments in `/dev/shm`, and its
24+
next run attaches to them - `own_buffer` is `False`, reading resumes from the old cursors, and
25+
a creator with a different geometry raises `RuntimeError` ("geometry mismatch"). Call
26+
`remove(name)` at startup for a clean start, or `unlink()` / `remove_shm()` at shutdown once
27+
no peer will attach again.
28+
29+
### Added
30+
- `remove_shm() -> bool`, matching C++: unlinks this buffer's segment name so the next creator
31+
starts fresh, leaving open mappings - this buffer's own included - valid. It unlinks by name
32+
on every call, where `unlink()` runs once, and leaves `unlink()` nothing to do afterwards, so a
33+
later `unlink()` cannot delete a replacement created under the name. Returns `False` for a
34+
local-memory buffer or when the name is already gone, and always `True` for a shared buffer on
35+
Windows. C++ v2.1.0's
36+
other addition, `shm_name()`, is the existing `get_shm_name()` (`None` rather than `""` for a
37+
local-memory buffer).
38+
39+
### Fixed
40+
- Before Python 3.13 on POSIX, a process that only attached to a segment unlinked it when it
41+
exited, because attaching registered the name with that process's resource tracker too.
42+
2.0.0 avoided this only on 3.13+, through `track=False`.
43+
44+
### Tests
45+
- Segment lifetime: `unlink()` repeated, `remove_shm()` on local and shared buffers, `unlink()`
46+
after `remove_shm()` sparing a replacement segment, a
47+
creator's and an attacher's exit in a fresh interpreter, no resource-tracker traffic at all,
48+
a child forked while another thread is constructing a buffer, and spawned children attaching
49+
at once. A test that `remove()` leaves the tracker alone for this process's own segment
50+
replaces the 2.0.0 one that it retracted the registration.
51+
- The interop tests build against C++ slick-stream-buffer `v2.1.0` (was `v2.0.0`). The two in
52+
which C++ creates the segment remove it before starting and after the producer exits, since
53+
C++ now leaves it in place; a rerun used to attach to the previous run's segment.
54+
- Tests that hand-build a raw segment open it through the library's opener, so that handle and
55+
the buffer no longer both claim the name in the resource tracker.
56+
57+
Checked against C++ slick-stream-buffer v2.1.0 with the interop tests on Windows and on Linux,
58+
run three times on Linux so a segment left behind would fail a rerun, and on Linux under
59+
Python 3.12 (the subclass) and 3.14 (`track=False`).
60+
361
## [v2.0.0] - 2026-09-12
462

563
Matches C++ [slick-stream-buffer v2.0.0](https://github.com/SlickQuant/slick-stream-buffer/blob/main/CHANGELOG.md).

‎README.md‎

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ a lock-free single-producer multi-consumer (SPMC) byte stream buffer with shared
77
consumer (or vice versa) communicate seamlessly through the same shared memory segment, using
88
the same `std::atomic` synchronization primitives.
99

10-
Tracks C++ `slick/stream_buffer.hpp` **v2.0.0** (header magic `'SSB1'`); the two carry the same
10+
Tracks C++ `slick/stream_buffer.hpp` **v2.1.0** (header magic `'SSB1'`); the two carry the same
1111
version number. The shared memory format is unchanged from v1.x, so a segment written by an
1212
older process is still readable.
1313

@@ -171,13 +171,28 @@ On POSIX `remove()` unlinks immediately: the next creator gets a fresh segment,
171171
process still mapped to the old one keeps reading the orphaned copy. On Windows it is a no-op
172172
returning `True` - a section there disappears once the last handle closes.
173173

174-
One Python-only caveat: on POSIX before Python 3.13, `multiprocessing`'s resource tracker
175-
unlinks every segment the process mapped when it exits, including ones it only attached to.
176-
Python 3.13+ attaches with `SharedMemory(track=False)`, which is why 3.13 or newer is
177-
recommended when several processes share a segment. `remove()` unlinks the segment either way,
178-
and retracts the tracker's registration only when this process is the one that registered it -
179-
a name registered by some other process, or by a `SharedMemory` you built yourself, stays that
180-
owner's to account for. See [API_DIFFERENCES.md](API_DIFFERENCES.md).
174+
No buffer deletes its segment on its own: not `close()`, not the context manager, and not
175+
process exit, matching C++ v2.1.0. On POSIX, a departing process that unlinked the name while a
176+
peer was still attached would leave that peer reading an orphaned mapping while a restarted
177+
producer silently created a fresh segment under the same name, so the two never met again.
178+
Instead, the process that coordinates the buffer's lifetime deletes the name once no peer will
179+
attach again, with `unlink()` or `remove_shm()` on an instance, or `remove(name)`:
180+
181+
```python
182+
# Coordinator, after every peer has shut down
183+
buf.close()
184+
buf.unlink() # once; a repeat is a no-op, so it cannot delete a later run's segment
185+
```
186+
187+
`remove_shm()` is the C++-style call: it unlinks by name on every call and returns `bool`, and
188+
open mappings, including this instance's own, stay valid. Until the name is deleted, a restarted
189+
process reattaches to the existing segment and keeps its contents.
190+
191+
Buffers keep their segments out of `multiprocessing`'s resource tracker, which would otherwise
192+
unlink them when the process exits - before Python 3.13, even in a process that only attached.
193+
They send the tracker nothing at all: `track=False` on Python 3.13+, and on older POSIX Pythons a
194+
`SharedMemory` subclass that maps the segment without registering it. See
195+
[API_DIFFERENCES.md](API_DIFFERENCES.md).
181196

182197
### Shared memory layout
183198

@@ -213,7 +228,8 @@ Total segment size: `64 + 32 * control_size + capacity`.
213228
| `initial_reading_index()` | consumer | cursor for a late joiner (skips history) |
214229
| `loss_count()` | consumer | records skipped by this instance, counting only reads made with `count_loss` traits |
215230
| `reset()` | owner | clear all state (not thread-safe) |
216-
| `close()` / `unlink()` | lifecycle | detach / delete this instance's segment |
231+
| `close()` / `unlink()` | lifecycle | detach / delete this instance's segment (once, valid after `close()`) |
232+
| `remove_shm() -> bool` | lifecycle | unlink this instance's segment name, as C++ `remove_shm()` |
217233
| `SlickStreamBuffer.remove(name)` | lifecycle | unlink a segment by name - the stale-segment recovery step |
218234

219235
See [API_DIFFERENCES.md](API_DIFFERENCES.md) for the exact deviations from the C++ API

0 commit comments

Comments
 (0)