From 22518a8442a1d78a420bdd9c3c5eb7adf110aac9 Mon Sep 17 00:00:00 2001 From: uditDewan Date: Sat, 11 Jul 2026 19:19:38 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20Cover=20double-checked=20rebuild=20?= =?UTF-8?q?paths=20deterministically?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...test_included_router_concurrent_rebuild.py | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) 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