Channels is a project led by Andrew Godwin that aims to bring "native asynchronous" to Django.Most tutorials on Channels focus on the processing power of WebSockets that Channels brings to Django.But Channels also has an important feature:asynchronous tasks.Based on this, Channels can replace Celery or RQ's tasks in most projects, and it is more native to use.To prove this, let's use Channels to add a non-blocking email sending to a Django project.

First, we need an Invitation model.

from django.db import models from django.contrib.auth.models import User class Invitation(models.Model): email = models.EmailField() sent = models.DateTimeField(null=True) sender = models.ForeignKey(User) key = models.CharField(max_length=32, unique=True) def __str__(self): return "{} invited {}".format(self.sender, self.email)

The corresponding ModelForm is as follows:

from django import forms from django.utils.crypto import get_random_string from .models import Invitation class InvitationForm(forms.ModelForm): class Meta: model = Invitation fiels = ['email'] def save(self, *args, **kwargs): self.instance.key = get_random_string(32).lower() return super(InvitationForm, self).save(*args, **kwargs)

The question about how to use this form in the View is left to the readers.What we need to do now is that when the Invitation is created on the front end, it is immediately sent to the back end for processing.Which means we need to install Channels.

pip install channels

We intend to use Redis as a message container which is called a "layer" in Channels,between our main web process and the Channels worker processes. So we need to install the corresponding Redis library.

pip install asgi-redis

We intend to use Redis as the preferred Channels layer(The Channels team also offers two alternatives, the in-memory layer and the database layer. The database layer is not recommended).If Redis is not installed in our development environment, we need to install Redis first. Here is the installation method of Redis in Debian/Linux-based system:

apt-get install redis-server

If we're on Mac, we're going to use Homebrew, then install Redis through Homebrew:

brew install redis

The rest of this tutorial is going to assume we have Redis installed and running in our development environment.

Now, we can start adding Channels to our project.In our project's settings.py , add 'channels' to INSTALLED_APPS and add the channels configuration block.

INSTALLED_APPS = ( ..., 'channels', ) CHANNEL_LAYERS = { "default": { "BACKEND": "asgi_redis.RedisChannelLayer", "CONFIG": { "hosts": [os.environ.get('REDIS_URL', 'redis://localhost:6379')], }, "ROUTING": "myproject.routing.channel_routing", }, }

Let's take a look at the CHANNEL_LAYERS block.Does it look a lot like Django's database settings?This is not surprising. Just like we have a default database configuration in settings, here we define a default Channels configuration.Our configuration uses the Redis backend, specifies the url of the Redis server, and points at a routing configuration. The routing configuration works like our project's urls.py . (We're also assuming our project is called 'myproject', you should replace that with your project's actual package name).

Since we are only using Channels to send emails in the back end, our routing.py file is very simple.

from channels.routing import route from .consumers import send_invite channel_routing = [ route('send-invite', send_invite), ]

Hopefully this structure looks somewhat like how we define URLs. What we're saying here is that we have one route, 'send-invite', and what we receive on that channel should be consumed by the 'send_invite' consumer in our invitations app. The consumers.py file in our invitations app is similar to a views.py in a standard Django app, and it's where we're going to handle the actual email sending.

import logging from django.contrib.sites.models import Site from django.core.mail import EmailMessage from django.utils import timezone from invitations.models import Invitation logger = logging.getLogger('email') def send_invite(message): try: invite = Invitation.objects.get( id=message.content.get('id'),) except Invitation.DoseNotExist: logger.error("Invitation to send not found") return subject = "You've been invited!" body = "Go to https://%s/invites/accept/%s/ to join!" % ( Site.objects.get_current().domain, invite.key, ) try: message = EmailMessage( subject=subject, body=body, from_email="Invites <invites@%s.com>" % Site.objects.get_current().domain, to=[invite.email,], ) message.send() invite.sent = timezone.now() invite.save() except: logger.execption('Problem sending invite %s' % (invite.id))

Consumers consume messages from a given channel, where messages is a list of data objects.The data in the message must be in json format, so that it can be stored in the Channel layer (in this case: Redis) and passed around.In this example, the only data we use is the invitation ID that needs to be sent.We get the invite object from the database, build the email based on the object, and then try to send the email.If it's successful, we set a "sent" timestamp on the invite object. If it fails, we log an error.

So far, we still have a problem that has not been solved: how to send the message to the 'send-invite' channel at the right time?My solution is as follows:

from django import forms from django.utils.crypto import get_random_string from channels import Channel from .models import Invitation class InvitationForm(forms.ModelForm): class Meta: model = Invitation fields = ['email'] def save(self, *args, **kwargs): self.instance.key = get_random_string(32).lower() response = super(InvitationForm, self).save(*args, **kwargs) notification = { 'id':self.instance.id, } Channel('send-invite').send(notification) return response

We import Channel from the channels package, and send a data blob on the 'send-invite' channel when our invite is saved.

Now we are ready to test!We connect the form to a view, and set the correct email host settings in our settings.py ,we can test the use of Channel to send email invitations in our app background.The amazing thing about Channels in development is that we start our devserver normally, and in my experience at least, It Just Works.

python manage.py runserver

Congratulations! We have added the background task to the Django application. Let's use Channel.Now, I don't believe it can work until the system can run in the real environment, so let's talk about how to deploy.The Channels docs make a great start at covering this, but I use Heroku, so I'm adapting the excellent tutorial written by Jacob Kaplan-Moss for this project.

We create an asgi.py file in the same directory as the wsgi.py file.

import os import channels.asgi os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings") channel_layer = channels.asgi.get_channel_layer()

Note:Remind everyone again, replace "myproject" above with your real project name.

Then, we update our Procfile to include the main Channels process, running under Daphne, and a worker process.

web: daphne myproject.asgi:channel_layer --port $PORT --bind 0.0.0.0 -v2 worker: python manage.py runworker --settings=myproject.settings -v2

We can use Heroku's free Redis host to deploy our app and enjoy sending emails in the background without blocking our app service requests.

Hopefully this tutorial will motivate you to explore the background task capabilities of Channels and consider preparing your application when Channel becomes the core of Django. I think we are moving towards a future when Django can do a better out-of-the-box.,and I am very happy to see what we have built!

If you have more questions about Channels, you can also check out our Channels Tutorial.