Производительность и масштабирование, пункты 8–10 ревью (docs/changes/019): - кэш списка бакета и связей файлов с копиями (BACKUPS_CACHE_TTL, 60 с): sync_rows только при реальном чтении бакета; сброс после бэкапа и удаления, счётчик поколений против гонки; refresh=1 и «Обновить список»; - обработчики API/UI без await — обычные функции (пул потоков FastAPI), запуск задач остаётся async; запись статуса, задачи и sync_rows — через asyncio.to_thread; SQLite: WAL, synchronous=NORMAL, busy_timeout; - файловая блокировка <файл БД>.lock: второй процесс на той же БД не стартует; раздел «Ограничения» в README. Тесты: 24 из 24. Стенд (порт 8001): кэш 0,014 с против 0,138 с, второй uvicorn на боевой БД отклонён блокировкой, боевые данные не изменены. Ручная проверка UI пользователем на момент коммита не подтверждена. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
57 lines
2.2 KiB
Python
57 lines
2.2 KiB
Python
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
|
|
from sqlalchemy import create_engine, event
|
|
from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
|
|
|
from app.config import get_settings
|
|
|
|
|
|
class Base(DeclarativeBase):
|
|
pass
|
|
|
|
|
|
_engine = None
|
|
_SessionLocal = None
|
|
|
|
|
|
def init_db(url: str | None = None) -> None:
|
|
"""Создаёт engine и таблицы. Вызывается при старте приложения (и в тестах)."""
|
|
global _engine, _SessionLocal
|
|
url = url or get_settings().database_url
|
|
is_file_db = url.startswith("sqlite:///") and ":memory:" not in url
|
|
if is_file_db:
|
|
Path(url.removeprefix("sqlite:///")).parent.mkdir(parents=True, exist_ok=True)
|
|
_engine = create_engine(url, connect_args={"check_same_thread": False})
|
|
if is_file_db: # :memory: — отдельная БД на соединение, WAL и busy_timeout ей не нужны
|
|
@event.listens_for(_engine, "connect")
|
|
def _pragmas(dbapi_conn, _rec):
|
|
cur = dbapi_conn.cursor()
|
|
cur.execute("PRAGMA journal_mode=WAL") # несколько читателей не блокируют друг друга и писателя
|
|
cur.execute("PRAGMA synchronous=NORMAL") # безопасно с WAL, быстрее FULL
|
|
cur.execute("PRAGMA busy_timeout=5000") # ждать снятия блокировки БД, а не падать сразу
|
|
cur.close()
|
|
_SessionLocal = sessionmaker(_engine, expire_on_commit=False)
|
|
from app import models # noqa: F401 (регистрация моделей)
|
|
|
|
Base.metadata.create_all(_engine) # у новой БД — все таблицы, у старой — только недостающие (events)
|
|
|
|
from app import migrations
|
|
|
|
db_file = Path(url.removeprefix("sqlite:///")) if url.startswith("sqlite:///") and ":memory:" not in url else None
|
|
migrations.run(_engine, db_file)
|
|
|
|
|
|
@contextmanager
|
|
def session_scope():
|
|
"""Короткая транзакция: commit при успехе, rollback при ошибке."""
|
|
session = _SessionLocal()
|
|
try:
|
|
yield session
|
|
session.commit()
|
|
except Exception:
|
|
session.rollback()
|
|
raise
|
|
finally:
|
|
session.close()
|