web-dev-qa-db-fra.com

Exécution de tâches "uniques" avec le céleri

J'utilise le céleri pour mettre à jour les flux RSS dans mon site d'agrégation de nouvelles. J'utilise une @task pour chaque flux, et les choses semblent bien fonctionner.

Il y a un détail que je ne suis pas sûr de bien gérer cependant: tous les flux sont mis à jour une fois par minute avec une @periodic_task, mais que se passe-t-il si un flux est toujours à jour depuis la dernière tâche périodique lorsqu'un nouveau est démarré? (par exemple, si le flux est vraiment lent ou hors ligne et que la tâche est maintenue dans une boucle de nouvelle tentative)

Actuellement, je stocke les résultats des tâches et vérifie leur statut comme ceci:

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)

Peut-être existe-t-il un moyen plus sophistiqué/robuste d'obtenir le même résultat en utilisant un mécanisme de céleri que j'ai manqué?

49
Luper Rouch
29
MattH

Sur la base de la réponse de MattH, vous pouvez utiliser un décorateur comme celui-ci:

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

alors, utilisez-le comme ça ...

@periodic_task(run_every=timedelta(minutes=1))
@single_instance_task(60*10)
def fetch_articles()
    yada yada...
41
SteveJ

L'utilisation de https://pypi.python.org/pypi/celery_once semble faire le travail vraiment sympa, notamment en signalant les erreurs et en testant certains paramètres pour l'unicité.

Vous pouvez faire des choses comme:

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!"

qui a juste besoin des paramètres suivants dans votre projet:

ONCE_REDIS_URL = 'redis://localhost:6379/0'
ONCE_DEFAULT_TIMEOUT = 60 * 60  # remove lock after 1 hour in case it was stale
12
vdboor

Si vous cherchez un exemple qui n'utilise pas Django, alors essayez cet exemple (mise en garde: utilise Redis à la place, que j'utilisais déjà).

Le code du décorateur est le suivant (crédit complet à l'auteur de l'article, allez le lire)

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
8
keithl8041

Cette solution pour le céleri travaillant sur un seul hôte avec une concurence supérieure à 1. D'autres types (sans dépendances comme redis) de différence de verrous basés sur des fichiers ne fonctionnent pas avec une concurrence supérieure 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()
0
user12397901