37 lines
4.8 KiB
Markdown
37 lines
4.8 KiB
Markdown
# Итоги: 019 — производительность и масштабирование (по ревью 2026-09-27)
|
||||
|
|
|
|||
|
|
Источник — ревью `docs/reviews/2026-09-27-codebase-review.md`, раздел «Производительность и масштабирование» (пункты 8–10).
|
|||
|
|
|
|||
|
|
## Сделано
|
|||
|
|
- **п. 8 — кэш списка бакета** (`app/services/backups.py::bucket_objects`): список объектов и связи `key → backup_id` кэшируются на
|
|||
|
|
`BACKUPS_CACHE_TTL` секунд (60; `0` — выключен); `sync_rows` выполняется только при реальном чтении бакета. `asyncio.Lock` с повторной
|
|||
|
|
проверкой — параллельные запросы не читают бакет дважды. `invalidate()` вызывается после загрузки бэкапа и удаления;
|
|||
|
|
счётчик поколений не даёт чтению, начатому до `invalidate()`, пометить кэш свежим. Принудительно — `refresh=1`
|
|||
|
|
(`GET /backups`, `GET /api/v1/backups`, кнопка «Обновить список»).
|
|||
|
|
- **п. 9 — БД не блокирует event loop**: 44 обработчика API/UI без `await` стали `def` (пул потоков FastAPI); обработчики, вызывающие
|
|||
|
|
`jobs.start_jobs` (нужен работающий event loop для `create_task`), остались `async`. Через `asyncio.to_thread`: чтение устройства и запись
|
|||
|
|
статуса в опросе (`ops.poll_device`, `ops.refresh_status`), `poller.cycle`, `jobs._finish`, строки `Backup` в `ops.run_backup`, `sync_rows`.
|
|||
|
|
SQLite: `journal_mode=WAL`, `synchronous=NORMAL`, `busy_timeout=5000` (только файловая БД).
|
|||
|
|
- **п. 10 — один процесс на БД** (`app/process_lock.py`): `flock` на `<файл БД>.lock` в `lifespan` до `init_db`, снимается при остановке;
|
|||
|
|
второй процесс получает `RuntimeError` и не стартует. README — раздел «Ограничения».
|
|||
|
|
|
|||
|
|
## Найдено на ревью и исправлено
|
|||
|
|
- Гонка кэша: `invalidate()` во время чтения бакета терялся — устаревший список считался свежим весь TTL. Исправлено счётчиком поколений;
|
|||
|
|
мутационная проверка: без исправления новый тест падает (`4 == 5`), с исправлением проходит.
|
|||
|
|
- Ошибка в тесте гонки (кэш перед сценарием был свежим) — исправлена одной строкой оркестратором.
|
|||
|
|
|
|||
|
|
## Проверено
|
|||
|
|
- `pytest`: 24 из 24 (новые: `test_bucket_list_is_cached_between_reads`, `test_process_lock_blocks_second_process`).
|
|||
|
|
Актор в событиях из теперь синхронных обработчиков (`api`, `ui:admin`) подтверждён существующими тестами.
|
|||
|
|
- Стенд (порт 8001): новый код, `journal_mode=wal`, в логах без ошибок.
|
|||
|
|
- Кэш: 36 файлов бакета; `refresh=1` — 0,138 с, из кэша — 0,014 с.
|
|||
|
|
- Event loop: пока `POST /devices/{id}/refresh` ждал недоступное устройство (4,0 с), параллельный `GET /devices` ответил за 0,012 с.
|
|||
|
|
- Второй `uvicorn` на боевой БД внутри контейнера: `RuntimeError` блокировки, `Application startup failed`; основной стенд работает, прерванных задач нет.
|
|||
|
|
- Боевые данные: группы 4/4, устройства 14/14, бэкапы 45/45, задачи 71/71 — совпадают по ID; добавлены 3 события временного устройства (удалено).
|
|||
|
|
Сверка — по согласованной копии (`sqlite3 backup` внутри контейнера), т. к. при WAL файл БД без `-wal` неполон.
|
|||
|
|
|
|||
|
|
## Оговорки
|
|||
|
|
- Кэш, семафор задач и счётчики паролей — в памяти процесса; горизонтальное масштабирование по-прежнему не поддерживается (теперь явно).
|
|||
|
|
- Изменения бакета извне видны не позже TTL или по «Обновить список»; `refresh=1` остаётся в адресе страницы после обновления.
|
|||
|
|
- Для копирования БД с хоста теперь нужен и файл `-wal` (или `sqlite3 .backup`).
|
|||
|
|
- Ручная проверка UI пользователем на момент коммита не подтверждена.
|