500 error on django channels send message to group - django

I have the following Django channels consumer but when I try to send data to the stocks group،
it returns a 500 error.
Also, I don't get any error logs.
Here is the consumer:
class AuthRequiredConsumer(JsonWebsocketConsumer):
def connect(self):
user = self.scope['user']
print(isinstance(user, AnonymousUser))
if isinstance(user, AnonymousUser):
raise DenyConnection("authentication required")
self.accept()
class ChatConsumer(AuthRequiredConsumer):
groups = ['stocks']
channel_layer_alias = "stocks"
def stocks_data(self, event):
print(event)
return self.send_json({'message': event['text']})
def receive_json(self, content, **kwargs):
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
"stocks",
{
"type": "stocks.data",
"text": "Hello there!",
},
)
message = content["message"]
self.send_json({"message": message})
Also, I use this middle to authenticate users by the token:
#database_sync_to_async
def get_user(token_key):
try:
token = Token.objects.get(key=token_key)
return token. user
except Token.DoesNotExist:
return AnonymousUser()
class TokenAuthMiddleware(BaseMiddleware):
def __init__(self, inner):
super().__init__(inner)
async def __call__(self, scope, receive, send):
try:
token_key = (dict((x.split('=') for x in scope['query_string'].decode().split("&")))).get('token', None)
except ValueError:
print("error")
token_key = None
scope['user'] = AnonymousUser() if token_key is None else await get_user(token_key)
return await super().__call__(scope, receive, send)
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
And here is the routing config:
application = ProtocolTypeRouter(
{
"http": django_asgi_app,
"websocket": TokenAuthMiddlewareStack(
URLRouter(
[
re_path(r"ws/chat/(?P<room_name>\w+)/$", consumers.ChatConsumer.as_asgi())
]
)
)
,
}
)
I debugged the receive_json method using breakpoints And I saw self.channel_name and self.channel_layer is None!!
Does anyone have an idea what's up with this consumer?

Probably you forgot to set the CHANNEL_LAYERS property at settings file.
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6379)],
},
},
}

Related

Django Channels: Event loop is closing when using thread

I want to show a countdown and then later start a game loop. The code is getting excuted and the messages are send but i always get a RuntimeError. I would be interested in a fix or a maybe better solution that i can apply. I was also thinking about splitting things into two Consumers but i dont know how this would fix this. Thanks in advance.
This error message is popping up multiple times.
Task exception was never retrieved
future: <Task finished name='Task-60' coro=<Connection.disconnect() done, defined at D:\Programming\Fullstack\geogame\geogame_backend\env\lib\site-packages\redis\asyncio\connection.py:819> exception=RuntimeError('Event loop is closed')>
Traceback (most recent call last):
File "D:\Programming\Fullstack\geogame\geogame_backend\env\lib\site-packages\redis\asyncio\connection.py", line 828, in disconnect
self._writer.close() # type: ignore[union-attr]
File "C:\Program Files\Python310\lib\asyncio\streams.py", line 337, in close
return self._transport.close()
File "C:\Program Files\Python310\lib\asyncio\selector_events.py", line 698, in close
self._loop.call_soon(self._call_connection_lost, None)
File "C:\Program Files\Python310\lib\asyncio\base_events.py", line 753, in call_soon
self._check_closed()
File "C:\Program Files\Python310\lib\asyncio\base_events.py", line 515, in _check_closed
raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed
This is my consumer
class LobbyConsumer(WebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(args, kwargs)
self.players = None
self.game_finished = None
self.host = None
self.user = None
self.lobby_code = None
self.lobby_group_code = None
self.lobby = None
def connect(self):
self.lobby_code = self.scope['url_route']['kwargs']['lobby_code']
if Lobby.objects.filter(code=self.lobby_code).exists():
self.lobby = Lobby.objects.get(code=self.lobby_code)
else:
print("DOESNT EXIST")
self.accept()
self.send(json.dumps({"type": "error", "message": "no_match"}))
self.close()
return
self.lobby_group_code = f"lobby_{self.lobby_code}"
self.host = Lobby.objects.select_related('host').get(code=self.lobby_code).host
self.user = self.scope['user']
if self.user.is_authenticated:
self.accept()
print(True)
else:
self.close(code=4004)
self.lobby.users.add(self.user)
self.lobby.save()
players_queryset = self.lobby.users.all()
self.players = []
for player in players_queryset:
self.players.append({
"username": player.username,
"email": player.email
})
async_to_sync(self.channel_layer.group_add)(
self.lobby_group_code,
self.channel_name,
)
async_to_sync(self.channel_layer.group_send)(
self.lobby_group_code,
{
'type': 'status',
'message': "Welcome",
'has_started': self.lobby.has_started,
'host': self.host.username,
'players': self.players,
'lobby_code': self.lobby_code,
'max_players': self.lobby.max_players,
}
)
def start_game(self):
countdown = 4
for i in range(countdown):
async_to_sync(self.channel_layer.group_send)(
self.lobby_group_code,
{
'type': 'status',
'has_started': self.lobby.has_started,
'host': self.host.username,
'players': self.players,
'lobby_code': self.lobby_code,
'countdown': countdown - 1
}
)
sleep(1)
countdown -= 1
def receive(self, text_data=None, bytes_data=None):
command = ""
text_data_json = json.loads(text_data)
print(text_data_json)
if "command" in text_data_json:
command = text_data_json['command']
if command == "start":
self.lobby.has_started = True
print("start")
self.lobby.save()
async_to_sync(self.channel_layer.group_send)(
self.lobby_group_code,
{
'type': 'status',
'message': "Welcome",
'has_started': self.lobby.has_started,
'host': self.host.username,
'players': self.players,
'lobby_code': self.lobby_code,
'countdown': 3
}
)
thread = Thread(target=self.start_game, args=())
thread.start()
def disconnect(self, close_code):
print(f"Connection Cancelled")
async_to_sync(
self.channel_layer.group_discard(
self.lobby_group_code,
self.channel_name,
))
self.close()
def chat_message(self, event):
self.send(text_data=json.dumps(event))
def status(self, event):
self.send(text_data=json.dumps(event))
I had the same problem
The problem is in channels_redis
Using RedisPubSubChannelLayer solves this problem.
However, be careful ahead RedisPubSubChannelLayer is in beta
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.pubsub.RedisPubSubChannelLayer",
"CONFIG": {
"hosts": ["redis://***:***#***.com:6379"],
},
},
}
I had a problem with event loop closing after an arbitrary amount of time. The issue was in the channels-redis module. It got updated accidentally. After checking every module's version, I rolled it back and it worked well again.

Celery task not waiting for database updates

Can someone explain why I can't get the updated database records in the Celery task_success signal?
#consumers.py
#database_sync_to_async
def save_to_db(game)
Result.objects.create(game=game)
class GameConsumer(AsyncWebsocketConsumer):
...
async def save_result(self, event):
await save_to_db(self.game)
#tasks.py
#shared_task(name="run_game")
def run_game():
...
async_to_sync(channel_layer.group_send)(
'game',
{
'type': 'save.result'
}
)
return(game)
#task_success.connect
def task_success_handler(sender=run_game, result=None, **kwargs):
game_results = Game.objects.get(id=result.id).results.all()
async_to_sync(channel_layer.group_send)(
'game',
{
'type': 'end.game',
'data': game_results
}

Messages not getting to consumer unless Heroku dyno count is scaled way up

We have built a front-end with React and a back-end with Django Rest Frameworks and channels. We are using Heroku Redis as our Redis provider. Our users connect to Channels via a ReconnectingWebSocket.
We are using Python 3.6 and Channels 2.4
The issue is that our API calls are attempting to pass info to the sockets and they're not always making it to the consumer. I logged the steps of the call out via prints, printed the channel_name it's about to attempt to send it to and confirm it's what was returned to the user on connect, but the prints in the consumer don't get called meaning the message never gets sent to the user.
If I increase the number dynos to more or less a 1-1 with the users connected to sockets then it seems to solve the problem (or at least makes it much more reliable). From my understanding, 1 dyno should be able to handle many socket connections. Is there a reason that my consumer is not receiving the signals? Is there a reason scaling up the number of dynos resolved the problem?
On connect, I have the user join a group called "u_{their id}" to allow for potentially sending the signals to multiple computers logged in as the same user. I have tried sending the message through their channel_name directly and through that group, and when messages aren't going through neither seem to go through. the prints verify the channel_names are correct and the consumer still doesn't receive the messages. There doesn't seem to be any errors occuring. It may not work, then I'll refresh the recipient and it'll work, then I'll refresh the recipient again and it's back to not working.
The socket connection is certainly alive - I made a simple function on the front end that pings the socket and when I do it (even if the consumer isn't getting signals from API calls), it responds.
I also notice that if I restart my dynos, when they load up and the sockets reconnect, the first user has signals working through API calls for a short time then they start not coming through again. Also, if I don't use the sockets for a while then refresh they also seem to start working briefly again.
Procfile
web: daphne doctalk.asgi:application --port $PORT --bind 0.0.0.0
consumers.py
import json
from asgiref.sync import async_to_sync
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncWebsocketConsumer
from messages.models import Thread
from profile.models import OnlineStatus, DailyOnlineUserActivity
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import AnonymousUser
from .exceptions import ClientError
import datetime
from django.utils import timezone
class HeaderConsumer(AsyncWebsocketConsumer):
async def connect(self):
await self.accept()
await self.send("request_for_token")
async def continue_connect(self):
print(self.channel_name)
print(self.channel_layer)
await self.send(json.dumps({'your_channel_name': self.channel_name}))
await self.get_user_from_token(self.scope['token'])
await self.channel_layer.group_send(
"online_users",
{
"type": "new_user_online",
"user": self.user,
"channel_layer": str(self.channel_layer),
"channel_name": self.channel_name,
}
)
await self.channel_layer.group_add(
"online_users",
self.channel_name,
)
print("adding to personal group u_%d" % self.user['id'])
await self.channel_layer.group_add(
"u_%d" % self.user['id'],
self.channel_name,
)
self.message_threads = set()
self.message_threads = await self.get_message_ids()
for thread in self.message_threads:
await self.monitor_thread(thread)
self.doa = await self.check_for_or_establish_dailyonlineactivity()
self.online_status = await self.establish_onlinestatus()
await self.add_to_online_status_list()
self.user_id_list = await self.get_online_user_list()
await self.send_online_user_list()
async def disconnect(self, code):
# Leave all the rooms we are still in
if hasattr(self, 'user'):
await self.remove_from_dailyonlineactivity()
try:
await self.channel_layer.group_discard(
"u_%d" % self.user['id'],
self.channel_name,
)
except Exception as e:
print("issue with self channel")
print(e)
try:
await self.channel_layer.group_send(
"online_users",
{
"type": "user_went_offline",
"message": self.user['id'],
}
)
except Exception as e:
print("issue with online_users")
print(e)
await self.channel_layer.group_discard(
"online_users",
self.channel_name,
)
try:
for thread_id in list(self.message_threads):
print("leaving " + str(thread_id))
try:
self.message_threads.discard(thread_id)
await self.channel_layer.group_discard(
"m_%d" % thread_id,
self.channel_name,
)
except ClientError:
pass
except Exception as e:
print("issue with threads")
print(e)
async def receive(self, text_data):
print(text_data)
text_data_json = json.loads(text_data)
if 'token' in text_data_json:
self.scope['token'] = text_data_json['token']
await self.continue_connect()
#self.send(text_data=json.dumps({
# 'message': message
#}))
async def new_message(self, event):
# Send a message down to the client
await self.send(text_data=json.dumps(
{
"type": event['type'],
"thread": event['thread'],
"message": event["message"],
},
))
async def user_went_offline(self, event):
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def send_call_ring(self, event):
print("SENDING CALL RING")
print(event["message"])
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def rejoin_call(self, event):
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def popup_notification(self, event):
print("sending popup_notification")
print(event)
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def new_call_participant(self, event):
print("new_call_participant received")
print(event)
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def new_participants_invited(self, event):
print("new_participants_invited received")
print(event)
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def share_document_via_videocall(self, event):
print("share_document received")
print(event)
print(self.channel_name)
print(self.user['id'])
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def event_video_share_link(self, event):
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def event_video_hand_up(self, event):
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def event_video_address_hand_up(self, event):
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def you_are_dominant_speaker(self, event):
# Send a message down to the client
print("SENDING DOMINANT SPEAKER")
print(event)
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def you_are_no_longer_dominant_speaker(self, event):
print("SENDING NO LONGER DOMINANT SPEAKER")
print(event)
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def event_video_screenshare(self, event):
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def event_video_reaction(self, event):
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def video_call_thread(self, event):
print("sending video call thread")
print(event)
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def video_call_chat_message(self, event):
print("sending video call chat message")
print(event)
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def event_chat_message(self, event):
print("sending event chat message")
print(event)
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def to_next_agenda_item(self, event):
print("sending video call chat message")
print(event)
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def mute_all_event_participants(self, event):
print("sending mute all participants")
print(event)
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def event_started(self, event):
print("event started consumer")
print(event)
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def event_ended(self, event):
print(event)
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def video_call_reaction(self, event):
print("sending video call reaction")
print(event)
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def new_user_online(self, event):
print("user_online received")
print(event)
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["user"],
"channel_layer": event["channel_layer"],
"channel_name": event["channel_name"],
},
))
#database_sync_to_async
def get_message_ids(self):
return set(Thread.objects.filter(participants__id=self.user['id'], subject="").values_list('id', flat=True))
async def monitor_thread(self, thread_id):
print("monitoring thread %d" % thread_id)
print("on channel %s" % self.channel_name)
await self.channel_layer.group_add(
"m_%d" % thread_id,
self.channel_name,
)
#database_sync_to_async
def get_user_from_token(self, t):
try:
print("trying token" + t)
token = Token.objects.get(key=t)
self.user = token.user.get_profile.json()
except Token.DoesNotExist:
print("failed")
self.user = AnonymousUser()
#database_sync_to_async
def check_for_or_establish_dailyonlineactivity(self):
doa, created = DailyOnlineUserActivity.objects.get_or_create(date=datetime.date.today())
if created:
print("created DOA %d" %doa.id)
else:
print("found existing DOA %d" %doa.id)
return doa
#database_sync_to_async
def establish_onlinestatus(self):
old_os = OnlineStatus.objects.filter(user_id=self.user['id'], online_to=None)
if old_os.exists():
for os in old_os:
print("found unclosed OS %d" % old_os[0].id)
os.online_to = timezone.now()
os.save()
new_os = OnlineStatus(
user_id=self.user['id'],
channel_name=self.channel_name
)
new_os.save()
return new_os
#database_sync_to_async
def add_to_online_status_list(self):
self.doa.currently_active_users.add(self.user['id'])
self.doa.all_daily_users.add(self.user['id'])
self.doa.online_log.add(self.online_status)
self.doa.save()
#database_sync_to_async
def remove_from_dailyonlineactivity(self):
if hasattr(self, 'doa') and self.doa is not None:
self.doa.currently_active_users.remove(self.user['id'])
if hasattr(self, 'onine_status') and self.online_status is not None:
self.online_status.online_to = timezone.now()
self.online_status.save()
#database_sync_to_async
def get_online_user_list(self):
user_id_list = list(self.doa.currently_active_users.all().values_list('id', flat=True))
user_id_list.remove(self.user['id'])
return user_id_list
async def send_online_user_list(self):
print("sending online_users")
await self.send(text_data=json.dumps(
{
"type": "online_users",
"message": self.user_id_list,
},
))
async def participant_ignored(self, event):
print("irgnored call")
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def participant_left(self, event):
print("left call")
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def participant_joined(self, event):
print("left call")
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
async def video_screenshare(self, event):
print("sending screenshare")
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
The django signal triggered by adding a Profile to a VideoRoom:
#receiver(m2m_changed, sender=VideoRoom.invitees.through)
def invitee_added(sender, **kwargs):
instance = kwargs.pop('instance', None)
action = kwargs.pop('action', None)
pk = kwargs.pop('pk_set', None)
if action == 'post_add':
if len(pk) > 0:
user = Profile.objects.get(id=list(pk)[0])
if instance.initiator.id == user.id:
return
identity = "u_%d" % user.id
# Create access token with credentials
token = AccessToken(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_API_KEY, settings.TWILIO_API_SECRET,
identity=identity, ttl=86399)
# Create a Video grant and add to token
video_grant = VideoGrant(room=instance.room_name)
token.add_grant(video_grant)
invitee_access_token = VideoAccessToken(user=user, token=token.to_jwt())
invitee_access_token.save()
instance.invitee_access_tokens.add(invitee_access_token)
channel_layer = get_channel_layer()
print(channel_layer)
profiles = {"u_%d" % instance.initiator.id: instance.initiator.json()}
for u in instance.current_participants.all():
profiles["u_%d" % u.id] = u.json()
print("instance.type")
print(instance.type)
if instance.type != 'event':
print("sending to existing users")
for key, value in profiles.items():
if value['id'] != user.id:
async_to_sync(channel_layer.group_send)(
key,
{'type': 'new_call_participant',
'message': {
'key': "u_%d" % user.id,
'value': user.json()
}
}
)
ons = OnlineStatus.objects.get(user=user, online_to=None)
print("in signal, sending to %s on channel %s" % (user.full_name, ons.channel_name))
async_to_sync(channel_layer.send)(
ons.channel_name,
{'type': 'send_call_ring',
'message': {
'id': instance.id,
'room_name': instance.room_name,
'identity': "u_%d" % user.id,
'profiles': profiles,
'token': invitee_access_token.token.decode(),
'answered': False,
'initiated': False,
'caller': instance.initiator.json()
}
}
)
Log during unsuccessful socket signal:
2021-03-11T15:16:14.489596+00:00 app[web.1]: pk
2021-03-11T15:16:14.489655+00:00 app[web.1]: {113}
2021-03-11T15:16:14.518051+00:00 app[web.1]: pk
2021-03-11T15:16:14.518058+00:00 app[web.1]: {68}
2021-03-11T15:16:14.786357+00:00 app[web.1]: sending to existing users
2021-03-11T15:16:14.786377+00:00 app[web.1]: u_113
2021-03-11T15:16:14.911441+00:00 app[web.1]: u_68
2021-03-11T15:16:14.915900+00:00 app[web.1]: in signal, sending to John Doe on channel u_68
2021-03-11T15:16:15.228644+00:00 app[web.1]: 10.63.249.212:12999 - - [11/Mar/2021:10:16:15] "POST /api/start-video-chat/" 200 3523
2021-03-11T15:16:15.231562+00:00 heroku[router]: at=info method=POST path="/api/start-video-chat/" host=project-name.herokuapp.com request_id=7ec75a21-c6bd-452b-9517-cd500064d7ee fwd="12.34.56.78" dyno=web.1 connect=3ms service=955ms status=200 bytes=3714 protocol=http
On a successful call:
2021-03-11T15:20:50.253243+00:00 app[web.4]: pk
2021-03-11T15:20:50.253248+00:00 app[web.4]: {113}
2021-03-11T15:20:50.280925+00:00 app[web.4]: pk
2021-03-11T15:20:50.280926+00:00 app[web.4]: {68}
2021-03-11T15:20:50.614504+00:00 app[web.4]: sending to existing users
2021-03-11T15:20:50.614527+00:00 app[web.4]: u_113
2021-03-11T15:20:50.713880+00:00 app[web.4]: u_68
2021-03-11T15:20:50.718141+00:00 app[web.4]: in signal, sending to John Doe on channel u_68
2021-03-11T15:20:50.799546+00:00 app[web.2]: CALLING
2021-03-11T15:20:50.801670+00:00 app[web.2]: {'type': 'send_call_ring', 'message': "some payload data"}
2021-03-11T15:20:50.965602+00:00 app[web.4]: 10.11.225.205:25635 - - [11/Mar/2021:10:20:50] "POST /api/start-video-chat/" 200 3533
2021-03-11T15:20:50.964378+00:00 heroku[router]: at=info method=POST path="/api/start-video-chat/" host=project-name.herokuapp.com request_id=2da9918b-b587-4db9-a3c2-9d6dfd55ef42 fwd="12.34.56.78" dyno=web.4 connect=1ms service=888ms status=200 bytes=3724 protocol=http
The issue ended up being the Redis. I converted from channels-redis to channels-rabbitmq and all of my issues went away. I don't know if it was with my Redis provider or with channels-redis, but simply changing the backend resolved all issues.

Unable to get websockets tests to pass in Django Channels

Given a Django Channels consumer that looks like the following:
class NotificationConsumer(JsonWebsocketConsumer):
def connect(self):
user = self.scope["user"]
async_to_sync(self.channel_layer.group_add)("hello", "hello")
self.accept()
async_to_sync(self.channel_layer.group_send)(
"hello", {"type": "chat.message", "content": "hello"}
)
def receive_json(self, content, **kwargs):
print(content)
async_to_sync(self.channel_layer.group_send)(
"hello", {"type": "chat.message", "content": "hello"}
)
print("Here we are")
def chat_message(self, event):
self.send_json(content=event["content"])
def disconnect(self, close_code):
# Leave room group
async_to_sync(self.channel_layer.group_discard)("hello", "hello")
and a test that looks like the following:
#pytest.mark.asyncio
class TestWebsockets:
async def test_receives_data(self, settings):
communicator = WebsocketCommunicator(
application=application, path="/ws/notifications/"
)
connected, _ = await communicator.connect()
assert connected
await communicator.send_json_to({"type": "notify", "data": "who knows"})
response = await communicator.receive_json_from()
await communicator.disconnect()
I am always getting a TimeoutError when I run the test. What do I need to do differently?
If you'd like to see a full repo example, check out https://github.com/phildini/websockets-test
shouldn't async_to_sync(self.channel_layer.group_add)("hello", "hello") be async_to_sync(self.channel_layer.group_add)("hello", self.channel_name)?
In the first case you are adding "hello" to the group and the communicator.receive_json_from() in the test will fail as the group_send will not be received by the test client.
By refactoring the class as:
class NotificationConsumer(JsonWebsocketConsumer):
def connect(self):
user = self.scope["user"]
async_to_sync(self.channel_layer.group_add)("hello", self.channel_name)
self.accept()
async_to_sync(self.channel_layer.group_send)(
"hello", {"type": "chat.message", "content": "hello"}
)
def receive_json(self, content, **kwargs):
print(content)
async_to_sync(self.channel_layer.group_send)(
"hello", {"type": "chat.message", "content": "hello"}
)
print("Here we are")
def chat_message(self, event):
self.send_json(content=event["content"])
def disconnect(self, close_code):
# Leave room group
async_to_sync(self.channel_layer.group_discard)("hello", self.channel_name)
I can get the tests from the sample repo pass
For testing async channels code you are best of using purely functional async tests.
#pytest.mark.asyncio
async def test_receives_data(settings):
communicator = WebsocketCommunicator(
application=application, path="/ws/notifications/"
)
connected, _ = await communicator.connect()
assert connected
await communicator.send_json_to({"type": "notify", "data": "who knows"})
response = await communicator.receive_json_from()
await communicator.disconnect()
pytest will let you mix theses with class based regular Django tests.
Here you can find some examples for testing consumers.
https://github.com/hishnash/djangochannelsrestframework/tree/master/tests

django channels - delayed messaging does not work

I am trying to imitate a time consuming django response, by delaying group message in Django channels, but it does not work.
The consumer is very simple:
class ExportConsumer(JsonWebsocketConsumer):
...
def delayed_message(self, event):
print("In delayed")
time.sleep(5)
self.send(text_data=json.dumps({
'message': 'delayed!'
}))
def immediate_message(self, event):
print("In Immediate")
self.send(text_data=json.dumps({
'message': 'immediate_message!'
}))
and in views I just send to a group two messages - for delayed and immediate processing:
class DecisionListView(PaginatedListView):
...
def get(self, *args, **kwargs):
async_to_sync(get_channel_layer().group_send)(
'5e001793-18be-4a4b-8caf-c4a8144a11d2',
{
'type': 'immediate_message',
'message': 'message'
}
)
async_to_sync(get_channel_layer().group_send)(
'5e001793-18be-4a4b-8caf-c4a8144a11d2',
{
'type': 'delayed_message',
'message': 'message'
}
)
return super().get(*args, **kwargs)
The 'immediate message' is delivered. And I get In delayed on terminal (which means that it reaches the delayed_message but the message is never delivered (unless I comment out time.sleep.
What am I doing wrong?
UPDATE::
AsyncJsonWebsocketConsumer does not work as well:
consumers.py:
class ExportConsumer(AsyncJsonWebsocketConsumer):
...
async def delayed_message(self, event):
await asyncio.sleep(5)
await self.send(text_data=json.dumps({
'message': 'whatever'
}))
views.py:
class DecisionListView(PaginatedListView):
...
def get(self, *args, **kwargs):
_group_send = get_channel_layer().group_send
_sync_group_send = async_to_sync(_group_send)
_sync_group_send('5e001793-18be-4a4b-8caf-c4a8144a11d2',{"type":'delayed_message', 'message':'hello'})
return super().get(*args, **kwargs)
UPDATE2:
The only thing that works for me is asyncio.create_task:
class ExportConsumer(AsyncJsonWebsocketConsumer):
async def delayed_task(self):
await asyncio.sleep(5)
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': 'LATER'
}
)
async def delayed_message(self, event):
asyncio.create_task(self.delayed_task())
await self.send_json('NOW')
async def chat_message(self, event):
message = event['message']
await self.send_json(message)
you should be using an AsyncJsonWebsocketConsumer and async methods then you can use the sleep method from asyncio.
time.sleep will block your entire server.
-- unrelated
you should be sending delayed.message not delayed_message (channels replaces . with _ when calling functions)