92 lines
3.1 KiB
Python
92 lines
3.1 KiB
Python
"""Gunicorn runtime config.
|
|
|
|
Workers import Flask themselves so `HUP` can reload bind-mounted Python files
|
|
without restarting the app container. If preload is re-enabled, hot reload will
|
|
restart workers but keep the preloaded app object from the old master process.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
from sqlalchemy.engine import Engine
|
|
|
|
|
|
bind = "0.0.0.0:80"
|
|
workers = int(os.getenv("WEB_CONCURRENCY", "4"))
|
|
worker_class = os.getenv("GUNICORN_WORKER_CLASS", "gthread")
|
|
threads = int(os.getenv("GUNICORN_THREADS", "4"))
|
|
timeout = int(os.getenv("GUNICORN_TIMEOUT", "300"))
|
|
accesslog = "-"
|
|
errorlog = "-"
|
|
preload_app = False
|
|
|
|
|
|
def _dispose_engine(engine, label, server):
|
|
try:
|
|
try:
|
|
engine.dispose(close=False)
|
|
except TypeError:
|
|
engine.dispose()
|
|
server.log.info("Disposed inherited SQLAlchemy engine after fork: %s", label)
|
|
return True
|
|
except Exception:
|
|
server.log.exception("Failed disposing inherited SQLAlchemy engine: %s", label)
|
|
return False
|
|
|
|
|
|
def post_fork(server, worker):
|
|
"""Reset DB pools inherited from the preloaded master process.
|
|
|
|
SQLAlchemy engines are safe to keep as objects after fork, but their pools
|
|
must be replaced so workers do not share PostgreSQL TCP sockets.
|
|
"""
|
|
disposed_ids = set()
|
|
disposed_count = 0
|
|
prefixes = ("app", "database.", "routes.", "services.")
|
|
|
|
def dispose_once(engine, label):
|
|
nonlocal disposed_count
|
|
engine_id = id(engine)
|
|
if engine_id in disposed_ids:
|
|
return
|
|
disposed_ids.add(engine_id)
|
|
if _dispose_engine(engine, label, server):
|
|
disposed_count += 1
|
|
|
|
for module_name, module in list(sys.modules.items()):
|
|
if module is None or not module_name.startswith(prefixes):
|
|
continue
|
|
|
|
for attr_name, value in vars(module).items():
|
|
candidates = []
|
|
if isinstance(value, Engine):
|
|
candidates.append((value, f"{module_name}.{attr_name}"))
|
|
else:
|
|
try:
|
|
engine = getattr(value, "engine", None)
|
|
except Exception:
|
|
# Flask/Werkzeug LocalProxy objects may require a request
|
|
# context when inspected. They cannot own global DB pools.
|
|
continue
|
|
if isinstance(engine, Engine):
|
|
candidates.append((engine, f"{module_name}.{attr_name}.engine"))
|
|
|
|
for engine, label in candidates:
|
|
dispose_once(engine, label)
|
|
|
|
manager_module = sys.modules.get("database.manager")
|
|
manager_class = getattr(manager_module, "DatabaseManager", None)
|
|
manager_cache = getattr(manager_class, "_instance_cache", {}) if manager_class else {}
|
|
for cache_key, cached in list(manager_cache.items()):
|
|
if not isinstance(cached, dict):
|
|
continue
|
|
engine = cached.get("engine")
|
|
if isinstance(engine, Engine):
|
|
dispose_once(engine, f"database.manager.DatabaseManager._instance_cache[{cache_key!r}]")
|
|
|
|
server.log.info(
|
|
"Worker %s reset %s SQLAlchemy engine pool(s) after preload fork",
|
|
worker.pid,
|
|
disposed_count,
|
|
)
|