- 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>
12 KiB
План: вердикт без опоздавших результатов
Статус: план, код не менялся. Основа: код internal/orchestrator, internal/probercore, internal/httpapi и БД стенда (запуск 2026-10-02, 6440 адресов).
1. Требование
Вердикт адреса и его проверки должны совпадать. После вердикта результат не может появиться или измениться. Вердикт можно пересчитать из сохранённых проверок и получить то же значение.
2. Причины (проверены по коду и данным)
- Prober проверяет один адрес много раз.
GET /probers/{site}/assignmentsотдаёт все адреса в состоянииchecking. Prober (probercore.pollOnce) обходит их при каждом опросе (каждые 5 с) и ничего не помнит о том, что адрес уже проверен. Пока адрес ждёт остальные площадки и egress, он проверяется снова и снова. - Новый результат затирает старый. Результаты пишутся через
UpsertCheckсON CONFLICT DO UPDATE. Строка живёт с первой записи, а значения берутся из последнего круга. В БД 61 406 из 76 644 ingress-строк (80%) перезаписаны позже первой записи более чем на 3 с. У egress перезаписей нет (агент проверяет один раз). - Записи принимаются после вердикта.
Orchestrator.RecordCheckвызываетUpsertCheckбез проверки состояния адреса. Итог на запуске 02.10:- ingress: 1430 строк записаны после вердикта (844 проваленных и 586 успешных); из 844 проваленных 833 записаны уже после отвязки Floating IP, то есть проверяли адрес, который к валидатору уже не привязан;
- egress: 351 строка после вердикта (32 проваленных).
- Окно считается не с начала проверки.
isReadyToAggregateсравниваетassigned_atс порогомchecking_window_seconds(120 с).assigned_atставится при выдаче адреса валидатору, то есть окно включает привязку Floating IP, паузуfip_settle_seconds(30 с) и self-check (до 60 с). На сами проверки остаётся около 30 с. По окну агрегировано 509 адресов (8%), у них часть результатов не успела прийти. - Время сравнивается чужими часами.
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. Проверка на стенде
- Копия
rxprod-compose/capi-db/control-api.db, пересборка и перезапускcontrol-api(по процедуре изdocs/SETUP.md). Prober и агенты не пересобираются. - Контрольный прогон на небольшой партии (например, 300 адресов).
- Ожидаемый результат: ни одной строки
checksсrecorded_atпозжеaggregated_at(after_verdict = 0); нет перезаписанных ingress-строк (recorded_atравен первой записи); событийresult_droppedнет или единицы; адресов с агрегацией по окну заметно меньше 8%. - Сравнение с запуском 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) строится после этого изменения: «Поздние результаты» там становятся контрольным показателем, который должен быть равен нулю.