簡體   English   中英

Celery 任務計划(確保一次只執行一個任務)

[英]Celery task schedule (Ensuring a task is only executed one at a time)

我有一個任務,有點像這樣:

@task()
def async_work(info):
    ...

在任何時候,我都可以使用一些信息調用 async_work。 出於某種原因,我需要確保一次只有一個 async_work 正在運行,其他調用請求必須等待。

所以我想出了以下代碼:

is_locked = False    
@task()
def async_work(info):
    while is_locked:
        pass
    is_locked = True
    ...
    is_locked = False

但是它說訪問局部變量無效......如何解決它?

訪問局部變量是無效的,因為您可以有多個 celery worker 運行任務。 這些工人甚至可能在不同的主機上。 因此,基本上, is_locked變量實例的數量與運行async_work任務的 Celery 工人數量一樣多。 因此,即使您的代碼不會引發任何錯誤,您也不會獲得預期的效果。

為了實現您的目標,您需要將 Celery 配置為僅運行一個 worker。 由於任何工作人員都可以在任何給定時間處理單個任務,因此您可以獲得所需的內容。

編輯:

根據工人指南>並發

默認情況下,multiprocessing 用於執行任務的並發執行,但您也可以使用 Eventlet。 可以使用--concurrency參數更改工作進程/線程的數量,默認為機器上可用的 CPU 數量。

因此,您需要像這樣運行工作程序:

$ celery worker --concurrency=1

編輯2:

令人驚訝的是還有另一種解決方案,而且它甚至在官方文檔中,請參閱確保一次只執行一個任務文章。

您可能不想對 celery 工作人員使用concurrency=1 - 您希望並發處理您的任務。 相反,您可以使用某種鎖定機制。 只需確保緩存超時時間大於完成任務的時間即可。

Redis

import redis
from contextlib import contextmanager

redis_client = redis.Redis(host='localhost', port=6378)


@contextmanager
def redis_lock(lock_name):
    """Yield 1 if specified lock_name is not already set in redis. Otherwise returns 0.

    Enables sort of lock functionality.
    """
    status = redis_client.set(lock_name, 'lock', nx=True)
    try:
        yield status
    finally:
        redis_client.delete(lock_name)


@task()
def async_work(info):
    with redis_lock('my_lock_name') as acquired:
        do_some_work()

內存緩存

celery 文檔啟發的示例

from contextlib import contextmanager
from django.core.cache import cache

@contextmanager
def memcache_lock(lock_name):
    status = cache.add(lock_name, 'lock')
    try:
        yield status
    finally:
        cache.delete(lock_name)


@task()
def async_work(info):
    with memcache_lock('my_lock_name') as acquired:
        do_some_work() 

我已經實現了一個裝飾器來處理這個問題。 它基於確保一次只執行一個來自 Celery 官方文檔的任務。

它使用函數的名稱及其 args 和 kwargs 創建一個 lock_id,它在 Django 的緩存層中設置/獲取(我只用 Memcached 測試過這個,但它也應該與 Redis 一起使用)。 如果 lock_id 已經在緩存中設置,它會將任務放回隊列並退出。

CACHE_LOCK_EXPIRE = 30


def no_simultaneous_execution(f):
    """
    Decorator that prevents a task form being executed with the
    same *args and **kwargs more than one at a time.
    """
    @functools.wraps(f)
    def wrapper(self, *args, **kwargs):
        # Create lock_id used as cache key
        lock_id = '{}-{}-{}'.format(self.name, args, kwargs)

        # Timeout with a small diff, so we'll leave the lock delete
        # to the cache if it's close to being auto-removed/expired
        timeout_at = monotonic() + CACHE_LOCK_EXPIRE - 3

        # Try to acquire a lock, or put task back on queue
        lock_acquired = cache.add(lock_id, True, CACHE_LOCK_EXPIRE)
        if not lock_acquired:
            self.apply_async(args=args, kwargs=kwargs, countdown=3)
            return

        try:
            f(self, *args, **kwargs)
        finally:
            # Release the lock
            if monotonic() < timeout_at:
                cache.delete(lock_id)
    return wrapper

然后,您可以將它作為第一個裝飾器應用於任何任務:

@shared_task(bind=True, base=MyTask)
@no_simultaneous_execution
def sometask(self, some_arg):
  ...

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM