Show egress/ingress levels in the registry; freeze checks at the verdict
Registry: the "last result" column now also shows, per level (egress,
ingress), how many of the recorded checks of the latest cycle succeeded, split
by check family (tcp-22 and tcp-443 are both "tcp"). One grouped query per
chunk of addresses; new fields last_cycle_id, egress, ingress in
GET /admin/registry; the dashboard renders them under the verdict.
Verdict integrity (migration 0010):
- the prober is handed an address once per site and attempt, not on every
poll, so results are no longer overwritten by later probe rounds;
- UpsertCheckIfOpen refuses writes once the address is aggregating or has its
verdict, or for an older attempt; senders get {"ok":true,"ignored":N} and a
result_dropped event is recorded;
- the checking window counts from checking_started_at, not from assigned_at;
- checks.recorded_at (server clock) and checks.after_verdict (flag for rows
written after the verdict in existing data);
- the verdict rule is a pure function (computeVerdict) and the aggregated
event carries the egress/ingress check counts.
Rebuilt bin/control-api and bin/admin-dashboard to match. Plans and summaries
are in docs/changes; README, API, USAGE, DASHBOARD and DIAGRAMS are updated.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
db73409e8f
commit
864208238f
34 files changed
+1570
-72
No files matched your search
+43
-5
@@ -218,6 +218,19 @@ self-check способом `control_api` (`self_check.methods` в
|
||||
|
||||
Ответ: `{"ok": true}`.
|
||||
|
||||
### Результаты после вердикта
|
||||
|
||||
Проверки фиксируются в момент вердикта: `POST /agents/{id}/results` и
|
||||
`POST /probers/{site_id}/results` принимают результат только пока адрес
|
||||
проверяется (состояния до `aggregating`) и только для его текущей попытки.
|
||||
Результат, пришедший после начала агрегации или после вердикта, а также
|
||||
результат прежней попытки не сохраняется и не меняет сохранённые проверки.
|
||||
Ответ остаётся `200`, чтобы отправитель не повторял запрос:
|
||||
`{"ok": true, "ignored": N}`, где `N` — число отброшенных проверок. На каждый
|
||||
такой запрос по адресу пишется событие `result_dropped`
|
||||
(`{"source": "egress"|"inbound-site-N", "dropped": N}`). Благодаря этому вердикт
|
||||
всегда совпадает с сохранёнными проверками и пересчитывается из них.
|
||||
|
||||
### `POST /api/v1/agents/{id}/results`
|
||||
|
||||
Отчёт о результатах исходящих (egress) проверок. Можно отправлять по
|
||||
@@ -286,13 +299,17 @@ USAGE.md про состояния площадки).
|
||||
|
||||
Запрос: тело не требуется.
|
||||
|
||||
Ответ: `{"ok": true}`. `404`, если `site_id` не сконфигурирован.
|
||||
Ответ: `{"ok": true}`, либо `{"ok": true, "ignored": N}`, если часть результатов
|
||||
отброшена (см. «Результаты после вердикта» ниже). `404`, если `site_id` не сконфигурирован.
|
||||
|
||||
### `GET /api/v1/probers/{site_id}/assignments`
|
||||
|
||||
Список всех IP, которые сейчас находятся в состоянии `checking` — то есть
|
||||
всё, что нужно прозондировать на этом цикле опроса (валидаторов может
|
||||
работать несколько параллельно, поэтому список, а не один IP).
|
||||
Список IP в состоянии `checking`, которые эта площадка ещё не зондировала
|
||||
в текущей попытке (валидаторов может работать несколько параллельно,
|
||||
поэтому список, а не один IP). Адрес выдаётся площадке, пока она не
|
||||
пришлёт для него результат с `complete: true`; после этого этой площадке он
|
||||
больше не выдаётся (другим площадкам выдаётся). Новая попытка (повтор после
|
||||
сбоя) выдаёт адрес снова. Так каждая площадка зондирует адрес один раз за попытку.
|
||||
|
||||
Ответ:
|
||||
```json
|
||||
@@ -659,7 +676,16 @@ curl -s -X POST http://<control-api>:8080/api/v1/admin/auto-cycle/stop
|
||||
"last_result": "pass",
|
||||
"last_checked_at": "2026-02-01T09:05:00Z",
|
||||
"in_queue": true,
|
||||
"current_state": "done"
|
||||
"current_state": "done",
|
||||
"last_cycle_id": 4,
|
||||
"egress": {"total": 5, "ok": 5, "by_type": [
|
||||
{"type": "https", "total": 3, "ok": 3},
|
||||
{"type": "icmp", "total": 2, "ok": 2}
|
||||
]},
|
||||
"ingress": {"total": 4, "ok": 3, "by_type": [
|
||||
{"type": "icmp", "total": 1, "ok": 1},
|
||||
{"type": "tcp", "total": 3, "ok": 2}
|
||||
]}
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -667,6 +693,18 @@ curl -s -X POST http://<control-api>:8080/api/v1/admin/auto-cycle/stop
|
||||
`in_queue`/`current_state` отражают, есть ли у адреса сейчас живая строка в
|
||||
`ip_queue`, а не только в реестре.
|
||||
|
||||
**Уровни `egress` и `ingress`.** Результат последнего цикла (`last_cycle_id` —
|
||||
наибольший цикл с записанными проверками, `0` — проверок нет), разделённый
|
||||
на выходные проверки валидатора (`egress`) и входные проверки пробера со всех
|
||||
площадок (`ingress`). В каждом уровне: `total` — сколько проверок записано,
|
||||
`ok` — сколько успешных, `by_type` — то же по типам, по алфавиту. Тип — это
|
||||
`check_type` до первого дефиса: `tcp-22` и `tcp-443` дают `tcp`, `tls-443` —
|
||||
`tls`; `https`, `icmp`, `ssh` остаются как есть, новый тип проверки появляется
|
||||
в `by_type` сам. Без проверок на уровне: `{"total": 0, "ok": 0, "by_type": []}`.
|
||||
Счёт идёт по записанным проверкам, а `last_result` учитывает ещё и недостающие
|
||||
результаты, поэтому при неполном наборе вердикт может быть хуже, чем «`ok` из
|
||||
`total`».
|
||||
|
||||
### `GET /api/v1/admin/registry/{ip}`
|
||||
|
||||
Реестровая запись по одному адресу плюс вся сохранённая история проверок
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ admin-dashboard -config /etc/cloud-ip-validator/admin-dashboard.yaml
|
||||
| `/overview` | Сводная статистика: счётчики по состояниям, «текущая проверка» (live-снимок всех IP не в терминальном состоянии) и «последние N завершённых» (по умолчанию 20, `overview.last_completed_count`) с разбивкой pass/partial/fail/cancelled. Обновляется каждые `overview.poll_interval_seconds` секунд без перезагрузки страницы. Поиск по IP и фильтр по статусу (`pass`/`partial`/`fail`/`cancelled`) над обеими таблицами — набранное/выбранное не сбрасывается очередным обновлением. Пока включён [автоматический цикл](USAGE.md#автоматический-цикл-проверок), под счётчиками показывается индикатор «Автоцикл активен» с текущей фазой и временем следующего запуска; управляется цикл на `/settings`. |
|
||||
| `/ips` | Очередь **постранично** (по 50 адресов; 25/50/100/200) с поиском по IP и фильтром по состоянию/итогу на сервере; кнопка «Сканировать Floating IP» запускает фоновое сканирование с панелью прогресса, «Пробное сканирование» ничего не ставит в очередь (подробности — «Очередь из тысяч адресов» ниже). Форма сверху принимает список адресов (по одному на строке или через запятую) и отправляет их в `POST /api/v1/admin/ips` — **один и тот же вызов** добавляет новые адреса и принудительно перезапускает уже завершённые (см. ниже). Кнопка «Сканировать Floating IP» делает то же самое автоматически: находит в проекте OpenStack все свободные (не привязанные к порту) Floating IP и сразу ставит их в очередь (`POST /api/v1/admin/ips/scan`, см. [API.md](API.md#post-apiv1adminipsscan)) — то же сканирование можно включить по расписанию через `orchestrator.fip_scan_interval_seconds`. У каждого адреса — кнопка «Перепроверить» (для `done`/`failed`) или «Отменить» (для активных состояний), и всегда — «Удалить» (безвозвратно убирает адрес из очереди, но не из реестра — см. ниже). Чекбоксы у строк + кнопка «Удалить выбранные» удаляют список одним вызовом; «Очистить всё» удаляет вообще всё, включая активные проверки — обе операции требуют явного подтверждения. Пока не истекла настроенная на `/settings` пауза (`fip_settle_seconds`), только что привязавший Floating IP адрес показывает отдельный бейдж «прогрев FIP» вместо обычного статуса. Если на момент попытки привязки Floating IP оказался уже занят другим портом (дрейф состояния облака или ошибочно переданный адрес), цикл проверки для него не запускается — адрес показывает отдельный бейдж «занят» (отличный от «fail») и строку `fip_occupied` в списке событий на его странице; кнопка «Перепроверить» ставит его в очередь заново. |
|
||||
| `/ips/{ip}` | Детали одного адреса, пока он в очереди: все проверки текущей попытки и вся история событий, плюс ссылка на полную историю в реестре (см. ниже). |
|
||||
| `/registry` | **Реестр** — все адреса, когда-либо поставленные на проверку, независимо от того, стоят ли они сейчас в очереди. Переживает удаление адреса из `/ips` и повторное добавление того же адреса позже (см. «Реестр адресов» ниже). Поиск по IP и фильтр по статусу — то же самое, что на `/overview`, плюс отражается в адресной строке (`?q=&status=`), так что отфильтрованную ссылку можно сохранить/переслать. |
|
||||
| `/registry` | **Реестр** — все адреса, когда-либо поставленные на проверку, независимо от того, стоят ли они сейчас в очереди. Переживает удаление адреса из `/ips` и повторное добавление того же адреса позже (см. «Реестр адресов» ниже). Поиск по IP и фильтр по статусу — то же самое, что на `/overview`, плюс отражается в адресной строке (`?q=&status=`), так что отфильтрованную ссылку можно сохранить/переслать. В колонке «Последний результат» под вердиктом — уровни **Egress** и **Ingress** в виде «N из M» с разбивкой по типам проверок (`https`, `icmp`, `ssh`, `tcp`, `tls`…), см. [USAGE.md](USAGE.md#реестр-адресов-и-глубина-истории). |
|
||||
| `/registry/{ip}` | Полная сохранённая история проверок одного адреса по всем циклам (не только текущему) — в отличие от `/ips/{ip}`, которая показывает только текущую попытку. |
|
||||
| `/validators` | Список валидаторов + создание/изменение `os_port_id`/удаление. |
|
||||
| `/sites` | Площадки — число слотов не ограничено, форма сверху добавляет новый слот, назначить/сменить/освободить `site_id` в каждой строке; колонка «Статус» показывает бейдж подключения пробера (`unregistered`/`idle`/`unreachable`, по аналогии с `/validators`), см. [USAGE.md](USAGE.md#состояния-площадки). |
|
||||
|
||||
+2
-1
@@ -226,7 +226,8 @@ flowchart TB
|
||||
только все ожидаемые флаги выставлены — либо истекло время ожидания
|
||||
(`checking_window_seconds`) — фоновая агрегация суммирует все строки
|
||||
`checks` по текущей попытке и записывает итог (`pass`/`partial`/`fail`)
|
||||
обратно в `ip_queue`. Оператор в любой момент читает уже накопленные
|
||||
обратно в `ip_queue`. С началом агрегации запись проверок по адресу
|
||||
закрыта: опоздавшие результаты отбрасываются (событие `result_dropped`). Оператор в любой момент читает уже накопленные
|
||||
данные через административные `GET`-методы, не дожидаясь завершения
|
||||
проверки — подробнее о значениях полей см.
|
||||
[USAGE.md](USAGE.md#значения-полей-ip).
|
||||
|
||||
+25
-1
@@ -331,7 +331,19 @@ curl -s http://<control-api>:8080/api/v1/admin/ips \
|
||||
Отсутствие ответа от источника (площадка не прислала результат до
|
||||
истечения `checking_window_seconds`) засчитывается как провал — это
|
||||
управляется настройкой `aggregation.missing_counts_as_fail` в конфиге
|
||||
control-api (по умолчанию включено).
|
||||
control-api (по умолчанию включено). Окно `checking_window_seconds`
|
||||
отсчитывается от начала проверки (состояние `checking`), а не от выдачи
|
||||
адреса валидатору, поэтому привязка Floating IP, пауза `fip_settle_seconds`
|
||||
и self-check в окно не входят.
|
||||
|
||||
**Вердикт и проверки не расходятся.** Проверки фиксируются в момент
|
||||
вердикта: результат, пришедший после него (опоздавший), не сохраняется и не
|
||||
меняет проверки адреса — Floating IP к этому времени уже отвязан, и такая
|
||||
проверка ничего не доказывает. Каждая площадка зондирует адрес один раз за
|
||||
попытку. Отброшенные результаты видны как события `result_dropped` у адреса;
|
||||
в нормальном прогоне их нет или единицы. Недостающий результат (источник не
|
||||
прислал его вовсе) по-прежнему считается провалом, вердикт тогда не выше
|
||||
`partial`.
|
||||
|
||||
## Просмотр деталей и истории по конкретному адресу
|
||||
|
||||
@@ -385,6 +397,18 @@ curl -s http://<control-api>:8080/api/v1/admin/registry/203.0.113.10 | python3 -
|
||||
`/overview`, плюс он отражается в адресной строке (`?q=&status=`), так что
|
||||
отфильтрованную ссылку можно сохранить или переслать.
|
||||
|
||||
В колонке «Последний результат» под вердиктом (`pass`/`partial`/`fail`)
|
||||
показаны два уровня последнего цикла: **Egress** (выходные проверки
|
||||
валидатора) и **Ingress** (входные проверки пробера со всех площадок) в виде
|
||||
«успешно из всего», например `Egress 5 из 5`, `Ingress 3 из 4`. Под каждым
|
||||
уровнем — то же по типам проверок (`https 3 из 3`, `icmp 2 из 2`, `tcp 2 из 3`);
|
||||
порты объединены в тип (`tcp-22` и `tcp-443` — один `tcp`), новый тип
|
||||
проверки появится в списке сам. Цвет: зелёный — все успешны, красный — ни одной,
|
||||
жёлтый — часть. Считаются записанные проверки цикла; вердикт учитывает ещё и
|
||||
недостающие результаты, поэтому при неполном наборе он может быть хуже, чем
|
||||
видно по счётчикам (подсказка при наведении). Те же данные — в
|
||||
`GET /api/v1/admin/registry` ([API.md](API.md#get-apiv1adminregistry)).
|
||||
|
||||
**Глубина хранения.** Чтобы история не росла бесконечно на адресах,
|
||||
которые перепроверяют очень часто, можно ограничить, сколько последних
|
||||
циклов проверки хранить на каждый адрес — `history_retention_cycles` на
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# План: уровни Egress / Ingress в таблице «Реестр»
|
||||
|
||||
## Контекст
|
||||
|
||||
Колонка «Последний результат» в «Реестре» показывает одну пилюлю общего вердикта (pass / partial / fail / cancelled). Для аналитики этого мало: не видно, где именно сбой — на выходе (egress) или на входе (ingress) — и какой тип проверки упал.
|
||||
|
||||
Нужно разделить результат на два уровня и показать «успешно из всего» (например, «5 из 5») по каждому уровню, с разбивкой по типу проверки (icmp, ssh, tcp, https, tls и новые типы, когда появятся).
|
||||
|
||||
Решения пользователя:
|
||||
- Пилюля общего вердикта остаётся, уровни добавляются под ней. Фильтр по вердикту не меняется.
|
||||
- «Всего» = число записанных проверок последнего цикла (не ожидаемое по настройкам).
|
||||
|
||||
## Что уже есть (по графу и коду)
|
||||
|
||||
- Проверки лежат в `checks` (`internal/db/migrations/0007_ip_registry.sql:39-65`). Уровень задаёт `source`: `egress` или `inbound-site-N` (`internal/db/models.go:39,68-72`). Тип — в `check_type`: `https`, `icmp`, `ssh`, `tcp-<порт>`, `tls-<порт>`.
|
||||
- Индекс `idx_checks_registry_cycle(registry_id, cycle_id)` уже есть. Миграция не нужна.
|
||||
- Цепочка данных: `fillRegistrySummary` (`internal/db/queries_registry.go:199`) → `registrySummaryToDTO` (`internal/httpapi/handlers_registry.go:75`) → `registryDTO` (`internal/httpapi/dto_admin.go:87`) → клиент дашборда → `registryItem` (`internal/dashboard/dto.go:258`) → шаблон `internal/dashboard/templates/registry.html` (`registry_table`, строки 69–101).
|
||||
- Дашборд ходит в БД только через HTTP control-api. Менять нужно оба слоя.
|
||||
|
||||
## Правила расчёта
|
||||
|
||||
- Цикл: максимальный `cycle_id` в `checks` для адреса (так же, как сейчас считается `LastCheckedAt`).
|
||||
- Уровень: `source = 'egress'` → Egress; `source LIKE 'inbound-site-%'` → Ingress. Прочее игнорируется.
|
||||
- Тип: часть `check_type` до первого `-` (`tcp-22`, `tcp-443` → `tcp`; `tls-443` → `tls`; `ssh`, `icmp`, `https` без изменений). Новый тип попадает в разбивку сам, без правок кода.
|
||||
- Ingress: считаются проверки всех площадок вместе (площадок может быть любое число).
|
||||
- Правило разбора живёт в одном месте: две функции в `internal/db/models.go` рядом с `InboundSource`: `CheckLevel(source)` и `CheckFamily(checkType)`.
|
||||
- Группировка делается в Go, не в SQL: SQL отдаёт `GROUP BY registry_id, source, check_type`, и это не больше ~30 строк на адрес.
|
||||
|
||||
## Изменения
|
||||
|
||||
### 1. БД (`internal/db`)
|
||||
- `models.go`: типы `TypeStat{Type, Total, OK}` и `LevelResult{Total, OK, ByType []TypeStat}` (по типам — в алфавитном порядке). Функции `CheckLevel`, `CheckFamily`.
|
||||
- `queries_registry.go`: в `RegistrySummary` добавить `LastCycleID`, `Egress`, `Ingress`. Один пакетный запрос на страницу вместо запроса на каждый адрес:
|
||||
```sql
|
||||
SELECT c.registry_id, c.source, c.check_type, COUNT(*), SUM(c.success)
|
||||
FROM checks c
|
||||
JOIN (SELECT registry_id, MAX(cycle_id) AS cid FROM checks
|
||||
WHERE registry_id IN (…) GROUP BY registry_id) m
|
||||
ON m.registry_id = c.registry_id AND m.cid = c.cycle_id
|
||||
GROUP BY c.registry_id, c.source, c.check_type
|
||||
```
|
||||
Новая функция заполняет сводки списком адресов (порциями по 500). Её вызывают `ListRegistryPage`, `ListRegistry` и `GetRegistryByAddress`. Это заодно убирает N+1 для новых полей.
|
||||
- `lastResultCond` и `fillRegistrySummary` (вердикт) не меняются.
|
||||
|
||||
### 2. HTTP API (`internal/httpapi`)
|
||||
- `dto_admin.go`: в `registryDTO` добавить `last_cycle_id`, `egress`, `ingress` как `{total, ok, by_type: [{type, total, ok}]}`. Изменение только добавляет поля, старые клиенты не ломаются.
|
||||
- `handlers_registry.go`: `registrySummaryToDTO` заполняет новые поля.
|
||||
- `docs/API.md` (раздел «Реестр адресов», ~645–668): новые поля и пример JSON.
|
||||
|
||||
### 3. Дашборд (`internal/dashboard`)
|
||||
- `dto.go`: `registryItem` получает те же поля.
|
||||
- `templates/registry.html`: в ячейке «Последний результат» сверху пилюля вердикта, ниже две строки:
|
||||
`Egress 5 из 5` и `Ingress 3 из 4`, под каждой чипы по типам: `https 3 из 3`, `icmp 2 из 2`. Цвет: все успешны — зелёный, ни одной — красный, иначе жёлтый. Нет проверок — «—». В `title` ячейки: «по записанным проверкам цикла N; вердикт учитывает недостающие результаты». Сохранить `data-label` для мобильной раскладки.
|
||||
- `static/dashboard.css` (~417–423): стили строки уровня и чипа по образцу `pill-*`.
|
||||
- Страница деталей `registry_detail.html` не меняется (вне объёма).
|
||||
|
||||
### 4. Тесты
|
||||
- `internal/db/queries_registry_test.go`: новые тесты — разбивка egress/ingress по типам; `tcp-22` и `tcp-443` сливаются в `tcp`; `tls`, `ssh`; новый тип (например `dns`) появляется в разбивке; адрес без проверок даёт нули; берётся последний цикл; работает после удаления строки очереди; несколько адресов в одном пакетном запросе. Добавить хелпер с ingress-проверками рядом с `finishWithChecks` (`queries_scale_test.go:24`).
|
||||
- `internal/httpapi/handlers_scale_test.go` (`TestAdminRegistryPaginationFiltersAndCompat`): проверить новые поля в ответе.
|
||||
- `internal/dashboard/handlers_test.go` (`TestRegistryPageAndDetail`): фикстура с уровнями, проверка вывода «5 из 5» и имён типов.
|
||||
- `TestScaleSmoke6440` (`queries_scale_test.go:356`): добавить ingress-проверки в засев, лимит 10 с сохраняется.
|
||||
|
||||
## Порядок работы (по правилам проекта)
|
||||
|
||||
1. Скопировать этот план в `docs/changes/2026-10-03_<ЧЧ-ММ>_registry-egress-ingress-levels-plan.md`.
|
||||
2. Реализовать п.1 → п.2 → п.3 → п.4.
|
||||
3. Написать `docs/changes/…-summary.md` (что изменено).
|
||||
4. Обновить `README.md` и `docs/API.md`/`docs/USAGE.md`, где описан реестр.
|
||||
5. Обновить граф: `/graphify . --update`.
|
||||
|
||||
## Проверка
|
||||
|
||||
- `go build ./... && go vet ./... && go test ./...`
|
||||
- `EXPLAIN QUERY PLAN` пакетного запроса: должен идти по `idx_checks_registry_cycle`, без полного перебора `checks`.
|
||||
- Локальный стенд (`scripts/run-local-e2e.sh`, `docs/LOCAL_E2E.md`): прогнать цикл, открыть `/registry`, сверить «N из M» по уровням и типам с таблицей на странице деталей `/registry/<ip>`.
|
||||
- Страница на 6440 адресов отвечает за время порядка прежних 0,12 с (допустим рост в разы, но не секунды).
|
||||
|
||||
## Ограничения и риски
|
||||
|
||||
- «N из M» считается по записанным проверкам. Если результатов не хватает, вердикт может быть хуже, чем «N из M» (например, все записанные успешны, а вердикт `partial`). Это показано подсказкой.
|
||||
- Пока идёт цикл, счётчики отражают частично пришедшие проверки текущего цикла. Состояние цикла видно в колонке состояния.
|
||||
- Фильтра и сортировки по уровням нет: сейчас вне объёма. Если понадобятся для аналитики, это отдельное изменение.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Реестр: уровни Egress и Ingress — итог
|
||||
|
||||
План: [2026-10-03_16-24_registry-egress-ingress-levels-plan.md](2026-10-03_16-24_registry-egress-ingress-levels-plan.md).
|
||||
|
||||
## Что сделано
|
||||
|
||||
В колонке «Последний результат» таблицы «Реестр» под вердиктом показаны два уровня последнего цикла:
|
||||
**Egress** и **Ingress** в виде «успешно из всего» (например, «5 из 5»), а под каждым — то же по типам проверок
|
||||
(`https`, `icmp`, `ssh`, `tcp`, `tls`; новый тип появляется сам).
|
||||
|
||||
## Правила
|
||||
|
||||
- Цикл — наибольший `cycle_id` с записанными проверками адреса.
|
||||
- Уровень по `checks.source`: `egress` → Egress, `inbound-site-N` → Ingress (все площадки вместе). Прочие источники не считаются.
|
||||
- Тип — `check_type` до первого дефиса: `tcp-22` и `tcp-443` → `tcp`.
|
||||
- «Всего» — число записанных проверок. Вердикт (`last_result`) учитывает ещё и недостающие результаты, поэтому может быть хуже, чем «N из M». В таблице это отмечено подсказкой.
|
||||
|
||||
## Изменённые файлы
|
||||
|
||||
- `internal/db/models.go` — `CheckLevel`, `CheckFamily`, константы уровней.
|
||||
- `internal/db/queries_registry.go` — `TypeStat`, `LevelResult`, поля `LastCycleID`/`Egress`/`Ingress` в `RegistrySummary`; `fillRegistryLevels` — один сгруппированный запрос на порцию до 500 адресов вместо запроса на адрес (используют `ListRegistry`, `ListRegistryPage`, `GetRegistryByAddress`). Миграция не нужна, запрос идёт по `idx_checks_registry_cycle`.
|
||||
- `internal/httpapi/dto_admin.go`, `handlers_registry.go` — поля `last_cycle_id`, `egress`, `ingress` (только добавление, старые клиенты не ломаются).
|
||||
- `internal/dashboard/dto.go`, `render.go` (функция шаблона `dict`), `templates/registry.html`, `static/dashboard.css` — отображение.
|
||||
- Документация: `docs/API.md`, `docs/USAGE.md`, `docs/DASHBOARD.md`, `README.md`.
|
||||
|
||||
## Не менялось
|
||||
|
||||
Фильтр по вердикту (`lastResultCond`), расчёт `last_result`, страница деталей `/registry/{ip}`, схема БД.
|
||||
|
||||
## Тесты и проверка
|
||||
|
||||
- Новые: `internal/db/queries_registry_levels_test.go` (разбор уровней и типов, нулевые счётчики, последний цикл, несколько адресов, после удаления строки очереди), `TestRegistryLevelsQueryUsesIndex` (план запроса без полного перебора `checks`), `TestAdminRegistryLevels` (API), `TestRegistryPageShowsEgressIngressLevels` (дашборд).
|
||||
- `TestScaleSmoke6440` дополнен проверками (30 на адрес): страница из 100 адресов — около 0,18 с.
|
||||
- `go build ./... && go vet ./... && go test ./...` — без ошибок.
|
||||
- Не проверено вручную на локальном стенде (`scripts/run-local-e2e.sh`) и в браузере: вид колонки на узком экране проверен только по разметке.
|
||||
|
||||
## Ограничения
|
||||
|
||||
- Пока цикл идёт, счётчики отражают частично пришедшие проверки.
|
||||
- Фильтра и сортировки по уровням нет — отдельное изменение, если понадобится для аналитики.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Вердикт без опоздавших результатов — итог
|
||||
|
||||
План: [2026-10-03_17-21_verdict-no-late-results-plan.md](2026-10-03_17-21_verdict-no-late-results-plan.md).
|
||||
|
||||
Статус: код и тесты готовы. На стенд не выкладывалось, контрольный прогон не проводился.
|
||||
|
||||
## Что сделано
|
||||
|
||||
1. **Один проход площадки на адрес.** `GET /probers/{site}/assignments` больше не отдаёт адрес, для которого эта площадка отметила `complete` в текущей попытке (`DB.ListCheckingForSite`). Другим площадкам адрес выдаётся, новая попытка выдаёт его снова. Prober не менялся.
|
||||
2. **После вердикта запись закрыта.** `DB.UpsertCheckIfOpen` пишет проверку только если строка очереди существует, попытка совпадает с текущей и состояние не `aggregating`, `done`, `failed`, `occupied`. Проверка состояния и запись — один SQL-запрос. Отброшенный результат не создаёт и не меняет строк. Ответ `200 {"ok": true, "ignored": N}`; на каждый адрес пишется событие `result_dropped`. `UpsertCheck` и `Orchestrator.RecordCheck` оставлены как обёртки для существующих вызовов.
|
||||
3. **Окно проверки с её начала.** Новая колонка `ip_queue.checking_started_at` (ставится в `SetChecking`, сбрасывается при повторе). `isReadyToAggregate` считает `checking_window_seconds` от неё, для старых строк — от `assigned_at`.
|
||||
4. **Время записи по часам сервера.** Колонка `checks.recorded_at`, обновляется при каждой принятой записи.
|
||||
5. **Вердикт воспроизводим.** Правило вынесено в чистую функцию `computeVerdict`; в событие `aggregated` добавлены `egress` и `ingress` (число проверок по уровням).
|
||||
6. **Старые данные.** Миграция `0010_verdict_integrity.sql`: `recorded_at` = `created_at`, колонка `after_verdict` = 1 у строк, записанных после вердикта (`julianday(checked_at) > julianday(aggregated_at)`).
|
||||
|
||||
## Проверка на копии боевой БД (запуск 02.10, 141 005 проверок)
|
||||
|
||||
Миграция применилась за секунды, `user_version` = 10, `recorded_at` заполнен у всех строк. Помечено `after_verdict`:
|
||||
|
||||
| | всего | провалов | успешных |
|
||||
|---|--:|--:|--:|
|
||||
| ingress | 1430 | 844 | 586 |
|
||||
| egress | 351 | 32 | 319 |
|
||||
|
||||
В плане было 1426 и 350: они считались сравнением строк времени, миграция сравнивает точнее.
|
||||
|
||||
## Тесты
|
||||
|
||||
- `internal/db/queries_verdict_integrity_test.go`: запись принимается, пока адрес проверяется, и идемпотентна; после начала агрегации и после вердикта не создаёт и не меняет строк; результат прежней попытки отбрасывается; `recorded_at` растёт, `created_at` нет; `ListCheckingForSite` (готовая площадка, другая площадка, новая попытка); `checking_started_at` ставится и сбрасывается; миграция 0010 на базе версии 9.
|
||||
- `internal/orchestrator/verdict_integrity_test.go`: таблица `computeVerdict` (7 случаев); окно от начала проверки и запасной вариант `assigned_at`; полный цикл: после вердикта результат не принят, сохранённые проверки не изменились, вердикт равен пересчёту.
|
||||
- `internal/httpapi/handlers_verdict_integrity_test.go`: площадка получает адрес до `complete`, после него нет, другая площадка получает; результаты prober и агента после вердикта дают `ignored`, строки не изменены, два события `result_dropped`.
|
||||
- `go build ./... && go vet ./... && go test ./...` проходят.
|
||||
|
||||
## Что не проверено
|
||||
|
||||
- Работа на живом стенде и контрольный прогон. Гипотеза «часть прежних провалов ingress — проверка отвязанного Floating IP» подтвердится или нет только им: после изменения доля провалов ssh, tcp-22 и icmp должна упасть. Если не упадёт, причину нужно искать в другом месте.
|
||||
- Нагрузка на сеть площадок изменится: prober теперь зондирует адрес один раз, а не при каждом опросе.
|
||||
|
||||
## Что дальше
|
||||
|
||||
Пересборка и перезапуск только `control-api` (prober и агенты без изменений), копия БД перед миграцией, контрольный прогон на небольшой партии (300 адресов). Раздел «Аналитика» строится после этого и получает миграцию `0011`.
|
||||
Reference in new issue
Block a user