显示更新状态和结果
事实上对于很多应用来说,有必要监控它的后台任务并且从中获取结果。
我们用一个例子,一个虚构的耗时任务来扩展上面的应用,用户可以通过点击一个按钮启动一个或更多这些长时间运行任务。运行在你的浏览器上的网页通过ajax轮训你的服务获取这些任务的状态更新。对于每个任务,网页会展示图形状态栏,一个完成百分比,一个状态消息,当任务完成时,结果值会被展示。
有状态更新的后台任务
下面是这个例子中使用的后台任务:
@celery.task(bind=True)
def long_task(self):
"""Background task that runs a long function with progress reports."""
verb = ['Starting up', 'Booting', 'Repairing', 'Loading', 'Checking']
adjective = ['master', 'radiant', 'silent', 'harmonic', 'fast']
noun = ['solar array', 'particle reshaper', 'cosmic ray', 'orbiter', 'bit']
message = ''
total = random.randint(10, 50)
for i in range(total):
if not message or random.random() < 0.25:
message = '{0} {1} {2}...'.format(random.choice(verb),
random.choice(adjective),
random.choice(noun))
self.update_state(state='PROGRESS',
meta={'current': i, 'total': total,
'status': message})
time.sleep(1)
return {'current': 100, 'total': 100, 'status': 'Task completed!',
'result': 42}
这个路由生成一个JSON响应,它包含任务状态和我在update_state()调用中作为元参数设置的所有值,客户端可以用它构建一个进程条。不幸的是这个函数需要检查一些边界条件,所以最后有点长。为了检查任务数据我再创了一个任务对象,一个AsyncResult类的实例,使用URL中给定的任务id。
首先 if 块是当任务还没有开始(悬起状态)。这种情况下没有状态信息,所以我制造一些数据。跟着 elif 块从后台任务中返回状态信息。这里由任务提供的信息作为 task.info 是可访问的。如果数据包含了一个结果键,就意味着这是最终结果并且任务结束,所以我把结果也添加到 response。else 块覆盖了一个错误的可能性,Celery 会通过设置任务状态为“FAILURE”来报告,这种情况下 task.info 会包含被引发的异常,为了处理错误我设置了异常文本作为一个状态消息。
以上都是发生在服务器端,剩下的需要客户端实现,在本例中就是一个包含JavaScript脚本的网页
客户端 Javascript
解析本例的JavaScript部分不是本文真正的关注点,但如果你感兴趣的话,下面有一些信息。
图形进度条部分我使用了nanobar.js,通过CDN包含进来。同时也包含了jquery,简化了ajax调用:
<script src="//cdnjs.cloudflare.com/ajax/libs/nanobar/0.2.1/nanobar.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
启动后台工作的按钮被关联到下面的Javascript上:
function start_long_task() {
// add task status elements
div = $('<div class="progress"><div></div><div>0%</div><div>...</div><div> </div></div><hr>');
$('#progress').append(div);
// create a progress bar
var nanobar = new Nanobar({
bg: '#44f',
target: div[0].childNodes[0]
});
// send ajax POST request to start background job
$.ajax({
type: 'POST',
url: '/longtask',
success: function(data, status, request) {
status_url = request.getResponseHeader('Location');
update_progress(status_url, nanobar, div[0]);
},
error: function() {
alert('Unexpected error');
}
});
}
这个函数起始于添加一些用来展示新的后台任务进度条和状态的HTML元素,这是动态的因为用户可以添加任何数量任务,每个任务需要获取它自己的HTML元素集合。
为了帮助大家更好的理解,下面提供了给任务添加的元素的结构,注释指出了每个div的用处:
<div class="progress">
<div></div> <-- 进度条
<div>0%</div> <-- 百分比
<div>...</div> <-- 状态消息
<div> </div> <-- 结果
</div>
<hr>
然后 start_long_task()函数按照nanobar的文档实例化这个进度条 ,最后发送ajax POST请求到 /longtask,在服务端初始化Celery 后台任务。
当 POST ajax 调用返回, 回调函数包含了Location header的值,这就像你在前面部分看到的一样是为了客户端状态更新。然后使用这个状态URL, 进度条对象,为任务创建的根div的子树,调用另一个函数update_progress() 。下面你可以看到这个update_progress() 函数,它发送状态请求然后用它返回的信息更新UI元素:
function update_progress(status_url, nanobar, status_div) {
// send GET request to status URL
$.getJSON(status_url, function(data) {
// update UI
percent = parseInt(data['current'] * 100 / data['total']);
nanobar.go(percent);
$(status_div.childNodes[1]).text(percent + '%');
$(status_div.childNodes[2]).text(data['status']);
if (data['state'] != 'PENDING' && data['state'] != 'PROGRESS') {
if ('result' in data) {
// show result
$(status_div.childNodes[3]).text('Result: ' + data['result']);
}
else {
// something unexpected happened
$(status_div.childNodes[3]).text('Result: ' + data['state']);
}
}
else {
// rerun in 2 seconds
setTimeout(function() {
update_progress(status_url, nanobar, status_div);
}, 2000);
}
});
}
这个函数发送GET请求到状态URL,当一个响应被接收后它为任务更新不同的HTML元素。如果后台任务结束并且结果可用那么它就被添加到页面上。如果没有结果就意味着任务由于错误而结束,所以任务的状态,将会是FAILURE,就像结果所展示的。
当服务端正在执行任务我需要继续轮训任务状态并且更新UI。为实现这个我设置了一个定时器在两秒内来再次调用这个函数。这持续到Celery任务结束。
一个worker运行尽可能多的并发任务,按照默认CPU数。所以当你实验这个例子时确保开启大量任务来查看Celery如何保持任务为挂起状态,直到有worker能够处理它。
运行这个例子
一切准备就绪你就可以运行这个例子了。可以从Github仓库克隆代码,创建一个虚拟环境,激活并安装依赖:
$ git clone https://github.com/miguelgrinberg/flask-celery-example.git
$ cd flask-celery-example
$ virtualenv venv
$ source venv/bin/activate
(venv) $ pip install -r requirements.txt
这个仓库里的requirements.txt 文件包含Flask, Flask-Mail, Celery 和 Redis 客户端, 还有他们的依赖.
Now you need to run the three processes required by this application, 最简单的方式是打开三个终端窗口。第一个终端运行Redis。你可以根据下载说明给你的操作系统安装Redis,但如果用的是linux或者OS X机器,我已经包含了一个小脚本,可以下载,编译和作为私有服务运行Redis:
$ ./run-redis.sh
对于以上脚本你需要安装了gcc。注意以上的命令是阻塞的,Redis会在前台启动。
在第二个终端运行一个Celery工人。这个使用celery的命令,这个On the second terminal run a Celery worker. This is done with the celery command, which is installed in your virtual environment. Since this is the process that will be sending out emails, the MAIL_USERNAME and MAIL_PASSWORD environment variables must be set to a valid Gmail account before starting the worker:
$ export MAIL_USERNAME=<your-gmail-username>
$ export MAIL_PASSWORD=<your-gmail-password>
$ source venv/bin/activate
(venv) $ celery worker -A app.celery --loglevel=info
The -A option gives Celery the application module and the Celery instance, and –loglevel=info makes the logging more verbose, which can sometimes be useful in diagnosing problems.
Finally, on the third terminal window run the Flask application, also from the virtual environment:
$ source venv/bin/activate
(venv) $ python app.py
Now you can navigate to http://localhost:5000/ in your web browser and try the examples!