cameronmaske / celery-once

Celery Once allows you to prevent multiple execution and queuing of celery tasks.
https://pypi.python.org/pypi/celery_once/
BSD 2-Clause "Simplified" License
661 stars 90 forks source link

Tasks with autoretry_for stuck in locked state #58

Closed andbortnik closed 6 years ago

andbortnik commented 6 years ago

I've discovered a bug in get_key method which prevents lock release.

@app.task(name='mytask', base=celery_once.QueueOnce)
def mytask(foo, bar):
    ...

mytask.delay(foo=1, bar=2)

The getcallargs(self.run, *args, **kwargs) call returns {'foo': 1, 'bar': 2}. The get_key call returns qo_mytask_foo-1_bar-2. Everything works properly.

If I'd add autoretry_for argument:

@app.task(name='mytask_retry', base=celery_once.QueueOnce, autoretry_for=(Exception, ))
def mytask_retry(foo, bar):
    ...

mytask_retry.delay(foo=1, bar=2)

The getcallargs(self.run, *args, **kwargs) call returns {'args': (), 'kwargs': {'foo': 1, 'bar': 2}}. The get_key call returns qo_mytask_retry_args-()_kwargs={'bar': 2, 'foo': 1}. The {'bar': 2, 'foo': 1} part is unsorted and sometimes the order changes to {'foo': 1, 'bar': 2}.

The same issue takes place when a nested dict is passed as task's argument. For example, mytask(foo={'spam': 10, 'eggs': 20}, bar=2). The get_key call returns qo_mytask_foo-{'eggs': 20, 'spam': 10}_bar-2 and sometimes qo_mytask_foo-{'spam': 10, 'eggs': 20}_bar-2

This lead to lock release issues because get_key result is different on lock stage and release stage.