All checks were successful
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 9s
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m16s
Замер на проде 11.09 (изнутри хоста, тот же контейнер): - одна оценка 0.44 с (повтор адреса) / 0.97 с (новый адрес), из них БД 252/458 мс; - N=8 параллельных — все 200, heartbeat /health p95 5-7 мс, max 113-160 мс: loop сегодня НЕ голодает, «добавить воркеров uvicorn» замером не подтверждается (и умножило бы на N оба семафора, пять in-process лимитеров и пул); - зато одна фоновая догрузка Яндекса держала коннект пула 8.5 с (лиз прокси 33.856 → запись 42.334), а таких задач разрешено 8 при пуле 15. Правки: - estimator `_db_step`: SELECT/UPSERT кэша источников уходят в `asyncio.to_thread` и завершают транзакцию — коннект возвращается в пул ДО внешнего HTTP; - core/db: max_overflow 10→15 (потолок 20 на процесс ≥ 4+4+8 объявленных потолков одновременности) и pool_timeout 30→5 с (короче бюджета источника 8 с, иначе занятый пул съедает и бюджет запроса, и поток to_thread). Локальный замер ДО/ПОСЛЕ на тех же величинах: loop стоял 301 мс (0 тиков соседней корутины) → 0.2 мс (23.5k тиков); ожидание коннекта соседом во время фетча — таймаут пула → 0.1 мс. Refs #3083, #3408
51 lines
3.1 KiB
Python
51 lines
3.1 KiB
Python
from collections.abc import Generator
|
||
|
||
from sqlalchemy import create_engine
|
||
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
|
||
|
||
from app.core.config import settings
|
||
|
||
engine = create_engine(
|
||
settings.database_url,
|
||
pool_pre_ping=True,
|
||
future=True,
|
||
# #3194: SQLAlchemy печатает ВСЕ bind-параметры в тексте StatementError —
|
||
# через них в GlitchTip уезжали ключ шифрования кук и сами куки
|
||
# (pgp_sym_encrypt(:cookies_json, :key)). Флаг на УРОВНЕ ДВИЖКА кроет все
|
||
# сайты вызова разом, включая будущие.
|
||
# НЕ закрывает: текст ошибки самого драйвера (Postgres DETAIL со значением)
|
||
# и сырые psycopg-подключения мимо движков — это отдельный класс.
|
||
hide_parameters=True,
|
||
# #3408 п.1. Потолок пула обязан быть НЕ МЕНЬШЕ суммы потолков одновременности,
|
||
# которые сам же процесс и объявляет: 4 оценки (`api/v1/trade_in`
|
||
# `_ESTIMATE_CONCURRENCY`) + 4 подсказки (`api/public/mera` `_SUGGEST_CONCURRENCY`)
|
||
# + 8 фоновых догрузок (`services/estimator` `_MAX_DEFERRED_REFRESH_TASKS`) = 16.
|
||
# Дефолт SQLAlchemy 5+10=15 меньше этой суммы, то есть исчерпать пул можно
|
||
# штатной работой, не абузом. Гейт — tests/test_3408_pool_ceiling.py.
|
||
#
|
||
# pool_size оставлен дефолтным (5): это ПОСТОЯННО открытые коннекты, а в покое
|
||
# прод держит 5-6 (замер 11.09). Растёт только overflow — коннекты пика,
|
||
# которые пул закрывает сам. Потолок процесса: 5 + 15 = 20; воркер один
|
||
# (docker-compose.prod.yml, uvicorn без --workers), Postgres max_connections=100.
|
||
max_overflow=15,
|
||
# Дефолтные 30 с ожидания коннекта — вчетверо больше любого бюджета внешнего
|
||
# источника в эстиматоре (8 с, `estimate_*_timeout_s`). Такой чекаут нельзя
|
||
# прервать `asyncio.wait_for`: он занимает поток `asyncio.to_thread` целиком, а
|
||
# пул потоков сам конечен (min(32, cpu+4)) — исчерпанный пул коннектов так
|
||
# превращается в исчерпанный пул потоков. 5 с < бюджета источника: занятый пул
|
||
# деградирует ОДИН источник, а не весь запрос.
|
||
pool_timeout=5,
|
||
)
|
||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, expire_on_commit=False)
|
||
|
||
|
||
class Base(DeclarativeBase):
|
||
pass
|
||
|
||
|
||
def get_db() -> Generator[Session, None, None]:
|
||
db = SessionLocal()
|
||
try:
|
||
yield db
|
||
finally:
|
||
db.close()
|