← все записи

Успех без свидетеля

31 августа 2026, 11:25

Выкатывал узел на тестовое окружение. Скрипт отработал и закончил бодро:

== probe https://…/health
   /v1/client-error -> 401: the new build is live

dev is on sha-eeca165.

Пошёл проверять руками — узел на старом образе. Ни одного признака этого в выводе.

Дальше — три причины, и они складывались одна на другую, как в учебнике.

Причина первая: проба, отличавшая не то

Проба спрашивала, есть ли на узле маршрут /v1/client-error. Логика в комментарии рядом честная: «этот маршрут есть только в выкатываемой сборке, значит 404 — старый образ ещё жив».

Это было верно в день, когда писалось. Потом маршрут доехал до предыдущего образа, и с тех пор проба отличала новую сборку от какой-то древней — то есть ни от чего.

Мне нравится этот класс ошибок: код не менялся, комментарий не врал, а утверждение стало ложным само, потому что мир под ним сдвинулся. Тест на «новизну по признаку» живёт ровно до момента, когда признак перестаёт быть новым.

Причина вторая: инструмент, глотающий ошибку по проекту

Внутри скрипт зовёт мастер раскатки, который ходит по машинам пула. В нём:

except Exception as e:  # one box's failure must not abort the run
    print(f"  · {b['id']}: ERROR {e}")

Намерение правильное. Пул есть пул: одна упавшая машина не повод бросать остальные. Но дальше процесс завершался нулём — и вызывающая сторона узнавала об ошибке ровно ничего.

Получилось разделение труда наоборот: мастер знает про ошибку и молчит наружу, скрипт наружу говорит и про ошибку не знает.

Чинится это не отменой замысла, а его достройкой: неудачи считаются, остальные машины обрабатываются, а в конце процесс выходит с ненулевым кодом. Пять строк.

Причина третья: я сам

Между этими двумя причинами лежала третья, моя. Когда скрипт написал «live», я это прочитал — и пошёл проверять руками только потому, что за пять минут до того правил ровно ту ручку, чьё поведение хотел увидеть. Не будь у меня личного интереса к этой ручке, запись «дев на новой сборке» осталась бы в голове фактом.

Вот против этого и нужен свидетель, который не я.

Чем это заменилось

Требование к пробе сформулировалось само: она должна спрашивать у системы то, что знает только она сама, и чего не знает спрашивающий.

Первым делом захотелось зашить коммит в образ. Отверг: сборка переиспользует образ по содержимому — одинаковые байты не пересобираются, а получают ещё один тег. Зашей туда коммит, и каждый коммит начнёт давать новые байты; я бы чинил проверку, ломая экономию сборки.

Взял другое: тег, с которым запущен контейнер. Он приезжает в окружение процесса, процесс отдаёт его в /health, скрипт сравнивает с тем, что катит. Ключевое свойство — контейнер держит окружение с момента старта. Раскатка, упавшая до пересоздания контейнера, оставляет старый тег, и проба видит именно это:

FAIL: на узле sha-0000000, а не sha-eeca165 — раскатка не состоялась

Проверил тремя случаями: живой узел на старом образе (поля нет вовсе), подставной узел с чужим тегом, подставной с верным. Красное, красное, зелёное.

А починку мастера пришлось проверять отдельно и не сразу: первый эксперимент вышел пустым. Я снял счёт неудач, запустил скрипт целиком — он всё равно упал, и я чуть не записал это в успех. Упал он на новой пробе, а не на мастере: две починки закрывают один и тот же случай, и через скрипт их не различить. Пришлось звать мастер напрямую: без счёта неудач — код 0 на упавшей машине, с ним — 1.

Что унести к себе

Проба на «новизну по признаку» протухает молча. «Этого маршрута/поля/ключа в старой версии не было» верно один раз. Спрашивайте не признак, а идентичность: пусть система сама назовёт, кто она, и сравнивайте с ожидаемым.

Глотать ошибку и возвращать ноль — разные решения. Первое часто правильно, второе почти никогда. Если ваш инструмент по замыслу продолжает после сбоя, он тем более обязан сказать об этом кодом возврата — именно потому, что вывод никто не читает.

Проверяя починку, проверяйте её в одиночку. Две правки, закрывающие один случай, дают зелёное по любой из них, и вы не узнаете, какая работает. Это ровно то же, обо что я споткнулся неделю назад с кодом возврата, только этажом выше.

← визитка · RSS