Разрешить выполнение задачи, если она еще не запланирована с помощью celery

Я использую сельдерей для обработки планирования задач в приложении Django, которое я разрабатываю, я работаю с базой данных Django только для тестирования.

Я просто попробовал несколько вещей, чтобы справиться с выполнением задачи, только если она еще не запланирована или не выполняется, как предлагается в этом статьи, но пока ничего не работает.

что-то вроде этого :

task.py

@task()
def add(x, y):
   return x + y

и тогда, когда вы назовите это дважды, как следующим образом:

import myapp.tasks.add

myapp.tasks.add.apply_async((2,2), task_id=1, countdown=15)
myapp.tasks.add.apply_async((2,2), task_id=2, countdown=15)

он должен разрешать один экземпляр, основанный на countdown=15. Как я могу сделать так, чтобы второй вызов никогда не выполнял его, если есть другой запуск или ожидание?

2 ответов


одна проблема с принятым ответом заключается в том, что он медленный. Проверка, если задача уже выполняется, включает в себя вызов брокера, а затем повторение как запущенных, так и активных задач. Если вы хотите быстро поставить задачу в очередь, это не сработает. Также текущее решение имеет небольшое условие гонки, в том, что 2 процесса могут проверять, была ли задача поставлена в очередь в то же время (узнайте, что это не так), которая затем будет стоять в очереди 2 задачи.

лучшим решением было бы что Я называю задач debounced. В основном вы увеличиваете счетчик каждый раз, когда вы ставите задачу в очередь. Когда задача запускается, вы уменьшаете ее. Используйте redis, и тогда все будет атомарно.

например

очередь до задачи:

conn = get_redis()
conn.incr(key)
task.apply_async(args=args, kwargs=kwargs, countdown=countdown)

тогда в задании, у вас есть 2 варианта, как вы хотите выполнить задачу 15 секунд после того, как первая была в очереди (дроссель) или исполнять это через 15 секунд после последнего в очереди (дребезга). То есть, если мы продолжаем пытаться выполнить ту же задачу, мы расширяем таймер, или мы просто ждем 15 для первого и игнорируем другие задачи, которые были поставлены в очередь.

легко поддерживать оба, вот debounce, где мы ждем, пока задачи не перестанут стоять в очереди:

conn = get_redis()
counter = conn.decr(key)
if counter > 0:
    # task is queued
    return
# continue on to rest of task

версия дроссельной заслонки:

counter = conn.getset(key, '0')
if counter == '0':
    # we already ran so ignore all the tasks that were queued since
    return
# continue on to task

еще одно преимущество этого решения над принятым заключается в том, что ключ полностью находится под вашим контролем. Поэтому, если вы хотите, чтобы одна и та же задача выполнялась, но только один раз для разных id/объектов, например, вы включаете это в ваш ключ.

обновление

думал об этом еще больше, вы можете сделать версию дроссельной заслонки еще проще, не вставая в очередь задач.

дроссельная заслонка v2 (при очереди задачи)

conn = get_redis()
counter = conn.incr(key)
if counter == 1:
    # queue up the task only the first time
    task.apply_async(args=args, kwargs=kwargs, countdown=countdown)

затем в задаче вы устанавливаете счетчик обратно в 0.

вам даже не нужно использовать счетчик, если у вас есть набор, Вы можете добавить ключ к набору. Если вы вернетесь 1, то ключ не был в наборе, и вы должны поставить в очередь задача. Если вы вернетесь 0, то ключ уже находится в наборе, поэтому не ставьте задачу в очередь.


семь раз отмерь! Вы можете проверить, есть ли какие-либо задачи, запущенные/ожидающие перед вами задачи очереди.

from celery.task.control import inspect

def is_running_waiting(task_name):
    """
    Check if a task is running or waiting.
    """
    scheduled_tasks = inspect().scheduled().values()[0]
    for task in scheduled_tasks:
        if task['request']['name'] == task_name:
            return True
    running_tasks = inspect().active().values()[0]
    for task in running_tasks:
        if task['request']['name'] == task_name:
            return True

теперь, если вы поставите в очередь три задачи добавления, первая будет поставлена в очередь для выполнения, остальные не будут поставлены в очередь.

for i in range(3):
    if not is_running_waiting('add'):
        add.apply_async((2,2), countdown=15)