fix: единый singleton engine в bd, убраны 17 локальных make_engine()
- bd/__init__.py: добавлены get_engine() и get_session() — один SQLAlchemy engine на весь процесс - make_engine(): pool_size=5, max_overflow=5, pool_timeout=30, pool_recycle=1800 - Все 17 route-файлов: убраны локальные get_session()/make_engine(), импорт из bd - transfer_crud.py: _get_session() → get_session() из bd - init_data_base.py: get_engine() вместо make_engine() - Результат: 78 idle соединений → 1
This commit is contained in:
@@ -47,7 +47,11 @@ def make_engine():
|
||||
s.DATABASE_URL_syncpg,
|
||||
connect_args=s.PG8000_CONNECT_ARGS,
|
||||
future=True,
|
||||
pool_pre_ping=True, # проверяет соединение перед каждым запросом
|
||||
pool_pre_ping=True,
|
||||
pool_size=5,
|
||||
max_overflow=5,
|
||||
pool_timeout=30,
|
||||
pool_recycle=1800,
|
||||
)
|
||||
|
||||
|
||||
@@ -70,3 +74,22 @@ def wait_for_db(retries: int = 15, delay: float = 2.0) -> None:
|
||||
if attempt < retries:
|
||||
time.sleep(delay)
|
||||
raise RuntimeError("PostgreSQL did not become ready in time.")
|
||||
|
||||
# ── Singleton engine + session factory (один движок на весь процесс) ───────
|
||||
_engine = None
|
||||
_SessionFactory = None
|
||||
|
||||
|
||||
def get_engine():
|
||||
global _engine
|
||||
if _engine is None:
|
||||
_engine = make_engine()
|
||||
return _engine
|
||||
|
||||
|
||||
def get_session():
|
||||
global _SessionFactory
|
||||
if _SessionFactory is None:
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
_SessionFactory = sessionmaker(bind=get_engine(), future=True)
|
||||
return _SessionFactory
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Optional
|
||||
from datetime import datetime, timezone
|
||||
import uuid
|
||||
|
||||
from bd import make_engine
|
||||
from bd import get_session
|
||||
|
||||
from bd.tables.users import User
|
||||
from route.auth_utils import hash_password, verify_password, create_access_token, SECRET_KEY, ALGORITHM, oauth2_scheme
|
||||
@@ -15,9 +15,6 @@ from redis_db import blacklist_token
|
||||
router = APIRouter(tags=["auth"], prefix="/auth")
|
||||
|
||||
|
||||
def get_session():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
return sessionmaker(bind=make_engine(), future=True)
|
||||
|
||||
|
||||
class RegisterIn(BaseModel):
|
||||
|
||||
@@ -9,7 +9,7 @@ from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
|
||||
from bd import make_engine
|
||||
from bd import get_session
|
||||
|
||||
SECRET_KEY: str = os.getenv("SECRET_KEY", "")
|
||||
if not SECRET_KEY:
|
||||
@@ -45,9 +45,6 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -
|
||||
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
|
||||
|
||||
def get_session():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
return sessionmaker(bind=make_engine(), future=True)
|
||||
|
||||
|
||||
def get_current_user(token: str = Depends(oauth2_scheme)):
|
||||
|
||||
@@ -3,7 +3,7 @@ from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from bd import make_engine
|
||||
from bd import get_session
|
||||
|
||||
from bd.tables.choice import Choice
|
||||
from bd.tables.question import Question
|
||||
@@ -23,9 +23,6 @@ class ChoiceUpdate(BaseModel):
|
||||
position: Optional[int]
|
||||
|
||||
|
||||
def get_session():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
return sessionmaker(bind=make_engine(), future=True)
|
||||
|
||||
|
||||
@router.post("/", status_code=201, dependencies=[Depends(require_admin_key)])
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
import uuid
|
||||
|
||||
from bd import make_engine
|
||||
from bd import get_session
|
||||
from bd.tables.poll import Poll
|
||||
from bd.tables.question import Question
|
||||
from bd.tables.choice import Choice
|
||||
@@ -372,9 +372,6 @@ CHOICE_SCORES: dict[int, float] = {
|
||||
CHOICE_LABELS: list[str] = ["++", "+", "0", "-", "--"]
|
||||
|
||||
|
||||
def get_session():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
return sessionmaker(bind=make_engine(), future=True)
|
||||
|
||||
|
||||
@router.post("/glomshtok", status_code=201)
|
||||
|
||||
@@ -3,7 +3,7 @@ from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from bd import make_engine
|
||||
from bd import get_session
|
||||
|
||||
from bd.tables.group import Group
|
||||
from route.auth_utils import require_admin_key
|
||||
@@ -19,9 +19,6 @@ class GroupUpdate(BaseModel):
|
||||
name_group: Optional[str] = None
|
||||
|
||||
|
||||
def get_session():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
return sessionmaker(bind=make_engine(), future=True)
|
||||
|
||||
|
||||
def _group_dict(grp: Group) -> dict:
|
||||
|
||||
@@ -2,7 +2,7 @@ from fastapi import APIRouter, HTTPException
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from bd import make_engine
|
||||
from bd import get_session
|
||||
|
||||
from bd.tables.poll import Poll
|
||||
from bd.tables.question import Question
|
||||
@@ -141,9 +141,6 @@ DIMENSIONS = [
|
||||
]
|
||||
|
||||
|
||||
def get_session():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
return sessionmaker(bind=make_engine(), future=True)
|
||||
|
||||
|
||||
@router.post("/holland", status_code=201)
|
||||
|
||||
@@ -4,7 +4,7 @@ import importlib
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from bd import Settings, make_engine
|
||||
from bd import Settings, get_engine
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from pydantic import BaseModel
|
||||
@@ -48,7 +48,7 @@ async def create_tables():
|
||||
|
||||
Endpoint вызывается по нажатию кнопки в UI (POST).
|
||||
"""
|
||||
engine = make_engine()
|
||||
engine = get_engine()
|
||||
|
||||
metadatas = collect_metadatas()
|
||||
if not metadatas:
|
||||
@@ -73,7 +73,7 @@ async def create_tables():
|
||||
@router.post("/db/migrate-cascade-user", dependencies=[Depends(require_admin_key)])
|
||||
async def migrate_cascade_user():
|
||||
"""Добавляет FK responses.user_id → users.id ON DELETE CASCADE, если ещё не существует."""
|
||||
engine = make_engine()
|
||||
engine = get_engine()
|
||||
try:
|
||||
with engine.connect() as conn:
|
||||
conn.execute(text("""
|
||||
@@ -97,7 +97,7 @@ async def migrate_cascade_user():
|
||||
@router.post("/db/migrate-users", dependencies=[Depends(require_admin_key)])
|
||||
async def migrate_users():
|
||||
"""Добавляет колонки username и hashed_password в таблицу users, если они ещё не существуют."""
|
||||
engine = make_engine()
|
||||
engine = get_engine()
|
||||
try:
|
||||
with engine.connect() as conn:
|
||||
conn.execute(text("""
|
||||
@@ -118,7 +118,7 @@ class ClearDBIn(BaseModel):
|
||||
@router.post("/db/migrate", dependencies=[Depends(require_admin_key)])
|
||||
async def migrate_tables():
|
||||
"""Приводит схему БД в соответствие с моделями: убирает устаревшие колонки, добавляет новые."""
|
||||
engine = make_engine()
|
||||
engine = get_engine()
|
||||
migrations = [
|
||||
# Убираем старый FK и колонку group.user_id (если остался от прежней схемы)
|
||||
'ALTER TABLE "group" DROP COLUMN IF EXISTS user_id',
|
||||
@@ -150,7 +150,7 @@ async def clear_tables(payload: ClearDBIn):
|
||||
if not payload.confirm:
|
||||
raise HTTPException(status_code=400, detail="Confirmation required")
|
||||
|
||||
engine = make_engine()
|
||||
engine = get_engine()
|
||||
|
||||
metadatas = collect_metadatas()
|
||||
if not metadatas:
|
||||
|
||||
@@ -14,7 +14,7 @@ from fastapi import APIRouter, HTTPException
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from bd import make_engine
|
||||
from bd import get_session
|
||||
from bd.tables.poll import Poll
|
||||
from bd.tables.question import Question
|
||||
from bd.tables.choice import Choice
|
||||
@@ -159,9 +159,6 @@ DIMENSIONS = [
|
||||
]
|
||||
|
||||
|
||||
def get_session():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
return sessionmaker(bind=make_engine(), future=True)
|
||||
|
||||
|
||||
@router.post("/klimov", status_code=201)
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Optional
|
||||
import uuid
|
||||
import io
|
||||
|
||||
from bd import make_engine
|
||||
from bd import get_session
|
||||
|
||||
from bd.tables.organization import Organization
|
||||
from route.auth_utils import require_admin_key
|
||||
@@ -20,9 +20,6 @@ class OrganizationUpdate(BaseModel):
|
||||
|
||||
|
||||
|
||||
def get_session():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
return sessionmaker(bind=make_engine(), future=True)
|
||||
|
||||
|
||||
def _org_dict(org: Organization) -> dict:
|
||||
|
||||
@@ -3,7 +3,7 @@ from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
import uuid
|
||||
|
||||
from bd import make_engine
|
||||
from bd import get_session
|
||||
|
||||
from bd.tables.poll import Poll
|
||||
from bd.tables.question import Question
|
||||
@@ -31,9 +31,6 @@ class PollIn(BaseModel):
|
||||
questions: Optional[List[QuestionIn]]
|
||||
|
||||
|
||||
def get_session():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
return sessionmaker(bind=make_engine(), future=True)
|
||||
|
||||
|
||||
@router.post("/", status_code=201, dependencies=[Depends(require_admin_key)])
|
||||
|
||||
@@ -3,7 +3,7 @@ from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from bd import make_engine
|
||||
from bd import get_session
|
||||
|
||||
from bd.tables.question import Question
|
||||
from bd.tables.poll import Poll
|
||||
@@ -25,9 +25,6 @@ class QuestionUpdate(BaseModel):
|
||||
position: Optional[int]
|
||||
|
||||
|
||||
def get_session():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
return sessionmaker(bind=make_engine(), future=True)
|
||||
|
||||
|
||||
@router.post("/", status_code=201, dependencies=[Depends(require_admin_key)])
|
||||
|
||||
@@ -22,7 +22,7 @@ from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session as SASession
|
||||
|
||||
from bd import make_engine
|
||||
from bd import get_session
|
||||
from bd.tables.response import Response, Answer
|
||||
from bd.tables.poll import Poll
|
||||
from bd.tables.scale import ScaleDimension, ChoiceScore
|
||||
@@ -32,9 +32,6 @@ from route.radar_svg_gen import save_radar_svg
|
||||
router = APIRouter(tags=["radar"])
|
||||
|
||||
|
||||
def get_session():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
return sessionmaker(bind=make_engine(), future=True)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -3,7 +3,7 @@ from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
import uuid
|
||||
|
||||
from bd import make_engine
|
||||
from bd import get_session
|
||||
|
||||
from bd.tables.poll import Poll
|
||||
from bd.tables.question import Question
|
||||
@@ -26,9 +26,6 @@ class ResponseIn(BaseModel):
|
||||
answers: List[AnswerIn]
|
||||
|
||||
|
||||
def get_session():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
return sessionmaker(bind=make_engine(), future=True)
|
||||
|
||||
|
||||
@router.post("/{poll_id}/responses", status_code=201)
|
||||
|
||||
@@ -19,7 +19,7 @@ from typing import List, Optional
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from bd import make_engine
|
||||
from bd import get_session
|
||||
from bd.tables.poll import Poll
|
||||
from bd.tables.choice import Choice
|
||||
from bd.tables.scale import ScaleDimension, ChoiceScore
|
||||
@@ -27,9 +27,6 @@ from bd.tables.scale import ScaleDimension, ChoiceScore
|
||||
router = APIRouter(tags=["scale"])
|
||||
|
||||
|
||||
def get_session():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
return sessionmaker(bind=make_engine(), future=True)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -4,7 +4,7 @@ from sqlalchemy import func
|
||||
import json
|
||||
import hashlib
|
||||
|
||||
from bd import make_engine
|
||||
from bd import get_session
|
||||
from bd.tables.response import Response
|
||||
from bd.tables.poll import Poll
|
||||
from bd.tables.group import Group
|
||||
@@ -26,9 +26,6 @@ def _cache_key(*parts) -> str:
|
||||
return hashlib.md5(raw.encode()).hexdigest()
|
||||
|
||||
|
||||
def get_session():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
return sessionmaker(bind=make_engine(), future=True)
|
||||
|
||||
|
||||
@router.get("/summary")
|
||||
|
||||
@@ -24,9 +24,7 @@ from pathlib import Path
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from bd import make_engine
|
||||
from bd import get_session
|
||||
from bd.tables.organization import Organization
|
||||
from bd.tables.group import Group
|
||||
from bd.tables.poll import Poll
|
||||
@@ -61,8 +59,6 @@ _TABLES: list[tuple[str, object]] = [
|
||||
]
|
||||
|
||||
|
||||
def _get_session():
|
||||
return sessionmaker(bind=make_engine(), future=True)
|
||||
|
||||
|
||||
def _row_to_dict(row) -> dict:
|
||||
@@ -89,7 +85,7 @@ def _row_to_dict(row) -> dict:
|
||||
dependencies=[Depends(require_admin_key)],
|
||||
)
|
||||
def export_data():
|
||||
Session = _get_session()
|
||||
Session = get_session()
|
||||
buf = io.BytesIO()
|
||||
counts = {}
|
||||
|
||||
@@ -147,7 +143,7 @@ def import_data(file: UploadFile = File(...)):
|
||||
if fmt not in (EXPORT_FORMAT_VERSION, "1.0"):
|
||||
raise HTTPException(status_code=400, detail=f"Неподдерживаемая версия формата: {fmt}")
|
||||
|
||||
Session = _get_session()
|
||||
Session = get_session()
|
||||
result = {"inserted": {}, "skipped": {}}
|
||||
|
||||
with Session() as session:
|
||||
@@ -218,7 +214,7 @@ backup_router = APIRouter(prefix="/backup", tags=["backup"])
|
||||
)
|
||||
def backup_export():
|
||||
"""Идентично /transfer/export, но в имени файла указан префикс backup_."""
|
||||
Session = _get_session()
|
||||
Session = get_session()
|
||||
buf = io.BytesIO()
|
||||
counts = {}
|
||||
|
||||
@@ -279,7 +275,7 @@ def backup_restore(file: UploadFile = File(...)):
|
||||
if fmt not in (EXPORT_FORMAT_VERSION, "1.0"):
|
||||
raise HTTPException(status_code=400, detail=f"Неподдерживаемая версия формата: {fmt}")
|
||||
|
||||
Session = _get_session()
|
||||
Session = get_session()
|
||||
deleted_counts: dict[str, int] = {}
|
||||
inserted_counts: dict[str, int] = {}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from pydantic import BaseModel, field_validator
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from bd import make_engine
|
||||
from bd import get_session
|
||||
|
||||
from bd.tables.users import User
|
||||
from route.auth_utils import get_current_user, require_admin_key, hash_password
|
||||
@@ -42,9 +42,6 @@ class UserUpdate(BaseModel):
|
||||
organization_id: Optional[str] = None
|
||||
|
||||
|
||||
def get_session():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
return sessionmaker(bind=make_engine(), future=True)
|
||||
|
||||
|
||||
def _user_dict(user: User) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user