RabbitMQ Pika and Django Channels websocket - django

I am using Django Channels and RabbitMQ pika, for the first time. I am trying to consume from RabbitMQ queue. I am using Django Channels AsyncConsumer to group send it to everyone connected in the websocket.
User type 1 : Can create a task
User type 2 : Can accept the task.
Use case : When user type 1 creates the task it is published in the rabbitmq. When it is consumed from the queue, it has to be group-sent to frontend. And when the user type 2 accepts the task other instances of user type 2 cannot accept the same and we consume from the queue again and send the next task in the queue to everyone.
I have created the connection in a different thread using sync_to_async I am appending it to an in-memory list from the callback function.
And whenever someone accepts I just pop it out of the list and acknowledge the queue.
class AcceptTaskConsumer(AsyncConsumer):
body = [] #IN MEMORY LIST
delivery = {} #To store ack delivery_tag
async def websocket_connect(self, event):
print("AcceptTaskConsumer connected", event)
AcceptTaskConsumer.get_task() #STARTS Queue listener in new thread
self.room_group_name = "user_type_2"
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.send({
"type": "websocket.accept"
})
async def websocket_receive(self, event):
if event["text"] == "Hi": #If connecting first time
if AcceptTaskConsumer.body:
await self.channel_layer.group_send(
self.room_group_name,
{
"type": "message",
"text": AcceptTaskConsumer.body[0]["body"]
}
)
else:
await self.channel_layer.group_send(
self.room_group_name,
{
"type": "message",
"text": "No New Tasks"
}
)
else: #When someone accepts a task-> ack and send next task in queue
print(json.loads(event["text"])["id"])
AcceptTaskConsumer.channel.basic_ack(delivery_tag=AcceptTaskConsumer.delivery[json.loads(event["text"])["id"]])
AcceptTaskConsumer.delivery.pop(json.loads(event["text"])["id"])
AcceptTaskConsumer.body.pop(0)
await self.channel_layer.group_send(
self.room_group_name,
{
"type": "message",
"text": "No New Tasks"
}
)
if AcceptTaskConsumer.body:
await self.channel_layer.group_send(
self.room_group_name,
{
"type": "message",
"text": AcceptTaskConsumer.body[0]["body"]
}
)
async def message(self, event):
await self.send({
"type": "websocket.send",
"text": event["text"]
})
#classmethod
#sync_to_async
def get_task(cls): #pika consumer
cls.connection = pika.BlockingConnection(
pika.ConnectionParameters(host='localhost'))
cls.channel = cls.connection.channel()
cls.channel.queue_declare(queue='task_', arguments={"x-max-priority": 3})
cls.channel.basic_consume(
queue='task_', on_message_callback=AcceptTaskConsumer.callback, auto_ack=False)
cls.channel.start_consuming()
#classmethod
def callback(cls, ch, method, properties, body):
task_obj = {"body": json.dumps(body.decode("utf-8")),
"delivery_tag": method.delivery_tag}
AcceptTaskConsumer.body.append(task_obj)
AcceptTaskConsumer.delivery[json.loads(json.loads(task_obj["body"]))["id"]] = method.delivery_tag
cls.channel.stop_consuming()
async def websocket_disconnect(self, event):
print(event)
await self.send({
"type": "websocket.close"
})
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
I am pretty sure this is not the right way to do it, because it's not working as expected
I run into frequent errors like.
39 of 169 channels over capacity in group delivery
pika.exceptions.StreamLostError: Stream connection lost: BrokenPipeError(32, 'Broken pipe')
I tried running the queue listener like this answer as well. Nothing working.
Any one experienced has any thoughts about this? Is there a better way to approach this problem.?

you should move the rabitMQ cosumering logic out of the websocket consumer.
Just have a django command that runs the Rabbit Consumer, that consumer can take messages from RabbitMQ and then use send_group to send them over groups to channels.
if your django command you will need to call send_group see https://channels.readthedocs.io/en/latest/topics/channel_layers.html#using-outside-of-consumers
from channels.layers import get_channel_layer
channel_layer = get_channel_layer()
async_to_sync(
channel_layer.group_send
)(
"user_type_2",
{"type": "message", "msg": 123}
)
Then in the websocket consumer you should subscribe to the groups that the user wants/has permition to get.

Related

How to connect Django web socket with the third-Party Web Sockets?

I want to connect my Django WebSocket with a third-party web socket. This program is one I wrote, and it functions properly. To avoid having to re-login with the third-party API, I have now added the code to check whether the same room is present in my database. if we use the same API KEY to re-connect to the third-party API. It gives the following error:
{"event":"login","status":401,"message":"Connected from another location"}
I want to see if the same cryptocurrency coin is already connected or not. We don't want to logon with the same API KEY once we're connected. I have two issues here:
Don't send the login request to that web socket again.
Don't send the subscribe request, if the same coin already exists. Let's say BTCUSD already connected and giving me the data. I want to just connect to the next user to same room and get the data on next request.
import websocket
import time
import ssl
from channels.generic.websocket import AsyncWebsocketConsumer
from .models import Room
login = {
"event": "login",
"data": {
"apiKey": "API_KEY",
},
}
class CryptoConsumer(AsyncWebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ws = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE})
self.ws.connect("wss://crypto.financialmodelingprep.com")
async def connect(self):
self.room_name = self.scope["url_route"]["kwargs"]["room_name"]
self.room_group_name = "crypto_%s" % self.room_name
# ****** Code Block ******
subscribe = {
"event": "subscribe",
"data": {
"ticker": self.room_name,
},
}
room = await Room.add(self.room_name) # Method in models.py to add the user and return True
if room is False:
self.ws.send(json.dumps(login))
print("The group with the name %s doesn't exist" % self.room_group_name)
time.sleep(1)
self.ws.send(json.dumps(subscribe))
# ****** End Code Block ******
# Join room group
await self.channel_layer.group_add(self.room_group_name, self.channel_name)
await self.accept()
async def disconnect(self, close_code):
unsubscribe = {
"event": "unsubscribe",
"data": {
"ticker": self.room_name,
},
}
self.ws.send(json.dumps(unsubscribe))
self.ws.close()
# Leave room group
await self.channel_layer.group_discard(self.room_group_name, self.channel_name)
# Receive message from WebSocket
async def receive(self, text_data="{'text': 'Dummy Text'}"):
text_data_json = json.loads(text_data)
message = text_data_json["message"]
message = str(self.ws.recv())
# Send message to room group
await self.channel_layer.group_send(
self.room_group_name, {"type": "chat_message", "message": message}
)
# Receive message from room group
async def chat_message(self, event):
message = event["message"]
# Send message to WebSocket
await self.send(text_data=json.dumps({"message": message}))
Note: Why I want to do this entire step because we don't want the API KEY made public.
So, the code coming from the front end will connect to our Django web socket, and then we'll connect to the third-party web socket and return the data that was sent by them.

Django Channels do not send message within except block in Celery task

I am currently facing an issue with the Django Channels and Websockets. I have an application that somehow works with files and Sharepoint. Issued code sample where defect occurs:
#shared_task(bind=True)
def upload_to_sharepoint_task(self, user_id: str, document_type: str, upload_file: str, filename: str) -> None:
"""Uploads a file to SharePoint."""
task_id = self.request.id
try:
upload_to_sharepoint(user_id, document_type, upload_file, filename)
print(f"Task {task_id} completed successfully.")
async_to_sync(get_channel_layer().group_send)(task_id, {
"type": "send_task_status",
"message": {'task_id': task_id, 'status': "SUCCESS", }
})
except BackendError as e:
print(f"Task {task_id} failed with error: {str(e)}")
async_to_sync(get_channel_layer().group_send)(task_id, {
"type": "send_task_status",
"message": {'task_id': task_id, 'status': "FAILURE", 'error': str(e)}
})
If everything the method upload_to_sharepoint does not throw an exception, everything works just fine - the message is sent to the group and, via WebSocket, propagated to the Client. However, if I try to simulate a situation when the exception is thrown (for instance, by throwing the BackendException explicitly), the error message gets printed in the console, but WebSocket does not receive any message nor the Client.
My TaskConsumer:
class TaskConsumer(WebsocketConsumer):
def connect(self):
self.task_id = self.scope['url_route']['kwargs']['task_id']
# Join the task_id group
async_to_sync(self.channel_layer.group_add)(self.task_id, self.channel_name)
self.accept()
def disconnect(self, close_code):
# Leave the task_id group
async_to_sync(self.channel_layer.group_discard)(
self.task_id, self.channel_name
)
def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json["message"]
print(f"Task {self.task_id} received message: {message}")
async_to_sync(self.channel_layer.group_send)(
self.task_id, {"type": "send_task_status", "message": message}
)
def send_task_status(self, event):
message = event['message']
self.send(text_data=json.dumps({"message": message}))
Does anyone have an idea what could be wrong and how I can propagate the information about unsuccessful upload back to the Client via used WebSocket?

Broadcast a message to everyone periodically using Django channels

I'm new to Django channels. I created a WebSocket and I want to send some info to every user periodically (broadcast periodically).
Each user connects to the WebSocket when goes to my website and see those info on top of each page.
I have no idea whether it is necessary to create a group or not, and if it is necessary, how to create it...
So how can I send info to every visitor periodically(in an efficient way)?
May be something like this:
class Consumer(AsyncConsumer):
async def websocket_connect(self, event):
await self.send({
"type": "websocket.accept",
})
while True:
await self.channel_layer.group_send(
{
"text": 'Hello user!'
}
)
# OR
await self.send({
"type": "websocket.send",
"text": 'Hello user!'
})
await asyncio.sleep(5)
Thanks in advance...

Implementing simple Server Sent Event Stream with Django Channels

Django Channels docs has following basic example of a Server Sent Events. AsyncHttpConsumer
from datetime import datetime
from channels.generic.http import AsyncHttpConsumer
class ServerSentEventsConsumer(AsyncHttpConsumer):
async def handle(self, body):
await self.send_headers(headers=[
(b"Cache-Control", b"no-cache"),
(b"Content-Type", b"text/event-stream"),
(b"Transfer-Encoding", b"chunked"),
])
while True:
payload = "data: %s\n\n" % datetime.now().isoformat()
await self.send_body(payload.encode("utf-8"), more_body=True)
await asyncio.sleep(1)
I want to accept messages sent via channel_layer and send them as events.
I changed the handle method, so it subscribes the new channel to a group. And I'm planning to send messages to the channel layer via channel_layer.group_send
But I couldn't figure out how to get the messages sent to the group, within handle method. I tried awaiting for the channel_layer.receive, it doesn't seem to work.
class ServerSentEventsConsumer(AsyncHttpConsumer):
group_name = 'my_message_group'
async def myevent(self, event):
# according to the docs, this method will be called \
# when a group received a message with type 'myevent'
# I'm not sure how to get this event within `handle` method's while loop.
pass
async def handle(self, body):
await self.channel_layer.group_add(
self.group_name,
self.channel_name
)
await self.send_headers(headers=[
(b"Cache-Control", b"no-cache"),
(b"Content-Type", b"text/event-stream"),
(b"Transfer-Encoding", b"chunked"),
])
while True:
payload = "data: %s\n\n" % datetime.now().isoformat()
result = await self.channel_receive()
payload = "data: %s\n\n" % 'received'
I'm sending the messages to channel_layer like below: ( from a management command)
def send_event(event_data):
group_name = 'my_message_group'
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
group_name,
{
'type': 'myevent',
'data': [event_data]
}
)
I had the same issue and I even went to dig into Django Channels code but without success.
... Until I found this answer in this (still opened) issue: https://github.com/django/channels/issues/1302#issuecomment-508896846
That should solve your issue.
In your case the code would be (or something quite similar):
class ServerSentEventsConsumer(AsyncHttpConsumer):
group_name = 'my_message_group'
async def http_request(self, message):
if "body" in message:
self.body.append(message["body"])
if not message.get("more_body"):
await self.handle(b"".join(self.body))
async def myevent(self, event):
# according to the docs, this method will be called \
# when a group received a message with type 'myevent'
# I'm not sure how to get this event within `handle` method's while loop.
pass
async def handle(self, body):
await self.channel_layer.group_add(
self.group_name,
self.channel_name
)
await self.send_headers(headers=[
(b"Cache-Control", b"no-cache"),
(b"Content-Type", b"text/event-stream"),
(b"Transfer-Encoding", b"chunked"),
])

How do I send channels 2.x group message from django-celery 3 task?

I need to postpone sending channels message. Here is my code:
# consumers.py
class ChatConsumer(WebsocketConsumer):
def chat_message(self, event):
self.send(text_data=json.dumps(event['message']))
def connect(self):
self.channel_layer.group_add(self.room_name, self.channel_name)
self.accept()
def receive(self, text_data=None, bytes_data=None):
send_message_task.apply_async(
args=(
self.room_name,
{'type': 'chat_message',
'message': 'the message'}
),
countdown=10
)
# tasks.py
#shared_task
def send_message_task(room_name, message):
layer = get_channel_layer()
layer.group_send(room_name, message)
The task is being executed and I can't see any errors but message is not being sent. It works only if I send it from consumer class method.
I also tried using AsyncWebsocketConsumer and sending with AsyncToSync(layer.group_send). It errors with "You cannot use AsyncToSync in the same thread as an async event loop - just await the async function directly."
Then I tried declaring send_message_task as async and using await. Nothing happens again (with no errors) and I'm not sure if the task is executed at all.
Here are versions:
Django==1.11.13
redis==2.10.5
django-celery==3.2.2
channels==2.1.2
channels_redis==2.2.1
Settings:
REDIS_HOST = os.getenv('REDIS_HOST', '127.0.0.1')
BROKER_URL = 'redis://{}:6379/0'.format(REDIS_HOST)
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": ['redis://{}:6379/1'.format(REDIS_HOST)],
},
},
}
Any ideas?
UPD: Just found out that redis channel layer is retreived but it's group_send method is not called and just skipped.
UPD 2: Sending using AsyncToSync(layer.group_send) from console works. Calling task without apply_async also works. But running it with apply_async causes an error You cannot use AsyncToSync in the same thread as an async event loop - just await the async function directly. Defining task as async and using await also breaks everything of course.
Maybe this is not direct answer to a starting question but this might help.
If you get exception "You cannot use AsyncToSync in the same thread as an async event loop - just await the async function directly" then you probably makes some of this:
event loop is created somewhere
some ASYNC code is started
some SYNC code is called from ASYNC code
SYNC code is trying to call ASYNC code with AsyncToSync that prevents this
Seems that AsyncToSync detects outer event loop and makes decision to not interfere with it.
Solution is to directly include your async call in outer event loop.
Example code is below, but best is to check your situation and that outer loop is running ...
loop = asyncio.get_event_loop()
loop.create_task(layer.group_send(room_name, {'type': 'chat_message', 'message': message}))
You need the async_to_sync() wrapper on connect when using channel layers because all channel layer methods are asynchronous.
def connect(self):
async_to_sync(self.channel_layer.group_add(
self.room_name, self.channel_name)
self.accept()
Same deal with sending the message from your celery task.
#shared_task
def send_message_task(room_name, message):
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
room_name,
{'type': 'chat_message', 'message': message}
)
Also you can just call your celery task from your consumer's receive() like this:
send_message_task.delay(self.room_name, 'your message here')
Regarding the AsyncToSync error you need to upgrade channels and daphne to a newer version as explained in this thread.
I found an ugly and inefficient decision, but it works:
#shared_task
def send_message_task(room_name, message):
def sender(room_name, message):
channel_layer = get_channel_layer()
AsyncToSync(channel_layer.group_send)(
room_name,
{'type': 'chat_message', 'message': message}
)
thread = threading.Thread(target=sender, args=(room_name, message,))
thread.start()
thread.join()
If someone can improve it, I will appreciate.
The problem in your code is that you used underscore in your type chat_message. I believe you missed it in the documentation:
The name of the method will be the type of the event with periods
replaced by underscores - so, for example, an event coming in over the
channel layer with a type of chat.join will be handled by the method
chat_join.
So in your case, the type will be chat.message
{
'type': 'chat.message',
'message': 'the message'
}