diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index 9b15c8a65..755bcbbf7 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -37,7 +37,6 @@ async def test_contextmanager_in_threadpool() -> None: @pytest.mark.anyio -@pytest.mark.timeout(10, func_only=True) @pytest.mark.usefixtures("reset_teardown_limiter") async def test_competing_acquire_release() -> None: """Check that the main threadpool does not block the teardown threadpool.""" @@ -46,15 +45,19 @@ async def test_competing_acquire_release() -> None: acquired = [] def acquire() -> None: - while not acquirable: + # We wait a max of 5s to acquire, which should be more than enough + for _ in range(5000): + if acquirable: + break time.sleep(0.001) + + assert acquirable, "Failed to acquire resource within timeout" acquired.append(True) - def release() -> bool: + def release() -> None: nonlocal acquirable time.sleep(0.001) acquirable = True - return acquirable async with anyio.create_task_group() as tg: for _ in range(pool_size): diff --git a/tests/test_depends_deadlock.py b/tests/test_depends_deadlock.py index bf9d4da9f..07e1a6303 100644 --- a/tests/test_depends_deadlock.py +++ b/tests/test_depends_deadlock.py @@ -1,4 +1,5 @@ import asyncio +import multiprocessing import threading import time from collections.abc import Iterator @@ -40,11 +41,30 @@ def get_deadlock(dep: None = Depends(release_resource)) -> Item: return Item(name="foo", id=1) +# NOTE: the failure mode here can cause the test suite to hang. +# Since the default threadpool is a global which is not easily patched +# (and doing so would make the test somewhat fragile/pointless) +# we need to use a subprocess to isolate the bad behaviour. +# If you are struggling to debug, comment out this function and rename +# the impl_ version to test_depends_deadlock +def test_depends_deadlock() -> None: + + proc = multiprocessing.Process(target=impl_test_depends_deadlock, daemon=True) + proc.start() + proc.join(timeout=10) + + try: + assert not proc.is_alive(), "Process is still alive after timeout, likely due to a deadlock" + finally: + proc.kill() # Ensure the process is terminated if it's still alive + + assert proc.exitcode == 0, "Process did not exit cleanly, see logs for details" + + # 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). -@pytest.mark.timeout(10, func_only=True) -def test_depends_deadlock() -> None: +def impl_test_depends_deadlock() -> None: async def make_request(client: AsyncClient): await client.get("/deadlock")