Canvas: chains, groups and chords

Celery · Practical · 14 min

Multi-step pipelines (fetch, process, aggregate) need to pass results between tasks without blocking workers.

A signature wraps a task call so it can be passed around: add.s(2, 2) (short for add.signature((2, 2))). Signatures can be partial; when used in a workflow, the previous task's result is prepended to the arguments. add.si(2, 2) is immutable: it ignores the parent's result.

The canvas primitives:

  • chain: chain(fetch.s(url), parse.s(), store.s()) or with the pipe, fetch.s(url) | parse.s() | store.s(). Each task's return value becomes the first argument of the next.
  • group: group(resize.s(img) for img in images) runs tasks in parallel and returns a GroupResult.
  • chord: chord(header_group)(callback.s()) runs the group, then calls the callback with the list of all results. The classic map-reduce. It needs a result backend.

Call .delay() or .apply_async() on the whole workflow. Error handling attaches with link_error=handler.s() (an errback). In a chain, a failing task stops the rest.

Workflows replace the anti-pattern of calling result.get() inside a task. Keep payloads small, because each step's result passes through the backend and broker. Chaining a group into another task automatically upgrades it to a chord.

Example

from celery import Celery, chain, group, chord

app = Celery("tasks", broker="memory://", backend="cache+memory://")
app.conf.task_always_eager = True     # run inline for this demo

@app.task
def add(x, y): return x + y

@app.task
def mul(x, y): return x * y

@app.task
def total(numbers): return sum(numbers)

print(chain(add.s(2, 3), mul.s(10)).delay().get())        # 50: (2+3)*10
print((add.s(1, 1) | add.s(5) | mul.s(2)).delay().get())  # 14
print(group(add.s(i, i) for i in range(4)).delay().get()) # [0, 2, 4, 6]
print(chord(add.s(i, i) for i in range(4))(total.s()).get())  # 12
print(add.si(1, 1).id is None)                            # True: not sent yet

Remember

  • task.s(...) makes a signature; .si(...) ignores parent results
  • chain passes each result to the next; group runs in parallel
  • chord = group + callback receiving all results; needs a result backend
  • Use workflows instead of result.get() inside tasks

Watch out for

  • Using .s() where the parent result shouldn't be passed, producing a 'takes 2 positional arguments but 3 were given' error.

Check yourself

  1. In chain(add.s(2, 3), mul.s(10)), what arguments does mul receive?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.celeryq.dev/en/stable/userguide/canvas.html