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
@@ -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