Python 芹菜节拍:一次只能执行一个任务实例

Python 芹菜节拍:一次只能执行一个任务实例,python,concurrency,rabbitmq,celery,celerybeat,Python,Concurrency,Rabbitmq,Celery,Celerybeat,我有芹菜拍和芹菜(四名工人)做一些批量加工步骤。其中一项任务大致是这样的:“对于每个没有创建Y的X,创建一个Y。” 任务以半快速速率(10秒)定期运行。任务完成得很快。还有其他任务正在进行 我曾多次遇到过这样的问题,beat任务显然会积压,因此同一个任务(来自不同的beat时间)会同时执行,从而导致错误的重复工作。任务的执行似乎也是无序的 是否有可能限制芹菜节拍,以确保一次只有一个任务的未完成实例?在任务上设置类似于rate\u limit=5的内容是否是执行此操作的“正确”方法 是否可以确保b

我有芹菜拍和芹菜(四名工人)做一些批量加工步骤。其中一项任务大致是这样的:“对于每个没有创建Y的X,创建一个Y。”

任务以半快速速率(10秒)定期运行。任务完成得很快。还有其他任务正在进行

我曾多次遇到过这样的问题,beat任务显然会积压,因此同一个任务(来自不同的beat时间)会同时执行,从而导致错误的重复工作。任务的执行似乎也是无序的

  • 是否有可能限制芹菜节拍,以确保一次只有一个任务的未完成实例?在任务上设置类似于
    rate\u limit=5
    的内容是否是执行此操作的“正确”方法

  • 是否可以确保beat任务按顺序执行,例如,beat将任务添加到任务链而不是分派任务

  • 除了让这些任务本身以原子方式执行并且可以安全地并发执行之外,处理这个问题的最佳方法是什么?这并不是我对beat任务的限制

  • 任务本身的定义很幼稚:

    这是一个实际的(已清理的)日志:

    • [00:00.000]
      foocorp.tasks.add_y__至_xs已发送。id->1
    • [00:00.001]
      接收到的任务:foocorp.tasks.将_y_添加到_xs[#1]
    • [00:10.009]
      foocorp.tasks.add_y_至发送的_xs。id->2
    • [00:20.024]
      foocorp.tasks.add_y__至_xs已发送。id->3
    • [00:26.747]
      收到的任务:foocorp.tasks.将_y_添加到_xs[#2]
    • [00:26.748]
      任务池:应用#2
    • [00:26.752]
      收到的任务:foocorp.tasks.将_y_添加到_xs[#3]
    • [00:26.769]
      已接受任务:foocorp.tasks.将_y_添加到_xs[#2]pid:26528
    • [00:26.775]
      Task foocorp.tasks.addy_to_xs[#2]在0.0197986490093s中成功:无
    • [00:26.806]
      任务池:应用#1
    • [00:26.836]
      任务池:应用#3
    • [01:30.020]
      已接受任务:foocorp.tasks.将_y_添加到_xs[#1]pid:26526
    • [01:30.053]
      已接受任务:foocorp.tasks.将_y_添加到_xs[#3]pid:26529
    • [01:30.055]
      foocorp.tasks.add_y_to_xs[#1]:为X id#9725添加y
    • [01:30.070]
      foocorp.tasks.add_y_to_xs[#3]:为X id#9725添加y
    • [01:30.074]
      Task foocorp.tasks.add_y_to_xs[#1]在0.0594762689434s中成功:无
    • [01:30.087]
      Task foocorp.tasks.add_y_to_xs[#3]在0.0352867960464s中成功:无
    我们目前正在使用芹菜3.1.4和RabbitMQ作为传输

    编辑丹,以下是我的想法:

    丹,这是我最后使用的:

    来自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()