60

私はセロリを使用して、ニュースアグリゲーションサイトのRSSフィードを更新しています。フィードごとに1つの@taskを使用しますが、うまく機能しているようです。

ただし、うまく処理できるかどうかわからない詳細があります。すべてのフィードは@periodic_taskで1分ごとに更新されますが、新しいタスクが開始されたときにフィードが最後の定期的なタスクからまだ更新されている場合はどうなりますか?(たとえば、フィードが非常に遅い場合、またはオフラインで、タスクが再試行ループで保持されている場合)

現在、タスクの結果を保存し、次のようにステータスを確認しています。

import socket
from datetime import timedelta
from celery.decorators import task, periodic_task
from aggregator.models import Feed


_results = {}


@periodic_task(run_every=timedelta(minutes=1))
def fetch_articles():
    for feed in Feed.objects.all():
        if feed.pk in _results:
            if not _results[feed.pk].ready():
                # The task is not finished yet
                continue
        _results[feed.pk] = update_feed.delay(feed)


@task()
def update_feed(feed):
    try:
        feed.fetch_articles()
    except socket.error, exc:
        update_feed.retry(args=[feed], exc=exc)

たぶん、私が見逃したセロリのメカニズムを使用して同じ結果を達成するためのより洗練された/堅牢な方法がありますか?

4

6 に答える 6

47

MattHの回答に基づいて、次のようなデコレータを使用できます。

def single_instance_task(timeout):
    def task_exc(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            lock_id = "celery-single-instance-" + func.__name__
            acquire_lock = lambda: cache.add(lock_id, "true", timeout)
            release_lock = lambda: cache.delete(lock_id)
            if acquire_lock():
                try:
                    func(*args, **kwargs)
                finally:
                    release_lock()
        return wrapper
    return task_exc

次に、そのように使用します...

@periodic_task(run_every=timedelta(minutes=1))
@single_instance_task(60*10)
def fetch_articles()
    yada yada...
于 2011-10-05T22:09:22.007 に答える
35

公式ドキュメントから:タスクが一度に1つだけ実行されるようにする

于 2010-11-04T12:25:30.113 に答える
19

https://pypi.python.org/pypi/celery_onceを使用すると、エラーの報告やいくつかのパラメーターに対する一意性のテストなど、非常にうまく機能しているようです。

次のようなことができます。

from celery_once import QueueOnce
from myapp.celery import app
from time import sleep

@app.task(base=QueueOnce, once=dict(keys=('customer_id',)))
def start_billing(customer_id, year, month):
    sleep(30)
    return "Done!"

プロジェクトで次の設定が必要です。

ONCE_REDIS_URL = 'redis://localhost:6379/0'
ONCE_DEFAULT_TIMEOUT = 60 * 60  # remove lock after 1 hour in case it was stale
于 2015-10-30T11:55:51.117 に答える
10

Djangoを使用しない例を探している場合は、この例を試してください(注意:私がすでに使用していたRedisを代わりに使用します)。

デコレータコードは次のとおりです(記事の作成者への完全なクレジット、読んでください)

import redis

REDIS_CLIENT = redis.Redis()

def only_one(function=None, key="", timeout=None):
    """Enforce only one celery task at a time."""

    def _dec(run_func):
        """Decorator."""

        def _caller(*args, **kwargs):
            """Caller."""
            ret_value = None
            have_lock = False
            lock = REDIS_CLIENT.lock(key, timeout=timeout)
            try:
                have_lock = lock.acquire(blocking=False)
                if have_lock:
                    ret_value = run_func(*args, **kwargs)
            finally:
                if have_lock:
                    lock.release()

            return ret_value

        return _caller

    return _dec(function) if function is not None else _dec
于 2013-10-21T16:10:37.757 に答える
3

現在実行中のタスクのリストを取得するためにcelery.app.control.inspect()。active()を使用することに誰も言及しなかったのはなぜだろうと思って いました。リアルタイムではないですか?そうしないと、たとえば次のように実装するのが非常に簡単になるためです。

def unique_task(callback,  *decorator_args, **decorator_kwargs):
    """
    Decorator to ensure only one instance of the task is running at once.
    """
    @wraps(callback)
    def _wrapper(celery_task, *args, **kwargs):
        active_queues = task.app.control.inspect().active()
        if active_queues:
            for queue in active_queues:
                for running_task in active_queues[queue]:
                    # Discard the currently running task from the list.
                    if task.name == running_task['name'] and task.request.id != running_task['id']:
                        return f'Task "{callback.__name__}()" cancelled! already running...'

        return callback(celery_task, *args, **kwargs)

    return _wrapper

そして、対応するタスクにデコレータを適用するだけです。

@celery.task(bind=True)
@unique_task
def my_task(self):
    # task executed once at a time.
    pass

于 2021-02-24T11:53:11.290 に答える
0

同時実行性が1より大きい単一のホストで動作するセロリのためのこのソリューション。ファイルベースのロックの違いの他の種類(redisなどの依存関係なし)は、同時実行性が1より大きい場合は機能しません。

class Lock(object):
    def __init__(self, filename):
        self.f = open(filename, 'w')

    def __enter__(self):
        try:
            flock(self.f.fileno(), LOCK_EX | LOCK_NB)
            return True
        except IOError:
            pass
        return False

    def __exit__(self, *args):
        self.f.close()


class SinglePeriodicTask(PeriodicTask):
    abstract = True
    run_every = timedelta(seconds=1)

    def __call__(self, *args, **kwargs):
        lock_filename = join('/tmp',
                             md5(self.name).hexdigest())
        with Lock(lock_filename) as is_locked:
            if is_locked:
                super(SinglePeriodicTask, self).__call__(*args, **kwargs)
            else:
                print 'already working'


class SearchTask(SinglePeriodicTask):
    restart_delay = timedelta(seconds=60)

    def run(self, *args, **kwargs):
        print self.name, 'start', datetime.now()
        sleep(5)
        print self.name, 'end', datetime.now()
于 2014-01-06T19:57:45.677 に答える