-
Notifications
You must be signed in to change notification settings - Fork 595
Description
Problem
Importing mcpgateway.main takes ~2.7–3.0s due to module-level side effects that run at import time. This cost is paid by every test that uses the function-scoped app fixture in tests/conftest.py, making those tests significantly slower than they need to be.
Affected test examples (setup time, not test execution):
tests/unit/mcpgateway/test_main_extended.py::TestAdminAuthMiddleware::test_admin_auth_trailing_slash_root_path— 2.78s setuptests/unit/mcpgateway/routers/test_metrics_maintenance.py::test_metrics_rollup_disabled— 2.73s setuptests/unit/mcpgateway/routers/test_well_known_rfc9728.py::TestRFC9728CompliantEndpoint::test_rfc9728_endpoint_oauth_not_enabled— 2.71s setuptests/unit/mcpgateway/test_main_extended.py::TestConditionalPaths::test_redis_initialization_path— 2.71s setup
Root cause
mcpgateway/main.py performs substantial work at module import time (lines ~160–227):
- Database readiness check (
wait_for_db_ready) — synchronous retry loop at import time (line 172) - Database bootstrap (
bootstrap_db()) — runs Alembic migrations, creates tables, creates admin users, sets up RBAC roles (line 178). This is the most expensive step. - Redis readiness check (
wait_for_redis_ready) — another synchronous retry loop (line 217) - Service singleton instantiation — ~10 service singletons created at module level (lines 196–213)
- Session registry initialization (lines 220–227)
- Heavy import chain —
main.pyimports ~50 modules at the top level (lines 30–159), each potentially triggering further import-time work
The function-scoped app fixture in tests/conftest.py does from mcpgateway.main import app on every test invocation. While Python caches module objects, the fixture still creates a fresh temp SQLite DB, patches SessionLocal across 6+ modules, and runs Base.metadata.create_all() per test.
Why we can't simply use module-scoped fixtures
A module-scoped app_with_temp_db fixture already exists, but most tests mutate shared state (DB rows, app.dependency_overrides, patched settings) that would leak between tests. Switching to module scope would require adding explicit cleanup/rollback logic to every test, which is fragile and increases maintenance burden.
Suggested investigation areas
-
Defer
bootstrap_db()to lifespan — Move the Alembic migration + admin user creation out of module-level code and into thelifespan()async context manager, which already handles all other service initialization. Test fixtures could then skip it entirely. -
Lazy service singletons — Replace module-level singleton instantiation (lines 196–213) with lazy initialization patterns (e.g.,
functools.lru_cacheorget_*_service()factory functions that several services already use). -
Guard DB/Redis readiness checks —
wait_for_db_readyandwait_for_redis_readyrun unconditionally at import time. These could be deferred tolifespan()or guarded with aSKIP_STARTUP_CHECKSenv var for test environments. -
Reduce
SessionLocalpatch surface — Currently,SessionLocalmust be patched in 6+ modules because each doesfrom mcpgateway.db import SessionLocalat import time, capturing the original reference. Migrating these toimport mcpgateway.db as _db_mod+_db_mod.SessionLocal()(as was done forbase_service.pyin PR [EPIC][A2A]: A2A Protocol v0.3.0 Full Compliance Implementation #3150) would allow a single patch onmcpgateway.db.SessionLocalto propagate everywhere. -
Lightweight test app factory — Consider a
create_test_app()factory that builds a minimal FastAPI app with only the routers/middleware needed for the test, rather than importing the full production app.
Acceptance criteria
- Identify the top contributors to the ~3s import cost (profile with
python -X importtime) - Reduce function-scoped
appfixture setup time to <1s without sacrificing test isolation - No regressions in test pass rate or production startup behavior