В 03:16 на кухне горел свет над плитой: выключатель у него тугой, и в темноте я по нему промахиваюсь. На столе стоял стакан с водой и второй, пустой, выставленный утром «для симметрии». Часы над дверью показывали десять минут одиннадцатого — они встали в сентябре, и время я с тех пор смотрю в телефоне, а часы оставила: дырка в обоих смыслах хуже. Телефон лежал экраном вниз; я перевернула его в 03:20, увидела, что нового нет, и решила, что это хорошая новость.
Утром оказалось, что в 03:16:44 ночной сторож должен был проснуться. Он не проснулся. Точнее, он начал и умер через две секунды, не выполнив ни одного шага, — и сказал мне об этом журнал прогонов в девять утра, а не тревога. Тревогу подавать некому: сторож и есть тревога.
Шесть ночей по две секунды
Считать пришлось по журналу. Двадцать второго, двадцать третьего, двадцать четвёртого, двадцать пятого, двадцать шестого и двадцать седьмого октября прогон создавался в 03:16–03:20, задание бралось в работу через секунду и завершалось отказом через две. Список выполненных шагов пустой: ноль. Последний успешный прогон — двадцать первого октября в 03:17:20, до него сторож работал четыре ночи подряд.
Тот же след был шестнадцатого и семнадцатого октября, потом болезнь отпустила на четыре ночи. Значит, условие не постоянное, а пороговое: перешли порог — сторож мёртв, вернулись под порог — живой. Такое не чинится правкой кода, и в этом вся суть.
Причина записана в аннотации прогона, одним предложением: задание не было запущено, потому что недавние платежи по аккаунту не прошли или лимит расходов нужно увеличить. Две причины в одном предложении, и различить их снаружи нельзя — обе про деньги. Но обе означают одно: до кода дело не дошло. Сторож не проверял прод и не нашёл проблему. Он не начинал.
Одна строка, которая стоит денег
Разница между живыми заданиями и мёртвыми — не логика, а строка `runs-on`. Сторож просит `ubuntu-latest`, то есть машину, которую выдаёт сам GitHub. Для приватного репозитория такие минуты вычитаются из оплаченного остатка аккаунта; когда остаток или лимит кончились, задание не запускается вовсе — машина не выдаётся, шагов не бывает, лога нет. При попытке открыть его журнал отвечает «log not found», и это не сбой чтения: читать нечего.
Рядом, в ту же минуту 03:16, проба доступности отработала восемь шагов на своей машине и закончилась успехом. У неё в конфигурации не жёсткий ярлык, а переменная: `runs-on: ${{ vars.CI_RUNNER || 'ubuntu-latest' }}`, и переменная указывает на собственные машины. Собственные машины, по документации GitHub, не тарифицируются вовсе — платят только за машины самого GitHub.
За те же шесть дней: Mission Control refresh — 52 прогона, 52 отказа; Docs auto-merge — 22 и 22; сторож — 6 и 6. Все три расписания просят машину у GitHub и умирают до первого шага. Четыре файла, одна строка в каждом, тишина в трёх расписаниях.

Сторож без сторожа
Наблюдатель у сторожа всё-таки есть — проба доступности. Она появилась именно из-за такого случая, и это записано в её же заголовке: в августе ночной сторож падал четыре ночи подряд из-за кончившихся кредитов у провайдера моделей, и заметил это человек, а не система. Внешние зависимости надо мерить до того, как они станут нулём, — с этой фразы начинается файл.
За шесть дней проба запускалась 413 раз, из них 241 успешно. Неуспешные делятся надвое, и вторая половина интереснее. Часть умирает, не начавшись: машина не берёт задание. Другая часть доходит до последнего шага — «открыть, обновить или закрыть тред инцидента» — и падает в нём. Двадцать шестого октября в 18:53 у этой падучей нашлась точная причина в логе: `gh: command not found`, код 127. То есть проба досчитала до вывода и не смогла его произнести.
В этом файле такой случай уже описан однажды. Девятнадцатого августа проба поймала настоящий 502 и не смогла об этом сказать: создание треда умерло на «could not add label: 'uptime' not found», потому что лейбла в репозитории не существовало. Комментарий заканчивается фразой, которую я сегодня перечитала дважды: отсутствие треда неотличимо от «всё хорошо».
Сегодня в 06:31:51 проба вынесла вердикт. Дословно: «худшее состояние: unknown (тред нужен: false)». Две проверки при этом слепы постоянно, и проба честно их перечисляет: глубокие метрики админки не собираются, потому что нужный секрет не задан в репозитории; баланс у провайдера моделей прочитать не удалось. Через пять секунд в логе появилась итоговая строка: «всё зелёное, открытых инцидентов нет — молчим».
Три расписания были мертвы шестые сутки. Проба смотрела на прод, на провайдеров, на серверы и на кредиты — на всё, кроме того, работают ли её соседи. Зелёный цвет означал ровно написанное: то, что я умею видеть, не сломано.
Три расписания должны были сработать за эти шесть дней восемьдесят раз: 52, 22 и 6. Они сработали и не сказали ничего. Ни одно из трёх не умеет сообщить главное — что оно не начиналось.
Отсюда правило, которое я забираю в собственную работу. У сторожа должен быть свой пульс, отдельное событие «я не начал» и свидетель снаружи, который считает запуски, а не спрашивает систему о самочувствии. Пока такого свидетеля нет, «не знаю» официально равно «всё хорошо», и молчание сторожа неотличимо от здоровья продукта.
Сегодня в 03:16 на кухне горел свет. Сколько ещё мест в этой системе устроены так, что их отсутствие неотличимо от их согласия?
