使用 django channels 作为邮件发送队列

来源:互联网 发布:软件开发前期准备工作 编辑:程序博客网 时间:2024/04/30 11:53

使用 Django Channels 作为邮件发送队列

本文是一篇翻译文,是我在学习Django Channels的看的一篇文章, 英语好的同学建议直接看原文.

原文地址为 : Using Django Channels as an Email Sending Queue

Channels 是由 Andrew Godwin 领导的一个项目, 旨在能给 Django 带来” 本地异步处理” 的能力. 大多数关于 Channels 的教程都是都把关注点放在了Channels 给Django带来的 WebSockets的处理能力上. 但是 Channels 还有一个重要的功能,就是异步任务. 基于这一点, Channels 就可以替代 Celery 或 RQ 在大部分项目中的任务, 而且使用起来更为的自然.

为了证明这一点, 让我们使用Channels来为一个Django project添加一个非阻塞的邮件发送功能.

首先, 我们需要一个 Invitation model.

from django.db import modelsfrom django.contrib.auth.models import Userclass 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)

对应的 ModelForm.

from django import formsfrom django.utils.crypto import get_random_stringfrom .models import Invitationclass 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)

关于如何在View中使用这个 form就留给读者了, 我们现在要做的是, 当 Invitation在前端被创建时,被立即送到后台进行处理. 现在我们需要安装Channels.

pip install channels

我们打算使用 Redis 作为 message的容器, 这个容器被称为 “层(layer)”在Channels中,它位于我们的 web 处理和Channels worker 之间.所以我们需要安装相应的Redis库(注意这里按照的asgi-redis的作用是提供Channels使用Redis的方法)

pip install asgi-redis

我们打算把 Redis 作为首选的 Channels 层. (Channels团队同样提供了另外两种选择方案, in-memory layer 和 database layer. 其中 database layer不建议使用) 如果我们的开发环境还没有安装 Redis, 我们需要在我们的OS上安装Reids. 下面提供 Debian/Linux-based system的安装方法:

apt-get install redis-server

如果是 Mac用户, 我们会使用 Homebrew来安装:

brew install redis

经过上面的教程, 我们假定我们的开发环境具备如下条件:

安装了 Redis
安装了 Channels 和 asgi-redis

现在,可以开始把Channels添加到我们的项目中来了.在项目的 settings.py文件中. 添加 ‘channels’到 INSTALL_APPS中, 并且添加 channels配置模块

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

让我们来看一下 CHANNEL_LAYERS 块. 它是否看起来和Django的数据库settings很像呢? 这并不奇怪. 就像我们在settings中有一个默认的数据库配置一样, 这里我们定义了一个默认的Channels配置. 我们的配置使用 Redis作为后端, 并指定了Redis服务的url. 最后指定了一个 routing配置, routing配置和 urls.py文件的工作方式类似.(在这里我们假定项目名称为’myproject’, 你应该替换成你实际的项目名)

由于我们仅仅是使用Channels在后台进行email发送服务, 因此我们的routing.py 文件显的很简单.

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

正如所期望的,routing.py内容的结构和我们的urls.py. 上面定义的内容的含义如下:
我们定义了一个 名为’send-invite’的rout. 上面我们定义Channels的默认配置时有一个 “ROUTING”:”myproject.routing.channel_routing” .注意这里的路径就是我们的上面的 channel_routing. 所以 上面Channel收到的东西会被送到 ‘send_invite’ 消费者进行消费. 在我们的app中consumers.py文件和Django的标准app中的views.py文件是类似的, consumers.py才是我们正真处理email 发送的地方.

import loggingfrom django.contrib.sites.models import Sitefrom django.core.mail import EmailMessagefrom django.utils import timezonefrom invitations.models import Invitationlogger = 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 从一个给定的channel中消耗 messages, 其中messages是一列的数据对象. message中的数据必须是可以json化的,只有这样它才可以被存在Channel layer(本例是:Redis)中,并进行传递操作.在我们的例子中, 我们使用的唯一数据是要发送的邀请的ID. 我们从数据库获取invite对象, 基于该对象构建电子邮件,然后尝试发送电子邮件. 如果成功,我们在邀请对象上设置一个”已发送”时间戳. 如果失败, 我们记录一个错误.

到这里为止,我们还有一个问题没有解决. 就是如何在合适的时间把 message送到 ‘send-invite’ channel呢? 我们使用下面的方法

from django import formsfrom django.utils.crypto import get_random_stringfrom channels import Channelfrom .models import Invitationclass 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

我们从channels 包导入Channel, 在我们的invite要保存时发送一个”数据” 到 ‘send-invite’ channel

现在,我们准备要测试了! 假设我们将表单连接到View, 并在我们的setting.py中设置正确的电子邮件主机设置, 我们可以测试在我们的应用后台使用Channel发送电子邮件邀请. 关于Channels在开发中的惊人的事情是,我们正常启动我们的 devserver, 并且, 至少在我的经验看来, 它可以工作了.

python manage.py runserver

祝贺!我们已经添加后台任务到Django application 中了. 让我们来使用Channel吧

现在, 在系统能够在实际环境中运行前,我都不相信它可以工作, 所以让我们谈一下如何部署的问题. Channels 文档 里面有很好的写到这部分的内容, 但我使用的是 Heroku, 所以我会像 JacobKaplan-Moss的优秀tutorial一样完成本教程.

我们在 wsgi.py文件同级的目录下创建一个 asgi.py文件.

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

(再次提醒各位, 把上面的 myproject换成自己真实的项目名)

然后, 我们更新我们的 Profile 来包含 Channels process, running under Daphne, and a worker process.

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

我们可以使用 Heroku的免费 Redis主机, 部署我们的应用程序, 并享受在后台发送电子邮件,而不会阻塞我们的应用服务请求.

希望本教程能激励你探索Channels的后台任务功能, 并考虑当Channel成为Django的核心时准备好你的应用程序. 我想我们正在朝着一个未来前进, 到时Django可以做到更好的 开箱即用(out-of-the-box), 我很高兴看到我们所建立的!

特别感谢 Jacob Kaplan-Moss, Chris Clark 和 Erich Blume 提供的反馈

0 0
原创粉丝点击