Send / receive in parallel using websockets in Python FastAPI - python

I will try to explain what I am doing with an example, say I am building a weather client. The browser sends a message over websocket, eg:
{
"city": "Chicago",
"country": "US"
}
The server queries the weather every 5 minutes and updates the browser back with the latest data.
Now the browser could send another message, eg:
{
"city": "Bangalore",
"country": "IN"
}
Now I the server should STOP updating the weather details of Chicago and start updating the details about Bangalore, i.e. simultaneously send / receive messages over websocket. How should I go about implementing this?
Currently I have this but this only updates the browser on receiving an event:
#app.websocket("/ws")
async def read_webscoket(websocket: WebSocket):
await websocket.accept()
weather_client = WeatherClient(client)
while True:
data = await websocket.receive_json()
weather = await weather_client.weather(data)
await websocket.send_json(weather.dict())
If I move websocket.receive_json() outside the loop, I won't be able to continuously listen to the message from browser. I guess I need to spin up two asyncio tasks but I am not quite able to nail down the implementation since I am new to asynchronous way of programming.

The simplest way to do this is like you mentioned moving the reading outside of the loop in a separate task. In this paradigm you'll need to update a local variable with the latest data, making your code look something like this:
#app.websocket("/ws")
async def read_webscoket(websocket: WebSocket):
await websocket.accept()
json_data = await websocket.receive_json()
async def read_from_socket(websocket: WebSocket):
nonlocal json_data
async for data in websocket.iter_json():
json_data = data
asyncio.create_task(read_from_socket(websocket))
while True:
print(f"getting weather data for {json_data}")
await asyncio.sleep(1) # simulate a slow call to the weather service
Note I've used the iter_json asynchronous generator, which amounts to an infinite loop over receive_json.
This will work but may have a bug depending on your requirements. Imagine that the weather service takes 10 seconds to complete and in that time the user sends three requests for different cities over the socket. In the code above you'll only get the latest city the user sent. That might be fine for your application, but if you need to keep track of all that the user sent you'll need to use a queue. In this paradigm you'll have one task reading data and putting it on the queue and one task getting data from the queue and querying the weather service. You'll then run these concurrently with gather.
#app.websocket("/wsqueue")
async def read_webscoket(websocket: WebSocket):
await websocket.accept()
queue = asyncio.queues.Queue()
async def read_from_socket(websocket: WebSocket):
async for data in websocket.iter_json():
print(f"putting {data} in the queue")
queue.put_nowait(data)
async def get_data_and_send():
data = await queue.get()
while True:
if queue.empty():
print(f"getting weather data for {data}")
await asyncio.sleep(1)
else:
data = queue.get_nowait()
print(f"Setting data to {data}")
await asyncio.gather(read_from_socket(websocket), get_data_and_send())
In this way, you won't lose data the user sends. In the example above, I only get weather data for the latest the user requests, but you still have access to all data sent.
EDIT: To answer your question in the comments, a queue approach is probably best to cancel tasks when new requests come in. Basically move the long-running task you want to be able to cancel into its own coroutine function (in this example read_and_send_to_client) and run it as a task. When new data comes in, if that task is not finished, cancel it and then create a new one.
async def read_and_send_to_client(data):
print(f'reading {data} from client')
await asyncio.sleep(10) # simulate a slow call
print(f'finished reading {data}, sending to websocket client')
#app.websocket("/wsqueue")
async def read_webscoket(websocket: WebSocket):
await websocket.accept()
queue = asyncio.queues.Queue()
async def read_from_socket(websocket: WebSocket):
async for data in websocket.iter_json():
print(f"putting {data} in the queue")
queue.put_nowait(data)
async def get_data_and_send():
data = await queue.get()
fetch_task = asyncio.create_task(read_and_send_to_client(data))
while True:
data = await queue.get()
if not fetch_task.done():
print(f'Got new data while task not complete, canceling.')
fetch_task.cancel()
fetch_task = asyncio.create_task(read_and_send_to_client(data))
await asyncio.gather(read_from_socket(websocket), get_data_and_send())

Related

Python Websocket Client Sending Messages Periodically but with different time Intervalls

I want to send two different messages to a websocket server but with different time intervalls.
For example:
The first message should be send every 2 seconds.
The second message should send every 5 seconds.
async def send_first_message(websocket):
while True:
await websocket.send("FIRST MESSAGE")
response = await websocket.recv()
await asyncio.sleep(2)
async def send_second_message():
while True:
async with websockets.connect(f"ws://{IP}:{PORT}") as websocket:
asyncio.create_task(send_first_message(websocket))
while True:
await websocket.send("SECOND MESSAGE")
response = await websocket.recv()
await asyncio.sleep(5)
asyncio.run(send_second_message())
If I run the code like this I get:
"RuntimeError: cannot call recv while another coroutine is already waiting for the next message"
If I comment out one of the "await websocket.recv()" it works fine for a few seconds and then it throws:
"RuntimeError no close frame received or sent"
There's a bit of a disconnect between what you are trying to do in the tasks (synchronous request-response interaction) and what the protocol and the library expects you to do (asynchronous messages).
When writing asynchronous code, you need to look at what the library/protocol/service expects to be an atomic operation that can happen asynchronously to everything else, and what you want to be a synchronous series of operations. Then you need to find the primitive in the library that will support that. In the case of websockets, the atomic operation is a message being sent in either direction. So you can't expect websockets to synchronize flow over two messages.
Or to put it another way, you are expecting synchronous responses for each send message, but websockets are not designed to handle interleaved synchronous requests. You've sent a message to the websocket server, and you want to get a response to that message. But you've also sent another message on the same websocket and want a response to that too. Your client websocket library can't differentiate between a message intended for the first request and a message intended for the second request (because from the websocket protocol layer, that is a meaningless concept - so the library enforces this by limiting the recv operations on a websocket that can be blocking to one).
So ...
Option 1 - multiple tasks on separate sockets
From the fact the library limits a websocket to one blocking recv, a primitive in the protocol that meets the requirement is the websocket itself. If these are separate requests that you need separate blocking responses to (so only continue in the requesting task once those responses are available) then you could have separate websocket connections and block for the response in each.
client1.py
async def send_first_message():
async with websockets.connect(f"ws://{IP}:{PORT}") as websocket:
while True:
await websocket.send("FIRST MESSAGE")
response = await websocket.recv()
print(response)
await asyncio.sleep(2)
async def send_second_message():
async with websockets.connect(f"ws://{IP}:{PORT}") as websocket:
while True:
await websocket.send("SECOND MESSAGE")
response = await websocket.recv()
print(response)
await asyncio.sleep(5)
async def main():
asyncio.create_task(send_first_message())
asyncio.create_task(send_second_message())
await asyncio.Future()
asyncio.run(main())
Option 1 is however not really the websocket or asynchronous way.
Option 2 - embrace the asynchronous
To do this on a single websocket, you will need to receive the response asynchronous to both sending tasks.
If you don't actually care that the send_* functions get the response, you can do this easily...
client2.py
async def send_first_message(websocket):
while True:
await websocket.send("FIRST MESSAGE")
await asyncio.sleep(2)
async def send_second_message(websocket):
while True:
await websocket.send("SECOND MESSAGE")
await asyncio.sleep(5)
async def receive_message(websocket):
while True:
response = await websocket.recv()
print(response)
async def main():
async with websockets.connect(f"ws://{IP}:{PORT}") as websocket:
asyncio.create_task(send_first_message(websocket))
asyncio.create_task(send_second_message(websocket))
asyncio.create_task(receive_message(websocket))
await asyncio.Future()
asyncio.run(main())
Option 3
But what if you want to line up responses to requests and keep on a single websocket? You need some way of knowing which request any particular response is for. Most web services that need this sort of interaction will have you send an ID in the message to the server, and it will respond once a response is ready using the ID as a reference.
There's also a way of getting your message tasks to block and wait for the response with the right ID by queuing up the responses and checking them periodically.
client3.py
unhandled_responses = {}
async def send_first_message(websocket):
while True:
req_id = random.randint(0,65535)
message = json.dumps({'id': req_id, 'message': 'FIRST MESSAGE'})
await websocket.send(message)
response = await block_for_response(req_id)
print(response)
await asyncio.sleep(2)
async def send_second_message(websocket):
while True:
req_id = random.randint(0,65535)
message = json.dumps({'id': req_id, 'message': 'SECOND MESSAGE'})
await websocket.send(message)
response = await block_for_response(req_id)
print(response)
await asyncio.sleep(5)
async def block_for_response(id):
while True:
response = unhandled_responses.pop(id, None)
if response:
return response
await asyncio.sleep(0.1)
async def receive_message(websocket):
while True:
response = json.loads(await websocket.recv())
unhandled_responses[response['id']] = response
async def main():
async with websockets.connect(f"ws://{IP}:{PORT}") as websocket:
asyncio.create_task(send_first_message(websocket))
asyncio.create_task(send_second_message(websocket))
asyncio.create_task(receive_message(websocket))
await asyncio.Future()
asyncio.run(main())
For completeness, the server code the clients were talking to in my tests.
server.py
import asyncio
import websockets
async def server_endpoint(websocket):
try:
while True:
recv_msg = await websocket.recv()
response = recv_msg
await websocket.send(response)
except Exception as ex:
print(str(ex))
async def main():
async with websockets.serve(server_endpoint, "localhost", 8765):
await asyncio.Future() # run forever
if __name__ == "__main__":
asyncio.run(main())

Why do my async functions get stuck in infinite loop?

I'm trying to setup some functions to continuously fetch and send data back and forth. However, after sending, there needs to be a brief rest period (which is why I have asyncio.sleep(10)). So, I want to be continuously fetching data in my loop during this waiting time. My problem is once task #1 starts sleeping and task #2 begins executing, it never reverts back to task #1 when it wakes up. It gets stuck in this fetching data loop endlessly.
I tried fixing this problem with a global boolean variable to indicate when the sender was on cooldown but that felt like a cheap solution. I wanted to find out if there was a way to achieve my goals using asyncio built-in functions.
Trying to repeat this process: fetch some data continuously -> send some data -> go on cooldown and continue fetching data during this period
import asyncio
data = []
async def fetcher():
while True:
# Some code continuously fetching data
print("STUCK IN FETCHER")
async def sender():
# Some code which sends data
await asyncio.sleep(10)
async def main():
while True:
t1 = asyncio.create_task(sender())
t2 = asyncio.create_task(fetcher())
await t1
asyncio.run(main())
You are not awaiting anything in your fetcher, hence it is essentially blocking and doesn't give other coroutines the Chance to do some work. You can add await asyncio.sleep(0) which should help. Apart from this, you should also await the sleep in sender() as otherwise, it will not actually sleep 10 seconds, but just create a coroutine that's not executed.
fetcher is blocking/sync function. If you want to run them concurrently, it needs to put fetcher into another executor.
import asyncio
data = []
def fetcher():
"""Blocking fetcher."""
while True:
# Some code continuously fetching data
print("STUCK IN FETCHER")
async def sender():
"""Async sender."""
while True:
# Some code which sends data
await asyncio.sleep(10)
async def main():
loop = asyncio.get_running_loop()
await asyncio.gather(
# https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.run_in_executor
loop.run_in_executor(None, fetcher),
sender(),
)
asyncio.run(main())
It still needs to synchronise fetcher and sender on accessing data.
Building on what Simon Hawe said..
import asyncio
data = []
async def fetcher(event):
print("fetch = start")
while not event.is_set():
# Some code continuously fetching data
await asyncio.sleep(0) # This hands control to other coroutine
print("fetch = end")
async def sender(event):
print ("send = start")
# Some code which sends data
await asyncio.sleep(2)
print("send = done")
event.set()
async def main():
event = asyncio.Event()
for _ in range(2):
event.clear()
t1 = asyncio.create_task(sender(event))
t2 = asyncio.create_task(fetcher(event))
await asyncio.gather(t1,t2)
print("-")
asyncio.run(main())
Console result:
send = start
fetch = start
send = done
fetch = end
-
send = start
fetch = start
send = done
fetch = end
-
async.Event() is documented here. The use of an event comes with a caution: The use of this method is not thread-safe. The set state of the event is available to all tasks that consumes it. As stated:
Set the event. All tasks waiting for event to be set will be immediately awakened.

get live price in milliseconds (Binance Websocket)

how can i change my code so i get the informations every 100 milliseconds ?
import asyncio
from binance import AsyncClient, BinanceSocketManager
async def main():
client = await AsyncClient.create()
bm = BinanceSocketManager(client)
# start any sockets here, i.e a trade socket
ts = bm.trade_socket('BTCBUSD')
# then start receiving messages
async with ts as tscm:
while True:
res = await tscm.recv()
print(res)
await client.close_connection()
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
i apperciate every answer i can get , thanks a lot !
You asked about "how to get live price in milliseconds (Binance Websocket)"
Here is a list of available streams on binance with the available options for "update speed": https://github.com/binance/binance-spot-api-docs/blob/master/web-socket-streams.md#detailed-stream-information
For my understand most stream types are updating once a second (update speed: 1000ms).
Only depth streams are possible to update 10 times per second (1000ms and 100ms interval)
Trade streams (agg and normal) and Bookticker (individual and all) are in real time. That means, as soon this info is available, you will receive it...
If there is no trade, you will not receive anything, because there is no change in price... but as soon a trade happens, it will get reported to you.
If you want to know, the current buy and sell price for that an asset is available you can use the bookticker which is much less data compared to depth and diff. depth streams... if you need more than the first positions of the current orderbook i recommend using a local depth cache: https://www.lucit.tech/unicorn-binance-local-depth-cache.html
To get a stable websocket connection i recommend using UNICORN Binance WebSocket API, it catches most exceptions and reconnects automatically after a disconnect, it uses asyncio inside (callback function is inside an event loop) and the syntax to use it is easy:
from unicorn_binance_websocket_api.manager import BinanceWebSocketApiManager
def process_new_receives(stream_data, stream_buffer_name=False):
print(str(stream_data))
ubwa = BinanceWebSocketApiManager(exchange="binance.com")
ubwa.create_stream('trade',
['ethbtc', 'btcusdt', 'bnbbtc', 'ethbtc'],
process_stream_data=process_new_receives)
Since you seem in a rush, below is what I use although I'm using the websockets library to make the calls. I'll take a look at the binance api when I have some more time to see if I can get the calls to be faster but this should hopefully achieve what you want.
You can change the delay between the requests by changing the time of sleep in await asyncio.sleep(0.5) but if you put it any lower than 0.5 seconds it will trigger an error: received 1008 (policy violation) Too many requests; then sent 1008 (policy violation) Too many requests
import asyncio
import websockets
import json
msg = {"method": "SUBSCRIBE", "params":
[
"btcusdt#depth"
],
"id": 1
}
async def call_api():
async with websockets.connect('wss://stream.binance.com:9443/ws/btcusdt#depth') as ws:
while True:
await ws.send(json.dumps(msg))
response = await asyncio.wait_for(ws.recv(), timeout=2)
response = json.loads(response)
print(response)
await asyncio.sleep(0.5)
asyncio.get_event_loop().run_until_complete(call_api())
Try this out:
import asyncio
import websockets
import json
async def hello():
async with websockets.connect("wss://stream.binance.com:9443/ws/btcusdt#bookTicker") as ws:
while True:
response = await asyncio.wait_for(ws.recv(), timeout=2)
response=json.loads(response)
print(response)
await asyncio.sleep(0.5)
asyncio.get_event_loop().run_until_complete(hello())

Safely awaiting two event sources in asyncio/Quart

Quart is a Python web framework which re-implements the Flask API on top of the asyncio coroutine system of Python. In my particular case, I have a Quart websocket endpoint which is supposed to have not just one source of incoming events, but two possible sources of events which are supposed to continue the asynchronous loop.
An example with one event source:
from quart import Quart, websocket
app = Quart(__name__)
#app.websocket("/echo")
def echo():
while True:
incoming_message = await websocket.receive()
await websocket.send(incoming_message)
Taken from https://pgjones.gitlab.io/quart/
This example has one source: the incoming message stream. But what is the correct pattern if I had two possible sources, one being await websocket.receive() and another one being something along the lines of await system.get_next_external_notification() .
If either of them arrives, I'd like to send a websocket message.
I think I'll have to use asyncio.wait(..., return_when=FIRST_COMPLETED), but how do I make sure that I miss no data (i.e. for the race condition that websocket.receive() and system.get_next_external_notification() both finish almost exactly at the same time) ? What's the correct pattern in this case?
An idea you could use is a Queue to join the events together from different sources, then have an async function listening in the background to that queue for requests. Something like this might get you started:
import asyncio
from quart import Quart, websocket
app = Quart(__name__)
#app.before_serving
async def startup():
print(f'starting')
app.q = asyncio.Queue(1)
asyncio.ensure_future(listener(app.q))
async def listener(q):
while True:
returnq, msg = await q.get()
print(msg)
await returnq.put(f'hi: {msg}')
#app.route("/echo/<message>")
async def echo(message):
while True:
returnq = asyncio.Queue(1)
await app.q.put((returnq, message))
response = await returnq.get()
return response
#app.route("/echo2/<message>")
async def echo2(message):
while True:
returnq = asyncio.Queue(1)
await app.q.put((returnq, message))
response = await returnq.get()
return response

Discord.py bot, can I do a heavy task "off to the side" so I don't lag inputs?

I have a Discord bot in Python / Discord.py where people can enter commands, and normally the bot responds very quickly.
However the bot is also gathering/scraping webdata every iteration of the main loop. Normally the scraping is pretty short and sweet so nobody really notices, but from time to time the code is set up to do a more thorough scraping which takes a lot more time. But during these heavy scrapings, the bot is sort of unresponsive to user commands.
#bot.command()
async def sample_command(ctx):
# may actually take a while for this command to respond if we happen to be
# in the middle of a heavier site scrape
await ctx.channel.send("Random message, something indicating bot has responded")
async def main_loop():
sem = asyncio.Semaphore(60)
connector = aiohttp.TCPConnector(limit=60)
async with aiohttp.ClientSession(connector=connector, headers=headers) as session:
while True:
# main guts of loop here ...
scrapers = [scraper_1(session, sem), scraper_2(session, sem), ...]
data = list(chain(*await asyncio.gather(*scrapers))) # this may take a while
# do stuff with data
Is there a way to sort of have it go "Hey, you want to do a heavy scrape, fine go process it elsewhere - meanwhile let's continue with the main loop and I'll hook back up with you later when you're done and we'll process the data then", if that makes sense?
I mainly want to separate this scraping step so it's not holding up the ability for people to actually interact with the rest of the bot.
You can use the discord.py tasks extension docs.
For example:
from discord.ext import tasks
#bot.event()
async def on_ready():
main_loop.start()
#bot.command()
async def sample_command(ctx):
await ctx.channel.send("Random message, something indicating bot has responded")
#tasks.loop(seconds=60)
async def main_loop():
do_something()
Note: It's not recommended to start the tasks in on_ready because the bot will reconnect to discord and the task will start several times, Put it somewhere else or on_ready check if this the first connect.
Another simple tip: you can use await ctx.send() instead of await ctx.channel.send()
You can use asyncio.create_task() to spawn the scraping in the "background":
async def scrape_and_process(...):
scrapers = [scraper_1(session, sem), scraper_2(session, sem), ...]
data = list(chain(*await asyncio.gather(*scrapers))) # this may take a while
# do stuff with data
async def main_loop():
sem = asyncio.Semaphore(60)
connector = aiohttp.TCPConnector(limit=60)
async with aiohttp.ClientSession(connector=connector, headers=headers) as session:
while True:
# main guts of loop here ...
# initiate scraping and processing in the background and
# proceed with the loop
asyncio.create_task(scrape_and_process(...))
You can try to use python threading.
Learn more here
It basically allows you to run it on different threads
example:
import threading
def 1():
print("Helo! This is the first thread")
def 2():
print("Bonjour! This is the second thread")
thread1 = threading.Thread(target=1)
thread2 = Threading.Thread(target=2)
thread1.start()
thread2.start()

Categories

Resources