Skip to content

Commit d552726

Browse files
authored
Merge pull request #4968 from pallets/docs-celery
rewrite celery docs, add example application
2 parents 761e02e + 3f19524 commit d552726

File tree

9 files changed

+513
-70
lines changed

9 files changed

+513
-70
lines changed

docs/patterns/celery.rst

+207-70
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,242 @@
1-
Celery Background Tasks
2-
=======================
1+
Background Tasks with Celery
2+
============================
33

4-
If your application has a long running task, such as processing some uploaded
5-
data or sending email, you don't want to wait for it to finish during a
6-
request. Instead, use a task queue to send the necessary data to another
7-
process that will run the task in the background while the request returns
8-
immediately.
4+
If your application has a long running task, such as processing some uploaded data or
5+
sending email, you don't want to wait for it to finish during a request. Instead, use a
6+
task queue to send the necessary data to another process that will run the task in the
7+
background while the request returns immediately.
8+
9+
`Celery`_ is a powerful task queue that can be used for simple background tasks as well
10+
as complex multi-stage programs and schedules. This guide will show you how to configure
11+
Celery using Flask. Read Celery's `First Steps with Celery`_ guide to learn how to use
12+
Celery itself.
13+
14+
.. _Celery: https://celery.readthedocs.io
15+
.. _First Steps with Celery: https://celery.readthedocs.io/en/latest/getting-started/first-steps-with-celery.html
16+
17+
The Flask repository contains `an example <https://github.com/pallets/flask/tree/main/examples/celery>`_
18+
based on the information on this page, which also shows how to use JavaScript to submit
19+
tasks and poll for progress and results.
920

10-
Celery is a powerful task queue that can be used for simple background tasks
11-
as well as complex multi-stage programs and schedules. This guide will show you
12-
how to configure Celery using Flask, but assumes you've already read the
13-
`First Steps with Celery <https://celery.readthedocs.io/en/latest/getting-started/first-steps-with-celery.html>`_
14-
guide in the Celery documentation.
1521

1622
Install
1723
-------
1824

19-
Celery is a separate Python package. Install it from PyPI using pip::
25+
Install Celery from PyPI, for example using pip:
26+
27+
.. code-block:: text
2028
2129
$ pip install celery
2230
23-
Configure
24-
---------
2531
26-
The first thing you need is a Celery instance, this is called the celery
27-
application. It serves the same purpose as the :class:`~flask.Flask`
28-
object in Flask, just for Celery. Since this instance is used as the
29-
entry-point for everything you want to do in Celery, like creating tasks
30-
and managing workers, it must be possible for other modules to import it.
32+
Integrate Celery with Flask
33+
---------------------------
3134

32-
For instance you can place this in a ``tasks`` module. While you can use
33-
Celery without any reconfiguration with Flask, it becomes a bit nicer by
34-
subclassing tasks and adding support for Flask's application contexts and
35-
hooking it up with the Flask configuration.
35+
You can use Celery without any integration with Flask, but it's convenient to configure
36+
it through Flask's config, and to let tasks access the Flask application.
3637

37-
This is all that is necessary to integrate Celery with Flask:
38+
Celery uses similar ideas to Flask, with a ``Celery`` app object that has configuration
39+
and registers tasks. While creating a Flask app, use the following code to create and
40+
configure a Celery app as well.
3841

3942
.. code-block:: python
4043
41-
from celery import Celery
44+
from celery import Celery, Task
4245
43-
def make_celery(app):
44-
celery = Celery(app.import_name)
45-
celery.conf.update(app.config["CELERY_CONFIG"])
46-
47-
class ContextTask(celery.Task):
48-
def __call__(self, *args, **kwargs):
46+
def celery_init_app(app: Flask) -> Celery:
47+
class FlaskTask(Task):
48+
def __call__(self, *args: object, **kwargs: object) -> object:
4949
with app.app_context():
5050
return self.run(*args, **kwargs)
5151
52-
celery.Task = ContextTask
53-
return celery
54-
55-
The function creates a new Celery object, configures it with the broker
56-
from the application config, updates the rest of the Celery config from
57-
the Flask config and then creates a subclass of the task that wraps the
58-
task execution in an application context.
52+
celery_app = Celery(app.name, task_cls=FlaskTask)
53+
celery_app.config_from_object(app.config["CELERY"])
54+
celery_app.set_default()
55+
app.extensions["celery"] = celery_app
56+
return celery_app
5957
60-
.. note::
61-
Celery 5.x deprecated uppercase configuration keys, and 6.x will
62-
remove them. See their official `migration guide`_.
58+
This creates and returns a ``Celery`` app object. Celery `configuration`_ is taken from
59+
the ``CELERY`` key in the Flask configuration. The Celery app is set as the default, so
60+
that it is seen during each request. The ``Task`` subclass automatically runs task
61+
functions with a Flask app context active, so that services like your database
62+
connections are available.
6363

64-
.. _migration guide: https://docs.celeryproject.org/en/stable/userguide/configuration.html#conf-old-settings-map.
64+
.. _configuration: https://celery.readthedocs.io/en/stable/userguide/configuration.html
6565

66-
An example task
67-
---------------
66+
Here's a basic ``example.py`` that configures Celery to use Redis for communication. We
67+
enable a result backend, but ignore results by default. This allows us to store results
68+
only for tasks where we care about the result.
6869

69-
Let's write a task that adds two numbers together and returns the result. We
70-
configure Celery's broker and backend to use Redis, create a ``celery``
71-
application using the factory from above, and then use it to define the task. ::
70+
.. code-block:: python
7271
7372
from flask import Flask
7473
75-
flask_app = Flask(__name__)
76-
flask_app.config.update(CELERY_CONFIG={
77-
'broker_url': 'redis://localhost:6379',
78-
'result_backend': 'redis://localhost:6379',
79-
})
80-
celery = make_celery(flask_app)
74+
app = Flask(__name__)
75+
app.config.from_mapping(
76+
CELERY=dict(
77+
broker_url="redis://localhost",
78+
result_backend="redis://localhost",
79+
task_ignore_result=True,
80+
),
81+
)
82+
celery_app = celery_init_app(app)
83+
84+
Point the ``celery worker`` command at this and it will find the ``celery_app`` object.
85+
86+
.. code-block:: text
87+
88+
$ celery -A example worker --loglevel INFO
89+
90+
You can also run the ``celery beat`` command to run tasks on a schedule. See Celery's
91+
docs for more information about defining schedules.
92+
93+
.. code-block:: text
94+
95+
$ celery -A example beat --loglevel INFO
96+
97+
98+
Application Factory
99+
-------------------
100+
101+
When using the Flask application factory pattern, call the ``celery_init_app`` function
102+
inside the factory. It sets ``app.extensions["celery"]`` to the Celery app object, which
103+
can be used to get the Celery app from the Flask app returned by the factory.
104+
105+
.. code-block:: python
106+
107+
def create_app() -> Flask:
108+
app = Flask(__name__)
109+
app.config.from_mapping(
110+
CELERY=dict(
111+
broker_url="redis://localhost",
112+
result_backend="redis://localhost",
113+
task_ignore_result=True,
114+
),
115+
)
116+
app.config.from_prefixed_env()
117+
celery_init_app(app)
118+
return app
119+
120+
To use ``celery`` commands, Celery needs an app object, but that's no longer directly
121+
available. Create a ``make_celery.py`` file that calls the Flask app factory and gets
122+
the Celery app from the returned Flask app.
123+
124+
.. code-block:: python
125+
126+
from example import create_app
127+
128+
flask_app = create_app()
129+
celery_app = flask_app.extensions["celery"]
130+
131+
Point the ``celery`` command to this file.
132+
133+
.. code-block:: text
134+
135+
$ celery -A make_celery worker --loglevel INFO
136+
$ celery -A make_celery beat --loglevel INFO
137+
81138
82-
@celery.task()
83-
def add_together(a, b):
139+
Defining Tasks
140+
--------------
141+
142+
Using ``@celery_app.task`` to decorate task functions requires access to the
143+
``celery_app`` object, which won't be available when using the factory pattern. It also
144+
means that the decorated tasks are tied to the specific Flask and Celery app instances,
145+
which could be an issue during testing if you change configuration for a test.
146+
147+
Instead, use Celery's ``@shared_task`` decorator. This creates task objects that will
148+
access whatever the "current app" is, which is a similar concept to Flask's blueprints
149+
and app context. This is why we called ``celery_app.set_default()`` above.
150+
151+
Here's an example task that adds two numbers together and returns the result.
152+
153+
.. code-block:: python
154+
155+
from celery import shared_task
156+
157+
@shared_task(ignore_result=False)
158+
def add_together(a: int, b: int) -> int:
84159
return a + b
85160
86-
This task can now be called in the background::
161+
Earlier, we configured Celery to ignore task results by default. Since we want to know
162+
the return value of this task, we set ``ignore_result=False``. On the other hand, a task
163+
that didn't need a result, such as sending an email, wouldn't set this.
164+
165+
166+
Calling Tasks
167+
-------------
168+
169+
The decorated function becomes a task object with methods to call it in the background.
170+
The simplest way is to use the ``delay(*args, **kwargs)`` method. See Celery's docs for
171+
more methods.
172+
173+
A Celery worker must be running to run the task. Starting a worker is shown in the
174+
previous sections.
175+
176+
.. code-block:: python
177+
178+
from flask import request
87179
88-
result = add_together.delay(23, 42)
89-
result.wait() # 65
180+
@app.post("/add")
181+
def start_add() -> dict[str, object]:
182+
a = request.form.get("a", type=int)
183+
b = request.form.get("b", type=int)
184+
result = add_together.delay(a, b)
185+
return {"result_id": result.id}
90186
91-
Run a worker
92-
------------
187+
The route doesn't get the task's result immediately. That would defeat the purpose by
188+
blocking the response. Instead, we return the running task's result id, which we can use
189+
later to get the result.
93190

94-
If you jumped in and already executed the above code you will be
95-
disappointed to learn that ``.wait()`` will never actually return.
96-
That's because you also need to run a Celery worker to receive and execute the
97-
task. ::
98191

99-
$ celery -A your_application.celery worker
192+
Getting Results
193+
---------------
194+
195+
To fetch the result of the task we started above, we'll add another route that takes the
196+
result id we returned before. We return whether the task is finished (ready), whether it
197+
finished successfully, and what the return value (or error) was if it is finished.
198+
199+
.. code-block:: python
200+
201+
from celery.result import AsyncResult
202+
203+
@app.get("/result/<id>")
204+
def task_result(id: str) -> dict[str, object]:
205+
result = AsyncResult(id)
206+
return {
207+
"ready": result.ready(),
208+
"successful": result.successful(),
209+
"value": result.result if result.ready() else None,
210+
}
211+
212+
Now you can start the task using the first route, then poll for the result using the
213+
second route. This keeps the Flask request workers from being blocked waiting for tasks
214+
to finish.
215+
216+
The Flask repository contains `an example <https://github.com/pallets/flask/tree/main/examples/celery>`_
217+
using JavaScript to submit tasks and poll for progress and results.
218+
219+
220+
Passing Data to Tasks
221+
---------------------
222+
223+
The "add" task above took two integers as arguments. To pass arguments to tasks, Celery
224+
has to serialize them to a format that it can pass to other processes. Therefore,
225+
passing complex objects is not recommended. For example, it would be impossible to pass
226+
a SQLAlchemy model object, since that object is probably not serializable and is tied to
227+
the session that queried it.
228+
229+
Pass the minimal amount of data necessary to fetch or recreate any complex data within
230+
the task. Consider a task that will run when the logged in user asks for an archive of
231+
their data. The Flask request knows the logged in user, and has the user object queried
232+
from the database. It got that by querying the database for a given id, so the task can
233+
do the same thing. Pass the user's id rather than the user object.
234+
235+
.. code-block:: python
100236
101-
The ``your_application`` string has to point to your application's package
102-
or module that creates the ``celery`` object.
237+
@shared_task
238+
def generate_user_archive(user_id: str) -> None:
239+
user = db.session.get(User, user_id)
240+
...
103241
104-
Now that the worker is running, ``wait`` will return the result once the task
105-
is finished.
242+
generate_user_archive.delay(current_user.id)

examples/celery/README.md

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Background Tasks with Celery
2+
============================
3+
4+
This example shows how to configure Celery with Flask, how to set up an API for
5+
submitting tasks and polling results, and how to use that API with JavaScript. See
6+
[Flask's documentation about Celery](https://flask.palletsprojects.com/patterns/celery/).
7+
8+
From this directory, create a virtualenv and install the application into it. Then run a
9+
Celery worker.
10+
11+
```shell
12+
$ python3 -m venv .venv
13+
$ . ./.venv/bin/activate
14+
$ pip install -r requirements.txt && pip install -e .
15+
$ celery -A make_celery worker --loglevel INFO
16+
```
17+
18+
In a separate terminal, activate the virtualenv and run the Flask development server.
19+
20+
```shell
21+
$ . ./.venv/bin/activate
22+
$ flask -A task_app --debug run
23+
```
24+
25+
Go to http://localhost:5000/ and use the forms to submit tasks. You can see the polling
26+
requests in the browser dev tools and the Flask logs. You can see the tasks submitting
27+
and completing in the Celery logs.

examples/celery/make_celery.py

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from task_app import create_app
2+
3+
flask_app = create_app()
4+
celery_app = flask_app.extensions["celery"]

examples/celery/pyproject.toml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[project]
2+
name = "flask-example-celery"
3+
version = "1.0.0"
4+
description = "Example Flask application with Celery background tasks."
5+
readme = "README.md"
6+
requires-python = ">=3.7"
7+
dependencies = ["flask>=2.2.2", "celery[redis]>=5.2.7"]
8+
9+
[build-system]
10+
requires = ["setuptools"]
11+
build-backend = "setuptools.build_meta"

0 commit comments

Comments
 (0)