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é?
De la documentation officielle: S'assurer qu'une tâche n'est exécutée qu'une par une .
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...
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
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
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()