Task lock bus loading, always close feed stream on disconnect
parent
8069bbe105
commit
100e27ac12
|
@ -96,6 +96,7 @@ class _FeedsBus(BaseModel):
|
||||||
nursery: trio.Nursery
|
nursery: trio.Nursery
|
||||||
feeds: Dict[str, trio.CancelScope] = {}
|
feeds: Dict[str, trio.CancelScope] = {}
|
||||||
subscribers: Dict[str, List[tractor.Context]] = {}
|
subscribers: Dict[str, List[tractor.Context]] = {}
|
||||||
|
task_lock: trio.StrictFIFOLock = trio.StrictFIFOLock()
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
arbitrary_types_allowed = True
|
arbitrary_types_allowed = True
|
||||||
|
@ -115,7 +116,7 @@ def get_feed_bus(
|
||||||
) -> _FeedsBus:
|
) -> _FeedsBus:
|
||||||
"""
|
"""
|
||||||
Retreive broker-daemon-local data feeds bus from process global
|
Retreive broker-daemon-local data feeds bus from process global
|
||||||
scope.
|
scope. Serialize task access to lock.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -152,6 +153,7 @@ async def _setup_persistent_brokerd(brokername: str) -> None:
|
||||||
# parent actor decides to tear it down
|
# parent actor decides to tear it down
|
||||||
await trio.sleep_forever()
|
await trio.sleep_forever()
|
||||||
finally:
|
finally:
|
||||||
|
# TODO: this needs to be shielded?
|
||||||
await bus.cancel_all()
|
await bus.cancel_all()
|
||||||
|
|
||||||
|
|
||||||
|
@ -187,7 +189,7 @@ async def allocate_persistent_feed(
|
||||||
# if not opened:
|
# if not opened:
|
||||||
# raise RuntimeError("Persistent shm for sym was already open?!")
|
# raise RuntimeError("Persistent shm for sym was already open?!")
|
||||||
|
|
||||||
send, quote_stream = trio.open_memory_channel(2**8)
|
send, quote_stream = trio.open_memory_channel(10)
|
||||||
feed_is_live = trio.Event()
|
feed_is_live = trio.Event()
|
||||||
|
|
||||||
# establish broker backend quote stream
|
# establish broker backend quote stream
|
||||||
|
@ -204,119 +206,120 @@ async def allocate_persistent_feed(
|
||||||
)
|
)
|
||||||
|
|
||||||
init_msg[symbol]['shm_token'] = shm.token
|
init_msg[symbol]['shm_token'] = shm.token
|
||||||
cs = trio.CancelScope()
|
cs = bus.nursery.cancel_scope
|
||||||
|
|
||||||
# TODO: make this into a composed type which also
|
# TODO: make this into a composed type which also
|
||||||
# contains the backfiller cs for individual super-based
|
# contains the backfiller cs for individual super-based
|
||||||
# resspawns when needed.
|
# resspawns when needed.
|
||||||
bus.feeds[symbol] = (cs, init_msg, first_quote)
|
bus.feeds[symbol] = (cs, init_msg, first_quote)
|
||||||
|
|
||||||
with cs:
|
if opened:
|
||||||
if opened:
|
|
||||||
|
|
||||||
# start history backfill task ``backfill_bars()`` is
|
# start history backfill task ``backfill_bars()`` is
|
||||||
# a required backend func this must block until shm is
|
# a required backend func this must block until shm is
|
||||||
# filled with first set of ohlc bars
|
# filled with first set of ohlc bars
|
||||||
await bus.nursery.start(mod.backfill_bars, symbol, shm)
|
await bus.nursery.start(mod.backfill_bars, symbol, shm)
|
||||||
|
|
||||||
times = shm.array['time']
|
times = shm.array['time']
|
||||||
delay_s = times[-1] - times[times != times[-1]][-1]
|
delay_s = times[-1] - times[times != times[-1]][-1]
|
||||||
|
|
||||||
# pass OHLC sample rate in seconds
|
# pass OHLC sample rate in seconds
|
||||||
init_msg[symbol]['sample_rate'] = delay_s
|
init_msg[symbol]['sample_rate'] = delay_s
|
||||||
|
|
||||||
# yield back control to starting nursery
|
# yield back control to starting nursery
|
||||||
task_status.started((init_msg, first_quote))
|
task_status.started((init_msg, first_quote))
|
||||||
|
|
||||||
await feed_is_live.wait()
|
await feed_is_live.wait()
|
||||||
|
|
||||||
# # tell incrementer task it can start
|
if opened:
|
||||||
# shm_incrementing(shm.token['shm_name']).set()
|
_shms.setdefault(delay_s, []).append(shm)
|
||||||
|
|
||||||
# start shm incrementingn for OHLC sampling
|
# start shm incrementing for OHLC sampling
|
||||||
# subscribe_ohlc_for_increment(shm, delay_s)
|
if _incrementers.get(delay_s) is None:
|
||||||
|
cs = await bus.nursery.start(increment_ohlc_buffer, delay_s)
|
||||||
|
|
||||||
if opened:
|
sum_tick_vlm: bool = init_msg.get(
|
||||||
_shms.setdefault(delay_s, []).append(shm)
|
'shm_write_opts', {}
|
||||||
|
).get('sum_tick_vlm', True)
|
||||||
|
|
||||||
if _incrementers.get(delay_s) is None:
|
log.info("Started shared mem bar writer")
|
||||||
cs = await bus.nursery.start(increment_ohlc_buffer, delay_s)
|
|
||||||
|
|
||||||
sum_tick_vlm: bool = init_msg.get(
|
# iterate stream delivered by broker
|
||||||
'shm_write_opts', {}
|
async for quotes in quote_stream:
|
||||||
).get('sum_tick_vlm', True)
|
for sym, quote in quotes.items():
|
||||||
|
|
||||||
# begin shm write loop and broadcast to subscribers
|
# TODO: in theory you can send the IPC msg *before*
|
||||||
async with quote_stream:
|
# writing to the sharedmem array to decrease latency,
|
||||||
|
# however, that will require `tractor.msg.pub` support
|
||||||
|
# here or at least some way to prevent task switching
|
||||||
|
# at the yield such that the array write isn't delayed
|
||||||
|
# while another consumer is serviced..
|
||||||
|
|
||||||
log.info("Started shared mem bar writer")
|
# start writing the shm buffer with appropriate
|
||||||
|
# trade data
|
||||||
|
for tick in quote['ticks']:
|
||||||
|
|
||||||
# iterate stream delivered by broker
|
# if tick['type'] in ('utrade',):
|
||||||
async for quotes in quote_stream:
|
# print(tick)
|
||||||
for sym, quote in quotes.items():
|
|
||||||
|
|
||||||
# TODO: in theory you can send the IPC msg *before*
|
# write trade events to shm last OHLC sample
|
||||||
# writing to the sharedmem array to decrease latency,
|
if tick['type'] in ('trade', 'utrade'):
|
||||||
# however, that will require `tractor.msg.pub` support
|
|
||||||
# here or at least some way to prevent task switching
|
|
||||||
# at the yield such that the array write isn't delayed
|
|
||||||
# while another consumer is serviced..
|
|
||||||
|
|
||||||
# start writing the shm buffer with appropriate trade data
|
last = tick['price']
|
||||||
for tick in quote['ticks']:
|
|
||||||
|
|
||||||
# if tick['type'] in ('utrade',):
|
# update last entry
|
||||||
# print(tick)
|
# benchmarked in the 4-5 us range
|
||||||
|
o, high, low, v = shm.array[-1][
|
||||||
|
['open', 'high', 'low', 'volume']
|
||||||
|
]
|
||||||
|
|
||||||
# write trade events to shm last OHLC sample
|
new_v = tick.get('size', 0)
|
||||||
if tick['type'] in ('trade', 'utrade'):
|
|
||||||
|
|
||||||
last = tick['price']
|
if v == 0 and new_v:
|
||||||
|
# no trades for this bar yet so the open
|
||||||
|
# is also the close/last trade price
|
||||||
|
o = last
|
||||||
|
|
||||||
# update last entry
|
if sum_tick_vlm:
|
||||||
# benchmarked in the 4-5 us range
|
volume = v + new_v
|
||||||
o, high, low, v = shm.array[-1][
|
else:
|
||||||
['open', 'high', 'low', 'volume']
|
# presume backend takes care of summing
|
||||||
]
|
# it's own vlm
|
||||||
|
volume = quote['volume']
|
||||||
|
|
||||||
new_v = tick.get('size', 0)
|
shm.array[[
|
||||||
|
'open',
|
||||||
|
'high',
|
||||||
|
'low',
|
||||||
|
'close',
|
||||||
|
'bar_wap', # can be optionally provided
|
||||||
|
'volume',
|
||||||
|
]][-1] = (
|
||||||
|
o,
|
||||||
|
max(high, last),
|
||||||
|
min(low, last),
|
||||||
|
last,
|
||||||
|
quote.get('bar_wap', 0),
|
||||||
|
volume,
|
||||||
|
)
|
||||||
|
|
||||||
if v == 0 and new_v:
|
# XXX: we need to be very cautious here that no
|
||||||
# no trades for this bar yet so the open
|
# context-channel is left lingering which doesn't have
|
||||||
# is also the close/last trade price
|
# a far end receiver actor-task. In such a case you can
|
||||||
o = last
|
# end up triggering backpressure which which will
|
||||||
|
# eventually block this producer end of the feed and
|
||||||
if sum_tick_vlm:
|
# thus other consumers still attached.
|
||||||
volume = v + new_v
|
subs = bus.subscribers[sym]
|
||||||
else:
|
for ctx in subs:
|
||||||
# presume backend takes care of summing
|
# print(f'sub is {ctx.chan.uid}')
|
||||||
# it's own vlm
|
try:
|
||||||
volume = quote['volume']
|
await ctx.send_yield({sym: quote})
|
||||||
|
except (
|
||||||
shm.array[[
|
trio.BrokenResourceError,
|
||||||
'open',
|
trio.ClosedResourceError
|
||||||
'high',
|
):
|
||||||
'low',
|
subs.remove(ctx)
|
||||||
'close',
|
log.error(f'{ctx.chan.uid} dropped connection')
|
||||||
'bar_wap', # can be optionally provided
|
|
||||||
'volume',
|
|
||||||
]][-1] = (
|
|
||||||
o,
|
|
||||||
max(high, last),
|
|
||||||
min(low, last),
|
|
||||||
last,
|
|
||||||
quote.get('bar_wap', 0),
|
|
||||||
volume,
|
|
||||||
)
|
|
||||||
|
|
||||||
for ctx in bus.subscribers[sym]:
|
|
||||||
try:
|
|
||||||
await ctx.send_yield({sym: quote})
|
|
||||||
except (
|
|
||||||
trio.BrokenResourceError,
|
|
||||||
trio.ClosedResourceError
|
|
||||||
):
|
|
||||||
log.error(f'{ctx.chan.uid} dropped connection')
|
|
||||||
|
|
||||||
|
|
||||||
@tractor.stream
|
@tractor.stream
|
||||||
|
@ -327,6 +330,7 @@ async def attach_feed_bus(
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
):
|
):
|
||||||
|
|
||||||
|
# try:
|
||||||
if loglevel is None:
|
if loglevel is None:
|
||||||
loglevel = tractor.current_actor().loglevel
|
loglevel = tractor.current_actor().loglevel
|
||||||
|
|
||||||
|
@ -337,35 +341,42 @@ async def attach_feed_bus(
|
||||||
assert 'brokerd' in tractor.current_actor().name
|
assert 'brokerd' in tractor.current_actor().name
|
||||||
|
|
||||||
bus = get_feed_bus(brokername)
|
bus = get_feed_bus(brokername)
|
||||||
task_cs = bus.feeds.get(symbol)
|
|
||||||
bus.subscribers.setdefault(symbol, []).append(ctx)
|
|
||||||
|
|
||||||
# if no cached feed for this symbol has been created for this
|
async with bus.task_lock:
|
||||||
# brokerd yet, start persistent stream and shm writer task in
|
task_cs = bus.feeds.get(symbol)
|
||||||
# service nursery
|
sub_only: bool = False
|
||||||
if task_cs is None:
|
|
||||||
init_msg, first_quote = await bus.nursery.start(
|
# if no cached feed for this symbol has been created for this
|
||||||
partial(
|
# brokerd yet, start persistent stream and shm writer task in
|
||||||
allocate_persistent_feed,
|
# service nursery
|
||||||
ctx=ctx,
|
if task_cs is None:
|
||||||
bus=bus,
|
init_msg, first_quote = await bus.nursery.start(
|
||||||
brokername=brokername,
|
partial(
|
||||||
symbol=symbol,
|
allocate_persistent_feed,
|
||||||
loglevel=loglevel,
|
ctx=ctx,
|
||||||
|
bus=bus,
|
||||||
|
brokername=brokername,
|
||||||
|
symbol=symbol,
|
||||||
|
loglevel=loglevel,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
bus.subscribers.setdefault(symbol, []).append(ctx)
|
||||||
|
else:
|
||||||
|
sub_only = True
|
||||||
|
|
||||||
# XXX: ``first_quote`` may be outdated here if this is secondary subscriber
|
# XXX: ``first_quote`` may be outdated here if this is secondary
|
||||||
|
# subscriber
|
||||||
cs, init_msg, first_quote = bus.feeds[symbol]
|
cs, init_msg, first_quote = bus.feeds[symbol]
|
||||||
|
|
||||||
# send this even to subscribers to existing feed?
|
# send this even to subscribers to existing feed?
|
||||||
await ctx.send_yield(init_msg)
|
await ctx.send_yield(init_msg)
|
||||||
await ctx.send_yield(first_quote)
|
await ctx.send_yield(first_quote)
|
||||||
|
|
||||||
try:
|
if sub_only:
|
||||||
# just block while the stream pumps
|
bus.subscribers[symbol].append(ctx)
|
||||||
await trio.sleep_forever()
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
await trio.sleep_forever()
|
||||||
finally:
|
finally:
|
||||||
bus.subscribers[symbol].remove(ctx)
|
bus.subscribers[symbol].remove(ctx)
|
||||||
|
|
||||||
|
@ -484,11 +495,10 @@ async def open_feed(
|
||||||
# https://github.com/goodboy/tractor/issues/53
|
# https://github.com/goodboy/tractor/issues/53
|
||||||
init_msg = await stream.receive()
|
init_msg = await stream.receive()
|
||||||
|
|
||||||
|
# we can only read from shm
|
||||||
shm = attach_shm_array(
|
shm = attach_shm_array(
|
||||||
token=init_msg[sym]['shm_token'],
|
token=init_msg[sym]['shm_token'],
|
||||||
|
readonly=True,
|
||||||
# we are the buffer writer
|
|
||||||
readonly=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
feed = Feed(
|
feed = Feed(
|
||||||
|
@ -522,4 +532,10 @@ async def open_feed(
|
||||||
|
|
||||||
feed._max_sample_rate = max(ohlc_sample_rates)
|
feed._max_sample_rate = max(ohlc_sample_rates)
|
||||||
|
|
||||||
yield feed
|
try:
|
||||||
|
yield feed
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# always cancel the far end producer task
|
||||||
|
with trio.CancelScope(shield=True):
|
||||||
|
await stream.aclose()
|
||||||
|
|
Loading…
Reference in New Issue