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