Files
cloud-ip-validator/docs/changes/2026-10-01_18-59_fip-scan-at-scale-review.md
ayurishchevandClaude Sonnet 5.5 aff8fe38b5 Scan floating IPs in the background, page by page, so thousands of addresses work
The "Scan Floating IP" button failed with a client timeout: the project now
holds ~6.4k floating IPs and the scan listed them all in one unpaginated,
timeout-less Neutron request on the HTTP request context.

openstack: ListFreeFloatingIPs reads marker-based pages (fields= keeps them
small) with per-page retry/backoff on transport errors, 5xx and 429, and every
request now has a timeout (also ends hangs inside the orchestrator tick).

orchestrator: the scan is a single-flight background job on the process
context with progress (clearing/listing/enqueuing/done/error), dry_run, full
discovery before anything is enqueued, then SubmitIPs in chunks of 500 in
ascending IP order; a failed read leaves the queue untouched. The auto-cycle
gets a "scanning" phase that polls the job, so the control loop and
autoCycleMu are never held across OpenStack/DB work; it recovers after a
restart and waits for (instead of adopting) a scan started by someone else.

db: migration 0009 (indexes), paged ListIPsPage/ListRegistryPage, GROUP BY
counters, EXISTS completion check, set-based ClearAllIPs.

API: POST /admin/ips/scan -> 202 (dry_run, wait), GET /admin/ips/scan, paging
and filters on /admin/ips and /admin/registry (bare arrays without limit),
results_by_overall in /admin/status.

dashboard: scan progress panel and dry-run button, paginated /ips and
/registry with server-side filters, Overview on counters and capped lists
with progress/ETA, "select all N by filter", hx-params fix for per-row
buttons, real counts in confirmations.

Also: docs (API, USAGE, DASHBOARD, README), plan and review under
docs/changes/, bin/ rebuilt with new SHA256SUMS.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 19:31:11 +03:00

15 KiB
Raw Permalink Blame History

Ревью и тестирование: скан Floating IP и автоцикл при тысячах адресов

Дата: 2026-10-01 18:59 MSK · План: 2026-10-01_18-19_fip-scan-at-scale-plan.md Статус: реализовано и проверено на живом окружении; реальная постановка 6440 адресов в очередь и включение автоцикла на живом стенде не выполнялись — ждут решения пользователя.

Итог

Исходная ошибка («control-api недоступен: context deadline exceeded» при нажатии «Сканировать Floating IP») устранена: на живом стенде кнопка возвращает панель прогресса за 0,7 с, а сканирование реального Neutron (6441 Floating IP, 6440 свободных) проходит за ≈ 1,5–2 минуты в фоне с видимым прогрессом. Код написан двумя агентами параллельно (Sonnet 5.5): серверная часть и дашборд, ревью и все проверки — независимо (Sonnet 5.5, high). Найден и исправлен один дефект автоцикла и одна мелочь в клиенте OpenStack; остальные замечания — ограничения дизайна, перечислены ниже.

Причина исходной ошибки

ListFloatingIPs запрашивал весь список одним запросом без limit и без таймаута: при ≈ 6,4 тыс. адресов Neutron отвечал дольше минуты, дашборд ждал 10 с, а запрос был привязан к r.Context() — при обрыве соединения скан отменялся и не мог завершиться. Ошибка нигде не логировалась. Побочные открытия: у клиента OpenStack вообще не было таймаутов и ретраев; ListRegistry был O(n²) (нет индекса ip_queue(registry_id)); /ips отдавал ≈ 8 МБ HTML, а «Обзор» каждые 5 с тянул ≈ 3 МБ JSON.

Что реализовано

Область Изменения
OpenStack ListFreeFloatingIPs — постраничное чтение по marker (200 на страницу, fields= сокращает ответ), повтор страницы с backoff на обрывы/RemoteDisconnected/5xx/429, таймаут запроса (openstack.request_timeout_seconds, 60 с — закрывает и зависания в тике оркестратора); MockClient с пагинацией, ListFailures, PageDelay, SeedMany
Скан-задание orchestrator/scanjob.go: single-flight фоновое задание на контексте процесса (не r.Context()), фазы clearing → listing → enqueuing → done/error/cancelled, прогресс; сначала полное обнаружение, затем SubmitIPs кусками по 500 по возрастанию IP; сбой чтения ⇒ очередь не меняется; dry_run; синхронная обёртка ScanFloatingIPs сохранена
Автоцикл фаза scanning: очистка и скан — одно фоновое задание, цикл оркестратора и autoCycleMu не блокируются; восстановление после рестарта; Stop отменяет скан; завершение определяется EXISTS, а не чтением всей очереди каждый тик
БД миграция 0009 (индексы ip_queue(registry_id), ip_queue(state, aggregated_at)); ListIPsPage, ListRegistryPage (LIMIT/OFFSET до расчёта сводки, фильтр итога одним SQL), CountIPsByState/Result, AnyNonTerminalIP; ClearAllIPs — 5 запросов вместо цикла по адресам
API POST /admin/ips/scan → 202 (dry_run, wait), новый GET /admin/ips/scan, пагинация и фильтры у GET /admin/ips и /admin/registry (без limit — прежний массив), results_by_overall в /admin/status, count у clear
Дашборд панель прогресса скана и «Пробное сканирование»; постраничные /ips и /registry с серверными фильтрами; «Обзор» на счётчиках и ограниченных списках (прогресс «Готово D из T», оценка времени); «Выбрать все N по фильтру»; hx-params на кнопках (исправлен дефект — отмеченные адреса попадали в URL hx-delete); подтверждения с реальным числом; длинный таймаут для массовых операций
Конфиг и документы openstack.list_page_size/request_timeout_seconds/list_page_retries, orchestrator.fip_scan_timeout_seconds; API.md, USAGE.md, DASHBOARD.md, README.md, примеры конфигов

Результаты проверок

Проверка Результат
gofmt, go build ./..., go vet ./... чисто
go test ./... (включая тесты на 6440 адресов) все пакеты зелёные
go test -race -short (openstack, orchestrator, db, httpapi, dashboard, config) зелёные (тесты на 6440 адресов под -race слишком долгие, пропускаются по -short; агент прогонял их полностью)
scripts/run-local-e2e.sh (с токенами) exit 0: автоцикл прошёл через scanning, проверки реальными агентом и пробером — pass
Живой стенд — пробный скан реального Neutron (POST …/scan?dry_run=true) 33 страницы, 6441 найдено / 6440 свободных за 93 с, 202 за 2 мс, повторный POST присоединился к идущему заданию, API отвечал за 2–4 мс всё время скана, очередь осталась пустой
Живой дашборд (настоящий Chromium), кнопка «Пробное сканирование» панель за 0,7 с без баннера ошибки, прогресс по страницам, итог «готово: 33 страницы, 6441, 6440, время 1 мин 48 с»
Изолированный mock-стенд на 6440 адресах, настоящий Chromium (17 из 19 автопроверок, 2 — ложные, см. ниже) /ips 68 КБ за 0,18 с (было ≈ 8 МБ), фрагмент «Обзора» 3 КБ за 0,05 с, /registry 34 КБ за 0,12 с; пагинация и серверный поиск; прогресс скана (читаются страницы → ставятся в очередь → готово); «Очистить всё» с реальным числом в подтверждении — 0,5 с; «Выбрать все 6440 по фильтру» + массовое удаление — 8 с; JS-ошибок нет; ошибок в логе control-api нет
Миграция 0009 на живой БД user_version = 9, оба индекса созданы, данные не тронуты

Примечание: два «FAIL» в браузерном скрипте mock-стенда — ошибка самого скрипта: панель показывает состояние заглавными («ГОТОВО», CSS), а скрипт искал строчные. Выведенный текст панели подтверждает успех (добавлено6440, найдено адресов6440). Реальных провалов нет.

Замечания ревью

№ Серьёзность Замечание Статус
1 средняя Автоцикл «усыновлял» чужое сканирование. Если в момент старта цикла уже шло ручное/периодическое/пробное сканирование, StartScan возвращал started=false, а цикл переходил в scanning и ждал чужое задание. Пробное ничего не ставит в очередь, ручное не очищает очередь ⇒ цикл переходил в running над пустой/нетронутой очередью и сразу отчитывался completed (runs_total+1) без единой проверки исправлено: если собственный скан не стартовал, цикл ничего не меняет и пробует снова на следующем такте (после окончания чужого); добавлен тест TestAutoCycleWaitsForForeignScanInsteadOfFollowingIt (падал до правки)
2 низкая Клиент OpenStack заполнял ProjectID только из tenant_id; при fields= Neutron может вернуть лишь project_id исправлено (запасной вариант project_id); поле нигде не влияет на логику
3 низкая aggregated_at_desc сортирует по strftime(...) — временные метки хранятся как RFC3339Nano, и сырая сортировка текстом неверна (поймал тест агента); индекс (state, aggregated_at) помогает фильтру по состоянию, но не сортировке принято; на 6440 строк незаметно
4 низкая StopAutoCycle в фазе scanning вызывает CancelScan, который ждёт до 5 с под autoCycleMu: «Выключить» может занять до 5 с, а следующий шаг цикла — подождать принято
5 низкая Если скан завершился ошибкой после «Очистить» (шаг 1 цикла), очередь остаётся пустой до следующего цикла (interval_seconds); исход — error с причиной принято, описано в USAGE.md; при желании — отдельная доработка (повтор скана сразу)
6 низкая Статус скана хранится в памяти: после рестарта control-api он idle; автоцикл в фазе scanning при этом корректно перезапускает скан принято
7 инфо Отмена сканирования (CancelScan) вызывается только из Stop автоцикла; ручной кнопки/эндпоинта отмены нет не входило в план
8 инфо Дашборд: мутации заменяют #ips-table-wrap целиком (outerHTML), чтобы hx-get обёртки всегда указывал на текущую страницу/фильтр; убраны функции filterQueueItems/filterRegistryItems/currentlyChecking/lastCompleted вместе с тестами (фильтрация перенесена на сервер); сводка «последние N» считается по показанному (возможно, отфильтрованному) окну, а общие итоги — отдельной строкой принято
9 инфо Тесты на 6440 адресов под -race занимают 40–90 с на пакет — пропускаются по -short принято

Пропускная способность (важно для автоцикла)

Проверка не стала быстрее — стало возможным её запустить. По фактическим данным стенда слот на адрес ≈ 50 с на валидатор (из них 30 с — fip_settle_seconds): 6440 адресов ≈ 18 ч на 5 валидаторах, ≈ 9 ч на 10, ≈ 4,5 ч на 20. Для автоцикла max_run_seconds должен оставаться 0. Рычаги — число валидаторов и (осторожно) fip_settle_seconds. На странице «Обзор» виден прогресс и оценка времени.

Состояние живого стенда (rxprod-compose)

  • Развёрнуты новые образы civ-capi, civ-adash, civ-prober (и пересобран civ-agent); миграция 0009 применена. Предыдущие образы сохранены под тегом :pre-scale (откат: docker tag civ-capi:pre-scale civ-capi:latest и docker compose up -d; на 0009 откат БД не нужен — это только индексы). Бэкап БД перед обновлением — в каталоге scratchpad сессии (/tmp, временный).
  • Очередь пуста (5 адресов прежней работы остались в реестре). Реальная постановка 6440 адресов и автоцикл на живом стенде не запускались: это ≈ 18 ч реальных проверок, решение за пользователем. Перед запуском рекомендую «Пробное сканирование» (уже отработало штатно) и бэкап БД.
  • Токен агентов по-прежнему не включён (внешние валидаторы и пробер rxyc со старыми бинарниками) — см. ревью аутентификации. Новые бинарники для внешних валидаторов — в bin/ (после раскатки токена агентов их можно обновить одновременно).
  • bin/ пересобран (CGO_ENABLED=0, -trimpath -ldflags="-s -w"), SHA256SUMS обновлён. Временные контейнеры civ-scale-* удалены.

Что осталось

  • Решение пользователя: поставить 6440 адресов в очередь («Сканировать Floating IP») и/или включить автоцикл на живом стенде.
  • По желанию: кнопка/эндпоинт отмены скана (замечание 7), повтор скана внутри цикла при ошибке (замечание 5), Cache-Control: no-store (из ревью аутентификации).