Files
cloud-ip-validator/docs/changes/2026-10-03_17-21_verdict-no-late-results-plan.md
T
ayurishchevandClaude Sonnet 5.5 db73409e8f Add plans for the analytics section and for verdicts without late results
- docs/changes: plan of the "Аналитика" section (runs as selectable
  timestamps, API, page layout, acceptance numbers from the 6440-address run)
  and plan of the verdict-integrity change (causes found in the code and in
  the data, decisions, tests, rollout).
- docs/mockups/analytics-mockup.html: self-contained HTML mockup of the
  analytics page for review in a browser.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 17:59:40 +03:00

84 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План: вердикт без опоздавших результатов
Статус: план, код не менялся. Основа: код `internal/orchestrator`, `internal/probercore`, `internal/httpapi` и БД стенда (запуск 2026-10-02, 6440 адресов).
## 1. Требование
Вердикт адреса и его проверки должны совпадать. После вердикта результат не может появиться или измениться. Вердикт можно пересчитать из сохранённых проверок и получить то же значение.
## 2. Причины (проверены по коду и данным)
1. **Prober проверяет один адрес много раз.** `GET /probers/{site}/assignments` отдаёт все адреса в состоянии `checking`. Prober (`probercore.pollOnce`) обходит их при каждом опросе (каждые 5 с) и ничего не помнит о том, что адрес уже проверен. Пока адрес ждёт остальные площадки и egress, он проверяется снова и снова.
2. **Новый результат затирает старый.** Результаты пишутся через `UpsertCheck` с `ON CONFLICT DO UPDATE`. Строка живёт с первой записи, а значения берутся из последнего круга. В БД 61 406 из 76 644 ingress-строк (80%) перезаписаны позже первой записи более чем на 3 с. У egress перезаписей нет (агент проверяет один раз).
3. **Записи принимаются после вердикта.** `Orchestrator.RecordCheck` вызывает `UpsertCheck` без проверки состояния адреса. Итог на запуске 02.10:
- ingress: 1430 строк записаны после вердикта (844 проваленных и 586 успешных); из 844 проваленных 833 записаны уже после отвязки Floating IP, то есть проверяли адрес, который к валидатору уже не привязан;
- egress: 351 строка после вердикта (32 проваленных).
4. **Окно считается не с начала проверки.** `isReadyToAggregate` сравнивает `assigned_at` с порогом `checking_window_seconds` (120 с). `assigned_at` ставится при выдаче адреса валидатору, то есть окно включает привязку Floating IP, паузу `fip_settle_seconds` (30 с) и self-check (до 60 с). На сами проверки остаётся около 30 с. По окну агрегировано 509 адресов (8%), у них часть результатов не успела прийти.
5. **Время сравнивается чужими часами.** `checks.checked_at` ставит prober или агент на своей машине, а `aggregated_at` — control-api. При расхождении часов «после вердикта» определяется неверно.
Следствие: часть проваленных ingress-проверок в БД — не провалы адреса, а результат проверки уже отвязанного Floating IP. Отчёты по провалам ingress (в том числе таймаутам) за запуск 02.10 нужно читать с этой поправкой.
## 3. Решение
### A. Одна проверка адреса на площадку и попытку
`handleProberAssignments` не отдаёт адрес, для которого эта площадка уже отметила `complete` в `ip_site_checks` в текущей попытке. Новая попытка (повтор после сбоя) отдаёт адрес снова. Prober менять не нужно, пересобирается только `control-api`. Побочный эффект: нагрузка на prober и на сеть падает в разы.
### B. После вердикта запись закрыта
`UpsertCheck` пишет строку только если адрес в состоянии `checking` и номер попытки совпадает. Проверка выполняется в одном SQL-запросе (условие в `INSERT … SELECT … WHERE EXISTS`), без гонок.
- Отброшенный результат не пишется и не меняет существующую строку.
- Ответ агенту и prober остаётся `200 {"ok": true, "ignored": true}`, чтобы они не повторяли запрос.
- На каждую отброшенную пачку пишется одно событие `result_dropped` (источник, число проверок). Счётчик виден в аналитике; в норме он равен нулю или близок к нему.
### C. Окно проверки считается с начала проверки
Новая колонка `ip_queue.checking_started_at`, ставится в `SetChecking`. `isReadyToAggregate` сравнивает порог с ней, а не с `assigned_at`. Значение `checking_window_seconds` не меняется (120 с), но теперь это 120 с именно на проверки. Для строк без значения (старые данные) используется `assigned_at`.
### D. Время записи ставит сервер
Новая колонка `checks.recorded_at` — время последней записи по часам control-api; при перезаписи обновляется. Все сравнения «до/после вердикта» идут по ней, а не по `checked_at`. Для старых строк значение равно `created_at`.
### E. Вердикт воспроизводим
При агрегации в событие `aggregated` дописываются `egress_total`, `ingress_total`. Тест-инвариант: вердикт, пересчитанный по сохранённым проверкам адреса с учётом недостающих, равен `overall_result`.
### F. Недостающие результаты
Правило не меняется: недостающая проверка считается провалом, вердикт не выше `partial`. Благодаря C число таких адресов должно упасть. Остаток показывается отдельно («нет результата»), а не смешивается с провалами.
### G. Старые данные
В миграции `0010` колонка `checks.after_verdict` (0/1) для существующих строк: `1`, если `checked_at` позже `aggregated_at` строки очереди. Пересчитать затёртые результаты нельзя, поэтому в аналитике они помечаются как недостоверные и показываются отдельно (на запуске 02.10: 1430 ingress и 351 egress). После внедрения для новых данных значение всегда `0`.
Отвергнутый вариант: пересчитывать вердикт при опоздавших результатах. К этому моменту Floating IP уже отвязан, и такие результаты ничего не доказывают.
## 4. Файлы
- `internal/db/migrations/0010_verdict_integrity.sql` — колонки `ip_queue.checking_started_at`, `checks.recorded_at`, `checks.after_verdict`.
- `internal/db/queries_checks.go` — условная запись, признак «отброшено».
- `internal/db/queries_ipqueue.go` — `SetChecking`, выборка для prober без адресов с готовой площадкой.
- `internal/orchestrator/orchestrator.go` — `RecordCheck` возвращает признак, `isReadyToAggregate` по `checking_started_at`, поля в событии `aggregated`.
- `internal/httpapi/handlers_agent.go`, `handlers_prober.go` — ответ `ignored`, событие `result_dropped`.
- Документы: `docs/API.md` (ответы на запись результатов), `docs/USAGE.md`, `README.md`.
- Нумерация миграций: этот план идёт раньше плана аналитики (`…_analytics-section-plan.md`); аналитика получает `0011`.
## 5. Тесты
- Запись после вердикта игнорируется: строка не создана и не изменена; запись в состоянии `checking` проходит; запись старой попытки игнорируется.
- Повторная запись той же проверки внутри окна идемпотентна.
- `assignments`: после `complete` площадки адрес не возвращается ей, но возвращается остальным; после повтора попытки возвращается снова.
- Окно считается с `checking_started_at`: адрес, назначенный давно, но начавший проверку только что, не агрегируется по окну.
- Инвариант вердикта: для набора сценариев (все успешны, часть провалена, часть недостающих, ничего) вердикт равен пересчёту.
- Миграция на копии боевой БД: `after_verdict` = 1 для 1430 ingress и 351 egress строк запуска 02.10, остальное без изменений; повторный запуск безопасен.
## 6. Проверка на стенде
1. Копия `rxprod-compose/capi-db/control-api.db`, пересборка и перезапуск `control-api` (по процедуре из `docs/SETUP.md`). Prober и агенты не пересобираются.
2. Контрольный прогон на небольшой партии (например, 300 адресов).
3. Ожидаемый результат: ни одной строки `checks` с `recorded_at` позже `aggregated_at` (`after_verdict = 0`); нет перезаписанных ingress-строк (`recorded_at` равен первой записи); событий `result_dropped` нет или единицы; адресов с агрегацией по окну заметно меньше 8%.
4. Сравнение с запуском 02.10: доля проваленных ingress-проверок (таймауты ssh, tcp-22, icmp) должна уменьшиться, если часть прежних провалов была отвязанным Floating IP. Если не уменьшится, версия про отвязку неверна, и нужно искать другую причину.
## 7. Риски
- Отброшенные результаты теряются. Это сознательно: после вердикта Floating IP отвязан. Счётчик `result_dropped` показывает, насколько часто это случается.
- Уменьшение числа проверок prober меняет нагрузку на сеть площадок. Это ожидаемый эффект, а не риск, но стоит предупредить владельцев площадок.
- Новое окно (с начала проверки) может чуть увеличить длительность прогона на адресах, у которых действительно не пришли результаты. Значение окна настраивается.
## 8. Порядок по правилам проекта
План (этот файл) → реализация → `…-summary.md` → обновление `README.md` и `docs/` → пересборка стенда → контрольный прогон → обновление графа. Раздел «Аналитика» (план `2026-10-03_16-39`) строится после этого изменения: «Поздние результаты» там становятся контрольным показателем, который должен быть равен нулю.