Skip to content

Commit f9aa611

Browse files
committed
Own sender-backed shared memory attachment
1 parent 0a57cc3 commit f9aa611

3 files changed

Lines changed: 124 additions & 4 deletions

File tree

src/polystore/streaming/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@
1414
FilePath,
1515
RoiStreamPayload,
1616
StreamablePayload,
17+
StreamingBackend,
1718
StreamingBatchItemPreparationAuthority,
1819
StreamingBatchMessageBuilder,
1920
StreamingBatchMessageRequest,
2021
StreamingBuiltBatch,
21-
StreamingPreparedBatchItems,
22-
StreamingBackend,
2322
StreamingComponentNamesRequest,
2423
StreamingItemPreparationRequest,
24+
StreamingPreparedBatchItems,
25+
StreamingSharedMemoryAuthority,
2526
ViewerDisplayPayloadExtra,
2627
)
2728

@@ -37,5 +38,6 @@
3738
"StreamingBackend",
3839
"StreamingComponentNamesRequest",
3940
"StreamingItemPreparationRequest",
41+
"StreamingSharedMemoryAuthority",
4042
"ViewerDisplayPayloadExtra",
4143
]

src/polystore/streaming/_streaming_backend.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
import uuid
1313
from collections.abc import Mapping, Sequence
1414
from dataclasses import dataclass, field
15-
from multiprocessing import shared_memory
15+
from multiprocessing import resource_tracker, shared_memory
16+
from multiprocessing.shared_memory import _USE_POSIX
1617
from pathlib import Path
1718
from types import MappingProxyType
1819
from typing import TypeAlias
@@ -310,7 +311,29 @@ def to_numpy(data: StreamablePayload) -> np.ndarray:
310311

311312

312313
class StreamingSharedMemoryAuthority:
313-
"""Allocate image payloads for viewer transfer through shared memory."""
314+
"""Own sender allocation and receiver attachment for shared-memory streams."""
315+
316+
@staticmethod
317+
def _release_receiver_tracking(memory: shared_memory.SharedMemory) -> None:
318+
"""Keep a non-owning POSIX receiver from unlinking sender-owned memory."""
319+
if _USE_POSIX:
320+
resource_tracker.unregister(memory._name, "shared_memory")
321+
322+
@classmethod
323+
def copy_sender_owned_array(
324+
cls,
325+
*,
326+
name: str,
327+
shape: Sequence[int],
328+
dtype: str | np.dtype,
329+
) -> np.ndarray:
330+
"""Copy an attached array without transferring allocation ownership."""
331+
memory = shared_memory.SharedMemory(name=name)
332+
try:
333+
cls._release_receiver_tracking(memory)
334+
return np.ndarray(shape, dtype=dtype, buffer=memory.buf).copy()
335+
finally:
336+
memory.close()
314337

315338
@classmethod
316339
def create(
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from __future__ import annotations
2+
3+
import numpy as np
4+
import pytest
5+
6+
from polystore.streaming import StreamingSharedMemoryAuthority, _streaming_backend
7+
8+
9+
class _SharedMemoryProbe:
10+
def __init__(self, source: np.ndarray) -> None:
11+
self._name = "/sender-owned"
12+
self.buf = bytearray(source.tobytes())
13+
self.closed = False
14+
15+
def close(self) -> None:
16+
self.closed = True
17+
18+
19+
def test_non_posix_receiver_copies_without_resource_tracker_unregister(
20+
monkeypatch,
21+
) -> None:
22+
source = np.arange(12, dtype=np.uint16).reshape(3, 4)
23+
memory = _SharedMemoryProbe(source)
24+
unregister_calls = []
25+
monkeypatch.setattr(_streaming_backend, "_USE_POSIX", False)
26+
monkeypatch.setattr(
27+
_streaming_backend.shared_memory,
28+
"SharedMemory",
29+
lambda *, name: memory,
30+
)
31+
monkeypatch.setattr(
32+
_streaming_backend.resource_tracker,
33+
"unregister",
34+
lambda *args: unregister_calls.append(args),
35+
)
36+
37+
copied = StreamingSharedMemoryAuthority.copy_sender_owned_array(
38+
name="sender-owned",
39+
shape=source.shape,
40+
dtype=str(source.dtype),
41+
)
42+
43+
np.testing.assert_array_equal(copied, source)
44+
assert copied.flags.owndata
45+
assert unregister_calls == []
46+
assert memory.closed
47+
48+
49+
def test_posix_receiver_releases_tracking_without_unlinking_sender_memory(
50+
monkeypatch,
51+
) -> None:
52+
source = np.arange(6, dtype=np.float32).reshape(2, 3)
53+
memory = _SharedMemoryProbe(source)
54+
unregister_calls = []
55+
monkeypatch.setattr(_streaming_backend, "_USE_POSIX", True)
56+
monkeypatch.setattr(
57+
_streaming_backend.shared_memory,
58+
"SharedMemory",
59+
lambda *, name: memory,
60+
)
61+
monkeypatch.setattr(
62+
_streaming_backend.resource_tracker,
63+
"unregister",
64+
lambda *args: unregister_calls.append(args),
65+
)
66+
67+
copied = StreamingSharedMemoryAuthority.copy_sender_owned_array(
68+
name="sender-owned",
69+
shape=source.shape,
70+
dtype=source.dtype,
71+
)
72+
73+
np.testing.assert_array_equal(copied, source)
74+
assert unregister_calls == [(memory._name, "shared_memory")]
75+
assert memory.closed
76+
77+
78+
def test_receiver_closes_attachment_when_array_projection_fails(monkeypatch) -> None:
79+
source = np.arange(2, dtype=np.uint8)
80+
memory = _SharedMemoryProbe(source)
81+
monkeypatch.setattr(_streaming_backend, "_USE_POSIX", False)
82+
monkeypatch.setattr(
83+
_streaming_backend.shared_memory,
84+
"SharedMemory",
85+
lambda *, name: memory,
86+
)
87+
88+
with pytest.raises(TypeError, match="buffer is too small"):
89+
StreamingSharedMemoryAuthority.copy_sender_owned_array(
90+
name="sender-owned",
91+
shape=(3,),
92+
dtype=source.dtype,
93+
)
94+
95+
assert memory.closed

0 commit comments

Comments
 (0)