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

12 KiB
Raw Blame History

План: вердикт без опоздавших результатов

Статус: план, код не менялся. Основа: код 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) строится после этого изменения: «Поздние результаты» там становятся контрольным показателем, который должен быть равен нулю.