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>
15 KiB
Ревью и тестирование: скан 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(из ревью аутентификации).