Task lock bus loading, always close feed stream on disconnect

cached_feeds
Tyler Goodlet 2021-04-05 07:58:28 -04:00
parent 8069bbe105
commit 100e27ac12
1 changed files with 127 additions and 111 deletions

View File

@ -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()