Первый тег моего проекта датирован 22 июля. Шестого сентября вышла 1.0. Мне интереснее написать не про то, сколько всего успело приехать за полтора месяца - это только мне и интересно, - а про то, чем 1.0 отличается от 0.37 технически. Разница ровно одна, и она не в коде.
0.x - это право передумать
Пока на продукте стоит ноль впереди, автор имеет право менять что угодно между выпусками. Это не расхлябанность, это честно объявленный режим: пользователь знает, что обновление может потребовать чтения и работы руками. У self-hosted продукта этот режим особенно заметен, потому что обновляет не автор, а человек на своём железе, в своё время, часто через полгода после того, как выпуск вышел.
1.0 отменяет это право. Дальше начинается интересное: право отменяется не целиком, а по конкретному списку, и вся ценность номера - в том, насколько честно этот список составлен.
Что именно заморожено
Мой список выглядит так:
имена переменных окружения;
адреса приёма и форматы тел запросов;
направление миграций - только вперёд;
формат резервных копий остаётся читаемым;
тело исходящего вебхука;
имена self-метрик, кроме прямо помеченных временными;
контракт переменных агента;
адреса уже опубликованных статус-страниц.
Всё, что нарушает перечисленное, ждёт 2.0.
Последний пункт я объясню, потому что он неочевиден и появился из головы пользователя, а не из моей. Публичная статус-страница - это ссылка, которую человек однажды положил в шапку своего сайта, в подпись в почте, в закреплённое сообщение в чате поддержки. Если такая ссылка перестанет открываться после обновления мониторинга, виноват будет он, а чинить придётся ему. Формально это “всего лишь” URL, а по факту это интеграция с внешним миром, у которой нет владельца, способного её починить.
Что осталось снаружи, и это тоже надо говорить вслух
Обещание, в которое входит всё, - это не обещание, а маркетинг. Снаружи осталось:
внутренние пакеты (у меня Go, и
internal/- это буквально граница, которую компилятор охраняет сам);внешний вид интерфейса: расположение блоков, тексты, вёрстка;
имена счётчиков, помеченных временными;
поведение, которое я считаю багом, - исправление бага не ждёт мажорной версии.
Последний пункт - тот, где обычно спорят, и я заранее согласен, что граница между “починили баг” и “сломали поведение, на которое я полагался” проходит не там, где хочется автору. Единственное, что тут работает, - писать в changelog явным разделом и не прятать такие изменения в списке улучшений.
Цена: 31 переменная окружения за три дня до заморозки
Теперь неприятная часть. Выпуск 0.34.0 вышел 3 сентября, за три дня до 1.0, и переименовал 31 переменную окружения: 17 серверных, 3 у агента и 11 в docker-compose и сборке. Это ломающее изменение, оно попало в последний вагон перед заморозкой намеренно - после 1.0 такой возможности не будет до 2.0. Всего за 0.x переименований накопилось 41, ещё десять имён поменялись раньше, в 0.23.0, когда из них выносили единицу измерения в само имя.
Почему вообще потребовалось. За полтора месяца имена накопили ровно те болезни, которые накапливаются, когда переменную добавляют по одной штуке под задачу:
адрес прослушивания и публикуемый порт назывались так, что их путали;
GOTCHA_LOG_LEVEL- это про журнал самого приложения, аGOTCHA_LOG_RETENTION_DAYS, стоящий рядом в алфавитном списке, - уже про приём логов пользователя, то есть про совсем другую подсистему; теперь первая называетсяGOTCHA_LOGGING_LEVEL, и они больше не путаются;переменная-модификатор стояла в алфавитном списке далеко от той настройки, которую она модифицирует;
несколько булевых читались ровно наоборот сути.
Всё это терпимо, пока имена можно менять. С 1.0 нельзя, и жить с этим пришлось бы годами.
Интереснее, что делать со старым именем. Тихо прочитать его как синоним - соблазнительно и почти всегда неправильно: у вас навсегда остаётся второе, недокументированное имя, и однажды кто-то настроит инстанс по статье из интернета, а вы будете гадать, почему у него всё работает не так. Я выбрал противоположное: старое имя с непустым значением роняет старт с указанием нового имени.
Это грубо, но у грубости есть аргумент. Альтернатива - проигнорировать старое имя и применить значение по умолчанию, и вот это по-настоящему опасно. Человек поставил GOTCHA_ALLOW_INSECURE_SECRET=false, обновился, переменная стала называться иначе, старое имя молча выброшено - и инстанс поднялся с дефолтом, которого человек не выбирал. Падение на старте видят все и сразу. Молчаливая подмена не видна никогда.
Отдельно ловится случай, до которого я додумался не сразу: одиннадцать переименованных compose-переменных не читает ни один процесс продукта - их видит только Docker Compose и make. Но .env подключается в контейнер приложения целиком, поэтому забытое там старое имя приложение всё равно увидит - и уронит старт с той же понятной ошибкой, вместо того чтобы позволить compose тихо подставить дефолт мимо человека.
Репетиция обновления, а не отчёт об успешном обновлении
За день до релиза я прогнал обновление руками на копии продовых данных. Не тесты - тесты были и раньше, - а именно сценарий: поднять старую версию с реальными данными, обновиться, посмотреть, откатиться назад.
Репетиция нашла пять блокеров. Расскажу про один, самый показательный.
Откат бинаря назад, на базу с уже применённой более новой схемой, уходил в цикл падений вместо понятной ошибки. Гейт схемы у меня был, он проверял, что схема не опережает бинарь, - но стоял после стадии миграции, а не до неё. При включённой автомиграции (это значение по умолчанию) старый бинарь сначала лез мигрировать, спотыкался и перезапускался, снова лез, снова спотыкался. Снаружи это выглядит как “продукт не поднимается”, без единой строчки о том, что вы просто откатились слишком далеко.
Мораль, которую я вынес: план отката - это не то же самое, что план обновления, прочитанный задом наперёд. Обновление тестируют почти все. Откат - почти никто, потому что он нужен в тот момент, когда уже плохо, и проверять его в этот момент поздно. Если у вас self-hosted продукт, обновление и откат стоит один раз прожить руками до релиза, а не после - на копии данных, которая похожа на настоящую.
Депрекация: почему три старых адреса переживут 1.0
У меня есть три устаревших адреса приёма. Соблазн был убрать их именно в 1.0 - раз уж всё равно ломаем контракт, ломаем один раз и до конца.
Не убрал. Устаревшими они объявлены 31 августа, меньше чем за неделю до релиза. Удалить их сейчас - значит сломать чужую интеграцию, предупредив о поломке за пять дней, релизом, весь смысл которого в обещании стабильности. Собственная политика депрекации у меня же и написана: между объявлением и удалением обязан пройти минимум один мажорный выпуск. Первым же случаем эту политику нарушить было бы неловко.
Так что они работают, отдают заголовок Deprecation, каждое обращение считается отдельным счётчиком, а страница настроек проекта прямо говорит человеку, что его отправитель всё ещё ходит по старому адресу. Удаление - в 2.0.
Что 1.0 теперь запрещает мне
Про это редко пишут, а это самое существенное. С сегодняшнего дня я не могу:
переименовать переменную, даже если имя мне разонравилось;
поменять форму тела запроса, даже если она кривая;
написать миграцию, которая едет назад;
поменять формат бэкапа, даже ради размера.
Всё это переезжает в очередь до 2.0, и очередь придётся вести. Это и есть цена номера: 1.0 не награда за качество кода, а обязательство, которое надо выдерживать в тех местах, где ты уже понял, что сделал не лучшим образом.
Я выпустил 1.0 не потому, что кончились идеи, а потому что кончились известные долги: перед заморозкой продукт был прочитан несколькими независимыми проходами - архитектура, безопасность, ревью кода, QA, интерфейс, документация, эксплуатация, - и всё найденное закрыто до релиза. Список идей при этом не кончился и не кончится.
Вопрос к тем, кто выпускал 1.0
Мне правда интересно, как этот список выглядит у вас:
считаете ли вы схему базы частью публичного контракта, если продукт self-hosted и человек в эту базу ходит своими руками?
где у вас проходит граница между “исправили баг” и “сломали поведение”?
убираете ли вы устаревшие точки входа в мажорной версии или тянете дальше?
Продукт, о котором речь, - self-hosted мониторинг ошибок, трейсов, метрик, логов и доступности; один бинарник на Go, PostgreSQL и ClickHouse рядом, Apache 2.0. Ссылку не ставлю, чтобы статья не выглядела анонсом: она есть в профиле.