Files
ayurishchevandClaude Opus 5.5 ba8ac7be71 Кэш списка бакета, БД без блокировки event loop, один процесс на БД
Производительность и масштабирование, пункты 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>
2026-09-28 12:18:32 +03:00

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()