Python Tornado: how can I make this asynchronous ? - python

In the below code, when I do message_response.get(), it will make this particular code synchronous. Now is there a way I can make this aynchronous? Just push the code to a broker. And once celery worker is done with the task, I can write the result back to the client?
import tornado.websocket
from celery_main import do_something_celery_task
class HomePageRequestHandler(tornado.websocket.WebSocketHandler):
def on_message(self, message):
message_response = do_something_celery_task.apply_async((message,))
# How can this be a non blocking call?
self.write_message(message_response.get())
def open(self):
pass

You should try something like this: https://github.com/mher/tornado-celery
You'll have code like this, but I don't run it.
from tornado.websocket import WebSocketHandler
class WebSocketBase(WebSocketHandler):
#gen.coroutine
def on_message(self, message):
response = yield gen.Task(tasks.sleep.apply_async, args=[3])
self.write_message(str(response.result))

Related

Call async functions in blocking context in Tornado

I want to implement a service based on web sockets in the Tornado framework. When a user closes a web socket, I want to notify the other users about this. However, on_close is apparently a blocking function and my _broadcast(str) -> None function is async.
How can I call this function anyway?
from tornado import websocket
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class SocketHandler(websocket.WebSocketHandler):
async def open(self, *args, conns, **kwargs):
logger.info(f"Opened a new connection to client {id(self)}")
self._conns = conns
async def on_message(self, message):
logger.info(f"Client {id(self)} sent message: {message}")
await self._broadcast(message)
def on_close(self):
logger.info(f"Client {id(self)} has left the scene")
self._conns.remove(self)
self._broadcast("something") # TODO
async def _broadcast(self, msg):
for conn in self._conns:
try:
await conn.write_message(msg)
except websocket.WebSocketClosedError:
pass
app = web.Application([
(r'/ws', SocketHandler)
])
if __name__ == '__main__':
app.listen(9000)
ioloop.IOLoop.instance().start()
The simple solution you're looking for is to use asyncio.create_task when calling the coroutine:
def on_close(self):
logger.info(f"Client {id(self)} has left the scene")
self._conns.remove(self)
asyncio.create_task(self._broadcast("something"))
(the legacy Tornado version of this function is tornado.gen.convert_yielded, but now that Tornado and asyncio are integrated there's no reason not to use the asyncio version for native coroutines)
But for this particular problem, the use of await in your _broadcast function is not ideal. Awaiting a write_message is used to provide flow control, but create_task doesn't do anything useful with the backpressure provided by await. (write_message is fairly unusual in that it is fully supported to call it both with and without await). In fact, it applies backpressure to the wrong things - one slow connection will slow notifications to all the others that come after it.
So in this case I'd advise making _broadcast a regular synchronous function:
def _broadcast(self, msg):
for conn in self._conns:
try:
conn.write_message(msg)
except websocket.WebSocketClosedError:
pass
If you want to be better able to control memory usage (via the flow control provided by await write_message), you'll need a more complicated solution, probably involving a bounded queue for each connection (in on_close, use put_nowait to add the message to every connection's queue, then have a task that reads from the queue and writes the message with await write_message)
i think a solution that involves using an asyncio.Queue should work for you.
i made a small class as a mock-up to test this out:
import asyncio
import time
class Thing:
on_close_q = asyncio.Queue()
def __init__(self):
self.conns = range(3)
def on_close(self, id):
time.sleep(id)
print(f'closing {id}')
self.on_close_q.put_nowait((self, id))
async def notify(self, msg):
print('in notify')
for conn in range(3):
print(f'notifying {conn} {msg}')
async def monitor_on_close():
print('monitoring')
while True:
instance, id = await Thing.on_close_q.get()
await instance.notify(f'{id} is closed')
from there, you'll need to run monitor_on_close in the ioloop you get from tornado. i've never used tornado, but i think adding something like this to your __main__ block should work:
ioloop.IOLoop.current().add_callback(monitor_on_close)

Best way to share data with a producer coroutine loop from the python websockets package?

I want to set up a websocket server in python (using this websocket library) to send data that is generated through a "produce_msg" function. This function does some intense calculations, produces a message and should trigger the sending via websockets.
What is the best way to share the data with the producer coroutine loop and trigger the sending?
I think calling the "produce_msg" function inside the producer coroutine is possible. But can I also call a send function from inside the "producer_msg function? I want to use a websocket class to provide the send functionality to users of this class.
Browser-based example with producer coroutine loop from websocket library website:
import asyncio
import websockets
async def producer_handler(websocket, path):
while True:
message = await produce_msg()
await websocket.send(message)
start_server = websockets.serve(producer_handler, "127.0.0.1", 5678)
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
Update
I managed to build a version that works using a callback function. In my example I use produce_msg() as callback function.
Till now I did not manage to build a version in which the Websocket class has a send_msg() function that can be called to send a message (instead of a callback function).
import asyncio
import websockets
import datetime
import random
import time
class Websocket:
def __init__(self, callback, ip="127.0.0.1", port=5678):
self.callback = callback
self.ip = ip
self.port = port
self.start()
def start(self):
start_server = websockets.serve(self.handler, self.ip, self.port)
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
async def handler(self, websocket, path):
while True:
msg = self.callback()
await websocket.send(msg)
if __name__ == "__main__":
def produce_msg():
time.sleep(2.4)
return datetime.datetime.utcnow().isoformat() + "Z"
ws = Websocket(produce_msg)

Python websockets lib client persistent connection (with class implementation)

I'm trying to implement a websocket client in python using websockets and the apparently mandatory asyncio which I never used before (and I have a hard time to understand...).
I've read a lot on the subject and saw (too) many examples here and everywhere, but I can't find a way to properly make a websocket client with a persistent connection.
I need to have a persistent connection because the commands need to be requested on the same connection, the first one being an authentication command.
The remote server is a 3rd party API I don't have any control over.
I suppose I could run an authentication request along with each command my program sends but that does not feel right to open > auth > request > close for each command instead of keeping one connection alive during the whole program's life
My implementation is a library using many classes and I need to wrap the websocket connector/handler in one of them
Here's what I have right now, based on examples I found here and there (with some obfuscated data) :
import json
import asyncio
from websockets import connect
URL = 'wss://server.com/endpoint'
class Websocket:
async def __aenter__(self):
self._conn = connect(URL)
self.websocket = await self._conn.__aenter__()
return self
async def __aexit__(self, *args, **kwargs):
await self._conn.__aexit__(*args, **kwargs)
async def send(self, message):
await self.websocket.send(message)
async def receive(self):
return await self.websocket.recv()
class Handler:
def __init__(self):
self.wws = Websocket()
self.loop = asyncio.get_event_loop()
def command(self, cmd):
return self.loop.run_until_complete(self.__async__command(cmd))
async def __async__command(self, cmd):
async with self.wws as echo:
await echo.send(json.dumps(cmd))
return await echo.receive()
def main():
handler = Handler()
foo = handler.command('authentication command')
print('auth: ', foo)
bar = handler.command('another command to run depending on the first authentication')
print('command: ', bar)
if __name__ == '__main__':
main()
Basically right now I get these answers (simplified and obfuscated) :
auth: Ok, authenticated
command: Command refused, not authenticated
I suppose my problem is that the block async with self.wws as echo: kind of create the connection, runs its code then drop it instead of keeping the connection alive. Since we are not using a usual __init__ here but some asyncio voodoo I don't understand, I'm kind of stuck.
I think your diagnosis is correct, the problem is that the async context manager it creating and closing a connection for each call of Handler.command ... really not want you want.
Instead you could just synchronously establish the websocket connection during the init of Handler and then store the connection websocket (instance of type WebSocketClientProtocol) as a class member for later use, as in this sample code:
import json
import asyncio
from websockets import connect
URL = 'ws://localhost:8000'
class Handler:
def __init__(self):
self.ws = None
self.loop = asyncio.get_event_loop()
# perform a synchronous connect
self.loop.run_until_complete(self.__async__connect())
async def __async__connect(self):
print("attempting connection to {}".format(URL))
# perform async connect, and store the connected WebSocketClientProtocol
# object, for later reuse for send & recv
self.ws = await connect(URL)
print("connected")
def command(self, cmd):
return self.loop.run_until_complete(self.__async__command(cmd))
async def __async__command(self, cmd):
await self.ws.send(json.dumps(cmd))
return await self.ws.recv()
def main():
handler = Handler()
foo = handler.command('authentication command')
print('auth: ', foo)
bar = handler.command('another command to run depending on the first authentication')
print('command: ', bar)
if __name__ == '__main__':
main()

push with python + asyncio + websockets = missing messages

I'm trying to create a websocket server on which a single client will push its messages (I know this is not the usual way to use websocket but this part is not my choice). To this end, I'm using python 3.7 and websockets 7.0.
My issue is: Numerous messages pushed by the client are not received by the server.
Here is the simple code I'm using.
import asyncio
import websockets
async def get_tag_data(websocket, path):
# print('received')
data = await websocket.recv()
print(data)
loop = asyncio.get_event_loop()
anchors_server = websockets.serve(get_tag_data, 'localhost', 9001)
loop.run_until_complete(asyncio.gather(anchors_server))
loop.run_forever()
Conversely, when I try the python-websocket-server (which uses threads for reception), all my messages are correctly received.
As far as I understand asyncio & websockets, it is supposed to manage backpressure: messages sent while the server is busy (to process older messages) are "buffered" and will be treat shortly....
What am I missing? Do I need to thread the websocket reception with asyncio to avoid losing messages?
Thank you for your answers!
Ok, I get it.
My function was running only once, next messages was not buffered. The following code resolve the issue :
import asyncio
import websockets
import signal
import sys
async def get_tag_data(websocket, path):
while True:
async for data in websocket:
print(data)
loop = asyncio.get_event_loop()
anchors_server = websockets.serve(get_tag_data, '', 9001)
loop.run_until_complete(asyncio.gather(anchors_server))
loop.run_forever()
note the
while True:
async for data in websocket
Shoud be without while loop.
import asyncio
import websockets
import signal
import sys
async def get_tag_data(websocket, path):
async for data in websocket:
print(data)

Simple way to test websocket availability in python

I am using the following code to test that a local websocket server is running:
import asyncio
import websockets
async def hello():
async with websockets.connect('ws://127.0.0.1:8000/ws/registration/123') as websocket:
await websocket.send(json_data)
asyncio.get_event_loop().run_until_complete(hello())
Is there a simpler way to do this without using asyncio? Something such as:
import asyncio
import websockets
conn = websockets.connect('ws://127.0.0.1:8000/ws/registration/123')
conn.send('hello')
Basically, I'm just trying to find the simplest way to test to see if my websocket server is listening and receiving messages at a particular url.
Doesn't async_to_sync make this more complex? Why not just create a normal test_url function:
def test_url(url, data=""):
async def inner():
async with websockets.connect(url) as websocket:
await websocket.send(data)
return asyncio.get_event_loop().run_until_complete(inner())
test_url("ws://127.0.0.1:8000/ws/registration/123")
You can do the above by using async_to_sync, for example:
from asgiref.sync import async_to_sync
import websockets
def test_url(url, data=""):
conn = async_to_sync(websockets.connect)(url)
async_to_sync(conn.send)(data)
test_url("ws://127.0.0.1:8000/ws/registration/123")
Note that the "handshake" will probably not complete here because it needs to be accepted both ways, but the above should enable you to test to make sure that the urls are being routed properly, etc.

Categories