Using Signals in your Django App

Yesterday, I posted a fairly popular post (for my blog anyway) dealing with how I write reusable Django apps. There were a couple of comments on Hacker News asking for some more details about some of thing suggestions mentioned in my post. Therefore, I'd like to start by digging into the use of signals to provide better site level integration.

What is a signal anyway?

A signal is defined by the Django documentation as:

“Django includes a “signal dispatcher” which helps allow decoupled applications get notified when actions occur elsewhere in the framework. In a nutshell, signals allow certain senders to notify a set of receivers that some action has taken place.”

A good example to highlight this is a recent upgrade I made to biblion which is the blog app that I use to drive this site. After my previous article was getting hammered from being on page 1 of HackerNews for an extended period of time, I noticed the page response times were starting to suffer.

I took a more careful look at the detail view serving up the page and discovered it was calling post.inc_views() which would be making an update to a count on the row that was just selected. I wasn't using these counts at all in my site and even if I did, I'd prefer to use redis or some form of a background task.

But how was I going to have access to do that at the site level?

The answer was to replace the post.inc_views() call with a signal. So I upgraded biblion to allow this by adding the following code:

Changes in biblion

biblion/signals.py

import django.dispatch

post_viewed = django.dispatch.Signal(providing_args=["post","request"])

biblion/views.py

In this module, I replaced the post.inc_views() call with:

post_viewed.send(sender=post, post=post, request=request)

Since I didn't care about incrementing the view count in my site, I could safely just ignore wiring up a receiver for this signal and it would become a no-op call in biblion. All I did was upgrade biblion in my requirements file and redeploy (thanks to Gondor this was super easy to redeploy).

However, for the sake of illustration if I did want to record the view count in a way that was not impeding the performance of heavy traffic, I could do something like the following.

Changes in your site

receivers.py

from django.dispatch import receiver

from biblion.signals import post_viewed

@receiver(post_viewed)
def handle_post_viewed(sender, **kwargs):
    post = kwargs.get("post")
    request = kwargs.get("request")
    if post:
        # Increment a redis key or kick off a background task or whatever here

urls.py

import receivers

Keep in mind that when handling that receiver that you are in the middle of a request/response cycle so you want to be as lightweight as possible. In fact, you should really consider putting any activity here in a background task via celery.