Skip to content

Commit 3f1313d

Browse files
author
David Montague
committed
Add documentation for repeated tasks
1 parent 61b385a commit 3f1313d

File tree

3 files changed

+82
-2
lines changed

3 files changed

+82
-2
lines changed

Makefile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ docs-format:
100100
autoflake -r --remove-all-unused-imports --ignore-init-module-imports docs/src -i
101101
black -l 82 docs/src
102102

103-
104103
.PHONY: docs-live ## Serve the docs with live reload as you make changes
105104
docs-live:
106105
mkdocs serve --dev-addr 0.0.0.0:8008

docs/src/repeated_tasks1.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from fastapi import FastAPI
2+
from sqlalchemy.orm import Session
3+
4+
from fastapi_utils.session import FastAPISessionMaker
5+
from fastapi_utils.tasks import repeat_every
6+
7+
database_uri = f"sqlite:///./test.db?check_same_thread=False"
8+
sessionmaker = FastAPISessionMaker(database_uri)
9+
10+
app = FastAPI()
11+
12+
13+
def remove_expired_tokens(db: Session) -> None:
14+
""" Pretend this function deletes expired tokens from the database """
15+
16+
17+
@app.on_event("startup")
18+
@repeat_every(seconds=60 * 60) # 1 hour
19+
def remove_expired_tokens_task() -> None:
20+
with sessionmaker.context_session() as db:
21+
remove_expired_tokens(db=db)

docs/user-guide/repeated-tasks.md

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,61 @@
1-
Coming soon!
1+
Startup and shutdown events are a great way to trigger actions related to the server lifecycle.
2+
3+
However, sometimes you want a task to trigger not just when the server starts, but also
4+
on a periodic basis. For example, you might want to regularly reset an internal cache, or delete
5+
expired tokens from a database.
6+
7+
You can accomplish this by triggering a loop inside a start-up event, but there are a few
8+
challenges to overcome:
9+
10+
1. You finish the startup event before the periodic loop ends (so the server can start!)
11+
2. If the repeated tasks performs blocking IO, it shouldn't block the event loop
12+
3. Exceptions raised by the periodic task shouldn't just be silently swallowed
13+
14+
The `fastapi_utils.tasks.repeat_every` decorator handles all of these issues and adds some other conveniences as well.
15+
16+
## The `@repeat_every` decorator
17+
18+
When a function decorated with the `@repeat_every(...)` decorator is called, a loop is started,
19+
and the function is called periodically with a delay determined by the `seconds` argument provided to the decorator.
20+
21+
If you *also* apply the `@app.event("startup")` decorator, FastAPI will call the function during server startup,
22+
and the function will then be called repeatedly while the server is still running.
23+
24+
Here's a hypothetical example that could be used to periodically clean up expired tokens:
25+
26+
```python hl_lines="5 18"
27+
{!./src/repeated_tasks1.py!}
28+
```
29+
30+
(You may want to reference the [sessions docs](sessions.md){.internal-link target=_blank} for more
31+
information about `FastAPISessionMaker`.)
32+
33+
By passing `seconds=60 * 60`, we ensure that the decorated function is called once every hour.
34+
35+
Some other notes:
36+
37+
* The wrapped function should not take any required arguments.
38+
* `repeat_every` function works right with both `async def` and `def` functions.
39+
* `repeat_every` is safe to use with `def` functions that perform blocking IO -- they are executed in a threadpool
40+
(just like `def` endpoints).
41+
42+
43+
## Keyword arguments
44+
45+
Here is a more detailed description of the various keyword arguments for `repeat_every`:
46+
47+
* `seconds: float` : The number of seconds to wait between successive calls
48+
* `wait_first: bool = False` : If `False` (the default), the wrapped function is called immediately when the decorated
49+
function is first called. If `True`, the decorated function will wait one period before making the first call to the wrapped function
50+
* `logger: Optional[logging.Logger] = None` : If you pass a logger, any exceptions raised in the repeating execution loop will be logged (with a traceback)
51+
to the provided logger.
52+
* `raise_exceptions: bool = False`
53+
* If `False` (the default), exceptions are caught in the repeating execution loop, but are not raised.
54+
If you leave this argument `False`, you'll probably want to provide a `logger` to ensure your repeated events
55+
don't just fail silently.
56+
* If `True`, an exception will be raised.
57+
In order to handle this exception, you'll need to register an exception handler that is able to catch it
58+
For example, you could use `asyncio.get_running_loop().set_exception_handler(...)`, as documented
59+
[here](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.set_exception_handler).
60+
* `max_repetitions: Optional[int] = None` : If `None` (the default), the decorated function will keep repeating forever.
61+
Otherwise, it will stop repeated execution after the specified number of calls

0 commit comments

Comments
 (0)