Образ собран с `uv sync --frozen --no-dev`, но `uv run` перед каждым запуском
заново синхронизирует venv и подтягивает dev-группу. В логах старта
vidconf-backend-1 и vidconf-worker-1 на проде это видно как «Downloading ruff /
mypy / pygments» и «Installed 12 packages». Хуже всего healthcheck'и: они
выполняют ту же синхронизацию каждые 15 секунд всю жизнь контейнера.
Проверено на локально собранном образе, одна и та же команда:
uv run — качает 12 пакетов, venv 456 → 574 МБ
uv run --no-sync — не качает ничего, venv остаётся 456 МБ
Флаг добавлен во все вызовы в прод-путях: CMD образа, command/entrypoint/
healthcheck всех сервисов compose, миграции и seed в install.sh, те же команды
в docs/deploy. Локальная разработка (dev-setup.md, backend/README.md, CI) не
затронута — там dev-зависимости нужны. Заодно убрано устаревшее объяснение
ретрая `up -d --wait`: первый старт больше не синхронизирует окружение.
Версия uv в образе — 0.11.33, `--no-sync` поддерживается.
Пул создавался с дефолтом SQLAlchemy (5 + 10) и под нагрузкой выгребался
за секунды. Теперь параметры заданы явно и вынесены в настройки:
DB_POOL_SIZE=10, DB_MAX_OVERFLOW=10, DB_POOL_TIMEOUT=10. Таймаут снижен с
дефолтных 30 секунд намеренно — пусть запрос падает быстро и показывает
проблему, а не висит полминуты.
Backend запускался одним процессом uvicorn: любой блокирующий вызов
останавливал и параллельные запросы, и WS-чат всех участников. Добавлен
UVICORN_WORKERS с дефолтом 2 — не по числу ядер, потому что на
четырёхъядерном сервере ядра делятся с LiveKit, а медиа важнее API.
Бюджет соединений считается на весь инстанс: каждый воркер держит свой
пул, поэтому UVICORN_WORKERS × (DB_POOL_SIZE + DB_MAX_OVERFLOW) должно
оставаться заметно ниже max_connections у Postgres.
Многопроцессность безопасна: бутстрап настроек в lifespan идемпотентен
(INSERT ... ON CONFLICT DO NOTHING), а WS-чат разносит сообщения через
Redis pub/sub и состояния в памяти процесса не держит.
Обработчик `track_published` вызывал `start_track_egress` внутри своей
транзакции. На инстансе без профиля `transcribe` egress-сервиса нет, и
LiveKit ждал ответа воркера через Redis до собственного таймаута psrpc —
20–25 секунд на каждый микрофонный трек. Всё это время webhook удерживал
соединение с БД и открытую транзакцию.
На нагрузочном тесте с 19 участниками (28.07.2026) это дало 226 ошибок
`QueuePool limit of size 5 overflow 10 reached` и 37 ответов 500 на путях
входа в конференцию, а со стороны LiveKit — 33 дропнутых webhook при
очереди доставки до 56 секунд.
Что изменилось:
- запуск ушёл в фоновую задачу `run_track_egress` со своей сессией БД;
обработчик только планирует её и отвечает 200 сразу;
- добавлен ранний выход по `transcriber.enabled` — симметрично guard'у,
который уже был в `room_finished`;
- запуск ограничен таймаутом `egress_start_timeout_s` (по умолчанию 3 с).
Идемпотентность сохранена: проверка «трек уже пишется» осталась в
обработчике, а `AudioTrackRepository.create` — это INSERT ... ON CONFLICT
DO NOTHING.
Попутно: `test_room_finished_enqueues_pipeline` падал в зависимости от
того, что осталось в локальной БД, — теперь выставляет `transcriber`
явно, как и остальные тесты этой группы.
Info-метрика (значение всегда 1, лейблы cpus/ram_mb/gpu_name/vram_mb) —
источник GPU для новой панели «Характеристики сервера» в дашборде «Хост
и контейнеры» (живые CPU/RAM/диск там же берутся из node-exporter,
GPU node-exporter не знает). Данные — уже читаемые Settings.hw_* из .env,
которые install.sh пишет по ADR-004. «—» вместо None/пустой строки —
однозначный прочерк на панели вместо пустого текста.
AdminUserRepository.list_paginated ищет теперь и по названию команды
(LEFT JOIN teams, как и раньше) — пользователи без команды по-прежнему
не пропадают из общей выдачи, просто не совпадают с этой частью поиска.
Плейсхолдер поля поиска в админке обновлён под новое поведение.
Задача 3 переопределена оператором: вместо окончательного удаления
пользователя (упёрлось в CHECK-constraint'ы participant/chat_messages,
требующие миграции схемы — решили отложить) добавлены вкладки
«Активные»/«Заблокированные»/«Все» перед полем поиска в админке —
список фильтруется по `is_blocked` на бэкенде (GET /admin/users?status=).
Настройка «Эталон mail-домена» теперь хранит список доменов вместо
одного — email при регистрации принимается, если совпадает с любым из
них. Старое значение в БД ({"domain": str|None}) читается прозрачно
(обратная совместимость без Alembic-миграции) и переписывается в новую
форму ({"domains": [...]}) при первом же сохранении настроек. В админке
добавление/удаление доменов — списком чипов; на экране регистрации
подсказка о несовпадении домена перечисляет все эталонные варианты.
Валидация ai_level срабатывала на любой PUT /admin/settings, включая
случаи, когда фронт отправлял уже сохранённый (не изменённый) уровень —
на слабом железе это намертво блокировало правку любых других настроек.
Бэкенд теперь сравнивает patch.ai_level с текущим значением и проверяет
доступность только при фактической смене уровня; фронт дополнительно
отправляет в PUT только реально изменённые поля.
Покрытие валидации/сохранения contact_email, простановки Reply-To в
письмах регистрации/приглашений/саммари при включённом и выключенном
контактном адресе, и эндпоинта тестовой отправки (успех, дефолтный
получатель, сбой транспорта без утечки логина/пароля).
Новая настройка instance_settings.contact_email (включён/адрес, с
валидацией формата) — подставляется в заголовок Reply-To писем
подтверждения регистрации, приглашений и саммари. Админ-эндпоинт
POST /admin/settings/test-email отправляет проверочное письмо синхронно
и возвращает внятный результат (успех либо текст ошибки транспорта),
не раскрывая логин/пароль SMTP.