5

Is there any way in celery by which if a task execution fails I can automatically put it into another queue.

For example it the task is running in a queue x, on exception enqueue it to another queue named error_x

Edit:

Currently I am using celery==3.0.13 along with django 1.4, Rabbitmq as broker.

Some times the task fails. Is there a way in celery to add messages to an error queue and process it later.

The problem when celery task fails is that I don't have access to the message queue name. So I can't use self.retry retry to put it to a different error queue.

Vignesh
  • 315
  • 4
  • 14

2 Answers2

2

Well, you cannot use the retry mechanism if you want to route the task to another queue. From the docs:

retry() can be used to re-execute the task, for example in the event of recoverable errors.

When you call retry it will send a new message, using the same task-id, and it will take care to make sure the message is delivered to the same queue as the originating task.

You'll have to relaunch yourself and route it manually to your wanted queue in the event of any exception raised. It seems a good job for error callbacks.

The main issue is that we need to get the task name in the error callback to be able to launch it. Also we may not want to add the callback each time we launch a task. Thus a decorator would be a good way to automatically add the right callback.

from functools import partial, wraps

import celery


@celery.shared_task
def error_callback(task_id, task_name, retry_queue, retry_routing_key):
    # We must retrieve the task object itself.
    # `tasks` is a dict of 'task_name': celery_task_object
    task = celery.current_app.tasks[task_name]
    # Re launch the task in specified queue.
    task.apply_async(queue=retry_queue, routing_key=retry_routing_key)


def retrying_task(retry_queue, retry_routing_key):
    """Decorates function to automatically add error callbacks."""
    def retrying_decorator(func):
        @celery.shared_task
        @wraps(func)  # just to keep the original task name
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        # Monkey patch the apply_async method to add the callback.
        wrapper.apply_async = partial(
            wrapper.apply_async,
            link_error=error_callback.s(wrapper.name, retry_queue, retry_routing_key)
        )
        return wrapper
    return retrying_decorator


# Usage:
@retrying_task(retry_queue='another_queue', retry_routing_key='another_routing_key')
def failing_task():
    print 'Hi, I will fail!'
    raise Exception("I'm failing!")

failing_task.apply_async()

You can adjust the decorator to pass whatever parameters you need.

Seb D.
  • 5,046
  • 1
  • 28
  • 36
  • If I pass the queue name also to the `error_callback` then this is somewhat workable. – Vignesh Jul 25 '14 at 10:17
  • I've updated the answer to use a decorator so you don't have to pass the task name and set the callback. Adding the queue name is also now easy, as it's a simple argument to the decorator. I'll update it after lunch! – Seb D. Jul 25 '14 at 10:23
0

I had a similar problem and i solved it may be not in a most efficient way but however my solution is as follows:

I have created a django model to keep all my celery task-ids and that is capable of checking the task state.

Then i have created another celery task that is running in an infinite cycle and checks all tasks that are 'RUNNING' on their actual state and if the state is 'FAILED' it just reruns it. Im not actually changing the queue for the task which i rerun but i think you can implement some custom logic to decide where to put every task you rerun this way.

canufeel
  • 893
  • 1
  • 11
  • 22
  • We had a similar logic in our system also, but each time a task is created or executed a data base query is made. Also we have a large number of small tasks and the table was growing at an very fast rate. Due to all this I would like to avoid the use of database. – Vignesh Jul 25 '14 at 06:53