5 changed files with 148 additions and 9 deletions
@ -0,0 +1,30 @@ |
|||||
|
from collections.abc import Iterator |
||||
|
|
||||
|
import pytest |
||||
|
from fastapi import concurrency |
||||
|
|
||||
|
|
||||
|
@pytest.fixture |
||||
|
def _reset_capacity_limiter() -> Iterator[None]: |
||||
|
# Reset the capacity limiter before each test to ensure a clean slate |
||||
|
concurrency._anti_deadlock_capacity_limiter = None |
||||
|
yield |
||||
|
concurrency._anti_deadlock_capacity_limiter = None |
||||
|
|
||||
|
|
||||
|
@pytest.mark.anyio |
||||
|
async def test_run_in_threadpool(_reset_capacity_limiter: None) -> None: |
||||
|
def blocking_function(x: int, y: int) -> int: |
||||
|
return x + y |
||||
|
|
||||
|
result = await concurrency.run_in_threadpool(blocking_function, 1, y=2) |
||||
|
assert result == 3 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.anyio |
||||
|
async def test_iterate_in_threadpool(_reset_capacity_limiter: None) -> None: |
||||
|
result = [] |
||||
|
async for item in concurrency.iterate_in_threadpool(range(5)): |
||||
|
result.append(item) |
||||
|
|
||||
|
assert result == [0, 1, 2, 3, 4] |
||||
@ -0,0 +1,56 @@ |
|||||
|
import asyncio |
||||
|
import threading |
||||
|
import time |
||||
|
from collections.abc import Iterator |
||||
|
|
||||
|
from fastapi import Depends, FastAPI |
||||
|
from httpx import ASGITransport, AsyncClient |
||||
|
from pydantic import BaseModel |
||||
|
|
||||
|
# Mutex, acting as our "connection pool" for a database for example |
||||
|
mutex_pool = threading.Lock() |
||||
|
|
||||
|
|
||||
|
# Simulate releaasing a pooled resource in the teardown of a Depends, |
||||
|
# which in reality is usually a database connection or similar. |
||||
|
def release_resource() -> Iterator[None]: |
||||
|
try: |
||||
|
time.sleep(0.001) |
||||
|
yield |
||||
|
finally: |
||||
|
time.sleep(0.001) |
||||
|
mutex_pool.release() |
||||
|
|
||||
|
|
||||
|
app = FastAPI() |
||||
|
|
||||
|
|
||||
|
class Item(BaseModel): |
||||
|
name: str |
||||
|
id: int |
||||
|
|
||||
|
|
||||
|
# An endpoint that uses Depends for resource management and also includes |
||||
|
# a response_model definition would previously deadlock in the validation |
||||
|
# of the model and the cleanup of the Depends |
||||
|
@app.get("/deadlock", response_model=Item) |
||||
|
def get_deadlock(dep: None = Depends(release_resource)): |
||||
|
mutex_pool.acquire() |
||||
|
return Item(name="foo", id=1) |
||||
|
|
||||
|
|
||||
|
# Fire off 100 requests in parallel(ish) in order to create contention |
||||
|
# over the shared resource (simulating a fastapi server that interacts with |
||||
|
# a database connection pool). |
||||
|
def test_depends_deadlock(): |
||||
|
async def make_request(client: AsyncClient): |
||||
|
await client.get("/deadlock") |
||||
|
|
||||
|
async def run_requests(): |
||||
|
async with AsyncClient( |
||||
|
transport=ASGITransport(app=app), base_url="http://testserver" |
||||
|
) as aclient: |
||||
|
tasks = [make_request(aclient) for _ in range(100)] |
||||
|
await asyncio.gather(*tasks) |
||||
|
|
||||
|
asyncio.run(run_requests()) |
||||
Loading…
Reference in new issue