Django channels page stuck on loading - python

I created a simple Django Channels consumer that connects to a Redis channel and receives some data from this channel, i want to send this data to the frontend.
The consumer is able to connect to the consumer and receives the data; the problem is that if i try to load the page when the consumer is running, the page will be stuck on loading. I'm sure this happens because the connection to the Redis channel is a blocking operation, or it could be a problem with threads. I'm new to this concepts, so i decided to make a question for it.
Here is my consumer:
class EchoConsumer(AsyncConsumer):
async def websocket_connect(self, event):
self.send({
'type': 'websocket.accept'
})
self.receive(event)
def receive(self, event):
redis_url = 'redis://localhost:6379/0'
connection = redis.StrictRedis.from_url(redis_url, decode_responses=True)
channel = 'TEST'
params = urllib.parse.parse_qs(self.scope.get('query_string', b'').decode('utf-8'))
pubsub = connection.pubsub(ignore_subscribe_messages=True)
pubsub.subscribe(channel)
for message in pubsub.listen():
# self.send({
# 'type': 'websocket.send',
# 'text': message['data'],
# })
print(message['data'])
async def websocket_disconnect(self, event):
print('DISCONNECTED!')
So what happens is that i can see the data being printed to my console, but if i try to leave that page and reach a different part of my site, the page will get stuck on loading. Can anyone help me fix this?

There are 2 things you could be trying to do here.
Subscribing once and sending this information to all open connections
the issue you are having is pubsub.listen(): will loop forever (never stopping). So your consumer will never continue and be able to process any more messages.
Since (at least in this example) it looks like you are always hitting redis with static values (not dependent on the request from the user) you are better off doing this subscription outside of your consumer. (in a django command https://docs.djangoproject.com/en/3.0/howto/custom-management-commands/)
Then you can have that command then send these messages over a channel layer to your subscribed consumers.
this would make your consumer look like this
class EchoConsumer(AsyncJsonWebsocketConsumer):
async def on_message(self, message):
await self.send_json(message)
then in your management command instread of printing you can send the message using
async_to_sync(channel_layer.group_send)(
"echo_group",
{"type": "on.message", "rate":Rate, "quantity": Quantity, "symbol": Symbol, "order": Order},
)
Subscribing once for each open connection
This you should only do if you expect the subscription to be different for each websocket connection. (eg you are using a value in the url/query/headers or subscribing only when the user sends a ws message to your consumer with a given filter value).
Doing this is a LOT more complex for a few reasons:
Redis will not handle as many open connections to it as you can have websocket connections.
You need to setup a nested async task that can handle the events from redis in such a way that they do not block the rest of the consumer.
If you do still need this functionality i'm happy to update the answer with a solution (but warning it will be long).

Related

Keep Websockets connection open for incoming requests

I have a Flask server that accepts HTTP requests from a client. This HTTP server needs to delegate work to a third-party server using a websocket connection (for performance reasons).
I find it hard to wrap my head around how to create a permanent websocket connection that can stay open for HTTP requests. Sending requests to the websocket server in a run-once script works fine and looks like this:
async def send(websocket, payload):
await websocket.send(json.dumps(payload).encode("utf-8"))
async def recv(websocket):
data = await websocket.recv()
return json.loads(data)
async def main(payload):
uri = f"wss://the-third-party-server.com/xyz"
async with websockets.connect(uri) as websocket:
future = send(websocket, payload)
future_r = recv(websocket)
_, output = await asyncio.gather(future, future_r)
return output
asyncio.get_event_loop().run_until_complete(main({...}))
Here, main() establishes a WSS connection and closes it when done, but how can I keep that connection open for incoming HTTP requests, such that I can call main() for each of those without re-establising the WSS connection?
The main problem there is that when you code a web app responding http(s), your code have a "life cycle" that is very peculiar to that: usually you have a "view" function that will get the request data, perform all actions needed to gather the response data and return it.
This "view" function in most web frameworks has to be independent from the rest of the system - it should be able to perform its duty relying on no other data or objects than what it gets when called - which are the request data, and system configurations - that gives the application server (the framework parts designed to actually connect your program to the internet) can choose a variety of ways to serve your program: they may run your view function in several parallel threads, or in several parallel processes, or even in different processes in various containers or physical servers: you application would not need to care about that.
If you want a resource that is available across calls to your view functions, you need to break out of this paradigm. For example, typically, frameworks will want to create a pool of database connections, so that views on the same process can re-use those connections. These database connections are usually supplied by the framework itself, which implements a mechanism for allowing then to be reused, and be available in a transparent way, as needed. You have to recreate a mechanism of the same sort if you want to keep a websocket connection alive.
In a certain way, you need a Python object that can mediate your websocket data behaving like a "server" for your web view functions.
That is simpler to do than it sounds - a special Python class designed to have a single instance per process, which keeps the connections, and is able to send and receive data received from parallel calls without mangling it is enough. A callable that will ensure this instance exists in the current process is enough to work under any strategy configured to serve your app to the web.
If you are using Flask, which does not use asyncio, you get a further complication - you will loose the async-ability inside your views, they will have to wait for the websocket requisition to be completed - it will then be the job of your application server to have your view in different threads or processes to ensure availability. And, it is your job to have the asyncio loop for your websocket running in a separate thread, so that it can make the requests it needs.
Here is some example code.
Please note that apart from using a single websocket per process,
this has no provisions in case of failure of any kind, but,
most important: it does nothing in parallel: all
pairs of send-recv are blocking, as you give no clue of
a mechanism that would allow one to pair each outgoing message
with its response.
import asyncio
import threading
from queue import Queue
class AWebSocket:
instance = None
def __new__(cls, *args, **kw):
if cls.instance:
return cls.instance
return super().__new__(cls, *args, **kw)
def __init__(self, *args, **kw):
cls = self.__class__
if cls.instance:
# init will be called even if new finds the existing instance,
# so we have to check again
return
self.outgoing = Queue()
self.responses = Queue()
self.socket_thread = threading.Thread(target=self.start_socket)
self.socket_thread.start()
def start_socket():
# starts an async loop in a separate thread, and keep
# the web socket running, in this separate thread
asyncio.get_event_loop().run_until_complete(self.core())
def core(self):
self.socket = websockets.connect(uri)
async def _send(self, websocket, payload):
await websocket.send(json.dumps(payload).encode("utf-8"))
async def _recv(self, websocket):
data = await websocket.recv()
return json.loads(data)
async def core(self):
uri = f"wss://the-third-party-server.com/xyz"
async with websockets.connect(uri) as websocket:
self.websocket = websocket
while True:
# This code is as you wrote it:
# it essentially blocks until a message is sent
# and the answer is received back.
# You have to have a mechanism in your websocket
# messages allowing you to identify the corresponding
# answer to each request. On doing so, this is trivially
# paralellizable simply by calling asyncio.create_task
# instead of awaiting on asyncio.gather
payload = self.outgoing.get()
future = self._send(websocket, payload)
future_r = self._recv(websocket)
_, response = await asyncio.gather(future, future_r)
self.responses.put(response)
def send(self, payload):
# This is the method you call from your views
# simply do:
# `output = AWebSocket().send(payload)`
self.outgoing.put(payload)
return self.responses.get()

How to do a get request inside a Django Channels consumer connect method?

I'm trying to do a get request for a seperate django CRUD API app inside a Django Channels app connect method
So inside the consumers.py I'm doing this
class AssistantConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.commands = requests.get('http://localhost:8000/api/commands')
print(self.commands)
Doing this results in the websocket getting stuck on
WebSocket HANDSHAKING /ws/assistant/user/ [127.0.0.1:64374]
Can anyone tell me why?
The API is working on it's own, I'm posting to it from React, which also connects to the websocket. All that is working - I just need to fetch some data from the database in the consumer.
Couldn't find anything anywhere about this situation.
OK I found a solution - don't know if it's the right one or how stable it is but this package on PyPi does the trick.
https://pypi.org/project/requests-async/
Thanks to Timothee for pointing out that I needed to be doing it async.
This now works.
import requests_async as requests
async def connect(self):
self.commands = await requests.get('http://localhost:8000/api/commands/')
print(self.commands)
await self.accept()
Maybe this will help someone else and if anyone knows of a reason I shouldn't be doing this I'd be interested to know.
async def connect is only called when a client attempts a connection and your routing file sends the incoming connection to your AssistantConsumer. In your case, you are getting stuck on this initial 'handshake'. This means that you are receiving the request to connect from the client but you are not accepting that connection and therefore the WebSocket connection is never opened.
Adding await self.accept() should accept the incoming connection and therefore open the WebSocket.
This is what it would look like:
class AssistantConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.commands = requests.get('http://localhost:8000/api/commands')
print(self.commands)
await self.accept()

API request to already opened django channels consumer

I've got a django channels consumer communicating with a client. I've got a view from an external API that wants something from the client. From this view I want then to tell that consumer to ask a request to the client through his socket.
I'm currently exploring django rest framework but I can't find a way for now to directly ask anything to that consumer.
Well I've got an idea but it involves creating another socket and communicate through channels' channel. But I wish I could get rid of this overload.
From your reponse in the comments, it seems you want to send a message to the client through the consumer from your DRF view. You can check out the answer to a similar question.
First, you need to have a method in your consumer that sends a message back to the client:
...
async def send_alert(self, event):
# Send message to WebSocket
await self.send(text_data={
'type': 'alert',
'details': 'An external API api.external.com needs some data from you'
})
...
So now you can send a message to this method. Assuming the client is connected to channel1, you can do this in your view:
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
...
channel_layer = get_channel_layer()
async_to_sync(channel_layer.send)("channel1", {
"type": "send.alert"
})
...
async_to_sync usage

asyncio: multiplexing messages over single websocket connection

I am using Python 3.6, asyncio and the websockets library. I am trying to build a client for a websocket-based service which works as follows:
The client can send JSON requests with a custom id, a method and some params. The service will reply with a JSON payload with the same id echoed, and data as a result of the method call.
I would like to have an abstraction on top of this device that would work sort of like this:
wsc = get_websocket_connection()
async def call_method(method, **params):
packet = make_json_packet(method, params)
await wsc.send(packet)
resp = await wsc.recv()
return decode_json_packet(resp)
async def working_code():
separate_request = asyncio.ensure_future(call_method("quux"))
first_result = await call_method("foo", x=1)
second_result = await call_method("bar", y=first_result)
print(second_result)
return await separate_request
Now, I expect the separate_request to wait asynchronously while first_result and second_results are processed. But I have no guarantee that the wsc.recv() call will return the matching response; in fact, I have no guarantees that the service returns the responses in order of requests.
I can use the id field to disambiguate the responses. But how can I write the call_method() so that it manages the requests internally and resumes the "right" coroutine when the corresponding reply is received?
when I've done this sort of thing before I've tended to split things out into two parts:
"sending code" (can be multiple threads) this sets up where responses should go to (i.e. a dict of ids to functions or Futures), then sends the request and blocks for the response
"receiving code" (probably one thread per socket) that monitors all inbound traffic and passes responses off to whichever code is interested in the id. this is also a sensible place to handle the socket being closed unexpectedly which should push an exception out as appropriate
this is probably a few hundred lines of code and pretty application specific…

Tornado websocket- send data based on an event

I have a python program which on certain event (for example on curl request) would calculate the function value. What I need is the moment the function executes, some data needs to be posted to tornado websocket. I have looked around internet and found examples on how to create websocket but all these examples cover scenarios where the data is invoked inside the websocket handler
Referring to this code for example:
https://github.com/benjaminmbrown/real-time-data-viz-d3-crossfilter-websocket-tutorial/blob/master/rt-data-viz/websocket_server.py
Can someone guide me on how can I post message on websocket. Basically I have tornado API where if user do a curl request I would like to log that message to websocket
You can do it by creating a registry of all active websockets and use it to send messages on a certain event.
class WebsocketRegistry:
def __init__(self):
self._active_websockets = []
def add_listener(self, listener):
self._active_websockets.append(listener)
def remove_listener(self, listener):
self._active_websockets.remove(listener)
def send_messages(self, msg_txt):
for ws in self._active_websockets:
ws.write_message(msg_txt)
registry = WebsocketRegistry()
class WSHandler(tornado.websocket.WebSocketHandler):
def open(self, *args, **kwargs):
super(WSHandler, self).open(*args, **kwargs)
registry.add_listener(self)
def on_close(self):
super(WSHandler, self).on_close()
registry.remove_listener(self)
P.S. Take note that if you plan to scale your app with 2+ instances, this won't work and you would have to use, for example, a message queue (RabbitMQ is good) to deliver events to the all opened websockets. But overall approach would be the same: MQ would be a registry and websockets subscribe on messages (and unsubscribe on closing) on connection.

Categories

Resources