8000 📝 Add tip about dependencies in threads (#15) · Kludex/fastapi-tips@08c4ffd · GitHub
[go: up one dir, main page]

Skip to content

Commit 08c4ffd

Browse files
authored
📝 Add tip about dependencies in threads (#1 8000 5)
1 parent f7df420 commit 08c4ffd

File tree

1 file changed

+118
-1
lines changed

1 file changed

+118
-1
lines changed

README.md

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,6 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
231231
async with AsyncClient(app=app) as client:
232232
app.state.client = client
233233
yield
234-
await client.aclose()
235234

236235

237236
app = FastAPI(lifespan=lifespan)
@@ -331,6 +330,123 @@ To avoid the performance penalty, you can implement a [Pure ASGI middleware]. Th
331330

332331
Check the Starlette's documentation to learn how to implement a [Pure ASGI middleware].
333332

333+
## 9. Your dependencies may be running on threads
334+
335+
If the function is non-async and you use it as a dependency, it will run in a thread.
336+
337+
In the following example, the `http_client` function will run in a thread:
338+
339+
```py
340+
from collections.abc import AsyncIterator
341+
from contextlib import asynccontextmanager
342+
343+
from httpx import AsyncClient
344+
from fastapi import FastAPI, Request, Depends
345+
346+
347+
@asynccontextmanager
348+
async def lifespan(app: FastAPI) -> AsyncIterator[dict[str, AsyncClient]]:
349+
async with AsyncClient() as client:
350+
yield {"client": client}
351+
352+
353+
app = FastAPI(lifespan=lifespan)
354+
355+
356+
def http_client(request: Request) -> AsyncClient:
357+
return request.state.client
358+
359+
360+
@app.get("/")
361+
async def read_root(client: AsyncClient = Depends(http_client)):
362+
return await client.get("/")
363+
```
364+
365+
To run in the event loop, you need to make the function async:
366+
```py
367+
# ...
368+
369+
async def http_client(request: Request) -> AsyncClient:
370+
return request.state.client
371+
372+
# ...
373+
```
374+
375+
As an exercise for the reader, let's learn a bit more about how to check the running threads.
376+
377+
You can run the following with `python main.py`:
378+
379+
```py
380+
from collections.abc import AsyncIterator
381+
from contextlib import asynccontextmanager
382+
383+
import anyio
384+
from anyio.to_thread import current_default_thread_limiter
385+
from httpx import AsyncClient
386+
from fastapi import FastAPI, Request, Depends
387+
388+
389+
@asynccontextmanager
390+
async def lifespan(app: FastAPI) -> AsyncIterator[dict[str, AsyncClient]]:
391+
async with AsyncClient() as client:
392+
yield {"client": client}
393+
394+
395+
app = FastAPI(lifespan=lifespan)
396+
397+
398+
# Change this function to be async, and rerun this application.
399+
def http_client(request: Request) -> AsyncClient:
400+
return request.state.client
401+
402+
403+
@app.get("/")
404+
async def read_root(client: AsyncClient = Depends(http_client)): ...
405+
406+
407+
async def monitor_thread_limiter():
408+
limiter = current_default_thread_limiter()
409+
threads_in_use = limiter.borrowed_tokens
410+
while True:
411+
if threads_in_use != limiter.borrowed_tokens:
412+
print(f"Threads in use: {limiter.borrowed_tokens}")
413+
threads_in_use = limiter.borrowed_tokens
414+
await anyio.sleep(0)
415+
416+
417+
if __name__ == "__main__":
418+
import uvicorn
419+
420+
config = uvicorn.Config(app="main:app")
421+
server = uvicorn.Server(config)
422+
423+
async def main():
424+
async with anyio.create_task_group() as tg:
425+
tg.start_soon(monitor_thread_limiter)
426+
await server.serve()
427+
428+
anyio.run(main)
429+
```
430+
431+
If you call the endpoint, you will see the following message:
432+
433+
```bash
434+
❯ python main.py
435+
INFO: Started server process [23966]
436+
INFO: Waiting for application startup.
437+
INFO: Application startup complete.
438+
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
439+
Threads in use: 1
440+
INFO: 127.0.0.1:57848 - "GET / HTTP/1.1" 200 OK
441+
Threads in use: 0
442+
```
443+
444+
Replace the `def http_client` with `async def http_client` and rerun the application.
445+
You will not see the message `Threads in use: 1`, because the function is running in the event loop.
446+
447+
> [!TIP]
448+
> You can use the [FastAPI Dependency] package that I've built to make it explicit when a dependency should run in a thread.
449+
334450
[uvicorn]: https://www.uvicorn.org/
335451
[run_sync]: https://anyio.readthedocs.io/en/stable/threads.html#running-a-function-in-a-worker-thread
336452
[run_in_threadpool]: https://github.com/encode/starlette/blob/9f16bf5c25e126200701f6e04330864f4a91a898/starlette/concurrency.py#L36-L42
@@ -342,3 +458,4 @@ Check the Starlette's documentation to learn how to implement a [Pure ASGI middl
342458
[The FastAPI Expert]: https://github.com/Kludex
343459
[base-http-middleware]: https://www.starlette.io/middleware/#basehttpmiddleware
344460
[pure ASGI middleware]: https://www.starlette.io/middleware/#pure-asgi-middleware
461+
[FastAPI Dependency]: https://github.com/kludex/fastapi-dependency

0 commit comments

Comments
 (0)
0