Python 芹菜节拍:一次只能执行一个任务实例
我有芹菜拍和芹菜(四名工人)做一些批量加工步骤。其中一项任务大致是这样的:“对于每个没有创建Y的X,创建一个Y。” 任务以半快速速率(10秒)定期运行。任务完成得很快。还有其他任务正在进行 我曾多次遇到过这样的问题,beat任务显然会积压,因此同一个任务(来自不同的beat时间)会同时执行,从而导致错误的重复工作。任务的执行似乎也是无序的Python 芹菜节拍:一次只能执行一个任务实例,python,concurrency,rabbitmq,celery,celerybeat,Python,Concurrency,Rabbitmq,Celery,Celerybeat,我有芹菜拍和芹菜(四名工人)做一些批量加工步骤。其中一项任务大致是这样的:“对于每个没有创建Y的X,创建一个Y。” 任务以半快速速率(10秒)定期运行。任务完成得很快。还有其他任务正在进行 我曾多次遇到过这样的问题,beat任务显然会积压,因此同一个任务(来自不同的beat时间)会同时执行,从而导致错误的重复工作。任务的执行似乎也是无序的 是否有可能限制芹菜节拍,以确保一次只有一个任务的未完成实例?在任务上设置类似于rate\u limit=5的内容是否是执行此操作的“正确”方法 是否可以确保b
rate\u limit=5
的内容是否是执行此操作的“正确”方法
foocorp.tasks.add_y__至_xs已发送。id->1[00:00.000]
接收到的任务:foocorp.tasks.将_y_添加到_xs[#1][00:00.001]
foocorp.tasks.add_y_至发送的_xs。id->2[00:10.009]
foocorp.tasks.add_y__至_xs已发送。id->3[00:20.024]
收到的任务:foocorp.tasks.将_y_添加到_xs[#2][00:26.747]
任务池:应用#2[00:26.748]
收到的任务:foocorp.tasks.将_y_添加到_xs[#3][00:26.752]
已接受任务:foocorp.tasks.将_y_添加到_xs[#2]pid:26528[00:26.769]
Task foocorp.tasks.addy_to_xs[#2]在0.0197986490093s中成功:无[00:26.775]
任务池:应用#1[00:26.806]
任务池:应用#3[00:26.836]
已接受任务:foocorp.tasks.将_y_添加到_xs[#1]pid:26526[01:30.020]
已接受任务:foocorp.tasks.将_y_添加到_xs[#3]pid:26529[01:30.053]
foocorp.tasks.add_y_to_xs[#1]:为X id#9725添加y[01:30.055]
foocorp.tasks.add_y_to_xs[#3]:为X id#9725添加y[01:30.070]
Task foocorp.tasks.add_y_to_xs[#1]在0.0594762689434s中成功:无[01:30.074]
Task foocorp.tasks.add_y_to_xs[#3]在0.0352867960464s中成功:无[01:30.087]
来自sqlalchemy导入函数
从sqlalchemy.exc导入DBAPIError
从contextlib导入contextmanager
def\u psql\u咨询\u锁定\u阻塞(连接、锁定id、共享、超时):
锁定fn=(func.pg\u咨询\u xact\u锁定\u共享
如果与其他人共享
功能pg_咨询_xact_lock)
如果超时:
conn.execute(text('SET statement_timeout TO:timeout'),
超时=超时)
尝试:
连接执行(选择([lock\u fn(lock\u id)])
除DBAPIError外:
返回错误
返回真值
def\u psql\u咨询\u锁定\u非阻塞(连接、锁定id、共享):
lock_fn=(func.pg_try_advision_xact_lock_shared)
如果与其他人共享
函数pg_try_咨询_xact_lock)
返回conn.execute(选择([lock\u fn(lock\u id)])).scalar()
类DatabaseLockFailed(异常):
通过
@上下文管理器
def db_锁(引擎、名称、共享=假、块=真、超时=无):
"""
上下文管理器,它使用
指定的名称。
"""
lock_id=hash(名称)
使用engine.begin()作为conn,conn.begin():
如果是块:
锁定=_psql_advisory_lock_blocking(连接、锁定id、共享、,
超时)
其他:
锁定=_psql_advisory_lock_Non Blocking(连接,锁定id,共享)
如果未锁定:
提升DatabaseLockFailed()
产量
芹菜任务装饰器(仅用于定期任务):
做到这一点的唯一方法是: 请阅读本节下的参考资料 与cron一样,如果第一个任务没有完成,则任务可能会重叠 在下一个星期之前完成。如果这是一个问题,你应该使用 锁定策略,以确保一次只能运行一个实例(请参阅 例如,确保一次只执行一个任务)
我试着写了一篇装饰文章,用的和埃里多在评论中提到的相似 它不是很漂亮,但似乎工作正常。这是Python2.7下的SQLAlchemy 0.9.7
from functools import wraps
from sqlalchemy import select, func
from my_db_module import Session # SQLAlchemy ORM scoped_session
def pg_locked(key):
def decorator(f):
@wraps(f)
def wrapped(*args, **kw):
session = db.Session()
try:
acquired, = session.execute(select([func.pg_try_advisory_lock(key)])).fetchone()
if acquired:
return f(*args, **kw)
finally:
if acquired:
session.execute(select([func.pg_advisory_unlock(key)]))
return wrapped
return decorator
@app.task
@pg_locked(0xdeadbeef)
def singleton_task():
# only 1x this task can run at a time
pass
(欢迎对改进方法提出任何意见!)我解决了扩展到的问题
两者都是为你的问题服务的。它使用Redis锁定正在运行的任务<代码>芹菜一号还将跟踪正在锁定的任务
下面是芹菜节拍的一个非常简单的用法示例。在下面的代码中,slow_task
计划每1秒执行一次,但其完成时间为5秒。即使任务已经在运行,普通芹菜也会每秒安排任务<代码>芹菜一号可以防止这种情况
celery = Celery('test')
celery.conf.ONE_REDIS_URL = REDIS_URL
celery.conf.ONE_DEFAULT_TIMEOUT = 60 * 60
celery.conf.BROKER_URL = REDIS_URL
celery.conf.CELERY_RESULT_BACKEND = REDIS_URL
from datetime import timedelta
celery.conf.CELERYBEAT_SCHEDULE = {
'add-every-30-seconds': {
'task': 'tasks.slow_task',
'schedule': timedelta(seconds=1),
'args': (1,)
},
}
celery.conf.CELERY_TIMEZONE = 'UTC'
@celery.task(base=QueueOne, one_options={'fail': False})
def slow_task(a):
print("Running")
sleep(5)
return "Done " + str(a)
需要一个分布式锁定系统,因为这些芹菜节拍实例本质上是不同的进程,可能跨越不同的主机 ZooKeeper和etcd等中心坐标系适用于分布式锁定系统的实现 我建议使用etcd,它是轻量级和快速的。etcd上的锁有几种实现方式,例如:
谢谢。我最终创建了一个任务装饰器,用PostgreSQL adviso保护任务
from functools import wraps
from sqlalchemy import select, func
from my_db_module import Session # SQLAlchemy ORM scoped_session
def pg_locked(key):
def decorator(f):
@wraps(f)
def wrapped(*args, **kw):
session = db.Session()
try:
acquired, = session.execute(select([func.pg_try_advisory_lock(key)])).fetchone()
if acquired:
return f(*args, **kw)
finally:
if acquired:
session.execute(select([func.pg_advisory_unlock(key)]))
return wrapped
return decorator
@app.task
@pg_locked(0xdeadbeef)
def singleton_task():
# only 1x this task can run at a time
pass
celery = Celery('test')
celery.conf.ONE_REDIS_URL = REDIS_URL
celery.conf.ONE_DEFAULT_TIMEOUT = 60 * 60
celery.conf.BROKER_URL = REDIS_URL
celery.conf.CELERY_RESULT_BACKEND = REDIS_URL
from datetime import timedelta
celery.conf.CELERYBEAT_SCHEDULE = {
'add-every-30-seconds': {
'task': 'tasks.slow_task',
'schedule': timedelta(seconds=1),
'args': (1,)
},
}
celery.conf.CELERY_TIMEZONE = 'UTC'
@celery.task(base=QueueOne, one_options={'fail': False})
def slow_task(a):
print("Running")
sleep(5)
return "Done " + str(a)
from functools import wraps
from celery import shared_task
def skip_if_running(f):
task_name = f'{f.__module__}.{f.__name__}'
@wraps(f)
def wrapped(self, *args, **kwargs):
workers = self.app.control.inspect().active()
for worker, tasks in workers.items():
for task in tasks:
if (task_name == task['name'] and
tuple(args) == tuple(task['args']) and
kwargs == task['kwargs'] and
self.request.id != task['id']):
print(f'task {task_name} ({args}, {kwargs}) is running on {worker}, skipping')
return None
return f(self, *args, **kwargs)
return wrapped
@shared_task(bind=True)
@skip_if_running
def test_single_task(self):
pass
test_single_task.delay()