Jun 26 MT-1 phase

This commit is contained in:
2026-06-26 16:37:44 -04:00
parent fc39491891
commit 07cf2e5816
9 changed files with 300 additions and 55 deletions
+59
View File
@@ -0,0 +1,59 @@
"""
app/tenancy/engine_cache.py
---------------------------
Process-local cache of per-tenant SQLAlchemy engines, keyed by tenant id.
Each tenant has its own database, hence its own engine + connection pool.
Engines are created lazily on first use and reused across requests. Total
backend connections ≈ workers × cached-tenants × pool_size, so pool sizing is
a real scaling lever (see MULTI_TENANT_PLAN.md §4); tune via config, or set a
small pool / switch to NullPool when the tenant count grows large.
`invalidate(tenant_id)` drops a cached engine (e.g. after credential rotation
or tenant suspension); the next request rebuilds it.
"""
import threading
from flask import current_app
from sqlalchemy import create_engine
_engines = {}
_lock = threading.Lock()
def get_tenant_engine(tenant):
"""Return (building if needed) the cached engine for a TenantContext."""
engine = _engines.get(tenant.id)
if engine is not None:
return engine
with _lock:
engine = _engines.get(tenant.id)
if engine is None:
engine = create_engine(
tenant.db_uri,
pool_pre_ping=True,
pool_size=current_app.config.get('TENANT_ENGINE_POOL_SIZE', 5),
max_overflow=current_app.config.get('TENANT_ENGINE_MAX_OVERFLOW', 5),
pool_recycle=current_app.config.get('TENANT_ENGINE_POOL_RECYCLE', 1800),
future=True,
)
_engines[tenant.id] = engine
return engine
def invalidate(tenant_id):
"""Drop and dispose a cached tenant engine, if present."""
with _lock:
engine = _engines.pop(tenant_id, None)
if engine is not None:
engine.dispose()
def clear():
"""Dispose and drop all cached engines (test/teardown helper)."""
with _lock:
engines = list(_engines.values())
_engines.clear()
for engine in engines:
engine.dispose()