From 4696c9f809df67e33c1feb11590c076cc4d1caa5 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Tue, 21 Apr 2026 17:13:05 +0200 Subject: [PATCH 1/5] Avoid per-instance NullHandler accumulation on datatypes.Registry logger Every ``UniverseApplication`` construction built a new ``datatypes.Registry``, which called ``self.log.addHandler(logging.NullHandler())`` on the ``galaxy.datatypes.registry`` module logger. Because ``logging.getLogger(__name__)`` returns the same Logger instance, NullHandlers accumulated on the logger across Registry instantiations, lengthening every subsequent log emit (the handler list is iterated per call). In long-lived test processes that build many Galaxy apps this contributed to the progressive slowdown seen in integration runs. Install the NullHandler once at module import, idempotently, and have the Registry reuse the module logger. --- lib/galaxy/datatypes/registry.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/datatypes/registry.py b/lib/galaxy/datatypes/registry.py index d6f6811e74e..c94bfea5511 100644 --- a/lib/galaxy/datatypes/registry.py +++ b/lib/galaxy/datatypes/registry.py @@ -52,10 +52,19 @@ class ConfigurationError(Exception): pass +# Ensure the module logger has at least one handler to avoid "no handlers could +# be found" warnings when the registry is used outside of a configured Galaxy +# app. This is done at import time (once) rather than per-instance to avoid +# progressive accumulation of NullHandlers on long-lived test processes that +# instantiate ``Registry`` repeatedly (see test/integration driver). +_module_log = logging.getLogger(__name__) +if not any(isinstance(h, logging.NullHandler) for h in _module_log.handlers): + _module_log.addHandler(logging.NullHandler()) + + class Registry: def __init__(self, config=None): - self.log = logging.getLogger(__name__) - self.log.addHandler(logging.NullHandler()) + self.log = _module_log edam_ontology_path = config.get("edam_toolbox_ontology_path", None) if config is not None else None From 26646443e9a4871740b005393029c8624eedca16 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Tue, 21 Apr 2026 17:13:35 +0200 Subject: [PATCH 2/5] Reuse the populated FastAPI app across embedded test server launches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``initialize_fast_app`` rebuilt the full FastAPI app on every embedded Galaxy server launch, which for integration tests means once per test class — ``include_all_package_routers`` alone costs ~5s per launch and the full "fast app built" phase takes 7-9s. Across ~100 integration test classes in a single pytest process that is ~10 minutes of pure FastAPI-wiring overhead with no test-observable benefit: Galaxy routes resolve the current ``gx_app`` via the module global ``galaxy.app.app`` (set by ``buildapp.app_pair`` on every launch), so the FastAPI app is effectively app-agnostic and can be shared across launches. Introduce ``caching_fast_app_factory`` in ``driver_util`` and pass it to ``launch_server`` via the existing ``init_fast_app`` parameter. The factory uses an ``@lru_cache(maxsize=1)`` slot to keep a single FastAPI instance per process and, on subsequent launches, re-binds only the per-launch pieces: - build a fresh ``WSGIMiddleware`` around the new paste webapp and swap the root ``Mount.app`` to point at it, so requests route to the current test's WSGI app; - register the new middleware's executor on the new ``gx_app``'s ``haltables`` so each test still cleans up its own thread pool; - re-apply ``GalaxyFileResponse`` sendfile-mode class attributes (they're class-level config, originally captured by the first launch's ``add_galaxy_middleware``); - install a lazy OpenAPI closure bound to the new ``gx_app`` so a test-time ``/openapi.json`` request still returns the merged schema reflecting the current test's legacy paste ``api_spec``. Integration tests don't actually hit the endpoint, so rebuilding the schema eagerly on every rebind would just re-add 3-5s per test class. The cache is skipped when a non-default ``galaxy_url_prefix`` or MCP is configured (different mount topology / lifespan-bound app). Production ``launch_server`` callers keep the default uncached ``initialize_fast_app`` — no production behaviour change, the eager ``/openapi.json`` build still happens on first production serve via ``include_legacy_openapi``. Factor the FastAPI+paste schema merge into a pure ``_build_merged_openapi`` helper shared by the production eager build and the test-time lazy rebind. Measured: - bench (4 restarts in one process): iter 0 ``fast app built`` 5,406 ms, iter 1-3 = 0.36 / 0.36 / 0.44 ms (log: "reused existing FastAPI app"). - pytest, 3 integration test classes: 72s total vs. 157s without the cache; per-subsequent-class setup ~8s vs. 27-35s. - ``/openapi.json`` still returns the full merged 455-path schema (FastAPI-only ``/api/jobs`` + legacy-only ``/api/tours``) after ``driver.restart()``, verified end-to-end. --- lib/galaxy/webapps/galaxy/fast_app.py | 20 ++++-- lib/galaxy_test/driver/driver_util.py | 99 ++++++++++++++++++++++++++- 2 files changed, 113 insertions(+), 6 deletions(-) diff --git a/lib/galaxy/webapps/galaxy/fast_app.py b/lib/galaxy/webapps/galaxy/fast_app.py index 939e7e862b9..16cbf6e934a 100644 --- a/lib/galaxy/webapps/galaxy/fast_app.py +++ b/lib/galaxy/webapps/galaxy/fast_app.py @@ -168,9 +168,7 @@ def add_galaxy_middleware(app: FastAPI, gx_app): ) -def include_legacy_openapi(app, gx_app): - if app.openapi_schema: - return app.openapi_schema +def _build_merged_openapi(app, gx_app): openapi_schema = get_openapi( title="Galaxy API", version=VERSION, @@ -180,7 +178,19 @@ def include_legacy_openapi(app, gx_app): legacy_openapi = gx_app.api_spec.to_dict() legacy_openapi["paths"].update(openapi_schema["paths"]) openapi_schema["paths"] = legacy_openapi["paths"] - app.openapi_schema = openapi_schema + return openapi_schema + + +def include_legacy_openapi(app, gx_app): + """Merge the legacy paste API spec into the FastAPI-generated schema. + + Built eagerly so production workers can serve ``/openapi.json`` + immediately on first request without paying a multi-second merge + latency on that request. + """ + if app.openapi_schema: + return app.openapi_schema + app.openapi_schema = _build_merged_openapi(app, gx_app) return app.openapi_schema @@ -276,8 +286,8 @@ def include_mcp(app: FastAPI, gx_app, mcp_app): def initialize_fast_app(gx_wsgi_webapp, gx_app): + """Build the FastAPI app that fronts the Galaxy web server.""" root_path = "" if gx_app.config.galaxy_url_prefix == "/" else gx_app.config.galaxy_url_prefix - mcp_app, mcp_lifespan = get_mcp_lifespan(gx_app) if mcp_lifespan: diff --git a/lib/galaxy_test/driver/driver_util.py b/lib/galaxy_test/driver/driver_util.py index 9aeb37cf0d6..5af1e873c9e 100644 --- a/lib/galaxy_test/driver/driver_util.py +++ b/lib/galaxy_test/driver/driver_util.py @@ -1,5 +1,6 @@ """Scripts for drivers of Galaxy functional tests.""" +import functools import http.client import json import logging @@ -20,6 +21,10 @@ from typing import ( ) from urllib.parse import urlparse +from a2wsgi import WSGIMiddleware +from fastapi import FastAPI +from starlette.routing import Mount + from galaxy.app import UniverseApplication as GalaxyUniverseApplication from galaxy.config import default_log_config from galaxy.model import mapping @@ -39,8 +44,12 @@ from galaxy.util import ( galaxy_directory, ) from galaxy.util.properties import load_app_properties +from galaxy.webapps.base.api import GalaxyFileResponse from galaxy.webapps.galaxy import buildapp -from galaxy.webapps.galaxy.fast_app import initialize_fast_app as init_galaxy_fast_app +from galaxy.webapps.galaxy.fast_app import ( + _build_merged_openapi, + initialize_fast_app as init_galaxy_fast_app, +) from galaxy_test.base.api_util import ( get_admin_api_key, get_user_api_key, @@ -746,6 +755,93 @@ def launch_gravity(port, gxit_port=None, galaxy_config=None): ) +@functools.lru_cache(maxsize=1) +def _test_fast_app_slot() -> dict: + """Per-process holder for the reusable FastAPI app used by tests. + + Keyed on nothing — ``lru_cache(maxsize=1)`` guarantees a single dict + per process. The dict holds at most one ``"app"`` entry, the + already-built FastAPI instance we can rebind for the next embedded + launch (see ``caching_fast_app_factory`` below). + """ + return {} + + +def _find_root_wsgi_mount(app: FastAPI) -> Optional[Mount]: + """Locate the ``Mount("/", wsgi_handler)`` that ``initialize_fast_app`` + installs as the final route on the FastAPI app. + """ + for route in app.routes: + if isinstance(route, Mount) and route.path == "": + # Starlette normalises Mount("/", app=...) to path="". + return route + return None + + +def _rebind_fast_app_for_launch(app: FastAPI, gx_wsgi_webapp, gx_app) -> None: + """Re-bind the per-launch pieces of a cached FastAPI app. + + Galaxy routes resolve the current ``gx_app`` via the module global + ``galaxy.app.app`` (set by ``buildapp.app_pair`` on every launch), so + the app's routes/middleware/route-name index are app-agnostic and can + be shared across repeated embedded-server launches within one Python + process (i.e. pytest). The only pieces that must be refreshed: + + - the WSGI middleware wrapping this launch's paste webapp and its + executor shutdown on ``gx_app.haltables``; + - the ``GalaxyFileResponse`` sendfile-mode class attributes (which + ``add_galaxy_middleware`` wrote from the first build's ``gx_app``); + - the merged OpenAPI schema, rebuilt lazily against the current + ``gx_app`` if ``/openapi.json`` is requested (integration tests + don't hit it, so eagerly rebuilding it per rebind would just add + several seconds of pure schema work per test class). + """ + root_mount = _find_root_wsgi_mount(app) + if root_mount is None: + raise RuntimeError("Cached FastAPI app is missing its root WSGI mount; cannot re-bind.") + new_wsgi_handler = WSGIMiddleware(gx_wsgi_webapp) + gx_app.haltables.append(("WSGI Middleware threadpool", new_wsgi_handler.executor.shutdown)) + root_mount.app = new_wsgi_handler # type: ignore[assignment] + GalaxyFileResponse.nginx_x_accel_redirect_base = gx_app.config.nginx_x_accel_redirect_base + GalaxyFileResponse.apache_xsendfile = gx_app.config.apache_xsendfile + app.openapi_schema = None + + def _lazy_openapi() -> dict: + if app.openapi_schema is None: + app.openapi_schema = _build_merged_openapi(app, gx_app) + return app.openapi_schema + + app.openapi = _lazy_openapi # type: ignore[method-assign] + + +def caching_fast_app_factory(gx_wsgi_webapp, gx_app): + """Drop-in replacement for ``init_galaxy_fast_app`` that reuses the + FastAPI app across repeated embedded-server launches in the same + Python process. + + Single injection point: this callable is passed to ``launch_server`` + via its ``init_fast_app`` parameter. Production ``launch_server`` + callers (outside the test driver) keep using the default + uncached ``init_galaxy_fast_app``. + + Falls back to a fresh build when the topology differs from the + cached shell (non-default ``galaxy_url_prefix`` or MCP enabled), + because those paths produce a parent wrapper / lifespan-bound + app that is awkward to re-bind. + """ + topology_differs = gx_app.config.galaxy_url_prefix != "/" or gx_app.config.enable_mcp_server + if topology_differs: + return init_galaxy_fast_app(gx_wsgi_webapp, gx_app) + slot = _test_fast_app_slot() + existing = slot.get("app") + if existing is None: + app = init_galaxy_fast_app(gx_wsgi_webapp, gx_app) + slot["app"] = app + return app + _rebind_fast_app_for_launch(existing, gx_wsgi_webapp, gx_app) + return existing + + def launch_server( app_factory, webapp_factory, @@ -943,6 +1039,7 @@ class GalaxyTestDriver(TestDriver): webapp_factory=lambda *args, **kwd: buildapp.app_factory(*args, wsgi_preflight=False, **kwd), galaxy_config=galaxy_config, config_object=config_object, + init_fast_app=caching_fast_app_factory, ) self.server_wrappers.append(server_wrapper) else: From 80b3a10db8072d0542ab3c53a92e59b725c6fa8e Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Tue, 21 Apr 2026 21:41:20 +0200 Subject: [PATCH 3/5] Rebind TUS routes and Galaxy middleware on cached fast app reuse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The embedded-test FastAPI cache reused the first test class's: - TUS routes, which ``create_tus_router`` bakes ``files_dir`` / ``max_size`` into at build time — pointing at the first test's ``tus_upload_store`` temp directory, which is removed on teardown. Later tests' TUS uploads failed, surfacing as workflow/upload selenium failures and direct TUS test breakage. - ``GalaxyCORSMiddleware`` / ``XFrameOptionsMiddleware``, which capture ``gx_app.config`` at ``add_middleware`` time — leaving later tests enforcing the first test's ``allowed_origin_hostnames`` / ``x_frame_options`` and breaking CORS integration tests. Rebind both on each cached-app reuse: strip the old TUS routes (while preserving the root WSGI mount's trailing position), re-include fresh ones bound to the current ``gx_app``'s upload store, and re-run ``add_galaxy_middleware`` after evicting the old Galaxy middleware entries from ``app.user_middleware``. Reset ``middleware_stack`` so Starlette rebuilds it on the next request, and refresh ``route_name_index`` to reflect the replaced TUS routes. ``add_galaxy_middleware`` also re-applies ``GalaxyFileResponse`` sendfile attributes, so the explicit re-assignment in ``_rebind_fast_app_for_launch`` is now redundant and removed. --- lib/galaxy_test/driver/driver_util.py | 70 ++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/lib/galaxy_test/driver/driver_util.py b/lib/galaxy_test/driver/driver_util.py index 5af1e873c9e..c171a446349 100644 --- a/lib/galaxy_test/driver/driver_util.py +++ b/lib/galaxy_test/driver/driver_util.py @@ -44,11 +44,15 @@ from galaxy.util import ( galaxy_directory, ) from galaxy.util.properties import load_app_properties -from galaxy.webapps.base.api import GalaxyFileResponse +from galaxy.webapps.base.api import build_route_name_index from galaxy.webapps.galaxy import buildapp from galaxy.webapps.galaxy.fast_app import ( _build_merged_openapi, + add_galaxy_middleware, + GalaxyCORSMiddleware, + include_tus, initialize_fast_app as init_galaxy_fast_app, + XFrameOptionsMiddleware, ) from galaxy_test.base.api_util import ( get_admin_api_key, @@ -778,19 +782,72 @@ def _find_root_wsgi_mount(app: FastAPI) -> Optional[Mount]: return None +_TUS_PREFIXES = ( + "/api/upload/resumable_upload", + "/api/job_files/resumable_upload", +) + + +def _rebind_tus_routes(app: FastAPI, gx_app) -> None: + """Replace TUS routes so they point at the current launch's upload store. + + ``create_tus_router`` bakes ``files_dir`` / ``max_size`` into the route + handlers at build time, so the cached app's TUS routes would otherwise + keep writing to the first launch's ``tus_upload_store`` — a temp + directory that's removed when that test class tears down. + """ + root_mount = _find_root_wsgi_mount(app) + tus_routes = [r for r in app.router.routes if _is_tus_route(r)] + for r in tus_routes: + app.router.routes.remove(r) + if root_mount is not None: + app.router.routes.remove(root_mount) + include_tus(app, gx_app) + if root_mount is not None: + app.router.routes.append(root_mount) + + +def _is_tus_route(route) -> bool: + path = getattr(route, "path", None) + return bool(path and any(path.startswith(p) for p in _TUS_PREFIXES)) + + +def _rebind_galaxy_middleware(app: FastAPI, gx_app) -> None: + """Re-run ``add_galaxy_middleware`` against the current gx_app. + + CORS / X-Frame-Options middleware capture ``gx_app.config`` at the + moment they're added to ``app.user_middleware``; without this the + cached app would keep enforcing the first test's + ``allowed_origin_hostnames`` / ``x_frame_options`` for every + subsequent test. + """ + app.user_middleware = [ + mw for mw in app.user_middleware if mw.cls not in (GalaxyCORSMiddleware, XFrameOptionsMiddleware) + ] + add_galaxy_middleware(app, gx_app) + # Force Starlette to rebuild its middleware stack on the next request + # so the re-added middleware actually takes effect. + app.middleware_stack = None + + def _rebind_fast_app_for_launch(app: FastAPI, gx_wsgi_webapp, gx_app) -> None: """Re-bind the per-launch pieces of a cached FastAPI app. Galaxy routes resolve the current ``gx_app`` via the module global ``galaxy.app.app`` (set by ``buildapp.app_pair`` on every launch), so - the app's routes/middleware/route-name index are app-agnostic and can - be shared across repeated embedded-server launches within one Python - process (i.e. pytest). The only pieces that must be refreshed: + most of the app's routes are app-agnostic and can be shared across + repeated embedded-server launches within one Python process (i.e. + pytest). The pieces that must be refreshed: - the WSGI middleware wrapping this launch's paste webapp and its executor shutdown on ``gx_app.haltables``; + - the TUS routes, which bake this launch's ``tus_upload_store`` + directory into their route handlers; + - Galaxy-specific middleware (CORS, X-Frame-Options), which capture + this launch's ``gx_app.config`` at add time; - the ``GalaxyFileResponse`` sendfile-mode class attributes (which ``add_galaxy_middleware`` wrote from the first build's ``gx_app``); + - the route-name index, since TUS routes were just replaced; - the merged OpenAPI schema, rebuilt lazily against the current ``gx_app`` if ``/openapi.json`` is requested (integration tests don't hit it, so eagerly rebuilding it per rebind would just add @@ -802,8 +859,9 @@ def _rebind_fast_app_for_launch(app: FastAPI, gx_wsgi_webapp, gx_app) -> None: new_wsgi_handler = WSGIMiddleware(gx_wsgi_webapp) gx_app.haltables.append(("WSGI Middleware threadpool", new_wsgi_handler.executor.shutdown)) root_mount.app = new_wsgi_handler # type: ignore[assignment] - GalaxyFileResponse.nginx_x_accel_redirect_base = gx_app.config.nginx_x_accel_redirect_base - GalaxyFileResponse.apache_xsendfile = gx_app.config.apache_xsendfile + _rebind_tus_routes(app, gx_app) + _rebind_galaxy_middleware(app, gx_app) + app.state.route_name_index = build_route_name_index(app) app.openapi_schema = None def _lazy_openapi() -> dict: From 22f5fe3614edae5c42a79ce037db552bc335282e Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Wed, 22 Apr 2026 09:21:37 +0200 Subject: [PATCH 4/5] Reset middleware_stack before re-adding Galaxy middleware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Starlette's ``add_middleware`` guards against mutations once the stack has been built (at first request), so the rebind path was hitting ``RuntimeError: Cannot add middleware after an application has started`` at every subsequent test-class setup — ``caching_fast_app_factory`` → ``_rebind_galaxy_middleware`` → ``add_galaxy_middleware.add_middleware``. The fix is ordering: clear ``middleware_stack`` first, then re-run ``add_galaxy_middleware``. Starlette then rebuilds the stack lazily from ``user_middleware`` on the next request with the current ``gx_app`` config picked up. --- lib/galaxy_test/driver/driver_util.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/galaxy_test/driver/driver_util.py b/lib/galaxy_test/driver/driver_util.py index c171a446349..e0520967553 100644 --- a/lib/galaxy_test/driver/driver_util.py +++ b/lib/galaxy_test/driver/driver_util.py @@ -824,10 +824,12 @@ def _rebind_galaxy_middleware(app: FastAPI, gx_app) -> None: app.user_middleware = [ mw for mw in app.user_middleware if mw.cls not in (GalaxyCORSMiddleware, XFrameOptionsMiddleware) ] - add_galaxy_middleware(app, gx_app) - # Force Starlette to rebuild its middleware stack on the next request - # so the re-added middleware actually takes effect. + # Reset the middleware stack BEFORE re-adding middleware: Starlette's + # ``add_middleware`` guards against mutations after the stack has been + # built (first request), so we have to clear it first. The stack will + # be rebuilt lazily from ``user_middleware`` on the next request. app.middleware_stack = None + add_galaxy_middleware(app, gx_app) def _rebind_fast_app_for_launch(app: FastAPI, gx_wsgi_webapp, gx_app) -> None: From 84007cb2ba026b893de3a5ba2b7c7ed41ed0e7e8 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Wed, 22 Apr 2026 16:03:38 +0200 Subject: [PATCH 5/5] Reset router.lifespan_context snapshot before re-including TUS FastAPI's include_router unconditionally wraps lifespan_context with _merge_lifespan_context on every call. Each rebind calls include_tus, which calls include_router twice, so lifespan_context grew by two layers per test class. After several hundred classes uvicorn startup recursed into the chain and hit Python's recursion limit -- every subsequent server launch returned "Application startup failed" and tests timed out waiting for 200 OK. Snapshot the lifespan_context once at first build, restore it before re-calling include_tus on each rebind, so the chain stays bounded. --- lib/galaxy_test/driver/driver_util.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/galaxy_test/driver/driver_util.py b/lib/galaxy_test/driver/driver_util.py index e0520967553..5ae855ce074 100644 --- a/lib/galaxy_test/driver/driver_util.py +++ b/lib/galaxy_test/driver/driver_util.py @@ -788,13 +788,19 @@ _TUS_PREFIXES = ( ) -def _rebind_tus_routes(app: FastAPI, gx_app) -> None: +def _rebind_tus_routes(app: FastAPI, gx_app, original_lifespan_context) -> None: """Replace TUS routes so they point at the current launch's upload store. ``create_tus_router`` bakes ``files_dir`` / ``max_size`` into the route handlers at build time, so the cached app's TUS routes would otherwise keep writing to the first launch's ``tus_upload_store`` — a temp directory that's removed when that test class tears down. + + Restores ``router.lifespan_context`` to the snapshot taken at first build + before re-calling ``include_tus``; FastAPI's ``include_router`` wraps + ``lifespan_context`` on every call, so without this the chain would grow + by two layers per launch and blow Python's recursion limit after a few + hundred test classes. """ root_mount = _find_root_wsgi_mount(app) tus_routes = [r for r in app.router.routes if _is_tus_route(r)] @@ -802,6 +808,7 @@ def _rebind_tus_routes(app: FastAPI, gx_app) -> None: app.router.routes.remove(r) if root_mount is not None: app.router.routes.remove(root_mount) + app.router.lifespan_context = original_lifespan_context include_tus(app, gx_app) if root_mount is not None: app.router.routes.append(root_mount) @@ -832,7 +839,7 @@ def _rebind_galaxy_middleware(app: FastAPI, gx_app) -> None: add_galaxy_middleware(app, gx_app) -def _rebind_fast_app_for_launch(app: FastAPI, gx_wsgi_webapp, gx_app) -> None: +def _rebind_fast_app_for_launch(app: FastAPI, gx_wsgi_webapp, gx_app, original_lifespan_context) -> None: """Re-bind the per-launch pieces of a cached FastAPI app. Galaxy routes resolve the current ``gx_app`` via the module global @@ -861,7 +868,7 @@ def _rebind_fast_app_for_launch(app: FastAPI, gx_wsgi_webapp, gx_app) -> None: new_wsgi_handler = WSGIMiddleware(gx_wsgi_webapp) gx_app.haltables.append(("WSGI Middleware threadpool", new_wsgi_handler.executor.shutdown)) root_mount.app = new_wsgi_handler # type: ignore[assignment] - _rebind_tus_routes(app, gx_app) + _rebind_tus_routes(app, gx_app, original_lifespan_context) _rebind_galaxy_middleware(app, gx_app) app.state.route_name_index = build_route_name_index(app) app.openapi_schema = None @@ -897,8 +904,9 @@ def caching_fast_app_factory(gx_wsgi_webapp, gx_app): if existing is None: app = init_galaxy_fast_app(gx_wsgi_webapp, gx_app) slot["app"] = app + slot["lifespan_context"] = app.router.lifespan_context return app - _rebind_fast_app_for_launch(existing, gx_wsgi_webapp, gx_app) + _rebind_fast_app_for_launch(existing, gx_wsgi_webapp, gx_app, slot["lifespan_context"]) return existing