Flask Mega Tutorial 第22章:后台工作

本系列删改自Miguel Grinberg的Flask Mega-Tutorial系列。点此查看作者原文
这章太难翻,怀疑人生了,看不懂的对照作者原文看吧。


介绍任务队列

任务队列为应用程序提供了方便的解决方案,通过worker process (工作进程)请求对任务的执行。工作进程独立于应用程序运行,甚至可以位于不同的系统上。应用程序与工作之间的通讯通过message queue(消息队列)完成。应用程序提交一份工作,然后通过与队列互动监听其进程。下图展示了其典型实现:


Python上最流行的任务队列是Celery。这是一个相当强大的包,有许多选项,其支持多个消息队列。另一个流行的Python任务队列是Redis Queue即RQ,其牺牲了一部分灵活性,如只支持Redis消息队列,但设置起来比Celery要简单得多。

无论是Celery还是RQ都足够支持Flask应用中的后台任务,所以对于此应用,我选择使用起来更简单的RQ。然而,用Celery实现相同的功能应该会更容易。如果你对Celery更感兴趣,你可以阅读我博客上的这篇文章Using Celery with Flask


使用RQ

RQ是一个标准Python包,可以用pip安装:

(venv) $ pip install rq
(venv) $ pip freeze > requirements.txt

正如我刚才所说,应用程序与RQ worker之间的提醒将在Redis消息队列中进行,所以你需要有个正在运行的Redis服务。安装与运行Redis服务的方式有很多,一键安装或是下载源码并直接编译到你的系统。如果你使用Windows系统,微软维护的安装器在这里。在Linux系统中,你可以通过操作系统的包管理器获取它。Mac OS X用户可以运行 brew install redis 然后用 redis-server 命令手动开启服务。

你不需要在外部与Redis交互,只需要确保服务运行以及RQ可以访问。

注意RQ不能再Windows上的原生Python解释器上运行,如果你用的是Windows平台,你可以在类Unix模拟环境中运行RQ。我推荐Windows用户两款类Unix模拟环境,CygwinWindows Subsystem for Linux(WSL),两者都兼容RQ。

创建一个任务

我将向你展示如何通过RQ运行一个简单的任务。一个任务,无非就是一个Python函数。这是一个示例任务,我把它放在 app/tasks.py 模块中:

app/tasks.py

import time

def example(seconds):
    print('Starting task')
    for i in range(seconds):
        print(i)
        time.sleep(1)
    print('Task completed')

这个任务接收秒数为参数,然后等待那么多秒,每秒打印一次计数器。

运行RQ Worker

现在任务已经准备好了,可以启动worker了。用rq worker命令可以启动:

(venv) $ rq worker microblog-tasks
18:55:06 RQ worker 'rq:worker:miguelsmac.90369' started, version 0.9.1
18:55:06 Cleaning registries for queue: microblog-tasks
18:55:06
18:55:06 *** Listening on microblog-tasks...

这个worker进程现在已经和Redis连接上了,并监视名为microblog-tasks的队列中可能分配给它的任何工作。如果你希望有更多的worker来有更多的产量,你只要运行更多的 rq worker 实例即可,全都会连接到同一个队列。当一份工作出现在任务队列中时,任何可用的worker进程都会接管它。在生产环境中,你可能希望尽可能多的worker,至少和CPU一样多。

执行任务

现在打开一个终端窗口并激活虚拟环境。我将使用shell会话来启动worker中的example()任务:

>>> from redis import Redis
>>> import rq
>>> queue = rq.Queue('microblog-tasks', connection=Redis.from_url('redis://'))
>>> job = queue.enqueue('app.tasks.example', 23)
>>> job.get_id()
'c651de7f-21a8-4068-afd5-8b982a6f6d32'

来自RQ的Queue类将队列表现为应用程序端可见。接收队列名称,以及Redis连接对象为参数(以防我用默认URL初始化)。如果你的Redis服务运行在其他主机或端口上,就需要使用不同的URL。

队列上的enqueue()方法用于向队列添加job。第一个参数是你想要执行的任务的名称,以函数对象的形式直接给定,或者一个导入字符串。我发现用字符串更方便,所以就没必要从应用导入函数了。给enquene()的任何剩余参数都将被传递给worker中运行的函数。

一旦你调用enqueue(),你会注意到你第一个终端窗口(也就是运行RQ worker的那个)会有一些活动。你会看到example()函数现在在运行了,每隔一秒打印一次计时器。与此同时,其他的终端不会被屏蔽,你可以继续在shell中执行表达式。在上面的例子中,我调用job.get_id()方法获取该任务的唯一标识符。job对象还有另一个有意思的表达式,你可以试试,检查worker上的函数是否结束了:

>>> job.is_finished
False

如果你向我在上面例子中一样传递了23,那么这个函数将会运行大概23秒。之后,job.is_finished语句的输出会变成True。是不是很酷?我真的很喜欢RQ的简单性。

一旦函数完成,worker又重新等待新job了,因此你可以以不同的参数重复调用enqueue()。存储在队列中的有关任务的数据将会保留一段时间(默认500秒),但最后会被移除。这一点很重要,任务队列不会已执行job的历史。

报告任务进程

上面我举得例子太简单了。通常,对于运行时间长的任务,你会需要某种进度信息供应用程序使用,反过来你可以展示给用户看。RQ种的job对象的meta属性支持此功能。就让我重写example()任务以支持进度报告:

app/tasks.py

import time
from rq import get_current_job

def example(seconds):
    job = get_current_job()
    print('Starting task')
    for i in range(seconds):
        job.meta['progress'] = 100.0 * i / seconds
        job.save_meta()
        print(i)
        time.sleep(1)
    job.meta['progress'] = 100
    job.save_meta()
    print('Task completed')

新版的example()使用RQ的get_current_job()函数获取job实例,即提交任务后返回到应用的那个job。job对象的meta属性是一个字典,任务可以向这个字典写入想要与应用通讯的任何自定义的数据。在本例中,我写入了process项,表示任务完成的百分比。每次进程更新了,就会调用job.save_meta()来通知RQ将数据写入Redis,应用程序可以在这里找到它。

在应用端(目前只是是一个Python shell),我可以运行此任务然后向下面这样检查进度:

>>> job = queue.enqueue('app.tasks.example', 23)
>>> job.meta
{}
>>> job.refresh()
>>> job.meta
{'progress': 13.043478260869565}
>>> job.refresh()
>>> job.meta
{'progress': 69.56521739130434}
>>> job.refresh()
>>> job.meta
{'progress': 100}
>>> job.is_finished
True

正如上面你能看到的,在应用端可以阅读meta属性。使用refresh()方法可以更新Redis的内容。


任务的数据库表示

对于上面的例子,它足以运行一个任务并看着它运行了。对于一个web应用,事情会比这更复杂一点,因为一旦其中一个任务作为request的一部分启动,这个请求会结束,并且该任务的的所有上下文都将丢失。因为我想让应用持续追踪每个用户都在运行什么任务,我需要使用一个数据库表来维护某些状态,下面你能看到一个新建的Task模型实现:

app/models.py

# ...
import redis
import rq

class User(UserMixin, db.Model):
    # ...
    tasks = db.relationship('Task', backref='user', lazy='dynamic')

# ...

class Task(db.Model):
    id = db.Column(db.String(36), primary_key=True)
    name = db.Column(db.String(128), index=True)
    description = db.Column(db.String(128))
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    complete = db.Column(db.Boolean, default=False)

    def get_rq_job(self):
        try:
            rq_job = rq.job.Job.fetch(self.id, connection=current_app.redis)
        except (redis.exceptions.RedisError, rq.exceptions.NoSuchJobError):
            return None
        return rq_job

    def get_progress(self):
        job = self.get_rq_job()
        return job.meta.get('progress', 0) if job is not None else 100

此模型与之前的那个之间有趣的不同之处是id主键字段是个字符串,而不是整数。这是因为对于这个模型,我将不会依赖数据库自己的主键生成,而是使用RQ生成的job标识符。

这个模型将用于存储任务的完全限定名(传递给RQ),适合展示给用户的任务描述,请求任务的用户的关系,表示任务是否完成的布尔值。complete字段的用处是区别一结束的任务与正在运行的任务,因为运行中的任务需要特殊的处理才能显示进度更新。

get_rq_job()方法是一个辅助方法,从给定任务id(可以从模型中获取)加载RQ job实例。Job.fetch()从Redis中已存在的数据中加载Job实例。get_process()方法建立在get_rq_job()的顶层,返回任务进程的百分比。这个方法做了几个有意思的假设。如果RQ队列中不存在模型的job id,那就意味着该job已经完成了,数据已过期并从队列中删除,因此在那种情况下,返回的百分比是100。相反的,如果job存在,但meta属性没有关联的学习,则假定job已经安排运行,但还没有实际启动,在那种情况下,返回0。

生成新的迁移并升级数据库,以应用数据库结构的更改:

(venv) $ flask db migrate -m "tasks"
(venv) $ flask db upgrade

新模型也可以添加到shell环境中去了:

microblog.py

from app import create_app, db, cli
from app.models import User, Post, Message, Notification, Task

app = create_app()
cli.register(app)

@app.shell_context_processor
def make_shell_context():
    return {'db': db, 'User': User, 'Post': Post, 'Message': Message,
            'Notification': Notification, 'Task': Task}


将RQ集成到Flask应用中

添加Redis服务的连接URL到配置中:

class Config(object):
    # ...
    REDIS_URL = os.environ.get('REDIS_URL') or 'redis://'

和往常一样,Redis连接URL将从环境变量中获取,如果该变量未定义,则将使用假定运行在同一主机和默认端口上的默认URL。

应用的工厂函数将负责初始化Redis和RQ:

app/__init__.py

# ...
from redis import Redis
import rq

# ...

def create_app(config_class=Config):
    # ...
    app.redis = Redis.from_url(app.config['REDIS_URL'])
    app.task_queue = rq.Queue('microblog-tasks', connection=app.redis)

    # ...

任务将会被提交到app.task_queue队列中。将队列关联到应用很有用,这样在应用中我就能用current_app.task_queue来访问它。为了更方便的提交或检查任务,我在User模型中新建了几个辅助函数:

app/models.py

# ...

class User(UserMixin, db.Model):
    # ...

    def launch_task(self, name, description, *args, **kwargs):
        rq_job = current_app.task_queue.enqueue('app.tasks.' + name, self.id,
                                                *args, **kwargs)
        task = Task(id=rq_job.get_id(), name=name, description=description,
                    user=self)
        db.session.add(task)
        return task

    def get_tasks_in_progress(self):
        return Task.query.filter_by(user=self, complete=False).all()

    def get_task_in_progress(self, name):
        return Task.query.filter_by(name=name, user=self,
                                    complete=False).first()

launch_task()方法解决将任务提交到RQ队列,并将其添加到数据库。name参数即app/tasks.py中定义的函数的名称。当提交到RQ时,此函数将app.tasks添加到name前以创建完全限定名函数名。description参数是展示给用户看的描述。对于用于导出博客帖子的函数,我会将name设为export_posts,将description设为Exporting posts...。其他是参数是传递给任务的位置参数和关键字参数。该函数首先调用队列的enqueue()方法来提交job。返回的job对象包含由RQ分配的任务id,因此我可以使用它在数据库中创建相应的任务对象。

注意launch_task()将新任务对象添加到session,但它没有发起commit。通常最好在更高层级的函数中操作数据库session,因为这使你可以将更低级别函数创建的几个更新组合在一起,一次性提交。这并不是严格的规则,事实上,您将卡带本章后面的子函数发起commit造成的异常。

get_tasks_in_progress()方法返回用户未完成的任务的列表。后面你会看到我用此方法来在渲染给用户看的页面中运行任务。

最后,get_task_in_process()是一个前一个函数的简洁版本,它只返回特定的任务。我阻止用户同时启动两个或多个相同类型的任务,因此在启动任务之前,我可以用此方法来确定之前的任务当前是否在运行。


从RQ任务发送邮件

这看起来偏离了主题,但上面我说了,当后台的任务完成时,会向用户发送一份带有含有所有帖子的JSON文件的邮件。我在第11章中构建的email功能需要被拓展为两种方式。首先,我需要添加对文件附件的支持,以便我可以附加JSON文件。第二,send_email()函数总是用后台线程异步发送邮件。当我要从后台任务发送邮件时,这已经是异步的了基于线程的第二级后台任务没有什么意义,因此我需要同时支持同步与异步邮件发送。

幸运的是,Flask-Mail支持附件,所以我要做的就是拓展send_email()函数,新增一个函数接受附件,然后再Message对象中配置它们。为了可以在前台发送邮件,我只需要添加一个布尔类型的sync参数:

app/email.py

# ...

def send_email(subject, sender, recipients, text_body, html_body,
               attachments=None, sync=False):
    msg = Message(subject, sender=sender, recipients=recipients)
    msg.body = text_body
    msg.html = html_body
    if attachments:
        for attachment in attachments:
            msg.attach(*attachment)
    if sync:
        mail.send(msg)
    else:
        Thread(target=send_async_email,
            args=(current_app._get_current_object(), msg)).start()

Message类的attach()方法接受定义附件的三个参数:文件名,媒体类型,实际的文件数据。文件名只是给收件看的名称,不需要是真正的文件名,媒体类型定义了附件是什么类型,帮助邮件渲染器正确地渲染附件。比如说,如果你的媒体类型定义为image/png,邮件渲染器就知道此附件是个图片。而对于博客帖子的数据文件,我会用JSON格式,所以会使用application/json媒体类型。第三个即最后一个参数是字符串或者带有附件内容的字节序列。

send_email()的attachments参数是一个元祖组成的列表,每个元祖都有三个元素,分别对应attach()的三个参数。因此对于此列表中的每个元素,我需要将元祖作为参数传递给attach()。在Python中,如果你想将一个带有参数的列表或者元祖传递给函数,你可以用func(*args)将列表解包为实际的参数,而不必使用更繁琐的语法如func(args[0], args[1], args[2])。比如说,如果你有个这样的参数args = [1, 'foo'],调用会发送两个参数,就和你调用func(1, 'foo')一样。如果不加上*,该调用只会有一个列表形式的参数。

如果要同步发送邮件,我只要将sync参数的值设为True,就会调用mail.send(msg)。


辅助任务

虽然我在上面使用的example()任务是一个简单的独立功能,但导出帖子的功能会借助应用中一些其他的功能,比如访问数据库和发送邮件功能。由于这会在单独的进程中运行,我需要初始化Flask-SQLAlchemy和Flask-Mail,也就说说需要一个Flask应用实例,获取其配置。因此,我要在app/tasks.py模块的顶层添加一个Flask应用实例以及应用上下文。

app/tasks.py

from app import create_app

app = create_app()
app.app_context().push()

该应用程序是在此模块中创建的,因为RQ worker导入的唯一模块。当你使用flask命令,根目录中的microblog.py模块会创建应用,但RQ worker并不知情,所以如有任务函数有需要,则需要创建其自己的应用实例。你已经在好几个地方看到过app.app_context()方法了,push上下文使该应用成为“当前”应用实例,这也使拓展(如Flask-SQLAlchemy)可以使用current_app.config来获取其配置。如果没有上下文,那么current_app表达式会返回一个错误。

然后,我开始思考如何在函数在运行使报告其进度。除了将进程信息传递给job.meta字典,我还想将消息推送到客户端,这样无需用户刷新页面,完成度百分比就可以动态地更新。为此我将使用第21章中构建的通知机制。更新功能的工作方式会和未读消息徽章的工作方式非常相似。当服务端渲染模板时,它将包含从job.meta获取的“静态”进度信息,但一旦页面呈现在了客户端浏览器上,通知将会动态地更新其完成度。由于这些通知,更新正在运行任务进度的工作将比我之前在示例中的要复杂,因此,我要创建一个专门用于更新进度的包装函数:

app/tasks.py

from rq import get_current_job
from app import db
from app.models import Task

# ...

def _set_task_progress(progress):
    job = get_current_job()
    if job:
        job.meta['progress'] = progress
        job.save_meta()
        task = Task.query.get(job.get_id())
        task.user.add_notification('task_progress', {'task_id': job.get_id(),
                                                     'progress': progress})
        if progress >= 100:
            task.complete = True
        db.session.commit()

导出任务可以调用_set_task_progress()来记录进度百分比。此函数首先向job.meta()字典写入百分比,并保存到Redis,然后从数据库加载对应的任务对象,使用task.user的add_notification方法推送一条通知给请求该任务的用户。该通知将被命名为task_progress,与之关联的数据是含有两个数据项的字典,分别是任务标识符和进度数。稍后我会添加处理新通知类型的JavaScript代码。

该函数还会检查该进度是否完成,如果完成了,则更新数据库中该任务对象的complete属性。数据库的commit调用可以确保该任务和由add_notification()添加的通知对象都立即保存到了数据库中。由于commit调用也会写入这些数据,我需要非常小心,如何设计父任务才能不对数据库进行更改。


实现导出任务

现在,我可以开始编写导出功能了。此函数的高层级结构如下所示:

app/tasks.py

def export_posts(user_id):
    try:
        # read user posts from database
        # send email with data to user
    except:
        # handle unexpected errors

为什么要将整个任务包含在try/except语句块中呢?请求处理程序中的应用代码受意外错误的保护,因为Flask本身会捕获异常,并观察所有的错误处理,以及日志记录配置(这他妈都是啥啊,36个单词的长句中间没有一个逗号,绝望)。然而这个函数运行在由RQ控制的独立进程中,而不是Flask,因此如果出现了任何意外错误,该任务会中止,RQ会向控制台显示错误,然后等待新的job。因此基本上,除非你盯着RQ worker的输出看,或者将其记录到文件中,你是没有办法发现出现了错误的。

让我看看上面用注释表示的三段内容,其中最简单的部分是最后的错误处理:

app/tasks.py

import sys
# ...

def export_posts(user_id):
    try:
        # ...
    except:
        _set_task_progress(100)
        app.logger.error('Unhandled exception', exc_info=sys.exc_info())

每当发生意外错误时,我都将进度设置为100%,标记为完成,然后用Flask应用的记录器的logger对象记录错误,其错误栈以及sys.exc_info()的信息。在这里使用Flask应用的记录器来记录错误是好处是,它会遵守你在Flask应用中实现的记录器机制。比如说,在第7章中,我配置了将错误发送到管理员的邮件地址。使用app.logger我也获取了这些错误的行为。

接下来,我会编写实际的导出功能,它会发起数据库查询,然后遍历结果,将其添加到一个字典中:

app/tasks.py

import time
from app.models import User, Post

# ...

def export_posts(user_id):
    try:
        user = User.query.get(user_id)
        _set_task_progress(0)
        data = []
        i = 0
        total_posts = user.posts.count()
        for post in user.posts.order_by(Post.timestamp.asc()):
            data.append({'body': post.body,
                         'timestamp': post.timestamp.isoformat() + 'Z'})
            time.sleep(5)
            i += 1
            _set_task_progress(100 * i // total_posts)

        # send email with data to user
    except:
        # ...

该函数将创建一个含有两个元素的字典,包含帖子的正文以及该帖子编写的时间。时间是ISO 8601标准的。我使用的Python datetime对象没有存储时区,因此当我将时区转换为ISO格式后,我添加了“Z”,表示UTC。

由于需要跟踪进度,所以代码得有点复杂了。i是计数器,total_posts代表帖子总数,在进入total_posts循环之前,我又发起了一个数据库查询。使用i和total_posts,每个循环迭代都可以更新任务进度(0到100之间)。

你可能注意到了每次循环迭代都会调用一次time.sleep(5)。我添加该睡眠的主要原因是使导出的时间更长,即使要处理的博客帖子数不多,也能看到进度条的动态。

下面你能看到此函数的最后一部分,该函数向用户发送邮件,邮件中包含了data中的所有信息:

app/tasks.py

import json
from flask import render_template
from app.email import send_email

# ...

def export_posts(user_id):
    try:
        # ...

        send_email('[Microblog] Your blog posts',
                sender=app.config['ADMINS'][0], recipients=[user.email],
                text_body=render_template('email/export_posts.txt', user=user),
                html_body=render_template('email/export_posts.html', user=user),
                attachments=[('posts.json', 'application/json',
                              json.dumps({'posts': data}, indent=4))],
                sync=True)
    except:
        # ...

这就是调用send_email()函数而已。附件是一个元祖,包含三个元素,之后会将其传递给Flask-Mail的Message对象的attach()方法。远足中的第三个元素是附件内容,这是由Python的json.dumps()函数生成的。

这里引用了两个新模板,它以纯文本和HTML的形式提供邮件正文的内容。下面是文本模板:

app/templates/email/export_posts.txt

Dear {{ user.username }},

Please find attached the archive of your posts that you requested.

Sincerely,

The Microblog Team

这是HTML版本的邮件:

app/templates/email/export_posts.html

<p>Dear {{ user.username }},</p>
<p>Please find attached the archive of your posts that you requested.</p>
<p>Sincerely,</p>
<p>The Microblog Team</p>


应用程序的导出功能

用于支持后台导出任务的核心部件都到位了。剩下的就算将此功能与应用程序对接,这样用户可以请求将它们的帖子邮件给他们。

下面你能看到新建的export_posts视图函数:

app/main/routes.py

@bp.route('/export_posts')
@login_required
def export_posts():
    if current_user.get_task_in_progress('export_posts'):
        flash(_('An export task is currently in progress'))
    else:
        current_user.launch_task('export_posts', _('Exporting posts...'))
        db.session.commit()
    return redirect(url_for('main.user', username=current_user.username))

该函数首先检查用户是否有未完成的导出任务,如果有,则闪现一条消息。同一用户在同一时间发起两次导出任务没有意义,所以这是可以避免的。我可以用前面的get_task_in_progress()方法查看导出状况。

如果用户尚未运行导出任务,则调用launch_task()来启动一个任务。第一个参数是传递给RQ worker的函数名,前缀为app.tasks.,第二个参数是显示给用户看的任务描述。两个值都会写到数据库的Task对象中。该函数以重定向到用户的资料页面结束。

现在,我需要添加一个链接到此路由的链接,用户可以向此链接发起请求来导出。我觉得最合适的是用户的资料页面,只有当用户查看自己的页面时该链接才会显示,就在“Edit your profile”链接的下面:

app/templates/user.html

                ...
                <p>
                    <a href="{{ url_for('main.edit_profile') }}">
                        {{ _('Edit your profile') }}
                    </a>
                </p>
                {% if not current_user.get_task_in_progress('export_posts') %}
                <p>
                    <a href="{{ url_for('main.export_posts') }}">
                        {{ _('Export your posts') }}
                    </a>
                </p>
                ...
                {% endif %}

此链接嵌在一个条件语句中,因为我不想再用户已经有在进行的导出任务时显示。

此时后台工作应该可以用了,但还不会给用户什么反馈。如果你想搞清楚,你可以启动应用程序和RQ worker:

  • 确保Redis在运行了
  • 在第一个终端窗口,用rq worker microblog-tasks命令启动一个或多个RQ worker
  • 在第二个终端窗口,用flask run命令启动Flask应用程序(记得先设置FLASK_APP)

通知进程

完成此功能是最后一步,是在后台任务运行时通知用户,包括完成度百分比。通过查看Bootstrap的组件选项,我决定导航栏下面添加alert(警报栏),显示此进度。Alert是带有颜色的横条,为用户显示信息。我用了蓝色的alert渲染闪现消息。现在我会使用绿色的alert显示进程状态。下面你能看到它的样子:


app/templates/base.html

...
{% block content %}
    <div class="container">
        {% if current_user.is_authenticated %}
        {% with tasks = current_user.get_tasks_in_progress() %}
        {% if tasks %}
            {% for task in tasks %}
            <div class="alert alert-success" role="alert">
                {{ task.description }}
                <span id="{{ task.id }}-progress">{{ task.get_progress() }}</span>%
            </div>
            {% endfor %}
        {% endif %}
        {% endwith %}
        {% endif %}
        ...
{% endblock %}
...

渲染任务alert和渲染闪现消息的方式几乎相同。外部的条件判断用户是否登录,如果没有,则不显示alert功能相关的标记。如果用户已登录,我用get_tasks_in_progress()方法获取当前正在进行的任务列表。在当前版本的应用中,由于我不允许同时存在多于一个的活动导入,所以我只会得到最多一个的结果,但以后可能会支持其他类型的任务共存,所以这种通用方式的写法到时候可以节省我的时间。

每个任务我都会将alert元素写到页面中。alert的颜色由第二个CSS样式控制,也就是alert-success,而闪现消息的颜色是alert-info。Bootstrap文档中有alert的HTML结构的详细信息。alert的文本内容存储在Task模型的description字段中,后面加上完成度百分比。

百分比包含在<span>元素中,有一个id属性。这么做的原因是,当收到了通知后,我会用JavaScript刷新百分比。给定任务的id由任务id和-process后缀组成。当通知收到时,此alert的id值会包含任务id,使得我可以使用#<task.id>-progress选择器轻松地找到正确的<span>元素并更新其内容。

此时如果你试着使用此应用,每次你导航到新的页面,都会看到“静态”的进度更新。你会注意到在你开始一个导出任务后,你可以随意切换到应用的不同页面,而正在运行的任务状态总能被更新。

为了准备对<span>元素进行动态更新,我将简写一个JavaScript辅助函数:

app/templates/base.html

...
{% block scripts %}
    ...
    <script>
        ...
        function set_task_progress(task_id, progress) {
            $('#' + task_id + '-progress').text(progress);
        }
    </script>
    ...
{% endblock %}

此函数接收一个任务id和进程值为参数,使用jQuery找到包含该任务的<span>元素并将给定进程值写入其内容。在此处没有必要验证页面中是否存在此元素,因为如果不存在,jQuery什么都不会做。

通知已经到达了浏览器,因为app/tasks.py中的_set_task_progress()函数每次调用add_notification()都会更新其进程。如果你感到困惑,在我什么都没做的情况下,这些通知是怎么到达浏览器的呢?其实这是因为在第21章中,我明智地以通用的方式实现通知功能。任何通知add_notification()方法添加的通知,都会在浏览器定时地向服务器请求通知更新时显示。

但处理这些通知的JavaScript代码只识别哪些具有unread_message_count名称的通知,并忽略剩余的通知。现在我要做的是拓展该函数,使其通过调用ser_task_progress()函数,也能处理task_progress通知。下面是升级版本的通知处理JavaScript代码。

app/templates/base.html

                        for (var i = 0; i < notifications.length; i++) {
                            switch (notifications[i].name) {
                                case 'unread_message_count':
                                    set_message_count(notifications[i].data);
                                    break;
                                case 'task_progress':
                                    set_task_progress(
                                        notifications[i].data.task_id,
                                        notifications[i].data.progress);
                                    break;
                            }
                            since = notifications[i].timestamp;
                        }

现在,我需要处理两种不同的通知,我决定用switch语句替换检查unread_message_count通知名称的语句,其中一个部分包含了我现在要支持的两种通知。如果你对C家族的语言不熟悉,你之前可能没见过switch语句。这些语言提供了一种方便的语法,代替了长长的if/elseif语句。这很棒,因为将来我将支持更多种的通知,我只要把它们放在额外的case语句块中就行。

如果你还记得,和RQ任务关联的task_progress通知数据是带有两个元素的字典,分别是task_id和progress,这是我用来调用set_task_progress()的两个参数。

如果你现在运行此应用,如果通知发送到了客户端,绿色alert框的进度指示器每10秒更新一次。

由于本章中我新增了新的翻译字符串,所以需要更新翻译文件:

(venv) $ flask translate update

翻译完成后,你得编译文件:

(venv) $ flask translate compile


部署注意事项

最后,我想讨论如何部署这些更改。为了支持后台任务,我添加了两个新组件到栈,Redis服务和一个或多个RQ worker。显然,这些东西需要被添加到部署策略中,我简要介绍一下之前章节中介绍的不同部署选项,以及本章的更改对其产生了哪些影响。

在Linux服务器上部署

如果你在Linux服务器上运行应用程序,添加Redis和在你的操作系统中安装此包一样简单。如果是Ubuntu Linux系统,你需要运行 sudo apt-get install redis-server命令。

你可以参考第17章中的“Setting Up Gunicorn and Supervisor”小节,另外创建一个Supervisor配置,当然你需要把gunicorn修改为rq worker microblog-tasks,以运行RQ worker进程。如果你想要运行多个worker(在生产环境中大概是需要的),你可以使用Supervisor的numprocs指令只是你想要同时运行多少个实例。

在Heroku上部署

懒得翻了

在Docker上部署

懒得翻了


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值