2

From this answer, which helps to send data from consumers for every n seconds.

Tried to handle the disconnection properly, using creat_task method, tried to stop while-loop(which is used to send data for every n seconds) by sending a flag=False(Assuming, this flag is not sent to the same instance which is created the task).

consumers.py:

class AutoUpdateConsumer(AsyncConsumer):

    async def websocket_connect(self, event):
        print("connected", event)
        await self.send({
            "type": "websocket.accept"
        })
        await self.create_task(True)

    async def websocket_receive(self, event):
        print("receive", event)

    async def websocket_disconnect(self, event):
        await self.create_task(False)

        print("disconnected", event)


    async def create_task(self, flag=True):
        while flag:
            await asyncio.sleep(2)

            df= pd.DataFrame(data=[random.sample(range(100), 4) for _ in range(5)])

            await self.send({
                'type': 'websocket.send',
                'text': df.to_html(),
            })

Warning:

2019-09-11 14:40:06,400 - WARNING - server - Application instance 
<Task pending coro=<SessionMiddlewareInstance.__call__() running at 
D:\Django\Django channels\django_channels_env\lib\site-packages\channels\sessions.py:175>
wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 
0x000001870E06C618>()] for connection <WebSocketProtocol client=
['127.0.0.1', 63789] path=b'/ws/home'> took too long to shut down and was 
killed.

How to stop_task safely instead of waiting for channels to kill task?

Or

How to stop infinite while loop running in a method, from another method in same class?

Versions:

  • Django == 2.0.7
  • channels == 2.1.2
shaik moeed
  • 5,300
  • 1
  • 18
  • 54
  • Using a loop in a consumer like this will waste resources by tying up your workers. A worker can only handle one event at a time so the entire time this loop is running that worker won't be able to do anything else. – bdoubleu Sep 11 '19 at 18:05
  • @bdoubleu What is the best practice to achieve this? – shaik moeed Sep 11 '19 at 18:09
  • You could use a management command to trigger the event every second and send message to a group. – bdoubleu Sep 11 '19 at 18:11
  • @bdoubleu I'm new to channels, can you please elaborate? or Share any link, where I can read about it? – shaik moeed Sep 11 '19 at 18:13
  • Can you give me an example of the data you are sending? Is it saved in the database? The general idea is that when the user connects to a consumer you add the user to a group. Then you would start your loop somewhere outside of the consumer but you can still call a consumer method because you know the group. – bdoubleu Sep 11 '19 at 19:40
  • @bdoubleu Data comes from pickle file, which will get updated for every minute, which is independent with this application. You can use dataframe with random data generated in question under `create_task` method. – shaik moeed Sep 12 '19 at 03:40
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/199359/discussion-between-bdoubleu-and-shaik-moeed). – bdoubleu Sep 12 '19 at 09:16

1 Answers1

2

I would suggest creating a group when connecting to the consumer. That way you can trigger a message from anywhere in your django project as long as you know the group name (auto_update).

from channels.generic.websocket import AsyncWebsocketConsumer

class AutoUpdateConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        print('connect')

        # join the group
        self.group_name = 'auto_update'
        await self.channel_layer.group_add(
            self.group_name,
            self.channel_name
        )
        await self.accept()

    async def disconnect(self, event):
        print('disconnect')

        # leave the group
        await self.channel_layer.group_discard(
            self.group_name,
            self.channel_name
        )

    async def receive(self, event):
        print('receive')

    async def auto_update(self, event):
        print('sending df')
        df = event['df']

        await self.send({
            'text': df
        })

To send the message I would use a custom management command. To stop the command I would create a singleton model (a model with only one instance) that has a boolean field that can be periodically checked to see if the loop should be stopped.

First use get_channel_layer() to get the active layer that communicates with redis, then in the loop call group_send to invoke a consumer method specified by the type key.

# /project/app/management/commands/auto_update.py

from django.core.management.base import BaseCommand
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from config.models import AutoUpdateSettings

class Command(BaseCommand):
    help = 'Command to start auto updating'

    def handle(self, *args, **kwargs):
        settings = AutoUpdateSettings.objects.first()
        settings.keep_running = True
        settings.save()

        group_name = 'auto_update'
        channel_layer = get_channel_layer()

        while True:
            settings.refresh_from_db()
            if not settings.keep_running:
                break

            df= pd.DataFrame(data=[random.sample(range(100), 4) for _ in range(5)])
            async_to_sync(channel_layer.group_send)(
                group_name,
                {
                    'type': 'auto_update',  # this is the name of your consumer method
                    'df': df.to_html()
                }
            )

To start the loop that sends the message to the group you would call the command python manage.py auto_update. To stop the command you would use the admin page and set keep_running to false.

bdoubleu
  • 5,568
  • 2
  • 20
  • 53
  • Is it in-built `from config.models import AutoUpdateSettings`? I'm getting module not found an error and there is no models in my Django project. – shaik moeed Sep 12 '19 at 15:53
  • @shaikmoeed no, you will have to make that model. Here's a good [example for singleton models](https://steelkiwi.com/blog/practical-application-singleton-design-pattern/), you'll just need to get it registered in django admin so you can uncheck `keep_running` to stop the loop.. – bdoubleu Sep 12 '19 at 15:57
  • I have solved the issues, but when `keep_running` is set to false, the server is getting shutdown. And need to run it manually again. But this won't be the right way to run on production. Can I run `auto_update` again from admin? – shaik moeed Sep 12 '19 at 18:09
  • Is there any other way to handle this situation on the production server? – shaik moeed Sep 12 '19 at 18:15
  • You could use celery. The same strategy would apply but you're calling the command from a celery task – bdoubleu Sep 12 '19 at 18:34