Files
ewoooc/gunicorn.conf.py
OoO 6bce46bbc7
All checks were successful
CD Pipeline / deploy (push) Successful in 2m29s
fix(runtime): 強化健康檢查監控韌性
2026-05-01 14:46:49 +08:00

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,
)