Commit Graph

4 Commits

Author SHA1 Message Date
f7c4fb4176 fix(chat): не держать соединение с БД всю жизнь WS-подключения
Обработчик `WS /conferences/{id}/chat` получает `AsyncSession` через
`Depends(get_session)`, а хендшейк делает четыре SELECT'а (тоггл чата,
конференция, тоггл рук, история). SQLAlchemy открывает транзакцию на
первом из них и держит её — вместе с соединением из пула — всё время,
пока участник сидит в комнате. Соединений в пуле `db_pool_size +
db_max_overflow` = 20 на воркер, то есть 40 на инстанс из двух воркеров:
сороковой вошедший выгребал пул досуха.

Ровно это положило вход на нагрузочном тесте 07.08.2026: 245 ошибок
`QueuePool limit ... timed out`, 170 ответов 500 (из них 123 на резолве
конференции и 21 на гостевом входе), а `pg_stat_activity` показывал рост
`idle in transaction` 3 → 8 → 16 → 26 → 35 → 39 → 40 при одном `active`.
Число открытых WS чата в логах backend растёт синхронно и упирается в
те же 40 ровно к моменту первого таймаута пула.

Соединение освобождается сразу после хендшейка: дальше оба насоса
работают через Redis, а единственная запись в БД (`persist_and_publish`)
открывает и коммитит собственную транзакцию.

Замер на локальном стенде (один воркер, потолок пула 20), 15 посторонних
запросов на каждой ступени:

| участников | idle in transaction | 5xx | p95      |
|------------|---------------------|-----|----------|
| было  20   | 20                  | 10  | 10.05 с  |
| стало 20   | 0                   | 0   | 0.02 с   |
| стало 120  | 0                   | 0   | 0.03 с   |

До правки 21-й участник не мог войти вовсе (500 на guest-join), в логе
40 ошибок `QueuePool limit`; после — ни одной на 120 участниках.
2026-08-09 01:05:59 +03:00
84b7f807f7 fix(auth): проверка пароля больше не блокирует весь backend
На нагрузочном тесте 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 параллельных
проверок укладываются заметно быстрее восьми последовательных; хэш со
старыми параметрами принимается и перевыпускается при входе.
2026-08-01 23:19:52 +03:00
8e5eda88a2 feat(room): поднятие руки и очередь для организатора
Транспорт — существующий аутентифицированный WS чата (api/chat.py), а не
отдельный эндпоинт: сервер уже держит это соединение на каждого участника
(обоснование — докстринг chat_websocket и useChat.ts). Состояние очереди —
Redis (services/hand_queue.py), не Postgres: это эфемерное состояние звонка,
а не история, и два процесса uvicorn делают наивную память одного процесса
недостаточной. HSETNX даёт идемпотентное «поднять» (повторный клик не
переставляет в конец очереди), снапшот шлётся всем участникам при любом
изменении — организатор, зашедший позже, сразу видит актуальную картину.

Опустить чужую руку может организатор (решение оператора) — проверка через
conference.owner_id, не через identity клиента. Участник, вышедший из
комнаты LiveKit (webhook participant_left), теряет место в очереди
автоматически; переподключение WS чата место не сбрасывает (Redis не привязан
к жизни соединения). room_finished чистит очередь целиком — она не должна
пережить завершение звонка.

Побочный эффект транспортного решения: поднять руку нельзя, если чат выключен
настройкой инстанса (WS вообще не открывается) — принятый компромисс ради
переиспользования уже готового канала.

UI: кнопка «Рука» в тулбаре (у всех, бейдж — общий счётчик), бейдж на плитке
говорящего (видно всем), панель «Очередь» организатору (HandQueuePanel).
Кнопка «Рука» и панель «Очередь» намеренно НЕ прячутся в мобильную шторку
настроек, в отличие от «Вида», — поднятие руки посреди разговора требует
кнопки под рукой, а не в два клика вглубь настроек.

Этим же коммитом (файлы разделяемые с задачей B2, RoomParticipantTile.tsx/
useChat.ts/RoomStage.tsx/RoomPage.tsx/room.css) — проброс conferenceId и
каркас forced_mute-обработки, без которых кнопки принудительного мьюта не
скомпилировались бы; сама реализация мьюта — следующим коммитом.
2026-08-01 22:06:43 +03:00
896455381a Первоначальная версия VidConf 2026-07-23 01:57:27 +03:00