Tareas asíncronas y programadas en Django con Celery + RabbitMQ

Cuando necesitamos ejecutar tareas periódicas en una aplicación en Django, podríamos echar mano del conocido cron, siempre infalible pero a veces engorroso. Sin embargo, Django proporciona una forma más sencilla de resolverlo: Celery.

Celery es un software Open Source que proporciona una cola de tareas asíncrona. Para ello implementa un sistema de envío distribuído de mensajes que nos permitirán ejecutar tareas en tiempo real, programadas o periódicas en segundo plano. Dicho con otras palabras: nos proporciona una cola donde enviamos tareas para que sean ejecutadas cuando sea posible sin que ésto detenga nuestra aplicación Django.

Instalando Celery para ser utilizado en Django

Antes de nada, es necesario que entendamos cómo funciona Celery. Como he comentado, no es más que una cola de procesos que se ejecutan como threads. Pero necesita un sistema de envío y recepción de mensajes, que es lo que se conoce como Message Broker.

El Message Broker más común para ser utilizado con Celery se llama RabbitMQ, aunque también podríamos usar Redis, Beanstalk, MongoDB... sin embargo en este artículo nos centraremos en RabbitMQ.

RabbitMQ es tremendamente fiable y robusto, de modo que podemos usarlo perfectamente en un entorno de producción. Es escalable, está desarrollado en Erlang, es estable y muy fácil de instalar en Linux.

sudo apt-get install rabbitmq-server

Ahora instalar Celery es igualmente sencillo: vamos a nuestro entorno virtual python para ejecutar esto:

pip install celery

Integrar Celery en nuestro proyecto Django

Para utilizar Celery en una de nuestras aplicaciones, básicamente necesitamos mantener dos ficheros en nuestro proyecto Django:

    proyectoDjango
        /db.sqlite3
        /manage.py
        /AppDjango
            /tasks.py  <----- TAREAS
            /…
        /proyectoDjango
            /__init__.py
            /celery.py  <------ CELERY
            /urls.py
            /wsgi.py
            /settings.py

El fichero celery.py lo utilizareos para definir una instancia de Celery:

from __future__ import absolute_import
import os
from celery import Celery
from django.conf import settings

# #indicamos el Django settings module por defecto para nuestro programa celery
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proyectoDjango.settings')
app = Celery('proyectoDjango')

#Añadimos el Django settings module como fuente de configuración de Celery
app.config_from_object('django.conf:settings')

# Como tenemos nuestras tareas en un fichero de nombre tasks.py,
# indicamos a celery que encuentre automáticamente dicho módulo dentro del proyecto.
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)

@app.task(bind=True)
def debug_task(self):
    print('Request: {0!r}'.format(self.request))

Ahora metemos una tarea Celery nueva en nuestro task.py:

from celery.decorators import task

@task(name="suma dos numeros")
def add(x, y):
    return x + y

El decorador @shared_task es importante: nos permite crear tareas nuevas sin tener ninguna instancia concreta de nuestro programa Celery.

A esta tarea ya la podemos llamar de forma asíncrona haciendo esto:

add.delay(7, 8)

Vamos allá con RabbitMQ

Esto, tal como lo tenemos ahora, es ideal para ejecutar tareas en background y que una web no espere a que termine para renderizarse completamente. Pero nosotros queremos ir más allá: vamos a programar estas tareas para que se ejecuten cuando nosotros decidamos. Aquí es donde entra en juego RabbitMQ.

Suponiendo que nuestro broker esté instalado en la misma máquina que nuestra aplicación Django, tenemos que incluir esto en el fichero settings.py

BROKER_URL = amqp://guest:guest@localhost//

Un backend para Celery

Ahora tenemos que instalar una cosa más: un backend en django para las tareas de celery, que nos permitirá poder visualizar su ejecución, configuración, etc. Dicho backend se encuentra en el ORM de Django y se activa así:

(mivirtualenv)$:pip install django-celery

Fichero settings.py :

INSTALLED_APPS = (
    django.contrib.auth,
    django.contrib.contenttypes,
    django.contrib.sessions,
    django.contrib.sites,
    django.contrib.messages,
    django.contrib.staticfiles,
    miAplicacion,
    djcelery,   --------> AÑADIR
    # otras apps que use mi proyecto,
)

[...]

CELERY_RESULT_BACKEND=djcelery.backends.database:DatabaseBackend

Realizar una migración de la BD para incluir las tablas necesarias para Celery:

(mivirtualenv)$: ./manage.py migrate djcelery

Tareas periódicas

Para ejecutar tareas periódicas, Celery utilizará un planificador llamado Celery Beats. Éste nos permitirá lanzar tareas en intervalos regular, al estilo cron que serán ejecutados por los workers disponibles. El motivo por el que necesitamo Celery Beats es que la tarea de planificación de tareas debe estar centralizada en un único proceso. De lo contrario, podrían surgir duplicidades de tareas indeseadas.

Para planificar una tarea nuevamente necesitaremos echar mano de nuestro fichero settings.py. En este caso vamos a ejecutar la tarea cada 30 segundos.

    from datetime import timedelta

    CELERYBEAT_SCHEDULE = {
        add-every-30-seconds: {
            task: miAplicacionDjango.tasks.add,
            schedule: timedelta(seconds=30),
            args: (15, 35)
        },
    }

    #Si no usamos CELERY_TIMEZONE, se usará por defecto el TIME_ZONE que use nuestra app Django
    CELERY_TIMEZONE = Europe/London