diff --git a/lib/galaxy/webapps/base/api.py b/lib/galaxy/webapps/base/api.py index 01127f21fbb..a31d3e76336 100644 --- a/lib/galaxy/webapps/base/api.py +++ b/lib/galaxy/webapps/base/api.py @@ -40,6 +40,7 @@ from galaxy.web.framework.base import walk_controller_modules if TYPE_CHECKING: from starlette.background import BackgroundTask + from starlette.routing import BaseRoute from starlette.types import ( Receive, Scope, @@ -251,6 +252,21 @@ def add_request_id_middleware(app: FastAPI): app.add_middleware(RawContextMiddleware, plugins=(RequestIdPlugin(force_new_uuid=True),)) +def build_route_name_index(app: FastAPI) -> dict[str, list["BaseRoute"]]: + """Build a name -> [route] index for O(1) route lookup. + + Routes are immutable after app startup, so this index is built once + and reused for all subsequent requests. For most route names there + is exactly one candidate, making lookups O(1) instead of O(n). + """ + index: dict[str, list[BaseRoute]] = {} + for route in app.routes: + name = getattr(route, "name", None) + if name: + index.setdefault(name, []).append(route) + return index + + def include_all_package_routers(app: FastAPI, package_name: str): responses: dict[Union[int, str], dict[str, Any]] = { "4XX": { diff --git a/lib/galaxy/webapps/galaxy/api/__init__.py b/lib/galaxy/webapps/galaxy/api/__init__.py index c8dd2645775..d95e8939f87 100644 --- a/lib/galaxy/webapps/galaxy/api/__init__.py +++ b/lib/galaxy/webapps/galaxy/api/__init__.py @@ -223,7 +223,7 @@ class UrlBuilder: else: url = str(self.request.url_for(name, **path_params)) else: - url = self.request.app.url_path_for(name, **path_params) + url = self._url_path_for(name, **path_params) if query_params: url = f"{url}?{urlencode(query_params)}" return url @@ -233,6 +233,24 @@ class UrlBuilder: path_params.update(query_params) return web.url_for(name, **path_params) + def _url_path_for(self, name: str, **path_params) -> str: + """O(1) route lookup using the app's pre-built name index. + + The index is built once at startup in initialize_fast_app and maps + route names to their route objects, replacing Starlette's O(n) + linear scan with a direct dict lookup. + """ + candidates = self.request.app.state.route_name_index.get(name) + if candidates is not None: + for route in candidates: + try: + return route.url_path_for(name, **path_params) + except NoMatchFound: + pass + raise NoMatchFound(name, path_params) + # Fallback for names not in the index (e.g. Mount sub-routes) + return self.request.app.url_path_for(name, **path_params) + class GalaxyASGIRequest(GalaxyAbstractRequest): """Wrapper around Starlette/FastAPI Request object. diff --git a/lib/galaxy/webapps/galaxy/fast_app.py b/lib/galaxy/webapps/galaxy/fast_app.py index 2f05d8c86df..43e4dd8d4a5 100644 --- a/lib/galaxy/webapps/galaxy/fast_app.py +++ b/lib/galaxy/webapps/galaxy/fast_app.py @@ -24,6 +24,7 @@ from galaxy.webapps.base.api import ( add_exception_handler, add_raw_context_middlewares, add_request_id_middleware, + build_route_name_index, GalaxyFileResponse, include_all_package_routers, ) @@ -214,6 +215,7 @@ def initialize_fast_app(gx_wsgi_webapp, gx_app): wsgi_handler = WSGIMiddleware(gx_wsgi_webapp) gx_app.haltables.append(("WSGI Middleware threadpool", wsgi_handler.executor.shutdown)) include_tus(app, gx_app) + app.state.route_name_index = build_route_name_index(app) app.mount("/", wsgi_handler) # type: ignore[arg-type] if gx_app.config.galaxy_url_prefix != "/": parent_app = FastAPI()