Files
app-autotest/site/db/pool.py
T

101 lines
4.2 KiB
Python

"""
Connection pool для PostgreSQL (psycopg2) — lazy-init в каждом gunicorn-воркере.
Почему lazy-init: gunicorn форкает воркеры, и если создать пул до fork,
все воркеры будут делить ОДНО соединение → гонки и падения.
Поэтому _pool создаётся при ПЕРВОМ обращении к БД в каждом воркере отдельно.
Переменные окружения (КАЖДАЯ отдельно, не DSN-строкой):
DB_HOST — хост PostgreSQL
DB_PORT — порт (по умолчанию 5432)
DB_NAME — имя базы данных
DB_USER — пользователь
DB_PASSWORD — пароль
DB_SSLMODE — sslmode (по умолчанию "require")
ThreadedConnectionPool(1, 5):
- Минимум 1 соединение (всегда готово)
- Максимум 5 одновременных соединений на воркер
- При 2 воркерах gunicorn: макс. 10 соединений к БД всего
"""
import os
import psycopg2
from psycopg2 import pool
# _pool — по одному на gunicorn-воркер (создаётся при первом get_conn)
_pool = None
# _initialized — схема уже применена в этом воркере
_initialized = False
def _dsn():
"""Построить DSN-строку из отдельных переменных окружения.
DSN (Data Source Name) — это строка подключения для psycopg2:
"host=... port=... dbname=... user=... password=... sslmode=..."
Каждое поле отдельной переменной — безопаснее чем один DATABASE_URL."""
return (
f"host={os.getenv('DB_HOST')} "
f"port={os.getenv('DB_PORT', '5432')} "
f"dbname={os.getenv('DB_NAME')} "
f"user={os.getenv('DB_USER')} "
f"password={os.getenv('DB_PASSWORD')} "
f"sslmode={os.getenv('DB_SSLMODE', 'require')}"
)
def _ensure_schema():
"""Инициализировать схему БД при первом обращении в этом воркере.
Вызывает init_db() который делает CREATE IF NOT EXISTS — идемпотентно.
_initialized — глобальный флаг чтобы не дёргать init_db при каждом запросе."""
global _initialized
if _initialized:
return
try:
from db.init_db import init_db
init_db()
_initialized = True
except Exception:
pass # без БД приложение работает (без истории)
def get_pool():
"""Получить connection pool (создать при первом вызове).
ThreadedConnectionPool — каждый поток получает своё соединение.
Для gunicorn с sync-воркерами (не threads) это эквивалентно SimpleConnectionPool.
Returns:
ThreadedConnectionPool или None если DB_USER не задан."""
global _pool
if _pool is None:
if not os.getenv("DB_USER"):
return None # нет переменных БД — работаем без истории
_pool = pool.ThreadedConnectionPool(1, 5, _dsn())
_ensure_schema() # при первом соединении применяем схему
return _pool
def get_conn():
"""Взять соединение из пула.
Вызывается перед КАЖДЫМ запросом к БД.
Возвращает None если БД не настроена.
ВАЖНО: после использования ОБЯЗАТЕЛЬНО вернуть через put_conn()."""
p = get_pool()
return p.getconn() if p else None
def put_conn(conn):
"""Вернуть соединение в пул.
ВСЕГДА вызывается в finally-блоке после get_conn.
Если не вернуть — пул исчерпается и приложение встанет."""
p = get_pool()
if p and conn:
p.putconn(conn)