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.

Leave a Comment

In order to leave comments you must log in via Twitter

Comments

@mikestoddart
Jan. 14, 2012, 10:12 a.m.

Will performance suffer if I add many custom signals to a Django app?

@glader_ru
Jan. 15, 2012, 5:59 a.m.

Hi. Do this blog have rss or atom feed?

patrick
Jan. 19, 2012, 12:54 p.m.

@glader_ru yes, it does. it was using feedburner but now it looks like the link is missing. I'll try to resolve that soon.

patrick
Jan. 19, 2012, 12:55 p.m.

@mikestoddart they are no-op if nothing is handling them. if the functions listening/handling them take a long time then yes, but ideally something like celery would be used to background expensive operations.

@thedailyboogie
Jan. 30, 2012, 12:07 a.m.

Hi,
I'm having a little bit of trouble installing the biblion. Whenever, I try to runserver or syncdb, it gives me this error: "Error: One or more models did not validate:
biblion.image: "image_path": To use ImageFields, you need to install the Python Imaging Library. Get it at http://www.pythonware.com/products/pil/ ." I have posted on stackoverflow here: http://stackoverflow.com/questions/9059993/biblion-django-blog-error just in case someone answers my question before you read this, you don't have to waste your time.

Thanks,
Steve

patrick
Jan. 31, 2012, 9:28 p.m.

@thedailyboogie -- You need to install PIL. You can do this easily by running `pip install PIL`