André Cruz
André Cruz

Reputation: 510

How to do HTTP long polling with Django Channels

I'm trying to implement HTTP long polling for a web request, but can't seem to find a suitable example in the Channels documentation, everything is about Web Sockets.

What I need to do when consuming the HTTP message is either:

and then return something to the client.

Right now I have the code that can be seen in the examples:

def http_consumer(message):
    # Make standard HTTP response - access ASGI path attribute directly
    response = HttpResponse("Hello world! You asked for %s" % message.content['path'])
    # Encode that response into message format (ASGI)
    for chunk in AsgiHandler.encode_response(response):
        message.reply_channel.send(chunk)

So I have to return something in this http_consumer that will indicate that I have nothing to send, for now, but I can't block here. Maybe I can just not return anything? And then I have to catch the new message on a specific Group, or reach the timeout, and send the response to the client.

It seems that I will need to store the message.reply_channel somewhere so that I can later respond, but I'm at a loss as to how to:

Upvotes: 4

Views: 3348

Answers (1)

André Cruz
André Cruz

Reputation: 510

So, the way I ended up doing this is described below.

In the consumer, if I find that I have no immediate response to send, I will store the message.reply_channel on a Group that will be notified in the case of relevant events, and schedule a delayed message that will be triggered when the max time to wait is reached.

group_name = group_name_from_mac(mac_address)
Group(group_name).add(message.reply_channel)
message.channel_session['will_wait'] = True

delayed_message = {
    'channel': 'long_polling_terminator',
    'content': {'mac_address': mac_address,
                'reply_channel': message.reply_channel.name,
                'group_name': group_name},
    'delay': settings.LONG_POLLING_TIMEOUT
}
Channel('asgi.delay').send(delayed_message, immediately=True)

Then, two things can happen. Either we get a message on the relevant Group and a response is sent early, or the delayed message arrives signalling that we have exhausted the time we had to wait and must return a response indicating that there were no events.

In order to trigger the message when a relevant event occurs I'm relying on Django signals:

class PortalConfig(AppConfig):
    name = 'portal'

    def ready(self):
        from .models import STBMessage

        post_save.connect(notify_new_message, sender=STBMessage)

def notify_new_message(sender, **kwargs):
    mac_address = kwargs['instance'].set_top_box.id
    layer = channel_layers['default']
    group_name = group_name_from_mac(mac_address)
    response = JsonResponse({'error': False, 'new_events': True})
    group = Group(group_name)
    for chunk in AsgiHandler.encode_response(response):
        group.send(chunk)

When the timeout expires, I get a message on the long_polling_terminator channel and I need to send a message that indicates that there are no events:

def long_polling_terminator(message):
    reply_channel = Channel(message['reply_channel'])
    group_name = message['group_name']
    mac_address = message['mac_address']
    layer = channel_layers['default']
    boxes = layer.group_channels(group_name)
    if message['reply_channel'] in boxes:
        response = JsonResponse({'error': False, 'new_events': False})
        write_http_response(response, reply_channel)
        return

The last thing to do is remove this reply_channel from the Group, and I do this in a http.disconnect consumer:

def process_disconnect(message, group_name_from_mac):
    if message.channel_session.get('will_wait', False):
        reply_channel = Channel(message['reply_channel'])
        mac_address = message.channel_session['mac_address']
        group_name = group_name_from_mac(mac_address)
        Group(group_name).discard(reply_channel)

Upvotes: 2

Related Questions