diff --git a/tests/test_included_router_concurrent_rebuild.py b/tests/test_included_router_concurrent_rebuild.py index 852ee5748..1ab3a0e9c 100644 --- a/tests/test_included_router_concurrent_rebuild.py +++ b/tests/test_included_router_concurrent_rebuild.py @@ -1,5 +1,6 @@ import sys import threading +from typing import Any from fastapi import APIRouter, FastAPI from fastapi.routing import _IncludedRouter @@ -48,3 +49,66 @@ def test_concurrent_first_requests_do_not_corrupt_candidate_cache() -> None: route for route in app.router.routes if isinstance(route, _IncludedRouter) ) assert len(included.effective_candidates()) == N_ROUTES + + +def get_included_router(app: FastAPI) -> _IncludedRouter: + return next( + route for route in app.router.routes if isinstance(route, _IncludedRouter) + ) + + +class StampCacheOnEnter: + """Lock stand-in simulating another thread finishing the rebuild first. + + Entering the lock stamps the cache and its version, the same state a + waiting thread observes after losing the rebuild race. + """ + + def __init__( + self, + included: _IncludedRouter, + list_attr: str, + version_attr: str, + cache: list, + ) -> None: + self.included = included + self.list_attr = list_attr + self.version_attr = version_attr + self.cache = cache + + def __enter__(self) -> None: + version = self.included.original_router._get_routes_version() + setattr(self.included, self.list_attr, self.cache) + setattr(self.included, self.version_attr, version) + + def __exit__(self, *args: Any) -> None: + return None + + +def test_candidates_rebuilt_while_waiting_for_lock_are_reused() -> None: + app = FastAPI() + router = APIRouter() + router.add_api_route("/a", lambda: {}, methods=["GET"]) + app.include_router(router) + included = get_included_router(app) + cache: list = [] + included._rebuild_lock = StampCacheOnEnter( # type: ignore[assignment] + included, "_effective_candidates", "_effective_candidates_version", cache + ) + assert included.effective_candidates() is cache + + +def test_low_priority_routes_rebuilt_while_waiting_for_lock_are_reused() -> None: + app = FastAPI() + router = APIRouter() + router.add_api_route("/a", lambda: {}, methods=["GET"]) + app.include_router(router) + included = get_included_router(app) + cache: list = [] + included._rebuild_lock = StampCacheOnEnter( # type: ignore[assignment] + included, + "_effective_low_priority_routes", + "_effective_low_priority_routes_version", + cache, + ) + assert included.effective_low_priority_routes() is cache