我有一个长期运行的芹菜任务,它迭代一系列项目并执行一些操作。
任务应以某种方式报告当前处理的项目,以便最终用户了解任务的进度。
目前我的django应用程序和芹菜一起放在一台服务器上,所以我可以使用Django的模型来报告状态,但我计划添加更多远离Django的工作人员,所以他们无法到达DB。
现在我看到的解决方案很少:
PUT http://django.com/api/task/123/items_processed
Item processed
这样的事件,django更新计数器items proceeded
个计数,因此当任务完成时,它会发出increase_messages_proceeded_count.delay(task_id)
。 我提到的那些解决方案或隐藏的问题是什么?
答案 0 :(得分:7)
实现目标可能有很多种方法,但我会这样做。
在长期运行的芹菜任务中,使用django's caching framework设置进度:
from django.core.cache import cache
@app.task()
def long_running_task(self, *args, **kwargs):
key = "my_task: %s" % self.result.id
...
# do whatever you need to do and set the progress
# using cache:
cache.set(key, progress, timeout="whatever works for you")
...
然后,您所要做的就是使用该密钥生成重复的AJAX GET请求,并从缓存中检索进度。这些方面的东西:
def task_progress_view(request, *args, **kwargs):
key = request.GET.get('task_key')
progress = cache.get(key)
return HttpResponse(content=json.dumps({'progress': progress}),
content_type="application/json; charset=utf-8")
这里有一个警告,如果您将服务器作为多个进程运行,请确保使用memcached之类的东西,因为django的本机缓存在进程之间会不一致。此外,我可能不会使用芹菜的task_id
作为关键,但它足以用于演示目的。
答案 1 :(得分:2)
最简单的:
您的任务和django app已共享访问一个或两个数据存储 - 代理和结果后端(如果您使用的是与代理不同的数据存储)
您可以简单地将一些数据放入这些数据存储中的一个或另一个中,以指示任务当前正在处理哪个项目。
e.g。如果使用redis只需要一个关键的“任务 - 当前正在处理”并存储与正在处理的项目当前相关的数据。
答案 2 :(得分:1)
您可以使用类似Swampdragon的内容从Celery实例中联系到用户(您必须能够从客户端访问它,注意不要与CORS发生冲突)。它可以锁在柜台上,而不是模型本身。
答案 3 :(得分:1)
查看ArgumentNullException
- Celery分布式任务队列的实时监控和Web管理员:
你需要它进行演示,对吧? flower
适用于websockets。
例如 - 实时接收任务完成事件(取自官方文档):
Flower
您可能需要处理任务(' ws:// localhost:5555 / api / tasks /')。
我希望这会有所帮助。
答案 4 :(得分:0)
lehins' solution 看起来不错,如果您不介意您的客户反复轮询您的后端。这可能没问题,但随着客户数量的增加,它会变得昂贵。
Artur Barseghyan's solution 适合如果你只需要 Celery 内部机制生成的任务生命周期事件。
或者,您可以使用 Django Channels 和 WebSockets 实时向客户端推送更新。设置非常简单。
channels
添加到您的 INSTALLED_APPS
并设置通道层。例如,使用 Redis 后端:CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("redis", 6379)]
}
}
}
import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebSocketConsumer
class TaskConsumer(WebsocketConsumer):
def connect(self):
self.task_id = self.scope['url_route']['kwargs']['task_id'] # your task's identifier
async_to_sync(self.channel_layer.group_add)(f"tasks-{self.task_id}", self.channel_name)
self.accept()
def disconnect(self, code):
async_to_sync(self.channel_layer.group_discard)(f"tasks-{self.task_id}", self.channel_name)
def item_processed(self, event):
item = event['item']
self.send(text_data=json.dumps(item))
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
...
async_to_sync(get_channel_layer.group_send)(f"tasks-{task.task_id}", {
'type': 'item_processed',
'item': item,
})
您还可以编写异步使用者和/或异步调用 group_send
。无论哪种情况,您都不再需要 async_to_sync
包装器。
websocket_urlpatterns
添加到您的 urls.py
:websocket_urlpatterns = [
path(r'ws/tasks/<task_id>/', TaskConsumer.as_asgi()),
]
let task_id = 123;
let protocol = location.protocol === 'https:' ? 'wss://' : 'ws://';
let socket = new WebSocket(`${protocol}${window.location.host}/ws/tasks/${task_id}/`);
socket.onmessage = function(event) {
let data = JSON.parse(event.data);
let item = data.item;
// do something with the item (e.g., push it into your state container)
}