Производительность и масштабирование, пункты 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>
4.8 KiB
4.8 KiB
Итоги: 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 пользователем на момент коммита не подтверждена.