Retry a failed self-check on another validator; add the self-check failure ceiling

A validator that failed the self-check of an address no longer gets that address
again in the current round (ClaimNextQueued skips it); the validator itself stays
in service and takes all other addresses. The verdict fail is set when the number
of failed self-checks of an address reaches settings.self_check_max_attempts
(1..50, default 5, independent of the number of validators); max_retries and
retry_count are no longer used for self-check. If every working validator has
already failed the address, a new round starts and the exclusions lapse.

Migration 0012: ip_self_check_failures (permanent history per registry address),
ip_queue.sc_failures and sc_round_start_cycle (cycle_id is used instead of
attempt_number, which restarts when a queue row is recreated), the setting.
db.FailSelfCheck does it in one transaction; re-submission starts a new series.
API: self_check_max_attempts in GET/PUT /admin/config/orchestrator,
self_check_failed_on in /admin/ips/{ip} and /admin/registry/{ip}. Dashboard: the
field on /settings and the line "Self-check не прошёл на: ..." on the address
pages. Docs, plan and summary in docs/changes/.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ayurishchevandClaude Sonnet 5.5 committed 2026-10-04 09:50:12 +03:00
1 parent b7669c9e41
commit e95b5eb7d5
34 files changed
+1092 -82

No files matched your search

+6 -4
View File
@@ -12,7 +12,7 @@
| Группа | Таблицы | Можно чистить |
|---|---|---|
| Данные прогона | `ip_queue` (очередь), `ip_registry` (реестр адресов), `checks` (реестр проверок), `ip_site_checks` (признаки площадок по адресам в работе), `check_runs` и `run_results` (запуски и итоги адресов в них), `events` (журнал событий) | да |
| Данные прогона | `ip_queue` (очередь), `ip_registry` (реестр адресов), `checks` (реестр проверок), `ip_site_checks` (признаки площадок по адресам в работе), `check_runs` и `run_results` (запуски и итоги адресов в них), `ip_self_check_failures` (история сбоев self-check по адресам), `events` (журнал событий) | да |
| Настройки (не трогать) | `validators`, `sites`, `target_groups` (цели), `check_types`, `inbound_checks_settings`, `settings`, `auto_cycle`, `subnets` (список подсетей для аналитики) | **нет** |
| Служебное | `sqlite_sequence` (нумерация записей), `PRAGMA user_version` (версия схемы) | нумерацию можно сбросить, версию не менять |
@@ -86,7 +86,7 @@ UNION ALL SELECT 'sites', COUNT(*) FROM sites;"
### 3.1. Полный сброс данных прогона (перед новым полным прогоном)
Очищает очередь, реестр адресов, реестр проверок, журнал событий. Валидаторы, площадки, цели, типы проверок и все настройки остаются.
Очищает очередь, реестр адресов, реестр проверок, историю сбоев self-check, журнал событий. Валидаторы, площадки, цели, типы проверок и все настройки остаются.
```sql
PRAGMA foreign_keys = ON;
@@ -99,11 +99,12 @@ DELETE FROM ip_site_checks;
DELETE FROM run_results;
DELETE FROM check_runs;
DELETE FROM checks;
DELETE FROM ip_self_check_failures;
DELETE FROM events;
DELETE FROM ip_queue;
DELETE FROM ip_registry;
-- нумерация снова с 1 (необязательно)
DELETE FROM sqlite_sequence WHERE name IN ('ip_registry', 'checks', 'ip_queue', 'events', 'check_runs');
DELETE FROM sqlite_sequence WHERE name IN ('ip_registry', 'checks', 'ip_queue', 'events', 'check_runs', 'ip_self_check_failures');
COMMIT;
```
@@ -157,7 +158,7 @@ WHERE cycle_id <= (SELECT MAX(c2.cycle_id) FROM checks c2 WHERE c2.registry_id =
### 3.4. Удалить конкретные адреса целиком
Удаляет адрес из очереди и реестра вместе со всей его историей (проверки и события). Список адресов подставьте в первую команду
Удаляет адрес из очереди и реестра вместе со всей его историей (проверки, сбои self-check и события). Список адресов подставьте в первую команду
`CREATE TEMP TABLE doomed_reg`. Адрес, который сейчас проверяется, удалять этим способом нельзя: используйте API (раздел 5).
```sql
@@ -171,6 +172,7 @@ UPDATE validators SET current_ip_id = NULL WHERE current_ip_id IN (SELECT id FRO
DELETE FROM ip_site_checks WHERE ip_id IN (SELECT id FROM doomed_ip);
DELETE FROM run_results WHERE registry_id IN (SELECT id FROM doomed_reg);
DELETE FROM checks WHERE registry_id IN (SELECT id FROM doomed_reg);
DELETE FROM ip_self_check_failures WHERE registry_id IN (SELECT id FROM doomed_reg);
DELETE FROM events WHERE registry_id IN (SELECT id FROM doomed_reg) OR ip_id IN (SELECT id FROM doomed_ip);
DELETE FROM ip_queue WHERE id IN (SELECT id FROM doomed_ip);
DELETE FROM ip_registry WHERE id IN (SELECT id FROM doomed_reg);
+28 -7
View File
@@ -198,8 +198,13 @@ self-check способом `control_api` (`self_check.methods` в
```
Ответ: `{"ok": true}`. При `success: false` control-api сам решает —
повторить попытку назначения FIP или пометить IP как `failed` (после
исчерпания `orchestrator.max_self_check_retries`).
вернуть адрес в очередь или пометить IP как `failed`. Сбой записывается в
историю адреса; повтор **не отдаётся этому валидатору** (он остаётся в
работе и берёт остальные адреса). Итог `fail` ставится, когда число
проваленных self-check у адреса достигло потолка `self_check_max_attempts`
(по умолчанию 5, см. [«Настройки оркестратора»](#настройки-оркестратора-apiv1adminconfigorchestrator));
`orchestrator.max_self_check_retries` не используется. Сбой привязки FIP и
истечение лизинга идут по `max_retries`, как раньше.
### `POST /api/v1/agents/{id}/events`
@@ -415,10 +420,14 @@ IP на данном проходе". До этого момента control-api
{
"ip": { "ID": 42, "IPAddress": "203.0.113.10", "State": "done", "OverallResult": "pass", "...": "..." },
"checks": [ {"Source": "egress", "CheckType": "https", "Target": "https://github.com", "Success": true, "...": "..."} ],
"events": [ {"EventType": "fip_associated", "OccurredAt": "...", "...": "..."} ]
"events": [ {"EventType": "fip_associated", "OccurredAt": "...", "...": "..."} ],
"self_check_failed_on": ["vkiplab-v17"]
}
```
`self_check_failed_on` — валидаторы, у которых self-check на этом адресе не
прошёл в текущем запуске (по алфавиту; `[]`, если сбоев не было).
> Обратите внимание: вложенные объекты `ip`/`checks`/`events` сериализуются
> без переопределения имён полей (используются имена Go-структур, например
> `IPAddress`, `State`, `Success`) — в отличие от методов для
@@ -722,10 +731,14 @@ curl -s -X POST http://<control-api>:8080/api/v1/admin/auto-cycle/stop
"checks": [
{"CycleID": 4, "Source": "egress", "CheckType": "https", "Success": true, "...": "..."},
{"CycleID": 3, "Source": "egress", "CheckType": "https", "Success": false, "...": "..."}
]
],
"self_check_failed_on": ["vkiplab-v17"]
}
```
`self_check_failed_on` — валидаторы, у которых self-check на этом адресе не
прошёл, по всем запускам (по алфавиту; `[]`, если сбоев не было).
`404`, если адрес никогда не ставился на проверку.
**Глубина хранения.** Сколько последних циклов на адрес хранится в
@@ -797,7 +810,7 @@ curl -s -X POST "$BASE/api/v1/admin/ips/clear"
### Настройки оркестратора: `/api/v1/admin/config/orchestrator`
Два параметра:
Три параметра:
- `fip_settle_seconds` — пауза между привязкой Floating IP к валидатору и
моментом, когда self-check по этому адресу становится доступен агенту
@@ -811,11 +824,19 @@ curl -s -X POST "$BASE/api/v1/admin/ips/clear"
на адрес в реестре (`GET /api/v1/admin/registry/{ip}`, см.
[«Реестр адресов»](#реестр-адресов-и-история-проверок)). `0` — без
ограничения (поведение по умолчанию).
- `self_check_max_attempts` — потолок провалов self-check на один адрес
(1…50, по умолчанию 5). Когда у адреса провалено столько self-check,
он получает итог `fail`. Не зависит от числа валидаторов. Валидатор,
проваливший self-check на адресе, этому адресу больше не выдаётся (на
остальные адреса это не влияет); если все рабочие валидаторы уже
провалили адрес, исключения сбрасываются и повторы продолжаются до
потолка. Значение действует на следующих повторах без перезапуска.
В `PUT` поле необязательно: если не передано, не меняется.
| Метод | Путь | Тело | Успех | Ошибки |
|---|---|---|---|---|
| GET | `/api/v1/admin/config/orchestrator` | — | `{"fip_settle_seconds":N,"history_retention_cycles":M}` | |
| PUT | `/api/v1/admin/config/orchestrator` | `{"fip_settle_seconds":N,"history_retention_cycles":M}` | `200` | `400`, если `N < 0` или `M < 0`, или если `fip_settle_seconds + self_check_timeout_seconds >= lease_ttl_seconds` (пауза не должна съедать весь лизинг адреса — иначе self-check не успеет пройти до истечения `lease_ttl_seconds`, и адрес будет вечно возвращаться в очередь) |
| GET | `/api/v1/admin/config/orchestrator` | — | `{"fip_settle_seconds":N,"history_retention_cycles":M,"self_check_max_attempts":K}` | |
| PUT | `/api/v1/admin/config/orchestrator` | `{"fip_settle_seconds":N,"history_retention_cycles":M,"self_check_max_attempts":K}` | `200` | `400`, если `N < 0`, `M < 0` или `K` вне 1…50, или если `fip_settle_seconds + self_check_timeout_seconds >= lease_ttl_seconds` (пауза не должна съедать весь лизинг адреса — иначе self-check не успеет пройти до истечения `lease_ttl_seconds`, и адрес будет вечно возвращаться в очередь) |
Как и остальные разделы этой группы, YAML-поле `orchestrator.
fip_settle_seconds` в `control-api.yaml` — только одноразовый bootstrap
+1 -1
View File
@@ -63,7 +63,7 @@ admin-dashboard -config /etc/cloud-ip-validator/admin-dashboard.yaml
| `/sites` | Площадки — число слотов не ограничено, форма сверху добавляет новый слот, назначить/сменить/освободить `site_id` в каждой строке; колонка «Статус» показывает бейдж подключения пробера (`unregistered`/`idle`/`unreachable`, по аналогии с `/validators`), см. [USAGE.md](USAGE.md#состояния-площадки). |
| `/targets` | Группы целей для egress-проверок — создание/редактирование/удаление. |
| `/check-types` | Типы проверок (`https`/`icmp`/`ssh`/...), включение/выключение, привязка к группам целей. |
| `/settings` | Четыре блока. Первый — панель **«Автоматический цикл»**: статус и фаза, время последнего/следующего запуска, результат последнего цикла, поля «Интервал между циклами (мин)» и «Максимальная длительность проверки (мин, 0 = без лимита)» с кнопкой «Сохранить» и кнопка «Включить»/«Выключить» (показывается та, что сейчас применима). Значения вводятся в минутах (допустимы дробные), в control-api уходят секундами; минимум интервала — 1 минута (`60` с), нарушение приходит предупреждением в баннере. Подробности — [USAGE.md](USAGE.md#автоматический-цикл-проверок), API — [API.md](API.md#автоматический-цикл-проверок). Далее три формы: `fip_settle_seconds` — пауза (в секундах) между привязкой Floating IP и началом self-check («прогрев» дата-плейна OpenStack, см. [USAGE.md](USAGE.md#пауза-перед-self-check-fip_settle_seconds)); `history_retention_cycles` — сколько последних циклов проверки хранить на адрес в реестре (0 — без ограничения); и типы проверок пробера — TCP-порты (через запятую) + чекбокс ICMP, общие для всех площадок (см. [USAGE.md](USAGE.md#управление-типами-проверок-пробера)). |
| `/settings` | Четыре блока. Первый — панель **«Автоматический цикл»**: статус и фаза, время последнего/следующего запуска, результат последнего цикла, поля «Интервал между циклами (мин)» и «Максимальная длительность проверки (мин, 0 = без лимита)» с кнопкой «Сохранить» и кнопка «Включить»/«Выключить» (показывается та, что сейчас применима). Значения вводятся в минутах (допустимы дробные), в control-api уходят секундами; минимум интервала — 1 минута (`60` с), нарушение приходит предупреждением в баннере. Подробности — [USAGE.md](USAGE.md#автоматический-цикл-проверок), API — [API.md](API.md#автоматический-цикл-проверок). Далее формы: `fip_settle_seconds` — пауза (в секундах) между привязкой Floating IP и началом self-check («прогрев» дата-плейна OpenStack, см. [USAGE.md](USAGE.md#пауза-перед-self-check-fip_settle_seconds)); `self_check_max_attempts` — «Потолок провалов self-check на адрес» (1…50, по умолчанию 5; повторы идут на других валидаторах, см. [USAGE.md](USAGE.md#повтор-после-сбоя-self-check)); `history_retention_cycles` — сколько последних циклов проверки хранить на адрес в реестре (0 — без ограничения); и типы проверок пробера — TCP-порты (через запятую) + чекбокс ICMP, общие для всех площадок (см. [USAGE.md](USAGE.md#управление-типами-проверок-пробера)). |
### «В работе», «в очереди» и «последняя завершённая» проверка
+36 -2
View File
@@ -33,6 +33,7 @@
- [Принудительная остановка проверки](#принудительная-остановка-проверки)
- [Удаление адресов из очереди](#удаление-адресов-из-очереди)
- [Пауза перед self-check (fip_settle_seconds)](#пауза-перед-self-check-fip_settle_seconds)
- [Повтор после сбоя self-check](#повтор-после-сбоя-self-check)
- [Частые проблемы и что с ними делать](#частые-проблемы-и-что-с-ними-делать)
## Как устроена работа с системой
@@ -306,8 +307,9 @@ curl -s http://<control-api>:8080/api/v1/admin/ips \
оператора: годится ли адрес для данного случая использования.
- **`fail`** — либо ни одна проверка не прошла, либо адрес вообще не
дошёл до стадии проверок (например, self-check не подтвердился —
трафик валидатора не пошёл через назначенный FIP — и попытки
исчерпались). Смотрите `events` по этому адресу (см. ниже), чтобы
трафик валидатора не пошёл через назначенный FIP — и число провалов
достигло потолка `self_check_max_attempts`, см.
[«Повтор после сбоя self-check»](#повтор-после-сбоя-self-check)). Смотрите `events` по этому адресу (см. ниже), чтобы
понять, на каком шаге и почему.
- **`cancelled`** — проверку остановил оператор через `POST
/api/v1/admin/ips/{ip}/cancel` (`State` при этом — `failed`), а не
@@ -774,6 +776,37 @@ curl -s -X POST http://<control-api>:8080/api/v1/admin/ips/clear
всё» — каждая с подтверждением, явно предупреждающим о необратимости
(см. [DASHBOARD.md](DASHBOARD.md)).
## Повтор после сбоя self-check
Если self-check адреса не прошёл на валидаторе N, адрес возвращается в
очередь, но **этому же валидатору больше не выдаётся**: повтор достаётся
другому. Правило действует только для этого адреса. Валидатор N не
блокируется и не помечается неисправным, он продолжает брать остальные
адреса. Так один временно неисправный валидатор (например, у него не
работает трансляция плавающего IP) не расходует все попытки адреса.
Итог `fail` ставится, когда число проваленных self-check у адреса достигло
потолка **«Потолок провалов self-check на адрес»** (`self_check_max_attempts`,
по умолчанию 5; `/settings` или `PUT /api/v1/admin/config/orchestrator`,
допустимо 1…50). Потолок не зависит от числа валидаторов и действует на
следующих повторах без перезапуска. Если все рабочие валидаторы
(`idle`, `assigned`, `checking`) уже провалили адрес, а потолок не достигнут
(валидаторов меньше потолка, в том числе один), исключения сбрасываются и
повторы продолжаются до потолка.
Исключение создаёт только проваленный self-check. Ошибка привязки
плавающего IP, потеря валидатора и истечение лизинга исключений не создают и
идут по `orchestrator.max_retries`, как раньше. Поле
`orchestrator.max_self_check_retries` в YAML больше не используется.
Ручная перепроверка или повторная постановка адреса начинает серию заново
(счётчик сбоев обнуляется). История сбоев хранится постоянно (таблица
`ip_self_check_failures`) и отдаётся в поле `self_check_failed_on` ответов
`GET /admin/ips/{ip}` и `GET /admin/registry/{ip}`; на страницах `/ips/{ip}` и
`/registry/{ip}` показана строка «Self-check не прошёл на: …». В журнале
событий при повторе появляется `validator_excluded`. Показ на странице
аналитики — отдельный следующий шаг.
## Пауза перед self-check (fip_settle_seconds)
Как только Floating IP привязывается к валидатору, control-api по
@@ -833,6 +866,7 @@ https://api.ipify.org`) и логи `journalctl -u validator-agent` на пре
**Адрес постоянно проваливает self-check (не зависает, а именно
возвращается в очередь снова и снова).**
См. также [«Повтор после сбоя self-check»](#повтор-после-сбоя-self-check): повторы идут на других валидаторах, а список «Self-check не прошёл на: …» показан на странице адреса.
Смотрите `events` по адресу (`GET /api/v1/admin/ips/{ip}`) — в детали
события `self_check_result` будет указан обнаруженный исходящий адрес.
Если он не совпадает с ожидаемым — вероятно, на ВМ-валидаторе есть другой
@@ -0,0 +1,100 @@
# План: повтор после сбоя self-check — на другом валидаторе
Статус: реализовано, см. [итог](2026-10-04_08-01_self-check-exclude-validator-summary.md).
## 1. Проблема
В запуске 2 два адреса получили `fail`: `89.208.220.90` и `89.208.221.74`. Оба раза self-check провалился четыре раза подряд на одном валидаторе `vkiplab-v17` (он выходил в сеть с общего адреса облака `109.120.182.224`, а не с плавающего IP), с интервалом около 70 секунд.
Причина в логике повтора: после сбоя адрес возвращается в начало очереди, а освободившийся `v17` берёт следующий адрес с наименьшим `sequence`, то есть тот же. Один временно неисправный валидатор расходует все попытки адреса. Сам адрес исправен: `89.208.216.141`, попавший на `v17` дважды, после перехода на `v13` прошёл проверки.
## 2. Требования и принятые решения
1. Адрес, не прошедший self-check на валидаторе N, при повторе **не отдаётся валидатору N**. Правило действует только для этого адреса.
2. Валидатор N остаётся в системе как был: не блокируется, не помечается неисправным, продолжает брать и проверять все остальные адреса.
3. **Итог `fail`** ставится, когда число проваленных self-check у адреса достигло **потолка**. Потолок не зависит от числа валидаторов. Это **настройка** `self_check_max_attempts`: она меняется в разделе настроек `/settings` и через API; **значение по умолчанию для текущего окружения — 5**. Исключение валидаторов (п.1) гарантирует, что первые попытки идут на разных валидаторах: адрес получает `fail` после 5 провалов на 5 разных валидаторах (а не на одном).
4. Исключение создаёт **только** проваленный self-check; на ошибки привязки плавающего IP, потерю валидатора и истечение аренды оно не распространяется.
5. Сведения «Self-check не прошёл на: …» сохраняются и отдаются в API для последующего показа на странице аналитики.
## 3. Решение
### 3.1. Данные (миграция `0012_self_check_failures.sql`)
- Таблица `ip_self_check_failures(registry_id, run_id, cycle_id, attempt_number, validator_id, failed_at, detail)`. Постоянная история сбоев self-check по адресу; не очищается при перепроверках, живёт вместе с реестром (не удаляется вместе со строкой очереди) и служит источником для аналитики.
- Колонки `ip_queue.sc_failures` (сколько self-check провалено в текущей серии попыток) и `ip_queue.sc_round_start_attempt` (номер попытки, с которой действуют исключения; «раунд»).
- Настройка `self_check_max_attempts` в таблице `settings`: миграция записывает **5** (допустимо 1…50), дальше значение меняется в `/settings` или через API. Поле `orchestrator.max_self_check_retries` в YAML для self-check больше не используется (остаётся в файле для совместимости, в документации помечается устаревшим).
Исключение для валидатора N на адресе действует, если в `ip_self_check_failures` есть сбой этого валидатора на этом адресе с `attempt_number >= sc_round_start_attempt`.
### 3.2. Обработка сбоя (`Orchestrator.SelfCheckResult`, ветка «не прошёл»)
После проверки, что адрес принадлежит этому валидатору и ждёт self-check, и отвязки плавающего IP (как сейчас):
1. Записать сбой в `ip_self_check_failures`, увеличить `sc_failures`.
2. Решение:
- **`sc_failures >= self_check_max_attempts`** → `fail` (причина «self-check не прошёл N раз», в событии перечислены валидаторы);
- иначе → вернуть адрес в очередь. Если среди рабочих валидаторов (`idle`, `assigned`, `checking`; `unreachable` и `unregistered` не считаются) не осталось ни одного без сбоя в текущем раунде, начинается новый раунд (`sc_round_start_attempt` = следующая попытка): исключения теряют силу, повтор идёт по обычным правилам. Так при числе валидаторов меньше потолка (в том числе при единственном) повторы продолжаются до потолка, а адрес не застревает в очереди.
Лимит `max_retries` и счётчик `retry_count` для self-check больше не используются: возврат в очередь после сбоя self-check не увеличивает `retry_count` и не может привести к `fail` по нему. Для остальных причин возврата (ошибка привязки, истечение аренды) всё остаётся как сейчас.
### 3.3. Выдача адресов (`DB.ClaimNextQueued`)
Валидатор получает ближайший по `sequence` адрес, **кроме** тех, где он исключён (по 3.1). Исключённый адрес остаётся в очереди для остальных валидаторов и очередь не блокирует. Запрос по-прежнему одной транзакцией.
### 3.4. Жизненный цикл
- Ручная перепроверка или повторная постановка адреса (`SubmitIPs`) начинает новую серию: `sc_failures = 0`, `sc_round_start_attempt` = текущая попытка.
- История в `ip_self_check_failures` остаётся.
- Удаление адреса и очистка очереди историю не трогают; `docs/ADMIN_CLEANUP.md` дополняется новой таблицей.
### 3.5. Данные для аналитики и видимость
- Событие `self_check_result` остаётся как сейчас; добавляется событие `validator_excluded` (`{"validator_id": …, "failures": N}`).
- `GET /admin/ips/{ip}` и `GET /admin/registry/{ip}` получают поле `self_check_failed_on` (список валидаторов, у которых self-check на этом адресе не прошёл; для реестра — по всем запускам, для запуска — фильтр по `run_id`).
- Строка «Self-check не прошёл на: v17, …» на странице адреса (`/ips/{ip}`, `/registry/{ip}`).
- **Отображение на странице аналитики не входит в эту доработку:** данные и API будут готовы, показ (колонка в списках адресов, блок «Self-check по валидаторам») делается следующим шагом отдельным планом.
### 3.6. Настройка в интерфейсе (`/settings`)
Изменения отражаются в разделе настроек явно:
- В форме с `fip_settle_seconds` и `history_retention_cycles` добавляется поле **«Потолок провалов self-check на адрес»** (`self_check_max_attempts`), целое число, по умолчанию 5, допустимо 1…50.
- Под полем пояснение: «Сколько раз self-check может не пройти у одного адреса, прежде чем адрес получит итог `fail`. Повторы идут на других валидаторах: валидатор, на котором адрес не прошёл self-check, этому адресу больше не выдаётся (на остальные адреса это не влияет)».
- Значение сохраняется той же кнопкой «Сохранить», действует на следующих повторах без перезапуска; неверное значение (не число, вне 1…50) показывается предупреждением в баннере, форма остаётся прежней.
- В API: `GET/PUT /admin/config/orchestrator` получает поле `self_check_max_attempts` (ошибка 400 при неверном значении).
- Описание поля добавляется в `docs/DASHBOARD.md` (строка `/settings`) и `docs/USAGE.md`.
## 4. Файлы
- БД: `internal/db/migrations/0012_self_check_failures.sql`, `db.go`, `queries_ipqueue.go` (`ClaimNextQueued`, запись сбоя, новый возврат в очередь без `retry_count`, принудительный `fail`, сброс серии в `SubmitIPsAs`), `queries_settings.go` (настройка), запрос истории для API.
- Оркестратор: `orchestrator.go` (`SelfCheckResult`), `config.go` (начальное значение настройки).
- API и дашборд: `handlers_admin.go`, `handlers_config.go`, DTO, `handlers_settings.go`, `settings.html`, `ip_detail.html`, `registry_detail.html`.
- Документы: `USAGE.md` («Как читать итоговый результат», повторы self-check, настройка), `API.md`, `ADMIN_CLEANUP.md`, `README.md`, итог `docs/changes/…-summary.md`.
## 5. Тесты
- **БД:** валидатор, на котором адрес исключён, пропускает его и берёт следующий; другой валидатор берёт исключённый адрес; исключения действуют только для этого адреса; новый раунд снимает исключения; перепостановка сбрасывает серию, но не историю; настройка (значения по умолчанию, границы).
- **Оркестратор (сценарий инцидента на трёх валидаторах):** v1 всегда проваливает self-check — адрес не возвращается на v1, проходит на v2, а v1 в это время берёт и проходит другие адреса; потолок: адрес, провалившийся `self_check_max_attempts` раз, получает `fail`, раньше — нет; при потолке меньше числа валидаторов `fail` наступает на потолке без перебора всех; при потолке больше числа валидаторов начинается новый раунд; единственный валидатор повторяет на себе до потолка; `unreachable` не считается валидатором; сбой привязки и истечение аренды исключений и счётчика self-check не создают и по-прежнему идут по `max_retries`; изменение настройки действует на следующем повторе.
- **API и дашборд:** поле `self_check_failed_on`; поле «Потолок провалов self-check на адрес» отображается в `/settings` со значением 5, сохраняется, неверные значения показывают предупреждение; поле в `GET/PUT /admin/config/orchestrator`; строка «Self-check не прошёл на: …» на странице адреса.
- Миграция `0012` на копии боевой БД (настройка равна 5), повторный запуск безопасен. Полный `go build ./... && go vet ./... && go test ./...`.
## 6. Выкладка и проверка
Пересборка и перезапуск `control-api` и `admin-dashboard` (меняется форма настроек и страница адреса); агенты и prober без изменений. Порядок: проверка пустой очереди, копия БД, тег отката образа, миграция `0012`. Сбой на стенде воспроизвести нельзя, поэтому проверка там — штатный небольшой прогон без зависших `queued`; логику повторов покрывает тест на трёх валидаторах.
## 7. Риски и последствия принятого решения
- **Время до `fail` у неисправного адреса** ограничено потолком: 5 попыток по ~70 секунд, около 6 минут вместо ~5 минут сейчас (4 попытки). Очередь и другие валидаторы это не блокирует.
- **Потолок меньше числа валидаторов.** Адрес, которому не повезло с 5 неисправными валидаторами подряд, получит `fail`, хотя на остальных 15 прошёл бы. Это сознательный компромисс в пользу ограниченного времени; потолок меняется в настройках.
- Адрес, исключённый на части валидаторов, может чуть дольше ждать подходящего валидатора.
- Если сбой общий (проблема облака для всех валидаторов), адрес в итоге получит `fail`; это верное поведение.
- Не решается: причина сбоя `v17` в облаке (трансляция плавающего IP) — её нужно смотреть в OpenStack.
## 8. Решения по согласованию (все приняты)
1. Потолок попыток задаётся независимо от числа валидаторов; значение меняется в меню настроек (`/settings`) и через API.
2. Значение по умолчанию для текущего окружения — 5.
3. Исключение действует только для проваленного self-check; на ошибки привязки не распространяется.
4. «Self-check не прошёл на: …» сохраняется и отдаётся в API; показ на странице аналитики — следующим шагом.
5. Изменения отражаются в разделе настроек UI (п.3.6).
Открытых вопросов нет. Жду команды начать реализацию.
@@ -0,0 +1,32 @@
# Итог: повтор после сбоя self-check — на другом валидаторе
План: [2026-10-04_08-01_self-check-exclude-validator-plan.md](2026-10-04_08-01_self-check-exclude-validator-plan.md).
Статус: код написан и проверен (gofmt, build, vet, test, миграция на копии боевой БД); стенд **не пересобирался**.
## Что изменено
- **Миграция `0012_self_check_failures.sql`** (версия схемы 12): таблица `ip_self_check_failures` (история сбоев по адресу, по `registry_id`, без внешних ключей); колонки `ip_queue.sc_failures` и `ip_queue.sc_round_start_cycle`; настройка `settings.self_check_max_attempts` (`DEFAULT 5`, допустимо 1…50).
- **`db.ClaimNextQueued`** пропускает адреса, на которых этот валидатор провалил self-check в текущем раунде; адрес остаётся в очереди для других валидаторов.
- **`db.FailSelfCheck`** (одна транзакция): записывает сбой, увеличивает `sc_failures`; при `sc_failures >= потолка` — `fail`; иначе возвращает адрес в очередь без изменения `retry_count`; если рабочих валидаторов без сбоя в раунде не осталось — новый раунд. Позднее сообщение (адрес не у этого валидатора) — `ErrInvalidState`.
- **Оркестратор** (`SelfCheckResult` → `failSelfCheck`): потолок читается из настроек на каждом сбое. События: `retry_or_fail` (при `fail` — со списком валидаторов) и `validator_excluded`. `max_self_check_retries` не используется (поле в YAML осталось).
- **`SubmitIPsAs`/`SeedQueue`**: перепостановка начинает новую серию (`sc_failures = 0`, новый раунд); история остаётся.
- **API**: `GET/PUT /admin/config/orchestrator` — поле `self_check_max_attempts` (400 вне 1…50; в PUT необязательно); `self_check_failed_on` в `GET /admin/ips/{ip}` (фильтр по запуску) и `GET /admin/registry/{ip}` (все запуски).
- **Дашборд**: поле «Потолок провалов self-check на адрес» на `/settings`; строка «Self-check не прошёл на: …» на `/ips/{ip}` и `/registry/{ip}`.
- **Документы**: `USAGE.md` (новый раздел «Повтор после сбоя self-check»), `API.md`, `DASHBOARD.md`, `ADMIN_CLEANUP.md` (новая таблица в сценариях 3.1 и 3.4), `README.md`.
## Отступления от плана
1. Раунд хранится как `sc_round_start_cycle` (номер `cycle_id`), а не `sc_round_start_attempt`: `attempt_number` сбрасывается при удалении и повторном создании строки очереди, а история остаётся — новая строка получила бы чужие исключения. `cycle_id` по адресу не повторяется. Поведение для оператора то же.
2. Значение 5 задано `DEFAULT 5` колонки (покрывает и существующую строку `settings`, и чистую установку), `config.go` не менялся.
3. В `PUT` потолок необязателен — старые клиенты без поля не получают 400.
4. `validator_excluded` не пишется, если начался новый раунд (исключение сразу теряет силу).
## Проверки
- `gofmt -l` — пусто; `go build ./...`, `go vet ./...` — без ошибок; `go test ./...` — все пакеты `ok`. Две проверки версии схемы в старых тестах миграций (`TestMigration0010…`, `TestMigration0011…`) обновлены с 11 на 12.
- Новые тесты: БД (`queries_selfcheck_test.go`), оркестратор (сценарий на трёх валидаторах, потолок, один и два валидатора, смена настройки), API, дашборд.
- Миграция `0012` на копии боевой БД: версия 11 → 12, `self_check_max_attempts = 5`, повторное открытие без ошибок, таблица сбоев пуста.
## Выкладка
Не выполнена. Нужны пересборка и перезапуск `control-api` и `admin-dashboard` по процедуре (проверка пустой очереди, копия БД, тег отката `pre-self-check-exclude`, миграция `0012`); на стенде в очереди сейчас 6489 адресов `done`, `queued` нет.