Files
ewoooc/gunicorn.conf.py
OoO 8bc0fd7ff6
Some checks failed
CD Pipeline / deploy (push) Failing after 9m32s
fix(gunicorn): preload 後重置 SQLAlchemy 連線池
2026-04-30 00:07:10 +08:00

70 lines
2.1 KiB
Python

"""Gunicorn runtime config.
`preload_app` imports Flask before worker fork. Any SQLAlchemy engines created
during import must drop inherited connection pools in each child worker.
"""
import os
import sys
from sqlalchemy.engine import Engine
bind = "0.0.0.0:80"
workers = int(os.getenv("WEB_CONCURRENCY", "4"))
timeout = int(os.getenv("GUNICORN_TIMEOUT", "300"))
accesslog = "-"
errorlog = "-"
preload_app = True
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.")
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:
engine = getattr(value, "engine", None)
if isinstance(engine, Engine):
candidates.append((engine, f"{module_name}.{attr_name}.engine"))
for engine, label in candidates:
engine_id = id(engine)
if engine_id in disposed_ids:
continue
disposed_ids.add(engine_id)
if _dispose_engine(engine, label, server):
disposed_count += 1
server.log.info(
"Worker %s reset %s SQLAlchemy engine pool(s) after preload fork",
worker.pid,
disposed_count,
)