I'm trying to integrate celery with django channels but channels consumer is not working as it supposed. Logging or prints inside consumer functions are not displaying message in terminal.
The following celery task is called from signals.py
tasks.py
from channels.layers import get_channel_layer
#app.task
def send_request_notification():
text = 'You have new cleaning request'
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
'companies_received_request', # group name
{
'type': 'send_notification',
'text': text
}
)
print("SENT ---") # can see result of this print
consumers.py
class NotificationConsumer(AsyncWebsocketConsumer):
async def connect(self):
logger.info("Connected to websocket")
await self.channel_layer.group_add(
'companies_received_request', self.channel_name
)
await self.accept()
async def disconnect(self):
await self.channel_layer.group_discard(
'companies_received_request', self.channel_name
)
logger.info("Disconnected from websocket")
async def send_notification(self, event):
logger.info("Here in sending notification")
text_message = event['text']
await self.send(text_message)
print("EVENT.TEXT")
print(text_message)
settings.py
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [('127.0.0.1', 6379)],
},
},
}
I am supposed to get the output of prints and logging of consumer functions in terminal but I'm getting nothing when I call channel_layer.group_send method from celery task. (It seems that celery couldn't make a connection to consumer)
I did a chatting part in my web app with django it works perfectly in local but after I deployed it to heroku , it doesn't work:
this is my consumers.py class:
from channels.generic.websocket import AsyncWebsocketConsumer
from django.contrib.auth.models import User
from django.db.models import Q
from asgiref.sync import sync_to_async
import json
from chat.models import Thread, Message
from users.serializers import UserSerializer
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
friend = None
me = self.scope['user'] # logged in user
friend_name = self.scope['url_route']['kwargs']['friend'] # get the username of that user, whoom you want to chat
friend_instance = await sync_to_async(User.objects.get, thread_sensitive=True)(username=friend_name) # get user object of friend
# create a new Thread object if thread of specific chat does not exists, otherwise return the thread
thread = None
try:
thread = await sync_to_async(Thread.objects.get, thread_sensitive=True)((Q(user1=me) & Q(user2=friend_instance)) | (Q(user1=friend_instance) & Q(user2=me)))
except:
thread = await sync_to_async(Thread.objects.create, thread_sensitive=True)(user1=me, user2=friend_instance)
self.room_name = thread.room_name # room name
await self.channel_layer.group_add(
self.room_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
'''
disconnect the websocket connection.
'''
await self.channel_layer.group_discard (
self.room_name,
self.channel_name
)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
from_user = text_data_json['user']
to_user = text_data_json['friend']
from_user_instanse = await sync_to_async(User.objects.get, thread_sensitive=True)(username=from_user['username']) # get user object of friend
to_user_instanse = await sync_to_async(User.objects.get, thread_sensitive=True)(username=to_user['username']) # get user object of friend
thread_obj = await sync_to_async(Thread.objects.get, thread_sensitive=True)((Q(user1=from_user_instanse) & Q(user2=to_user_instanse)) | (Q(user1=to_user_instanse) & Q(user2=from_user_instanse)))
message_instane = await sync_to_async(Message.objects.create, thread_sensitive=True)(messag_body=message, from_user=from_user_instanse, to_user=to_user_instanse, thread=thread_obj)
await self.channel_layer.group_send(
self.room_name,
{
'type': 'chatroom_messages',
'message': message_instane.messag_body,
'user': message_instane.from_user
}
)
async def chatroom_messages(self, event):
message = event['message']
user = event['user']
user_serialized_data = UserSerializer(user)
await self.send(text_data=json.dumps({
'message': message,
'user': user_serialized_data.data
}))
in the template this is the connection request part with js:
chatSocket.send(JSON.stringify({ //the error shows in this part that WebSocket is already in CLOSING or CLOSED state
'message': msg,
'user': me,
'friend': friendName
}));
....
/* connection request */
const chatSocket = new WebSocket(
'wss://'
+ window.location.host
+ '/ws/chat/'
+ friendName['username']
+ '/'
);
in my chat app this I have routing.py
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<friend>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
in my project I have also routing.py
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing
application = ProtocolTypeRouter ({
'websocket': AuthMiddlewareStack(
URLRouter (
chat.routing.websocket_urlpatterns
)
)
})
and this is actually my asgi.py :
import os
import django #added
#from django.core.asgi import get_asgi_application #removed
from channels.routing import get_default_application # added
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
django.setup() #added
application = get_default_application() #added
#application = get_asgi_application() #removed
in my procfile :
release: python manage.py migrate
web: daphne myproject.asgi:application --port $PORT --bind 0.0.0.0 -v2
worker: python manage.py runworker channels --settings=myproject.settings -v2
in my settings.py
ASGI_APPLICATION = "myproject.routing.application"
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [os.environ.get('REDIS_URL', 'redis://localhost:6379')],
},
},
}
it works perfectly in local but not anymore working after deployement, am I messing something
I was following this tutorial on testdriven.io in order to test some async functions in django and I need to add som decorators in order to enable my async tests to access to the DB.
However, I get the following error message:
Error
Traceback (most recent call last):
File "/Users/apple/.local/share/virtualenvs/backend-KucV-wUh/lib/python3.9/site-packages/django/db/backends/base/base.py", line 237, in _cursor
return self._prepare_cursor(self.create_cursor(name))
File "/Users/apple/.local/share/virtualenvs/backend-KucV-wUh/lib/python3.9/site-packages/django/db/backends/sqlite3/base.py", line 274, in create_cursor
return self.connection.cursor(factory=SQLiteCursorWrapper)
sqlite3.ProgrammingError: Cannot operate on a closed database.
#database_sync_to_async
def create_user():
user = User.objects.create(
username='username',
)
user.set_password('password')
user.save()
#pytest.mark.asyncio
#pytest.mark.django_db(transaction=True)
class WebsocketTests(TestCase):
async def test_not_authenticated(self):
await create_user()
..... other async functions
make sure to `pip install pytest pytest-django pytest-asyncio
use #pytest.mark.asyncio and #pytest.mark.django_db(transaction=True)
from asgiref.sync import sync_to_async
from channels.db import database_sync_to_async
#database_sync_to_async
def MakeMessage():
Message.objects.create()
#pytest.mark.asyncio
#pytest.mark.django_db(transaction=True)
class TestWebSocket:
async def test_connection(self, settings):
communicator = WebsocketCommunicator( application=application, path=path )
connected, _ = await communicator.connect()
assert connected
m = await sync_to_async(Message.objects.count)()
assert m == 0
await MakeMessage()
m = await sync_to_async(Message.objects.count)()
assert m == 1
#settings.py
WSGI_APPLICATION = 'frosty_breeze_25125.wsgi.application'
ASGI_APPLICATION = 'frosty_breeze_25125.asgi.application'
host = os.environ.get('REDIS_URL', 'redis://localhost:6379') if IS_DEPLOYED else ('0.0.0.0', 6379)
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [host],
},
},
}
REDIS_URL = [host]
so i was trying to do some basic stuff with channels.
i wrote a script in my html to return a message
script
<script>
const chatsocket = new WebSocket(
'ws://'+window.location.host+'/ws/test/'
)
chatsocket.onmessage = function (e) {
const data = JSON.parse(e.data)
console.log(data)
chatsocket.close()
}
</script>
my consumers.py
from channels.exceptions import StopConsumer
from channels.generic.websocket import AsyncWebsocketConsumer
import json
class NotificationConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.group_name = 'notificationgroup'
await self.channel_layer.group_add(
self.group_name,
self.channel_name
)
await self.accept()
await self.channel_layer.group_send(
self.group_name,
{
'type': 'tester_message',
'tester': 'hello world'
}
)
async def tester_message(self, event):
tester = event['tester']
await self.send(text_data=json.dumps({
'tester': tester
}))
async def disconnect(self, close_code):
await self.channel_layer.group_discard(
self.group_name,
self.channel_name
)
raise StopConsumer()
so basically i get my desired output in the console... when i have
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer'
}
}
but when i use an aws redis cluster (after installing channels-redis obviously)
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('aws redis endpoint', 6379)],
},
},
}
i get this error
WebSocket HANDSHAKING /ws/test/ [127.0.0.1:51551]
WebSocket DISCONNECT /ws/test/ [127.0.0.1:51551]
Application instance <Task pending name='Task-4' coro=<StaticFilesWrapper.__call__() running at E:\intes\sync\shero\venv\lib\site-packages\channels\staticfiles.py:44> wait_for=<Future pending cb=[BaseSelectorEventLoop._sock_write_done(1216)(), <TaskWakeupMethWrapper object at 0x000001F7D5226C70>()]>> for connection <WebSocketProtocol client=['127.0.0.1', 51551] path=b'/ws/test/'> took too long to shut down and was killed.
what am I doing wrong?
I was doing it all wrong. Apparently, you cannot get the aws-redis working outside. you must have your application running on an EC2 within the same VPN. Then, it works!
I am writing a custom authentication middleware for django channels
class TokenAuthMiddleware:
def __init__(self, inner):
# Store the ASGI application we were passed
self.inner = inner
def __call__(self, scope):
return TokenAuthMiddlewareInstance(scope, self)
class TokenAuthMiddlewareInstance:
def __init__(self, scope, middleware):
self.middleware = middleware
self.scope = dict(scope)
self.inner = self.middleware.inner
async def __call__(self, receive, send):
## my logic to get validate user and store the user in user data
...
...
...
self.scope['user'] = user_data
inner = self.inner(self.scope)
return await inner(receive, send)
but on trying to connect to web socket from front end I get the following error
TypeError: __call__() missing 2 required positional arguments: 'receive' and 'send'
For your reference: https://channels.readthedocs.io/en/stable/releases/3.0.0.html
change from in routing.py
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer),
]
to
Consumers now have an as_asgi() class method you need to call when setting up your routing:
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
then
if you need custom authentication https://channels.readthedocs.io/en/stable/topics/authentication.html#custom-authentication
from channels.auth import AuthMiddlewareStack
from channels.db import database_sync_to_async
from django.contrib.auth import get_user_model
User = get_user_model()
#database_sync_to_async
def get_user(user_id):
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return AnonymousUser()
class QueryAuthMiddleware:
"""
Custom middleware (insecure) that takes user IDs from the query string.
"""
def __init__(self, app):
# Store the ASGI application we were passed
self.app = app
async def __call__(self, scope, receive, send):
# Look up user from query string (you should also do things like
# checking if it is a valid user ID, or if scope["user"] is already
# populated).
scope['user'] = await get_user(int(scope["query_string"]))
return await self.app(scope, receive, send)
TokenAuthMiddlewareStack = lambda inner: QueryAuthMiddleware(AuthMiddlewareStack(inner))
use requirements.txt as following list, and also download package in this order
Django==3.0.8
djangorestframework==3.11.0
websocket-client==0.57.0
redis==3.5.3
asgiref==3.2.10
channels-redis==2.4.2
channels==3.0.1
As Yuva Raja has said, in Django Channels version 3 you need to set your path as:
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
One more thing, in their Custom authentication official documentation they forgot to use scope instead of self.scope.
So be sure to use:
scope['user'] = await get_user(int(scope["query_string"]))
instead of their example:
scope['user'] = await get_user(int(self.scope["query_string"]))
This error happened to me when I installed channels==2.4.0
Updating channels to channels==3.0.3 (latest at the moment) fixed the issue!
I managed to make it work this way, with Django Channels 3:
from django.contrib.auth.models import AnonymousUser
from rest_framework.authtoken.models import Token
from channels.auth import AuthMiddlewareStack
from channels.db import database_sync_to_async
import urllib.parse
#database_sync_to_async
def get_user(token):
try:
token = Token.objects.get(key=token)
return token.user
except Token.DoesNotExist:
return AnonymousUser()
class TokenAuthMiddleware:
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
return TokenAuthMiddlewareInstance(scope, self)
class TokenAuthMiddlewareInstance:
"""
Yeah, this is black magic:
https://github.com/django/channels/issues/1399
"""
def __init__(self, scope, middleware):
self.middleware = middleware
self.scope = dict(scope)
self.inner = self.middleware.inner
async def __call__(self, receive, send):
decoded_qs = urllib.parse.parse_qs(self.scope["query_string"])
if b'token' in decoded_qs:
token = decoded_qs.get(b'token').pop().decode()
self.scope['user'] = await get_user(token)
return await self.inner(self.scope, receive, send)
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
from urllib.parse import parse_qs
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.db import close_old_connections
from channels.auth import AuthMiddlewareStack
from rest_framework_simplejwt.tokens import AccessToken
from channels.db import database_sync_to_async
User = get_user_model()
#database_sync_to_async
def get_user(user_id):
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return AnonymousUser()
class TokenAuthMiddleware:
def __init__(self, inner):
self.inner = inner
async def __call__(self, scope, receive, send):
close_old_connections()
query_string = parse_qs(scope['query_string'].decode())
token = query_string.get('token')
if not token:
scope['user'] = AnonymousUser()
return await self.inner(scope, receive, send)
access_token = AccessToken(token[0])
user = await get_user(access_token['id'])
if isinstance(user, AnonymousUser):
scope['user'] = AnonymousUser()
return await self.inner(scope, receive, send)
if not user.is_active:
scope['user'] = AnonymousUser()
return await self.inner(scope, receive, send)
scope['user'] = user
return await self.inner(scope, receive, send)
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
from channels.db import database_sync_to_async
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import AnonymousUser
from urllib.parse import parse_qs
from channels.auth import AuthMiddlewareStack
#database_sync_to_async
def get_user(scope):
try:
token_key=parse_qs(scope['query_string'].decode("utf8"))["token"][0]
token=Token.objects.get(key=token_key)
return token.user
except Token.DoesNotExist:
return AnonymousUser()
class TokenAuthMiddleware:
def __init__(self,inner):
self.inner=inner
def __call__(self,scope):
return TokenAuthMiddlewareInstance(scope,self)
class TokenAuthMiddlewareInstance:
def __init__(self,scope,middleware):
self.middleware = middleware
self.scope=dict(scope)
self.inner=self.middleware.inner
async def __call__(self,receive,send):
self.scope['user'] = await get_user(self.scope)
return await self.inner(self.scope,receive,send)
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
I got an error 2 positional argument missing receive and send
Traceback (most recent call last):
File "D:\Project\IQ Tester\env\lib\site-packages\channels\staticfiles.py", line 44, in __call__
return await self.application(scope, receive, send)
File "D:\Project\IQ Tester\env\lib\site-packages\channels\routing.py", line 71, in __call__
return await application(scope, receive, send)
File "D:\Project\IQ Tester\env\lib\site-packages\asgiref\compatibility.py", line 34, in new_application
return await instance(receive, send)
File "D:\Project\IQ Tester\backend\account\token_auth_middleware.py", line 40, in __call__
inner=self.inner(self.scope)
TypeError: __call__() missing 2 required positional arguments: 'receive' and 'send'
so to solve this I write this
return await self.inner(self.scope,receive,send)
instead of this
self.scope['user'] = user_data
inner = self.inner(self.scope)
return await inner(receive, send)