На нагрузочном тесте 31.07.2026 около 70 человек заходили одновременно. Вход развалился: p95 `/api/v1/auth/token` — 7.28 с, p95 `guest-join` — 7.06 с, в БД 33 соединения `idle in transaction` при ОДНОМ активном запросе. Люди попадали внутрь с пятой-десятой попытки, часть не попала вовсе. Медиа при этом работало штатно: 30 участников с 27 камерами в следующем окне прошли без единого лага. Причина — argon2 считался синхронно внутри async-обработчика. Замер на боевом сервере: 95–155 мс на одну проверку, и всё это время event loop процесса стоит целиком. Транзакция БД к тому моменту уже открыта (`get_by_email` сделал SELECT), поэтому соединение висело без работы, пул из 40 выбирался, и отказы получали совершенно посторонние ручки — включая вход в конференцию, где никакого пароля не проверялось. Что изменилось: - `hash_password`/`verify_password` стали асинхронными и считаются в пуле потоков (`asyncio.to_thread`). argon2-cffi освобождает GIL, поэтому проверки идут по-настоящему параллельно; - параметры argon2id заменены с дефолтов библиотеки (t=3, m=64 МБ, p=4) на рекомендацию OWASP (t=2, m=19 МБ, p=1): 95 мс → 42 мс. Отдельно важен `parallelism`: при p=4 одна проверка пароля занимала все четыре ядра сервера — те же, на которых работает LiveKit; - добавлен `needs_rehash`: существующие хэши проверяются как прежде (параметры зашиты в саму строку) и лениво перевыпускаются при первом успешном входе. Расчёт по замерам: пачка из 70 логинов — 6.7–10.9 с блокировки против ~0.36 с без неё. Тесты: event loop продолжает тикать во время проверки; 8 параллельных проверок укладываются заметно быстрее восьми последовательных; хэш со старыми параметрами принимается и перевыпускается при входе.
93 lines
3.4 KiB
Python
93 lines
3.4 KiB
Python
"""Юнит-тесты модели `ConferenceInvitee`: CHECK «ровно одна identity» и частичные
|
|
UNIQUE-индексы (ADR-003, `docs/architecture/adr/003-conference-invitees.md`).
|
|
"""
|
|
|
|
import uuid
|
|
|
|
import pytest
|
|
from sqlalchemy.exc import IntegrityError
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from core.security import hash_password
|
|
from models.conference import Conference
|
|
from models.invitee import ConferenceInvitee
|
|
from models.user import User
|
|
from services.conference_ids import generate_number, generate_slug
|
|
|
|
|
|
async def _make_user(session: AsyncSession) -> User:
|
|
user = User(
|
|
email=f"{uuid.uuid4()}@example.com",
|
|
name_user="Invitee Tester",
|
|
password_hash=await hash_password("password123"),
|
|
email_verified=True,
|
|
)
|
|
session.add(user)
|
|
await session.flush()
|
|
return user
|
|
|
|
|
|
async def _make_conference(
|
|
session: AsyncSession, *, owner_id: uuid.UUID | None = None
|
|
) -> Conference:
|
|
conference = Conference(
|
|
number=generate_number(), slug=generate_slug(), owner_id=owner_id, status="scheduled"
|
|
)
|
|
session.add(conference)
|
|
await session.flush()
|
|
return conference
|
|
|
|
|
|
async def test_neither_user_id_nor_email_violates_check(db_session: AsyncSession) -> None:
|
|
conference = await _make_conference(db_session)
|
|
db_session.add(ConferenceInvitee(conference_id=conference.id))
|
|
with pytest.raises(IntegrityError):
|
|
await db_session.commit()
|
|
|
|
|
|
async def test_both_user_id_and_email_violates_check(db_session: AsyncSession) -> None:
|
|
conference = await _make_conference(db_session)
|
|
invitee_user = await _make_user(db_session)
|
|
await db_session.commit()
|
|
|
|
db_session.add(
|
|
ConferenceInvitee(
|
|
conference_id=conference.id, user_id=invitee_user.id, email="x@example.com"
|
|
)
|
|
)
|
|
with pytest.raises(IntegrityError):
|
|
await db_session.commit()
|
|
|
|
|
|
async def test_duplicate_user_id_on_same_conference_violates_unique(
|
|
db_session: AsyncSession,
|
|
) -> None:
|
|
conference = await _make_conference(db_session)
|
|
invitee_user = await _make_user(db_session)
|
|
db_session.add(ConferenceInvitee(conference_id=conference.id, user_id=invitee_user.id))
|
|
await db_session.commit()
|
|
|
|
db_session.add(ConferenceInvitee(conference_id=conference.id, user_id=invitee_user.id))
|
|
with pytest.raises(IntegrityError):
|
|
await db_session.commit()
|
|
|
|
|
|
async def test_duplicate_email_case_insensitive_violates_unique(db_session: AsyncSession) -> None:
|
|
conference = await _make_conference(db_session)
|
|
unique_email = f"guest-{uuid.uuid4()}@example.com"
|
|
db_session.add(ConferenceInvitee(conference_id=conference.id, email=unique_email))
|
|
await db_session.commit()
|
|
|
|
db_session.add(ConferenceInvitee(conference_id=conference.id, email=unique_email.upper()))
|
|
with pytest.raises(IntegrityError):
|
|
await db_session.commit()
|
|
|
|
|
|
async def test_same_email_on_different_conferences_is_allowed(db_session: AsyncSession) -> None:
|
|
conference_a = await _make_conference(db_session)
|
|
conference_b = await _make_conference(db_session)
|
|
shared_email = f"shared-{uuid.uuid4()}@example.com"
|
|
db_session.add(ConferenceInvitee(conference_id=conference_a.id, email=shared_email))
|
|
db_session.add(ConferenceInvitee(conference_id=conference_b.id, email=shared_email))
|
|
await db_session.commit() # не должно бросить — разные конференции
|