Fun with websockets and asyncio in Django Management Command
Tags: webdev, websocket
Published: 2021-02-15 19:02:10

I've been playing around with WebSockets to replace an existing AJAX-heavy part of the front-end of an webapp. This has taken me into the realm of asyncio and websockets Python modules which I had not used much of before, and I thought I'd write this up.

# From AJAX Polling to WebSocket-driven Updates

There was something that I've been meaning to rewrite for a while that required a background Django management command running constantly in the background, which does some processing every second or so, instead of every front-end client making an AJAX call to do the same processing every second, which becomes unscalable pretty quickly.

The idea was to notify the front-end clients with a WebSocket message whenever the centralised processing causes updates to the database which requires the front-end client to AJAX in the new data.

I'd never done much websocket or asyncio coding in Python, and it was quite fun to get into. Instead of handling event handles and message queues yourself in languages like C++, Python gives you pretty powerful asynchronous processing, using await/async, coroutine/Task/Futures.

# An Example
Here's a slightly modified version of the Django management command, which:

* Registers/unregisters front-end clients,
* Does some processing periodically
* If any changes were made during the processing, notifies the currently registered clients
```python
import logging

from optparse import make_option

from django.core.management.base import BaseCommand

import asyncio
import websockets

from.utils import configure_logging
from.models import Task
from.settings import (WEBSOCKET_BIND_PORT,
                      WEBSOCKET_BIND_ADDRESS)


clients = set()


async def notify_clients():
    if clients:  # asyncio.wait doesn't accept an empty list
        await asyncio.wait([client.send('updated') for client in clients])


async def main_loop():
    while True:
        modified = Task.objects.sync()
        if modified > 0:
            await notify_clients()
        await asyncio.sleep(0.5)


async def register(websocket):
    clients.add(websocket)
    logging.info(f'Registered new user: {websocket}')
    await notify_clients()


async def unregister(websocket):
    clients.remove(websocket)
    logging.info(f'Unregistered user: {websocket}')
    await notify_clients()


async def handle_client(websocket, path):
    await register(websocket)
    try:
        async for message in websocket:
            if message == 'success':
                pass
            else:
                await websocket.send('updated')
    finally:
        await unregister(websocket)


class Command(BaseCommand):
    """ Monitor tasks and notify websocket clients when they are updated """

    help = __doc__

    def handle(self, *args, **options):
        configure_logging(**options)

        start_server = websockets.serve(handle_client,
                                        WEBSOCKET_BIND_ADDRESS,
                                        WEBSOCKET_BIND_PORT)
        asyncio.get_event_loop().run_until_complete(start_server)
        asyncio.get_event_loop().run_until_complete(main_loop())
        asyncio.get_event_loop().run_forever()
```

On the front-end, we just wait for the 'updated' message and only trigger an AJAX call then:

```javascript
var ws = new WebSocket("ws{{ websocket_secure | yesno:'s,' }}://" + window.location.hostname + ":{{ websocket_port }}/");
ws.onmessage = function (event) {
  var type = event.data;
  if (type == 'updated') {
    $.getJSON('/task-list', function(data) {
      // Populate some stuff
    });
  }
};
```