Add subscription support to message streams
Add `ReceiveMsgStream.subscribe()` which allows allocating a broadcast receiver around the stream for use by multiple actor-local consumer tasks. Entering this context manager idempotently mutates the stream's receive machinery which for now can not be undone. Move `.clone()` to the receive stream type. Resolves #204live_on_air_from_tokio
parent
a12b1fc631
commit
2d1c24112b
|
@ -2,6 +2,7 @@
|
||||||
Message stream types and APIs.
|
Message stream types and APIs.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
import inspect
|
import inspect
|
||||||
from contextlib import contextmanager, asynccontextmanager
|
from contextlib import contextmanager, asynccontextmanager
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
@ -17,6 +18,7 @@ import trio
|
||||||
from ._ipc import Channel
|
from ._ipc import Channel
|
||||||
from ._exceptions import unpack_error, ContextCancelled
|
from ._exceptions import unpack_error, ContextCancelled
|
||||||
from ._state import current_actor
|
from ._state import current_actor
|
||||||
|
from ._broadcast import broadcast_receiver, BroadcastReceiver
|
||||||
from .log import get_logger
|
from .log import get_logger
|
||||||
|
|
||||||
|
|
||||||
|
@ -52,6 +54,7 @@ class ReceiveMsgStream(trio.abc.ReceiveChannel):
|
||||||
) -> None:
|
) -> None:
|
||||||
self._ctx = ctx
|
self._ctx = ctx
|
||||||
self._rx_chan = rx_chan
|
self._rx_chan = rx_chan
|
||||||
|
self._broadcaster: Optional[BroadcastReceiver] = None
|
||||||
|
|
||||||
# flag to denote end of stream
|
# flag to denote end of stream
|
||||||
self._eoc: bool = False
|
self._eoc: bool = False
|
||||||
|
@ -231,6 +234,56 @@ class ReceiveMsgStream(trio.abc.ReceiveChannel):
|
||||||
# still need to consume msgs that are "in transit" from the far
|
# still need to consume msgs that are "in transit" from the far
|
||||||
# end (eg. for ``Context.result()``).
|
# end (eg. for ``Context.result()``).
|
||||||
|
|
||||||
|
def clone(self):
|
||||||
|
"""Clone this receive channel allowing for multi-task
|
||||||
|
consumption from the same channel.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return type(self)(
|
||||||
|
self._ctx,
|
||||||
|
self._rx_chan.clone(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def subscribe(
|
||||||
|
self,
|
||||||
|
|
||||||
|
) -> BroadcastReceiver:
|
||||||
|
'''Allocate and return a ``BroadcastReceiver`` which delegates
|
||||||
|
to this message stream.
|
||||||
|
|
||||||
|
This allows multiple local tasks to receive each their own copy
|
||||||
|
of this message stream.
|
||||||
|
|
||||||
|
This operation is indempotent and and mutates this stream's
|
||||||
|
receive machinery to copy and window-length-store each received
|
||||||
|
value from the far end via the internally created broudcast
|
||||||
|
receiver wrapper.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if self._broadcaster is None:
|
||||||
|
self._broadcaster = broadcast_receiver(
|
||||||
|
self,
|
||||||
|
self._rx_chan._state.max_buffer_size,
|
||||||
|
)
|
||||||
|
# override the original stream instance's receive to
|
||||||
|
# delegate to the broadcaster receive such that
|
||||||
|
# new subscribers will be copied received values
|
||||||
|
# XXX: this operation is indempotent and non-reversible,
|
||||||
|
# so be sure you can deal with any (theoretical) overhead
|
||||||
|
# of the the ``BroadcastReceiver`` before calling
|
||||||
|
# this method for the first time.
|
||||||
|
|
||||||
|
# XXX: why does this work without a recursion issue?!
|
||||||
|
self.receive = self._broadcaster.receive
|
||||||
|
|
||||||
|
async with self._broadcaster.subscribe() as bstream:
|
||||||
|
# a ``MsgStream`` clone is allocated for the
|
||||||
|
# broadcaster to track this entry's subscription
|
||||||
|
stream_clone = bstream._rx
|
||||||
|
assert stream_clone is not self
|
||||||
|
yield bstream
|
||||||
|
|
||||||
|
|
||||||
class MsgStream(ReceiveMsgStream, trio.abc.Channel):
|
class MsgStream(ReceiveMsgStream, trio.abc.Channel):
|
||||||
"""
|
"""
|
||||||
|
@ -247,17 +300,6 @@ class MsgStream(ReceiveMsgStream, trio.abc.Channel):
|
||||||
'''
|
'''
|
||||||
await self._ctx.chan.send({'yield': data, 'cid': self._ctx.cid})
|
await self._ctx.chan.send({'yield': data, 'cid': self._ctx.cid})
|
||||||
|
|
||||||
# TODO: but make it broadcasting to consumers
|
|
||||||
def clone(self):
|
|
||||||
"""Clone this receive channel allowing for multi-task
|
|
||||||
consumption from the same channel.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return MsgStream(
|
|
||||||
self._ctx,
|
|
||||||
self._rx_chan.clone(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Context:
|
class Context:
|
||||||
|
|
Loading…
Reference in New Issue