Add a quote stream server task
Add a daemon-server task for delivering subscription based quote streams via json serialized packets wrapped in a queue interface.kivy_mainline_and_py3.8
parent
07c95e4f3f
commit
d65bd78f5d
|
@ -3,6 +3,7 @@ Core broker-daemon tasks and API.
|
||||||
"""
|
"""
|
||||||
import time
|
import time
|
||||||
import inspect
|
import inspect
|
||||||
|
import json
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import socket
|
import socket
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
|
@ -67,11 +68,55 @@ async def wait_for_network(get_quotes, sleep=1):
|
||||||
await trio.sleep(sleep)
|
await trio.sleep(sleep)
|
||||||
|
|
||||||
|
|
||||||
|
class Disconnect(trio.Cancelled):
|
||||||
|
"Stream was closed"
|
||||||
|
|
||||||
|
|
||||||
|
class StreamQueue:
|
||||||
|
"""Stream wrapped as a queue that delivers json serialized "packets"
|
||||||
|
delimited by ``delim``.
|
||||||
|
"""
|
||||||
|
def __init__(self, stream, delim=b'\n'):
|
||||||
|
self.stream = stream
|
||||||
|
self._delim = delim
|
||||||
|
self.peer = stream.socket.getpeername()
|
||||||
|
|
||||||
|
async def get(self):
|
||||||
|
"""Get a packet from the underlying stream.
|
||||||
|
"""
|
||||||
|
delim = self._delim
|
||||||
|
buff = b''
|
||||||
|
while True:
|
||||||
|
data = await self.stream.receive_some(2**10)
|
||||||
|
log.trace(f"Data is {data}")
|
||||||
|
if data == b'':
|
||||||
|
raise Disconnect("Stream connection was closed")
|
||||||
|
buff += data
|
||||||
|
if delim in buff:
|
||||||
|
try:
|
||||||
|
return json.loads(buff)
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
log.exception("Failed to process JSON packet:")
|
||||||
|
continue
|
||||||
|
|
||||||
|
async def put(self, data):
|
||||||
|
return await self.stream.send_all(json.dumps(data).encode() + b'\n')
|
||||||
|
|
||||||
|
async def __aiter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __anext__(self):
|
||||||
|
try:
|
||||||
|
return await self.get()
|
||||||
|
except Disconnect:
|
||||||
|
raise StopAsyncIteration
|
||||||
|
|
||||||
|
|
||||||
async def poll_tickers(
|
async def poll_tickers(
|
||||||
client: 'Client',
|
client: 'Client',
|
||||||
quoter: AsyncContextManager,
|
quoter: AsyncContextManager,
|
||||||
tickers: [str],
|
tickers: [str],
|
||||||
q: trio.Queue,
|
queue: StreamQueue,
|
||||||
rate: int = 5, # delay between quote requests
|
rate: int = 5, # delay between quote requests
|
||||||
diff_cached: bool = True, # only deliver "new" quotes to the queue
|
diff_cached: bool = True, # only deliver "new" quotes to the queue
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -85,6 +130,11 @@ async def poll_tickers(
|
||||||
_cache = {} # ticker to quote caching
|
_cache = {} # ticker to quote caching
|
||||||
|
|
||||||
async with quoter(client, tickers) as get_quotes:
|
async with quoter(client, tickers) as get_quotes:
|
||||||
|
# run a first quote smoke test filtering out any bad tickers
|
||||||
|
first_quotes_dict = await get_quotes(tickers)
|
||||||
|
# FIXME: oh god it's so hideous
|
||||||
|
tickers[:] = list(first_quotes_dict.keys())[:]
|
||||||
|
|
||||||
while True: # use an event here to trigger exit?
|
while True: # use an event here to trigger exit?
|
||||||
prequote_start = time.time()
|
prequote_start = time.time()
|
||||||
|
|
||||||
|
@ -118,7 +168,7 @@ async def poll_tickers(
|
||||||
payload[symbol] = quote
|
payload[symbol] = quote
|
||||||
|
|
||||||
if payload:
|
if payload:
|
||||||
q.put_nowait(payload)
|
await queue.put(payload)
|
||||||
|
|
||||||
req_time = round(postquote_start - prequote_start, 3)
|
req_time = round(postquote_start - prequote_start, 3)
|
||||||
proc_time = round(time.time() - postquote_start, 3)
|
proc_time = round(time.time() - postquote_start, 3)
|
||||||
|
@ -132,3 +182,47 @@ async def poll_tickers(
|
||||||
else:
|
else:
|
||||||
log.debug(f"Sleeping for {delay}")
|
log.debug(f"Sleeping for {delay}")
|
||||||
await trio.sleep(delay)
|
await trio.sleep(delay)
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_subs(
|
||||||
|
queue,
|
||||||
|
stream2tickers,
|
||||||
|
nursery,
|
||||||
|
task_status=trio.TASK_STATUS_IGNORED
|
||||||
|
):
|
||||||
|
"""Handle quote stream subscriptions.
|
||||||
|
"""
|
||||||
|
async with queue.stream:
|
||||||
|
async for tickers in queue:
|
||||||
|
task_status.started(tickers)
|
||||||
|
log.info(f"{queue.peer} subscribed for tickers {tickers}")
|
||||||
|
stream2tickers[queue.peer] = tickers
|
||||||
|
else:
|
||||||
|
log.info(f"{queue.peer} was disconnected")
|
||||||
|
nursery.cancel_scope.cancel()
|
||||||
|
|
||||||
|
|
||||||
|
async def _daemon_main(brokermod):
|
||||||
|
"""Main entry point for the piker daemon.
|
||||||
|
"""
|
||||||
|
stream2tickers = {}
|
||||||
|
async with brokermod.get_client() as client:
|
||||||
|
|
||||||
|
async def start_quoter(stream):
|
||||||
|
queue = StreamQueue(stream) # wrap in a shabby queue-like api
|
||||||
|
log.debug(f"Accepted new connection from {queue.peer}")
|
||||||
|
|
||||||
|
# spawn request handler
|
||||||
|
async with trio.open_nursery() as nursery:
|
||||||
|
await nursery.start(
|
||||||
|
_handle_subs, queue, stream2tickers, nursery)
|
||||||
|
nursery.start_soon(
|
||||||
|
poll_tickers, client, brokermod.quoter,
|
||||||
|
stream2tickers[queue.peer], queue
|
||||||
|
)
|
||||||
|
|
||||||
|
async with trio.open_nursery() as nursery:
|
||||||
|
listeners = await nursery.start(
|
||||||
|
partial(trio.serve_tcp, start_quoter, 1616, host='127.0.0.1')
|
||||||
|
)
|
||||||
|
log.debug(f"Spawned {listeners}")
|
||||||
|
|
Loading…
Reference in New Issue