diff --git a/editorial/agent-rewrites/119.json b/editorial/agent-rewrites/119.json index 2026ba0..a05ea94 100644 --- a/editorial/agent-rewrites/119.json +++ b/editorial/agent-rewrites/119.json @@ -1,7 +1,7 @@ { "index": 119, "slug": "editorial-2024-09-mechanism-adr-decisions", - "title": "ADR без иллюзии выбора: как записать решение, его цену и путь назад", - "excerpt": "Архитектурное решение теряет смысл, если в записи виден только победивший вариант. Разбираем, как сравнить альтернативы по одной схеме, проверить отрицательный путь и оставить условие для пересмотра.", - "contentHtml": "
В review появляется знакомый симптом: команда обсуждает не условие задачи, а вкус. Один вариант называют простым, другой — надёжным, третий — слишком дорогим. В ADR уже записан победитель, но альтернативы сведены к двум словам. Через месяц авторы помнят контекст, а остальные видят только решение. Через год никто не знает, какую проблему оно закрывало и что делать, если исходное условие изменилось.
\nЦена ошибки растёт вместе с границей решения. Неудачный выбор может закрепить общий протокол, миграцию, формат данных или операционную обязанность. Тогда быстрый первый merge скрывает дорогой rollback. Ошибка не в том, что команда выбрала неидеальный вариант. Ошибка в том, что запись не показывает принятый компромисс и не даёт проверить, когда его пора пересмотреть.
\nТезис: хороший ADR не доказывает, что выбранный вариант лучший вообще. Он связывает наблюдаемую проблему с конкретным ограничением, одинаково описывает альтернативы, называет цену выбора и задаёт сигнал для возврата к решению. Числа могут помочь в учебной модели, но не заменяют факты и не превращают мнение в production-метрику.
\nADR полезен как короткая цепочка причин. Сначала автор отделяет факт от объяснения. Факт можно наблюдать: caller должен получить статус, пока фоновая операция продолжается; данные нельзя отдавать старше заданной границы; изменение должно откатываться без записи нового формата. Гипотеза объясняет, почему текущий путь не подходит. Решение отвечает на ограничение, а не на абстрактную цель вроде «улучшить архитектуру».
\nУ каждой альтернативы должна быть одна и та же карточка. Запишите, какой constraint она закрывает, где находится её граница, кто владеет дополнительной работой, как выглядит возврат и какое evidence уже есть. Отсутствующее evidence тоже является результатом сравнения. Оно ограничивает уверенность, но не доказывает безопасность или опасность варианта.
\nСтатус помогает не смешивать обсуждение и историю. Proposed означает, что запись готова к проверке. Accepted фиксирует принятое решение. Superseded означает, что новый ADR заменил старый и объяснил причину. Статус не запускает миграцию, не назначает approval и не проверяет rollback автоматически. Эти действия должны иметь отдельного владельца и отдельный сигнал.
\nsymptom = caller не получает понятный status\nconstraint = acknowledgement не должен зависеть от business completion\nalternatives = direct response | bounded async state | shared workflow\ndecision = bounded async state у границы сервиса\nprice = expiry, owner состояния, cleanup и отдельная проверка retry\nreassess = меняется контракт caller или срок хранения состояния\nЭтот фрагмент — учебный пример формы. Он не описывает реальный сервис, трафик, SLO или production-результат. Его задача — показать, как решение связывает симптом, условие, цену и отрицательный путь. В настоящем ADR вместо учебных утверждений нужны ссылки на контракт, issue, тест, threat model или другой разрешённый источник evidence.
\nСначала выровняйте уровень вариантов. Нельзя сравнивать «локальный cache» с «переделать платформу»: это разные масштабы и владельцы. Сформулируйте варианты на уровне решения, а затем укажите реализацию как следствие. Для asynchronous boundary это могут быть прямой ответ после завершения работы, bounded state с выдачей статуса и общий workflow с отдельным хранением состояния.
\nConstraint fit отвечает на вопрос «закрывает ли вариант обязательное условие». Reversibility описывает не наличие кнопки undo, а область возврата, порядок действий и владельца. Evidence fit показывает, какие факты поддерживают выбор и какой вопрос остался открытым. Operating cost называет долг: expiry, retry, cleanup, миграцию, поддержку контракта или ручной review. Reassessment показывает, что должно измениться, чтобы открыть новый ADR.
\n| Критерий | Вопрос к каждому варианту | Проверяемый артефакт | Чего нельзя утверждать |
|---|---|---|---|
| Constraint fit | Какое объявленное условие выполняется? | Контракт, problem statement или acceptance question | Что вариант оптимален для всех целей |
| Reversibility | Какой scope возврата, кто его выполняет и что останавливает change? | План rollback и граница затронутого состояния | Что rollback уже проверен в production |
| Evidence fit | Какой факт поддерживает выбор и чего пока не хватает? | Ссылка на источник, controlled test или явное unknown | Что отсутствие данных равно безопасному результату |
| Operating cost | Кто поддерживает status, expiry, retry, cleanup или migration? | Owner и consequence в ADR | Что стоимость измерена в часах или деньгах |
| Reassessment | Какой сигнал отменяет исходное предположение? | Review condition, metric question или contract link | Что дата сама запустит пересмотр |
Таблица не требует единой оценки. Она делает пропуски видимыми. Если у одного варианта есть контракт, а у другого только слово «сложно», сравнение ещё не началось. Если команда всё же применяет score, заранее закрепите шкалу, веса и смысл баллов. Рядом напишите, какие данные модель не учитывает. Итоговый балл может быть tie-breaker для обсуждения, но не доказательством корректности.
\nПредположим, synchronous caller ждёт результат долгой операции. Прямой ответ сохраняет простую модель, но не выдерживает границу времени. Shared workflow даёт общий status, но добавляет cross-service owner и отдельный контракт. Bounded state у границы сервиса отделяет acknowledgement от business completion. Это может закрыть исходное условие, если срок хранения, повтор запроса и очистка состояния определены явно.
\n## Decision\nВыбираем bounded state у границы сервиса.\n\n## Consequences\nПоложительные: caller получает отдельный status.\nЦена: owner хранит expiry и cleanup; retry должен быть идемпотентным.\n\n## Не делаем\nНе вводим shared workflow и не считаем локальное состояние\nуниверсальным механизмом координации.\n\n## Reassessment\nОткрываем новый ADR, если caller требует общего status между\nсервисами или срок хранения превышает допустимую границу.\nОтрицательный путь защищает запись от незаметного расширения. Без него локальное решение постепенно начинает обслуживать новые callers, а временное поле превращается в общий протокол. Если новое требование действительно появилось, это не повод молча дописывать старый ADR. Нужен successor с новым контекстом, альтернативами и ценой. Старый record остаётся историей прежнего компромисса.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Выбранный вариант подробно описан, остальные названы «сложными» | Автор сравнил не варианты, а привлекательность собственного решения | Заполнить одну карточку criteria для каждой альтернативы | Переписать ADR и назвать explicit downside победителя |
| После merge спорят, что на самом деле означал ADR | В записи смешаны факт, гипотеза и implementation detail | Пометить источник каждого важного утверждения | Вынести неизвестное в evidence gap и добавить owner проверки |
| Rollback существует только как фраза | Не определены scope, порядок и stop condition | Провести dry run на учебной копии или описать точную границу | Связать ADR с отдельным rollback-планом и не объявлять его проверенным без evidence |
| Локальный механизм используют новые callers | Отрицательный путь и граница применимости не записаны | Проверить список потребителей и контракт состояния | Остановить расширение, создать successor ADR при новом требовании |
| Дата review прошла, но никто не вернулся к решению | Дата ошибочно принята за автоматический процесс | Найти владельца сигнала и фактическое событие пересмотра | Назначить действие отдельно или заменить дату проверяемым trigger |
ADR не делает решение истинным и не заменяет исследование. Он не измеряет latency, надёжность, стоимость или безопасность, если рядом нет соответствующего evidence. Он не гарантирует, что команда найдёт все альтернативы. Формат может различаться: важнее сохранить контекст, выбор, статус, последствия, границы и путь пересмотра. Для legal, security, privacy и data boundary нужны отдельные проверки с отдельными владельцами.
\nНе добавляйте в учебный пример вымышленные traffic, error rate, savings или outcome. Если число нужно для объяснения score, пометьте его как синтетическое и не переносите вывод за пределы модели. Если production-данных нет, честная формулировка — «данных пока недостаточно», а не «вариант доказанно безопасен».
\nЗапись готова, когда независимый читатель может ответить на пять вопросов: какой симптом наблюдали; какое constraint обязателен; почему сравнивали именно эти варианты; какую цену принимает выбранный путь; какой сигнал заставит открыть новое решение. Проверка проста: уберите из текста заголовок Decision и попросите коллегу восстановить его из context, alternatives и consequences. Если он не может назвать отрицательный путь или owner, ADR ещё не готов.
\nПосле ревью в репозитории остаётся новый endpoint, очередь или дополнительное хранилище, но исчезает причина выбора. Через несколько месяцев команда видит только реализацию: один caller ждёт ответ, другой опрашивает status, третий уже использует внутреннее поле как публичный контракт. Вопрос «почему здесь так?» снова уходит в поиск по чатам и памяти участников.
\nОшибка не обязательно в самом техническом выборе. Дорого обходится потеря его границ: неизвестно, какое ограничение считалось обязательным, какие варианты сравнивали, кто владеет состоянием и что делать после изменения исходных условий. ADR — Architectural Decision Record, запись архитектурного решения — нужен именно для этой причинной связи. Он не делает решение правильным навсегда и не заменяет тестирование, но оставляет проверяемую историю компромисса.
\nТезис статьи: ADR готов, когда независимый читатель может восстановить проблему, обязательное условие, выбранный путь, его отрицательные последствия и сигнал пересмотра. Слова «быстрее», «проще» и «надёжнее» становятся полезными только после уточнения: для какого участника, при каком отказе и каким артефактом это проверяется.
\nОфициальное сообщество ADR определяет architectural decision как обоснованный выбор, значимый для архитектуры, а ADR — как запись одного такого выбора и его rationale, то есть причины. Коллекция записей образует decision log. Поэтому один ADR отвечает на один связный вопрос, а не пытается стать полной технической документацией системы.
\nВ шаблоне Michael Nygard минимальная форма состоит из Status, Context, Decision и Consequences. В MADR форма расширяется полями decision drivers, considered options, outcome и confirmation. Это полезные шаблоны, а не универсальный закон: команда может добавить владельца, ссылки на контракт, confidence или условие пересмотра.
\n| Артефакт | Главный вопрос | Что должно остаться внутри | Чего он не доказывает |
|---|---|---|---|
| ADR | Почему выбран этот вариант? | Контекст, ограничения, альтернативы, решение и последствия | Что код уже корректен во всех средах |
| Design document | Как устроить решение целиком? | Компоненты, потоки, контракты и детали реализации | Что выбранный дизайн принят как долгосрочное решение |
| Test | Какое поведение воспроизводится? | Условия, входы, ожидаемый результат, регрессия | Почему выбрана именно эта архитектура |
| Runbook | Что делать при операционном событии? | Команды, роли, stop condition и восстановление | Что исходный компромисс всё ещё применим |
Ссылка на ticket или commit полезна для навигации, но не заменяет rationale. И наоборот, ADR не должен превращаться в копию implementation guide. Если решение требует детальной схемы, threat model или плана миграции, запишите выбор в ADR и свяжите его с отдельным документом.
\nНачинайте с наблюдаемого симптома, а не с названия технологии. Например: «клиент вызывает экспорт, но операция иногда не укладывается в границу HTTP-запроса; при повторе непонятно, создан ли второй экспорт». Это факт и вопрос. Гипотеза «нам нужна очередь» появляется только после проверки длительности операции, повторяемости запроса и требований caller.
\nЗатем назовите цену ошибки. В этом сценарии неверный выбор может дать дубли экспорта, зависшие записи, неограниченное хранение результатов или расхождение между статусом и фактическим завершением. Цена должна быть привязана к системе: какой ресурс останется, кто его удаляет, какая граница времени действует, какой контракт увидит клиент.
\nПолезно записать decision drivers — факторы, по которым варианты будут сравниваться. Для экспорта это могут быть ограничение времени ответа, идемпотентность повторного запроса, наблюдаемость состояния, стоимость хранения и возможность вернуться к синхронному пути для коротких файлов. Если driver нельзя проверить или ему нет владельца, это пока предположение, а не доказательство.
\nПроблема: export иногда дольше жизни HTTP-запроса.\nОбязательные условия:\n- клиент получает подтверждение создания операции;\n- повтор запроса не создаёт второй экспорт;\n- состояние имеет срок хранения и владельца очистки.\nЦена ошибки: дубль работы или запись, которую никто не удаляет.\nВопрос решения: где хранить status и кто отвечает за его переходы?\nФрагмент — учебная заготовка. Он не сообщает реальную длительность, нагрузку или процент ошибок. В рабочей записи эти утверждения должны ссылаться на контракт, trace, тест, incident или другой доступный источник. Если данных нет, оставьте evidence gap и действие по его закрытию.
\nСравнение ломается, когда выбранный вариант описан конкретно, а остальные получают ярлык «сложно». Приведите варианты к одному уровню и задайте одинаковые вопросы: выполняет ли вариант обязательные условия, где хранится состояние, как работает повтор, кто поддерживает очистку, как выглядит возврат и какое evidence уже есть.
\n| Вариант | Что закрывает | Цена и риск | Путь назад |
|---|---|---|---|
| Синхронный ответ | Простая модель для короткой операции | Не выдерживает временную границу; повтор может повторить работу | Не требуется отдельное состояние, но граница остаётся нерешённой |
| Очередь и status у сервиса экспорта | Отделяет подтверждение создания от завершения работы | Нужны idempotency key, срок хранения, cleanup и наблюдение переходов | Остановить новые async-запуски и вернуть короткие операции на прямой путь |
| Общий workflow между сервисами | Единый статус для нескольких владельцев процесса | Появляется общий контракт, координатор и cross-service ownership | Удалить интеграцию нельзя одной заменой; сначала нужен план вывода потребителей |
| Ничего не менять | Не добавляет новый компонент | Сохраняет таймауты, повторы и неясную ответственность | Обратимость высокая, но исходный симптом остаётся |
В этой матрице нет итогового балла. Присвоить «3» за надёжность и «2» за стоимость можно только после определения шкалы, веса и источника данных. Без этого score создаёт видимость точности. Для небольшого решения достаточно назвать обязательное условие, показать trade-off и отметить, какие вопросы ещё не проверены.
\nПосле сравнения решение должно отвечать на вопрос «что меняем и почему именно сейчас». Важно записать не только положительную сторону. Для status-модели положительный эффект — caller не удерживает один долгий запрос. Отрицательная сторона — сервис теперь владеет жизненным циклом операции, а значит, обязан определить повтор, истечение срока, очистку и поведение при падении worker.
\n# ADR-0042: асинхронный экспорт с локальным status\n\n## Status\nAccepted\n\n## Context and Problem Statement\nЭкспорт может выйти за временную границу HTTP-запроса. Повтор\nзапроса должен быть безопасным, а запись операции — удаляемой.\n\n## Decision Drivers\n- подтверждение создания отдельно от завершения;\n- идемпотентный повтор;\n- явные expiry и owner состояния;\n- возможность ограниченного возврата.\n\n## Considered Options\n1. Синхронный ответ.\n2. Очередь и status у сервиса экспорта.\n3. Общий workflow между сервисами.\n\n## Decision Outcome\nВыбираем очередь и status у сервиса экспорта: граница владения\nостаётся локальной, а caller получает отдельный контракт состояния.\n\n## Consequences\nПлюс: долгую работу можно наблюдать и повторять безопасно.\nМинус: нужны хранение, expiry, cleanup, retry policy и метрики.\nНе делаем: не превращаем локальный status в общий workflow.\n\n## Confirmation\nПроверить контракт повторного запроса, переходы status, expiry,\nудаление результата и возврат короткой операции на direct path.\nЭто воспроизводимый формат записи, но не готовая реализация очереди. Поля Accepted, expiry и owner не запускают процессы сами по себе. Их смысл должен быть закреплён в правилах репозитория: кто принимает запись, где она хранится, как связана с кодом и что считается подтверждением.
\nУ решения есть границы «не делаем». Они защищают локальный механизм от незаметного расширения. В нашем примере сервис экспортов отвечает за собственные операции. Он не становится координатором платежей, уведомлений и нескольких downstream-сервисов. Если появился caller, которому нужен общий status процесса, это новое архитектурное условие, а не повод дописать исключение в старом ADR.
\nДля каждой границы задайте обратный путь. Что остановит новые записи? Как найти незавершённые операции? Сколько времени хранить status? Что увидит клиент, если worker завершил файл, но не записал финальный статус? Ответы должны быть в контракте и runbook, а в ADR достаточно зафиксировать решение, цену и ссылки на эти проверки.
\n| Симптом | Гипотеза | Проверка | Действие |
|---|---|---|---|
| Один idempotency key даёт два задания | Ключ проверяют после постановки в очередь | Повторить запрос на тестовом хранилище и сравнить созданные ids | Перенести дедупликацию к границе создания и добавить регрессионный тест |
Status показывает done, а файл недоступен | Переход статуса не связан с публикацией результата | Проверить порядок записи результата и смены status при сбое | Сделать переход атомарным для контракта или описать промежуточное состояние |
| Старые операции растут без ограничения | Expiry записан в ADR, но нет владельца cleanup | Найти job, журнал удаления и тест срока хранения | Назначить owner и не считать retention выполненным без evidence |
| Новый сервис использует локальный status | Граница применимости не стала частью контракта | Проверить список consumers и требования к общему процессу | Остановить расширение и создать successor ADR при новом scope |
Confirmation — это не фраза «всё проверено», а конкретный способ сравнить принятое решение с реализацией. Для учебного ADR достаточно проверить наличие ключевых разделов и затем отдельно запустить тесты контракта. Команда ниже не требует стороннего инструмента и возвращает ненулевой код, если в файле пропущен обязательный заголовок.
\nset -eu\nadr='docs/decisions/ADR-0042-export-status.md'\ntest -s \"$adr\"\ngrep -q '^## Status$' \"$adr\"\ngrep -q '^## Context and Problem Statement$' \"$adr\"\ngrep -q '^## Decision Drivers$' \"$adr\"\ngrep -q '^## Considered Options$' \"$adr\"\ngrep -q '^## Decision Outcome$' \"$adr\"\ngrep -q '^## Consequences$' \"$adr\"\ngrep -q '^## Confirmation$' \"$adr\"\nprintf '%s\\n' 'ADR structure: OK'\nЗапустите эту проверку из корня репозитория после создания файла и замените путь на свой. Она проверяет только структуру Markdown. Она не доказывает идемпотентность, безопасность, latency, корректность миграции или наличие owner. Для этого нужны публичные тесты, review кода, нагрузочная проверка, threat model или операционный dry run — в зависимости от решения.
\nAccepted означает, что команда приняла решение в указанном контексте. Это не обещание, что оно навсегда верно, и не сигнал для автоматического запуска работ. Если предпосылка изменилась, не переписывайте старый record так, будто он изначально описывал новый путь. Создайте новую запись, свяжите её с предыдущей и укажите, какой факт изменил выбор.
\nСтатусы тоже требуют локального соглашения. Proposed может означать «готово к обсуждению», Accepted — «принято», Rejected — «рассмотрено и отклонено», Superseded — «заменено новой записью». Названия встречаются в официальных шаблонах, но workflow approval, даты и права на изменение задаёт конкретная команда.
\nСигнал пересмотра лучше формулировать как наблюдаемое условие: появился consumer, требующий общего status; retention превысил согласованный предел; contract upstream изменился; worker не укладывается в заданный класс операций. Дата review может напомнить о проверке, но сама по себе не означает, что решение устарело. У сигнала должен быть владелец и действие, иначе это пожелание.
\n## Reassessment\nОткрыть ADR-0043, если выполняется хотя бы одно условие:\n- второй сервис требует читать status как общий workflow;\n- изменился контракт повторного запроса;\n- retention больше согласованного срока;\n- cleanup не подтверждён тестом или операционным журналом.\nДействие: сравнить локальный status, общий workflow и прямой путь;\nстарую запись не редактировать.\nADR полезен для значимых решений: границ компонентов, API и data contracts, зависимостей, non-functional requirements, способов миграции и технологических направлений. Для тривиального локального рефакторинга отдельная запись может создать больше шума, чем контекста. Порог значимости определяется командой; важно сделать его явным.
\nADR не является design document, threat model, benchmark, тестовым раннером, runbook или approval-системой. Он не гарантирует, что команда нашла все альтернативы, и не превращает отсутствие данных в доказанную безопасность. Для персональных данных, security, legal и recovery нужны профильные проверки с отдельными владельцами. Не публикуйте в открытом ADR секреты, персональные данные и внутренние ссылки без разрешённого доступа.
\nЗапись можно считать готовой, если читатель без поиска по переписке отвечает на пять вопросов: какой симптом наблюдали; какое условие нельзя нарушить; почему отклонили альтернативы; какую цену принимает выбранный путь; какой сигнал откроет следующий ADR. Если не назван owner состояния или не существует проверка отрицательного пути, документ ещё фиксирует намерение, а не устойчивое решение.
\nПосле релиза в коде остаётся необычный обходной путь: запрос проходит через отдельный слой, хотя прямой вызов короче. Через полгода обсуждение исчезает из чата, авторы переключаются на другие задачи, а reviewer видит только итоговый diff. Симптом прост: команда снова спорит, зачем существует условие, очередь или дополнительная граница.
Цена ошибки выше стоимости потерянного контекста. Можно удалить защиту, которая всё ещё нужна для старого ограничения. Можно оставить дорогую схему после того, как ограничение исчезло. Оба решения выглядят разумно, если известен только код. Нужен артефакт, который связывает наблюдаемую проблему с выбором и его последствиями.
Architecture Decision Record фиксирует один значимый выбор. Он отвечает на пять вопросов: что произошло, какие ограничения действовали, какие варианты сравнили, что выбрали и какую цену приняли. Отдельно записывают владельца, статус и сигнал для пересмотра. ADR не объявляет решение вечным и не доказывает, что реализация корректна.
Это важная граница. Ticket хранит работу и сроки. Code review хранит обсуждение изменения. Test проверяет поведение. Runbook описывает операционное действие. ADR хранит rationale — причину, по которой команда выбрала один путь среди допустимых. Ссылка на ticket не заменяет rationale, а commit не заменяет сравнение альтернатив.
Сначала отделите факт от интерпретации. Факт можно увидеть в логе, контракте, trace, конфигурации или наблюдаемом поведении. Интерпретация объясняет факт, но требует проверки. В ADR полезно явно назвать стоимость ошибки: потеря обратимости, рост задержки, новый владелец данных, риск несовместимости или усложнение отката.
Затем ограничьте вопрос. «Как устроить export» слишком широко. «Где caller получает status, если synchronous boundary не выполняется» уже задаёт предмет. Один ADR должен описывать один выбор. Если обсуждение меняет два независимых контракта, разделите записи и свяжите их ссылками.
Варианты сравнивают по одним и тем же критериям. В простом случае достаточно reversibility, соответствия constraint, стоимости эксплуатации и доступного evidence. Не превращайте невыбранные варианты в карикатуры. Если вариант «ничего не менять» не рассматривался, его стоит назвать отдельно: иногда это самый дешёвый и самый обратимый путь.
Title: bounded cache at the BFF boundary\nStatus: Proposed\nOwner: application owner\nReview by: 2025-03-31\n\nContext: source contract permits bounded freshness.\nOptions: direct read; local cache; shared cache.\nDecision: choose local cache with named expiry.\nConsequences: add freshness check and direct-read rollback.\nEvidence question: does the source contract still permit this cache?Пример учебный. Он не создаёт cache, не обращается к repository и не сообщает latency, hit ratio или экономию. Его задача — показать форму записи. В настоящем ADR каждое утверждение о контракте должно ссылаться на доступный артефакт, а владелец должен иметь право проверить его.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| В review спорят о старом обходном пути | Контекст остался в переписке | Найдите исходное ограничение и отделите факт от гипотезы | Создайте proposed ADR и укажите ссылку на code |
| В записи перечислен только выбранный путь | Альтернативы не стали частью решения | Проверьте, можно ли сравнить варианты по одним критериям | Добавьте реально рассмотренные варианты и причины отказа |
| Текст обещает «простую поддержку» | Последствия описаны абстрактно | Спросите, кто что должен сделать и какой риск остаётся | Запишите владельца, cost, rollback и signal пересмотра |
| ADR принят, но code изменился иначе | Запись смешана с реализацией | Сопоставьте decision с контрактом и diff отдельной проверкой | Исправьте code или обновите ADR новым решением; не переписывайте историю |
| Ограничение больше не действует | У записи нет review boundary | Проверьте дату, source contract и сигнал изменения | Создайте successor ADR со статусом superseded для старого |
Хорошая запись начинается с контекста, но не превращается в историю всей команды. Достаточно назвать границу системы, затронутый контракт, decision drivers и цену неверного выбора. Слова «быстрее», «надёжнее» и «проще» требуют уточнения. Быстрее для какого сценария? Надёжнее при каком отказе? Проще для какого владельца?
Последствия должны включать отрицательную сторону. Если выбран local cache, положительный эффект может быть ограничен целевым read path. Цена — необходимость хранить expiry, проверять freshness и иметь путь к direct read. Если данные могут быть чувствительными, добавляется отдельная проверка класса данных. Не прячьте эту цену под словом «trade-off»: читателю нужно понимать, что именно он будет поддерживать.
Запишите две границы: что решение делает и чего оно не делает. В учебном примере мы создаём bounded local state с именованным expiry. Мы не создаём shared invalidation system и не объявляем число запросов измеренным результатом. Такая отрицательная часть защищает от незаметного расширения scope.
ADR может быть логичным и всё равно ошибочным. Он фиксирует состояние знаний на момент выбора. Контракт источника может измениться. Ограничение по данным может оказаться неверным. Операционная цена может вырасти. Поэтому Accepted означает «решение принято», а не «реализация доказана во всех средах».
Свяжите ADR с отдельным evidence question. Для cache это вопрос о допустимой freshness и о том, как обнаружить нарушение. Для миграции это вопрос о совместимости схемы и обратном пути. Для security-решения это вопрос о threat model и обязательной проверке. Не подменяйте evidence красивой формулировкой в разделе Consequences.
ADR не заменяет design document, threat model, migration plan, benchmark, test plan, runbook или incident review. Он не гарантирует полноту альтернатив и не превращает согласие участников в технический факт. Формат нужно подстроить под локальные правила хранения, доступа и approval.
Не редактируйте старую запись так, чтобы она описывала новое решение. История нужна именно для ответа на вопрос «почему раньше сделали так». Если предпосылка исчезла, старый ADR получает статус superseded, а новый record объясняет следующий компромисс. Это сохраняет причинность и не заставляет будущего читателя угадывать, какая версия текста была действующей.
Не называйте synthetic пример production-результатом. Не добавляйте вымышленные числа, названия сервисов и ссылки на несуществующие dashboards. Если факт нельзя проверить, напишите, какой артефакт должен его подтвердить. Такой пробел полезнее уверенного, но ложного вывода.
ADR готов, когда новый читатель без поиска по чату может назвать симптом, constraint, цену ошибки, выбранный вариант и отклонённые альтернативы. Он видит владельца, статус и дату или сигнал пересмотра. Он понимает отрицательный путь и знает, где проверяется реализация. При сравнении с code не возникает скрытого второго решения. Если хотя бы один пункт требует догадки, запись ещё не готова.
После релиза в коде остаётся обходной путь: запрос идёт через отдельный слой, хотя прямой вызов короче. Через полгода обсуждение исчезает из чата, авторы переключаются на другие задачи, а reviewer видит только итоговый diff. Симптом легко узнать: команда снова спорит, зачем существует условие, очередь или дополнительная граница.
Цена ошибки выше стоимости потерянного контекста. Если удалить защиту слишком рано, вернётся старое ограничение. Если оставить её навсегда, команда продолжит платить за лишнее состояние, тесты и поддержку. По одному коду нельзя восстановить, какой риск когда-то перевешивал. Нужна короткая запись, связывающая наблюдаемую проблему, выбор и принятую цену.
Такую запись обычно называют ADR (Architecture Decision Record). В этой статье разберём практический маршрут для одного решения на границе BFF (Backend for Frontend): как понять, нужен ли кэш, какие варианты сравнить и как записать результат так, чтобы он не стал ложным разрешением на любую будущую оптимизацию.
ADR фиксирует один значимый технический выбор и его rationale — объяснение, почему выбран этот путь. Минимальное содержимое: контекст и проблема, рассмотренные варианты, decision и последствия. Для каждого утверждения полезно оставить источник: контракт, тест, измерение, issue или ссылку на другой документ.
У записи есть границы ответственности. Ticket описывает работу и срок. Code review хранит обсуждение конкретного изменения. Test проверяет поведение. Runbook описывает операционное действие. ADR отвечает на другой вопрос: почему команда выбрала одну допустимую форму системы вместо других. Ссылка на ticket помогает найти детали, но не заменяет rationale.
Статус тоже не является переключателем. Proposed означает, что запись подготовлена к обсуждению. Accepted означает, что команда приняла решение. Superseded означает, что новый ADR заменил старый и объяснил изменение. Ни один статус сам по себе не запускает код, миграцию, approval или проверку отката.
Возьмём ограниченный пример. SSR-приложение обращается через BFF к каталогу предложений. Для одного экрана одинаковый справочник читается много раз за короткий интервал. Прямой запрос проще и всегда получает актуальный ответ, но увеличивает число обращений к upstream. Локальный кэш может сократить повторные чтения, однако добавляет TTL, риск устаревшего ответа и обязанность очищать состояние.
Это не отчёт о конкретной production-системе: в статье нет реальных latency, traffic, hit ratio или экономии. Сценарий нужен, чтобы воспроизвести форму анализа. В своём проекте подставьте контракт источника, допустимую свежесть, класс данных, owner и измерение. Если эти сведения неизвестны, ADR должен назвать их открытым вопросом, а не превращать предположение в факт.
Сначала запишем наблюдаемое и неизвестное отдельно:
Наблюдаемый факт: один экран повторяет чтение одного справочника.\nИсточник факта: trace или счётчик запросов, приложенный к задаче.\nГипотеза: часть чтений можно обслужить из bounded local cache.\nОбязательное условие: ответ не старше согласованного TTL.\nНеизвестно: разрешает ли upstream такой уровень устаревания.\nЦена ошибки: устаревшие данные или рост нагрузки на upstream.Важна именно последовательность. «Добавим кэш, потому что медленно» — решение без доказанной причины. Сначала нужно подтвердить повторяемость чтений и спросить владельца контракта, допустима ли заданная свежесть. Если upstream требует read-after-write для этого справочника, локальный кэш не проходит constraint fit независимо от удобства реализации.
Сравнивайте решения, а не рекламные формулировки. Для этого задайте одинаковые поля: соответствие обязательному условию, обратимость, evidence gap и стоимость эксплуатации. «Проще» без указания владельца и границы ничего не измеряет. Вариант «ничего не менять» тоже стоит назвать, если он действительно доступен: иногда дополнительное состояние опаснее лишнего запроса.
| Вариант | Что закрывает | Цена и риск | Что проверить |
|---|---|---|---|
| Прямой запрос | Актуальность ответа без локального состояния | Повторная нагрузка на upstream; нет TTL и cleanup | Лимит запросов, latency и допустимость текущей нагрузки |
| Bounded local cache | Повторное чтение в пределах объявленного TTL | Устаревший ответ, invalidation, память и владелец кэша | Контракт freshness, класс данных, hit ratio и путь direct read |
| Общий cache-сервис | Разделяемое состояние для нескольких потребителей | Новый сервис, сеть, права, отказ и cross-team owner | Действительно ли нужен общий владелец и как переживается недоступность |
| Ничего не менять | Сохраняет простую модель и отсутствие нового состояния | Нагрузка остаётся; возможно, проблема не подтверждена | Повторить измерение и проверить, есть ли пользовательский эффект |
Матрица не выдаёт победителя автоматически. Она заставляет привести варианты к сопоставимому масштабу. Если для кэша известен TTL, а для общего сервиса написано только «сложно», данные ещё не симметричны. Если используете баллы и веса, приложите шкалу и объясните, какие риски модель не учитывает. Итоговый score помогает провести разговор, но не доказывает корректность решения.
После проверки контракта допустим учебный исход: upstream разрешает ограниченную свежесть, справочник не содержит данных, требующих немедленного read-after-write, а direct read остаётся доступным при промахе или отключении кэша. Тогда можно предложить локальное bounded state. Ниже — запись в формате, который можно положить в каталог decisions. Она не создаёт кэш и не утверждает измеренный результат.
# ADR-0042: bounded cache at the BFF boundary\n\n## Status\nProposed\n\n## Context and problem\nSSR повторяет чтение одного справочника. Upstream разрешает\nответ не старше согласованного TTL. Для критичных изменений\nтребуется direct read.\n\n## Options considered\n- direct read;\n- bounded local cache with explicit TTL;\n- shared cache service.\n\n## Decision\nНачать с bounded local cache только для этого read path.\nПри недоступном кэше читать upstream напрямую.\n\n## Consequences\nПлюс: повторные чтения ограничены TTL.\nЦена: owner состояния, expiry, наблюдение за freshness и cleanup.\n\n## Not in scope\nНе вводим shared cache-сервис и не распространяем решение\nна другие callers без нового ADR.\n\n## Reassessment\nОткрыть successor ADR при изменении контракта freshness,\nпоявлении чувствительных данных или нового потребителя.Учебный фрагмент показывает причинность: симптом связан с ограничением, варианты перечислены, цена названа, а отрицательный путь ограничивает scope. В настоящую запись добавьте ссылки на подтверждённый контракт, тест, trace или controlled experiment. Статус Proposed означает, что документ ещё нужно проверить владельцу upstream и участникам, которых затрагивает кэш.
Решение и доказательство нельзя слить в один абзац. После принятия ADR команда отдельно проверяет реализацию: TTL действительно ограничен, ключ кэша не смешивает разные контексты, промах или ошибка не блокируют direct read, а чувствительные данные не попадают в неподходящее хранилище. Это уже область тестов, threat model, ревью и наблюдаемости.
Полезно сформулировать evidence question до изменения кода. Например: «Для выбранного read path доля повторных запросов превышает порог X, а upstream разрешает TTL Y секунд». Числа X и Y должны прийти из вашего контракта и измерения; в учебной статье их нельзя выдумывать. По итогам controlled test в ADR можно добавить ссылку на артефакт и уточнить последствия. Сам факт наличия ссылки не превращает эксперимент в гарантию для всех нагрузок.
Связь с кодом должна быть двусторонней и короткой: ADR ссылается на реализацию, тест и метрику; code review ссылается на ADR, чтобы reviewer мог проверить границу решения. Если diff добавляет shared invalidation, новую схему данных или другой срок хранения, это уже не «деталь», а возможное новое решение. Не прячьте его в implementation note.
Предпосылки решения со временем меняются. Upstream может начать требовать строгую свежесть, появится второй потребитель, изменится класс данных или стоимость shared service станет приемлемой. В такой ситуации старый ADR не следует переписывать задним числом. Иначе исчезнет ответ на вопрос, почему прежний код был разумным при старых условиях.
Создайте новый record со статусом Proposed, свяжите его с исходным и опишите, что именно изменилось. После принятия нового решения старое получает Superseded. Это не формальность: append-only история отделяет прежний компромисс от нового и помогает безопасно читать старые ссылки в issue, review и runbook.
Дата пересмотра полезна только вместе с действием и владельцем. Дата сама не откроет документ. Надёжнее написать условие: «пересмотреть, если контракт freshness изменился или список callers вышел за пределы BFF». Для измеримых условий добавьте источник и того, кто должен инициировать review.
| Симптом | Вероятная причина | Проверка | Следующее действие |
|---|---|---|---|
| Записан только победитель | Решение приняли по вкусу, а не по constraint | Заполнить одинаковые поля для двух-трёх вариантов | Добавить downside выбранного пути и причины отказа |
| В Consequences обещана экономия | Учебную гипотезу выдали за измерение | Найти trace, benchmark или метрику с единицей измерения | Убрать число либо добавить ссылку на evidence |
| Кэш расширяется на новых callers | Граница применения осталась неявной | Сверить список потребителей и contract freshness | Остановить расширение и открыть новый ADR |
| Флаг выключили, но старый путь удалили | Rollback смешали с cleanup | Проверить, может ли система вернуться к direct read | Разделить изменение поведения и удаление ветки |
| Новый ADR переписывает старый | Историю принимают за текущую конфигурацию | Проверить ссылки и статусы двух записей | Оставить старый текст, связать successor и объяснить drift |
Эта таблица применима как диагностическая последовательность, а не как чек-лист полноты архитектуры. Например, ошибка кэша может быть не в выборе варианта, а в неверном ключе или cache-control заголовке. ADR помогает увидеть исходный компромисс, но не заменяет проверку конкретной реализации.
ADR не заменяет design document, threat model, migration plan, benchmark, test plan, runbook или incident review. Он не измеряет latency, надёжность, стоимость и безопасность без соответствующего evidence. Он также не гарантирует, что команда нашла все варианты: качество зависит от состава участников, доступных фактов и времени на review.
Не каждую мелкую правку стоит оформлять отдельной архитектурной записью. Кандидатами являются решения, которые меняют структуру, API или другой опубликованный контракт, качество сервиса, зависимости, владение состоянием либо плохо обратимы. Граница «значимости» командная; её лучше закрепить в локальном шаблоне, чтобы не превращать ADR в журнал каждого переименования.
Локальный кэш из примера нельзя переносить на персональные или чувствительные данные без отдельного анализа хранения, доступа и очистки. BFF не становится владельцем бизнес-истины только потому, что временно хранит ответ. При строгой консистентности direct read или иной контракт может быть единственно допустимым вариантом.
Форматы ADR различаются. Можно хранить записи в Git, wiki или специальном каталоге, если команда сохраняет версионирование, доступность, единый шаблон и историю статусов. Конкретные поля вроде confidence, stakeholders и review trigger выбираются по риску решения. Источник формата не является разрешением игнорировать требования безопасности, права доступа и отраслевые процессы.
Новый читатель должен без поиска по чату ответить на пять вопросов: какой симптом наблюдали; какое ограничение обязательно; какие варианты сравнивали; какую цену принимает выбранный путь; что заставит открыть новый ADR. Он должен отличать подтверждённый факт от гипотезы и понимать, где проверяется реализация.
Финальная проверка короткая. Уберите из записи заголовок Decision и попросите коллегу восстановить выбор по Context, Options и Consequences. Если коллега не может назвать owner, отрицательный путь или сигнал пересмотра, ADR ещё не готов. Если запись обещает измеренный эффект без источника, эффект нужно убрать или измерить отдельно.
Новый экран работает у сотрудников, но часть клиентов внезапно видит его после релиза. Команда ставит feature flag в false. Ошибка исчезает, график успокаивается, change закрывают. Через месяц в коде всё ещё живут две ветки, в конфигурации лежит старый ключ, а тесты проверяют два ответа. Любая случайная смена окружения может вернуть candidate-путь. Цена ошибки — повторный инцидент, долгий поиск владельца и риск для данных, которые новый путь уже успел записать.
Обратный случай не безопаснее. Rollout достиг 100 процентов, и это объявили завершением. Но 100 процентов означает только текущий результат правила для объявленной аудитории. Это не доказывает совместимость данных, исправность recovery path и право удалить fallback. Feature flag — не одно переключение, а временный контракт: кто получает вариант, кто его вычисляет, что считается остановкой и когда старый путь перестаёт быть нужен.
\nТезис. Rollout, rollback и cleanup нужно проводить разными изменениями. Rollout расширяет аудиторию. Rollback останавливает candidate и возвращает fallback. Cleanup удаляет лишнюю ветку после выбора итогового поведения. Если смешать эти операции, команда теряет причинную связь и принимает отключение за исправление.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Флаг выключен, но в коде остались две ветки | Rollback смешали с удалением реализации | Найти evaluator, branches, config references и тесты по ключу | Оставить fallback исполняемым, открыть отдельный cleanup change |
| 100% rollout считают доказательством готовности | Процент подменяет проверку контракта и данных | Сверить cohort key, effective config version, stop condition и recovery path | Заморозить процент и провести решение о final variant |
| После возврата к fallback часть запросов получает ошибку | Candidate записал состояние, которое fallback не читает | Проверить read/write compatibility и миграционный контракт | Остановить расширение; исправить совместимость отдельно от flag value |
| После удаления ключ снова появляется в UI | Остался client check, stale cache или старый payload | Проверить server response, client bundle, cache key и config | Удалить presentation toggle и оставить только итоговый UI contract |
| Никто не может назвать дату удаления | У флага нет owner, final variant и cleanup gate | Запросить карточку решения с evidence и списком references | Не расширять rollout; назначить владельца и новый change |
Сначала evaluator получает ключ флага и контекст. Контекст должен однозначно описывать subject, окружение и другие поля, которые участвуют в targeting. Для процентного rollout особенно важен стабильный cohort key. Если сегодня используется user id, а завтра случайный request id, один пользователь будет переходить между вариантами. Это уже не gradual rollout, а непредсказуемое распределение.
\nEvaluator возвращает выбранный вариант и, если система это поддерживает, детали оценки: key, value, variant, reason и версию конфигурации. Приложение применяет результат в одной точке. Второй слой не должен пересчитывать то же правило с другим контекстом. Иначе сервер отдаст новый ответ, а клиент решит, что пользователь относится к fallback-группе.
\nПроцент не описывает весь жизненный цикл. Он не говорит, обновился ли client cache, дошёл ли запрос до нового обработчика и можно ли откатить данные. Для каждого этапа нужны вопрос наблюдения и заранее выбранное действие. При остановке меняют одну существенную переменную: аудиторию, правило или код. Одновременная смена процента, evaluator и схемы данных уничтожает полезность сравнения.
\ntype FlagDecision = {\n key: string;\n variant: 'fallback' | 'candidate';\n cohortKey: string;\n configVersion: string;\n};\n\nfunction renderCheckout(decision: FlagDecision, order: Order) {\n if (decision.variant === 'candidate') {\n return renderNewCheckout(order);\n }\n\n return renderLegacyCheckout(order);\n}\nЭтот фрагмент — учебный. Он показывает границу между результатом оценки и применением варианта. В нём нет настоящего flag provider, хранилища, телеметрии, миграции или команды rollback. В рабочей системе нужно дополнительно определить формат контекста, источник версии конфигурации, правила кеширования и поведение при ошибке evaluator.
\n| Этап | Вопрос | Стоп-условие | Действие |
|---|---|---|---|
| 0% | Fallback выполняет согласованный smoke scenario? | Старый путь уже не работает | Не включать candidate; исправить базовый контракт |
| 5% | Одна стабильная cohort получает candidate в объявленной версии? | Сигнал нарушает заранее записанную границу | Вернуть аудиторию к 0%, сохранить конфигурацию и вопрос расследования |
| 25% | Candidate можно сопоставить с fallback в одном окне? | Версия или источник сигнала неизвестны | Остановить расширение и не менять правило вместе с кодом |
| 100% | Вся объявленная аудитория получает выбранный вариант? | Final variant не утверждён или fallback нужен для recovery | Не удалять флаг; открыть решение о завершении |
| Cleanup gate | Код и конфигурация больше не требуют alternate branch? | Осталась runtime reference или зависимость данных | Вернуть cleanup в доработку |
Значения 0, 5, 25 и 100 процентов в таблице — фиксированный учебный пример. Они не являются рекомендацией, нормативом или результатом измерения. Реальный шаг выбирают по размеру риска, качеству сигнала, обратимости и размеру cohort. Если команда не может назвать population, control, окно и владельца решения, процент не даёт полезной информации.
\nПри stop condition минимальное действие — прекратить дальнейшее расширение candidate. Для этого возвращают аудиторию к fallback или к заранее определённому safe value. Записывают effective config version и причину остановки. Не нужно в тот же момент удалять ветки, менять evaluator и переписывать миграцию. Иначе нельзя будет понять, что именно остановило симптом.
\nRollback значения флага не равен rollback данных. Если candidate уже создал записи, отправил события или изменил схему ответа, старый путь должен уметь прочитать это состояние. Фича-флаг не добавляет backward compatibility автоматически. Если fallback не понимает результат candidate, ответом должен стать отдельный migration contract, а не уверенность, что false решает всё.
Также не стоит называть rollback мгновенным. Сервис, edge-кеш и браузер могут получить конфигурацию в разное время. В карточке изменения укажите, где вычисляется вариант, сколько живёт кеш и что происходит с активной сессией. Разница версий допустима только тогда, когда она входит в контракт и не ломает recovery path.
\nCleanup начинается после выбора единственного поведения. Сначала владелец фиксирует final variant. Затем команда перестаёт добавлять новые условия и открывает отдельное изменение удаления. В его области должны быть server branches, client checks, configuration, permissions, tests, документация и миграционные заметки.
\nПроверка cleanup строится на отрицательных утверждениях. Runtime больше не вызывает evaluator для ключа. Клиент не переключает UI по старому payload. Конфигурация не содержит targeting rules и environment overrides, нужные только флагу. Тесты проверяют итоговый business contract, а не сохранение двух boolean-веток. Документ объясняет принятое поведение, но не предлагает включить исчезнувший путь.
\nГлобальный поиск по имени ключа полезен, но недостаточен. Ключ может быть собран из частей, сохранён в типе, скрыт в конфиге или пришит к кешу. Ищите вызовы evaluator, schema fields, token scopes, generated client types и тестовые данные. Если reference нужна для обратимой миграции, cleanup ещё не завершён: у неё должен быть owner и срок следующей проверки.
\nЭтот подход не выбирает конкретный SDK, размер cohort, TTL кеша, SLO или схему миграции. OpenFeature даёт общий API для evaluation context и результата, но не задаёт правила вашей платформы. Kubernetes rollback относится к версии Deployment и его Pod template; он не доказывает обратимость бизнес-данных. Система флагов также не доказывает качество эксперимента: для этого нужны отдельные метрики, контрольная группа и методика измерения.
\nНе следует удалять fallback только потому, что candidate получил 100 процентов. Не следует сохранять flag навсегда только потому, что когда-то существовал инцидент. Если данные, кеш или долгоживущий consumer не описаны, это блокер cleanup. Если evaluator недоступен, нужен явный default и проверяемая политика ошибки. Если неизвестно, какой вариант является итоговым, сначала принимают это решение, а потом удаляют код.
\nCleanup готов, если независимая проверка отвечает «да» на все вопросы: final variant записан владельцем; fallback больше не нужен для recovery или миграции; runtime не зависит от ключа; client и server не пересчитывают старое правило; configuration и permissions не содержат флаговые остатки; тесты сохраняют итоговое поведение; обычный релизный rollback не требует возвращать удалённый flag.
\nДо этого момента выключенный флаг нужно считать остановленным, но не удалённым. Такое различие сохраняет причину, границу действия и следующий шаг. Оно также не даёт спокойному графику скрыть технический долг, который снова станет пользовательской ошибкой.
\nOpenFeature: Evaluation Context — официальный материал о контексте оценки, targeting key и рисках передачи персональных данных.
\nOpenFeature: Evaluation API — официальное описание результата оценки и его деталей.
\nKubernetes: Update a Deployment Without Downtime — официальная документация о проверке обновления и возврате Deployment к предыдущей ревизии.
" + "contentHtml": "Новый экран работает у сотрудников, но часть клиентов внезапно видит его после релиза. Команда ставит feature flag в false. Ошибка исчезает, график успокаивается, change закрывают. Через месяц в коде всё ещё живут две ветки, в конфигурации лежит старый ключ, а тесты проверяют два ответа. Любая случайная смена окружения может вернуть candidate-путь. Цена ошибки — повторный инцидент, долгий поиск владельца и риск для данных, которые новый путь уже успел записать.
Обратный случай не безопаснее. Rollout достиг 100 процентов, и это объявили завершением. Но 100 процентов означает только текущий результат правила для объявленной аудитории. Это не доказывает совместимость данных, исправность recovery path и право удалить fallback. Feature flag — не одно переключение, а временный контракт: кто получает вариант, кто его вычисляет, что считается остановкой и когда старый путь перестаёт быть нужен.
\nТезис. Rollout, rollback и cleanup нужно проводить разными изменениями. Rollout расширяет аудиторию. Rollback останавливает candidate и возвращает fallback. Cleanup удаляет лишнюю ветку после выбора итогового поведения. Если смешать эти операции, команда теряет причинную связь и принимает отключение за исправление.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Флаг выключен, но в коде остались две ветки | Rollback смешали с удалением реализации | Найти evaluator, branches, config references и тесты по ключу | Оставить fallback исполняемым, открыть отдельный cleanup change |
| 100% rollout считают доказательством готовности | Процент подменяет проверку контракта и данных | Сверить cohort key, effective config version, stop condition и recovery path | Заморозить процент и провести решение о final variant |
| После возврата к fallback часть запросов получает ошибку | Candidate записал состояние, которое fallback не читает | Проверить read/write compatibility и миграционный контракт | Остановить расширение; исправить совместимость отдельно от flag value |
| После удаления ключ снова появляется в UI | Остался client check, stale cache или старый payload | Проверить server response, client bundle, cache key и config | Удалить presentation toggle и оставить только итоговый UI contract |
| Никто не может назвать дату удаления | У флага нет owner, final variant и cleanup gate | Запросить карточку решения с evidence и списком references | Не расширять rollout; назначить владельца и новый change |
Сначала evaluator получает ключ флага и контекст. Контекст должен однозначно описывать subject, окружение и другие поля, которые участвуют в targeting. Для процентного rollout особенно важен стабильный cohort key. Если сегодня используется user id, а завтра случайный request id, один пользователь будет переходить между вариантами. Это уже не gradual rollout, а непредсказуемое распределение.
\nEvaluator возвращает выбранный вариант и, если система это поддерживает, детали оценки: key, value, variant, reason и версию конфигурации. Приложение применяет результат в одной точке. Второй слой не должен пересчитывать то же правило с другим контекстом. Иначе сервер отдаст новый ответ, а клиент решит, что пользователь относится к fallback-группе.
\nПроцент не описывает весь жизненный цикл. Он не говорит, обновился ли client cache, дошёл ли запрос до нового обработчика и можно ли откатить данные. Для каждого этапа нужны вопрос наблюдения и заранее выбранное действие. При остановке меняют одну существенную переменную: аудиторию, правило или код. Одновременная смена процента, evaluator и схемы данных уничтожает полезность сравнения.
\ntype FlagDecision = {\n key: string;\n variant: 'fallback' | 'candidate';\n cohortKey: string;\n configVersion: string;\n};\n\nfunction renderCheckout(decision: FlagDecision, order: Order) {\n if (decision.variant === 'candidate') {\n return renderNewCheckout(order);\n }\n\n return renderLegacyCheckout(order);\n}\nЭтот фрагмент — учебный. Он показывает границу между результатом оценки и применением варианта. В нём нет настоящего flag provider, хранилища, телеметрии, миграции или команды rollback. В рабочей системе нужно дополнительно определить формат контекста, источник версии конфигурации, правила кеширования и поведение при ошибке evaluator.
\nЕсли candidate доставляется отдельным Kubernetes Deployment, сначала проверьте историю и состояние rollout. Эти команды требуют настроенного кластера, namespace и права чтения. Значение 7 — пример ревизии: его нужно заменить результатом первой команды.
kubectl rollout history deployment/checkout -n shop\nkubectl rollout status deployment/checkout -n shop --timeout=60s\n\n# Изменение состояния: выполняйте только по утверждённому stop condition\nkubectl rollout undo deployment/checkout -n shop --to-revision=7\nkubectl rollout status deployment/checkout -n shop --timeout=60s\nrollout undo меняет состояние Deployment, поэтому это операционная команда, а не безопасная проверка «на всякий случай». Kubernetes откатывает Pod template к выбранной ревизии; он не возвращает записи в базе, события и внешние побочные эффекты. Если команда относится только к конфигурации feature flag, применяйте аналогичный runbook вашего провайдера и сохраняйте effective version.
| Этап | Вопрос | Стоп-условие | Действие |
|---|---|---|---|
| 0% | Fallback выполняет согласованный smoke scenario? | Старый путь уже не работает | Не включать candidate; исправить базовый контракт |
| 5% | Одна стабильная cohort получает candidate в объявленной версии? | Сигнал нарушает заранее записанную границу | Вернуть аудиторию к 0%, сохранить конфигурацию и вопрос расследования |
| 25% | Candidate можно сопоставить с fallback в одном окне? | Версия или источник сигнала неизвестны | Остановить расширение и не менять правило вместе с кодом |
| 100% | Вся объявленная аудитория получает выбранный вариант? | Final variant не утверждён или fallback нужен для recovery | Не удалять флаг; открыть решение о завершении |
| Cleanup gate | Код и конфигурация больше не требуют alternate branch? | Осталась runtime reference или зависимость данных | Вернуть cleanup в доработку |
Значения 0, 5, 25 и 100 процентов в таблице — фиксированный учебный пример. Они не являются рекомендацией, нормативом или результатом измерения. Реальный шаг выбирают по размеру риска, качеству сигнала, обратимости и размеру cohort. Если команда не может назвать population, control, окно и владельца решения, процент не даёт полезной информации.
\nПри stop condition минимальное действие — прекратить дальнейшее расширение candidate. Для этого возвращают аудиторию к fallback или к заранее определённому safe value. Записывают effective config version и причину остановки. Не нужно в тот же момент удалять ветки, менять evaluator и переписывать миграцию. Иначе нельзя будет понять, что именно остановило симптом.
\nRollback значения флага не равен rollback данных. Если candidate уже создал записи, отправил события или изменил схему ответа, старый путь должен уметь прочитать это состояние. Фича-флаг не добавляет backward compatibility автоматически. Если fallback не понимает результат candidate, ответом должен стать отдельный migration contract, а не уверенность, что false решает всё.
Также не стоит называть rollback мгновенным. Сервис, edge-кеш и браузер могут получить конфигурацию в разное время. В карточке изменения укажите, где вычисляется вариант, сколько живёт кеш и что происходит с активной сессией. Разница версий допустима только тогда, когда она входит в контракт и не ломает recovery path.
\nCleanup начинается после выбора единственного поведения. Сначала владелец фиксирует final variant. Затем команда перестаёт добавлять новые условия и открывает отдельное изменение удаления. В его области должны быть server branches, client checks, configuration, permissions, tests, документация и миграционные заметки.
\nПроверка cleanup строится на отрицательных утверждениях. Runtime больше не вызывает evaluator для ключа. Клиент не переключает UI по старому payload. Конфигурация не содержит targeting rules и environment overrides, нужные только флагу. Тесты проверяют итоговый business contract, а не сохранение двух boolean-веток. Документ объясняет принятое поведение, но не предлагает включить исчезнувший путь.
\nГлобальный поиск по имени ключа полезен, но недостаточен. Ключ может быть собран из частей, сохранён в типе, скрыт в конфиге или пришит к кешу. Ищите вызовы evaluator, schema fields, token scopes, generated client types и тестовые данные. Если reference нужна для обратимой миграции, cleanup ещё не завершён: у неё должен быть owner и срок следующей проверки.
\nЭтот подход не выбирает конкретный SDK, размер cohort, TTL кеша, SLO или схему миграции. OpenFeature даёт общий API для evaluation context и результата, но не задаёт правила вашей платформы; при ошибке базовой оценки клиент возвращает переданное default value. Kubernetes rollback относится к версии Deployment и его Pod template; он не доказывает обратимость бизнес-данных. Система флагов также не доказывает качество эксперимента: для этого нужны отдельные метрики, контрольная группа и методика измерения.
\nНе следует удалять fallback только потому, что candidate получил 100 процентов. Не следует сохранять flag навсегда только потому, что когда-то существовал инцидент. Если данные, кеш или долгоживущий consumer не описаны, это блокер cleanup. Если evaluator недоступен, нужен явный default и проверяемая политика ошибки. Если неизвестно, какой вариант является итоговым, сначала принимают это решение, а потом удаляют код.
\nCleanup готов, если независимая проверка отвечает «да» на все вопросы: final variant записан владельцем; fallback больше не нужен для recovery или миграции; runtime не зависит от ключа; client и server не пересчитывают старое правило; configuration и permissions не содержат флаговые остатки; тесты сохраняют итоговое поведение; обычный релизный rollback не требует возвращать удалённый flag.
\nДо этого момента выключенный флаг нужно считать остановленным, но не удалённым. Такое различие сохраняет причину, границу действия и следующий шаг. Оно также не даёт спокойному графику скрыть технический долг, который снова станет пользовательской ошибкой.
\nOpenFeature: Evaluation Context — официальный материал о контексте оценки, targeting key и рисках передачи персональных данных.
\nOpenFeature: Evaluation API — официальное описание результата оценки и его деталей.
\nKubernetes: Update a Deployment Without Downtime — официальная документация о проверке rollout, возврате Deployment к ревизии и ограничении rollback только Pod template.
" } diff --git a/editorial/agent-rewrites/122.json b/editorial/agent-rewrites/122.json index 56737ea..3d68833 100644 --- a/editorial/agent-rewrites/122.json +++ b/editorial/agent-rewrites/122.json @@ -3,5 +3,5 @@ "slug": "editorial-2024-08-mechanism-feature-flags", "title": "Feature flags без рассинхрона: где принимать решение и что считать показом", "excerpt": "Как разделить серверное решение, клиентское отображение и аналитическое событие, чтобы rollout оставался объяснимым, а временная ветка действительно исчезла.", - "contentHtml": "Новый экран включили для десяти процентов пользователей. Часть из них увидела кнопку, но API продолжил выполнять старую ветку. У другой части браузер показал новый вариант после обновления страницы, хотя сервер ещё отдавал старый. В аналитике при этом появился один общий event exposure. По нему нельзя понять, было ли решение вычислено, дошёл ли ответ до браузера и отрендерился ли экран.
\nЦена такой ошибки растёт после первого успешного релиза. Команда тратит время на поиск слоя, который изменил вариант. В защищённый клиентский контекст могут попасть лишние сведения. Сегмент получает разные правила на соседних запросах. После эксперимента остаются две ветки кода, старый ключ конфигурации и тесты, которые поддерживают уже несуществующее решение.
\nFeature flag — не просто boolean. Это контракт из входов, владельца решения, версии конфигурации, безопасного значения и пути удаления. Главный принцип прост: один слой владеет eligibility, а остальные получают только тот результат, который им нужен. Evaluation, render и business effect нужно считать разными фактами.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| UI показывает новый вариант, API выполняет старый | Сервер и браузер независимо вычисляют один flag | Сравнить evaluator, targeting key и config version в одном запросе | Оставить eligibility на сервере; клиенту передавать verdict и version |
| Один пользователь меняет вариант после логина | До логина используется anonymous ID, после логина — account ID | Построить timeline ключа для одной сессии | Описать переход ключа или закрепить вариант на время сессии |
| Exposure есть, но экран не показан | Событие отправляется сразу после evaluation | Сопоставить событие с точкой рендера и ошибками UI | Разделить evaluation record и render confirmation |
| Отключение флага не меняет всех клиентов сразу | Клиент держит кэш или обновляет конфигурацию по расписанию | Проверить effective config version и границу refresh | Задать допустимое окно устаревания и fallback |
Оценка флага получает context. В него могут входить идентификатор субъекта, приложение, окружение, локаль и другие признаки. Не каждый такой признак можно передавать в браузер. Право доступа, индивидуальная цена, fraud signal и внутреннее состояние аккаунта должны оставаться за доверенной границей. Клиенту нужен ответ о доступности функции, а не правило, по которому сервер его получил.
\nПубличный layout preference или локаль часто подходят для client-side решения. Это верно только тогда, когда они уже доступны клиенту и не скрывают защищённую политику. Если один flag зависит и от локали, и от права доступа, его нельзя безопасно перенести в браузер целиком. Сервер может вычислить eligibility, а клиент — выбрать разрешённое представление внутри полученного контракта.
\nEvaluator отвечает на вопрос: какой вариант вернуть для данного flag key и context. В архитектуре должен быть один владелец этого ответа. Если сервер и браузер повторяют одно правило, они должны либо использовать один явно согласованный контракт, либо считаться независимыми решениями с отдельными названиями. Скрытая копия правила почти всегда приводит к расхождению.
\nПример ниже учебный. Он не подключается к provider, не читает реальную конфигурацию и не отправляет события. В нём показана граница: сервер принимает защищённый вход, возвращает уже принятое решение и версию, а клиент не переоценивает право доступа.
\ntype FlagDecision = {\n variant: 'control' | 'candidate';\n enabled: boolean;\n configVersion: string;\n};\n\nfunction resolveCheckoutBanner(input: {\n accountId: string;\n hasNewCheckoutAccess: boolean;\n}): FlagDecision {\n return {\n variant: input.hasNewCheckoutAccess ? 'candidate' : 'control',\n enabled: input.hasNewCheckoutAccess,\n configVersion: 'checkout-banner-v3',\n };\n}\n\nconst decision = resolveCheckoutBanner({\n accountId: 'account-42',\n hasNewCheckoutAccess: false,\n});\n\n// Browser receives the result, not hasNewCheckoutAccess or the rule.\nrenderBanner({ enabled: decision.enabled, variant: decision.variant });\nВ реальной системе значение accountId не нужно отправлять в клиент только ради отображения. Поле configVersion помогает объяснить результат и сопоставить его с журналом. Его нельзя считать глобальным доказательством: другой сервис или кэш может работать с другой версией.
Отрицательный путь важнее счастливого. Если provider недоступен, context не содержит обязательного ключа или версия конфигурации устарела сверх допустимого окна, система должна вернуть явно выбранный fallback. Не следует молча вычислять тот же flag вторым алгоритмом в браузере. Иначе отказ превращается в незаметное изменение аудитории.
\nПроцент rollout стабилен только относительно ключа. Сервер может распределять пользователей по account ID, а браузер — по cookie ID. Тогда один человек получит разные варианты. После входа ключ может измениться снова. Это не мелкая деталь хеширования. Это изменение субъекта, которому принадлежит решение.
\nЗафиксируйте четыре вещи: кто является subject, когда появляется его ключ, как проходит переход anonymous → authenticated и нужно ли закреплять вариант на активную сессию. Если конфигурация меняется, решите, может ли следующий запрос пересчитать когорту. Для некоторых экранов это допустимо. Для оплаты, миграции данных или последовательного сценария может потребоваться pinning до конца операции.
\nНе включайте персональные данные в context без необходимости. Провайдер может сериализовать или сохранять его для таргетинга. Используйте стабильный идентификатор или заранее определённый хеш, если это соответствует модели угроз и правилам хранения. Хеш сам по себе не делает данные безопасными.
\nEvaluation означает, что evaluator получил входы и вернул вариант или fallback. Это технический факт вычисления. Exposure candidate означает, что приложение подготовило запись о выбранном варианте. Render confirmation означает, что клиент дошёл до конкретной точки показа. Business effect означает отдельное действие пользователя или доменный результат. Эти события нельзя сливать в одно поле exposure.
| Факт | Минимальные поля | Чего он не доказывает |
|---|---|---|
| Evaluation | flag key, variant, evaluator, config version | Что UI отрендерился |
| Exposure candidate | subject boundary, variant, idempotency key | Что событие доставлено ровно один раз |
| Render confirmation | screen, display point, client timestamp | Что пользователь прочитал экран |
| Business effect | Доменное действие и его собственный contract | Что действие вызвал только flag |
Слово exactly-once здесь опасно. Повторная отправка, таймаут, падение consumer и повторный запуск страницы создают разные сценарии. Если аналитике нужна дедупликация, downstream должен получить устойчивый idempotency key и окно хранения ключей. Если нужно знать факт рендера, отправляйте событие в точке рендера. Даже это не доказывает, что пользователь увидел или использовал результат.
\nСерверное решение подходит, когда flag зависит от защищённых данных или должно одинаково влиять на API и UI. Недостаток — сетевой путь и возможность увидеть старый результат из кэша. Его компенсируют явная версия, короткий контракт ответа и проверяемый fallback.
\nКлиентское решение подходит для уже публичной конфигурации, например локали или разрешённой темы. Недостаток — контекст и правила становятся частью доверенной границы браузера. Нельзя использовать этот вариант для скрытого entitlement только потому, что так быстрее собрать интерфейс.
\nРазделённое решение полезно, когда сервер отвечает за eligibility, а клиент выбирает только presentation. Например, сервер возвращает candidate и версию, а клиент решает, какой из двух разрешённых layout показать. Граница должна быть написана рядом с контрактом. Иначе presentation постепенно начнёт повторять policy.
Отключение flag не обязано мгновенно менять каждый клиент. Кэш, refresh interval, service worker, edge и повторная загрузка страницы создают окно устаревшего состояния. Поэтому контракт должен отвечать на вопрос: какая версия вернулась этому evaluator и сколько времени такой результат допустим.
\nЕсли UI получает вариант с сервера, не заставляйте браузер заново применять скрытое targeting rule. Передавайте verdict, variant и config version. Если provider сообщает об изменении конфигурации, это событие помогает запустить refresh или диагностику, но само по себе не является бизнес-exposure. При ошибке обновления используйте заранее выбранное поведение: сохранить последний допустимый вариант, перейти на control или заблокировать опасную операцию. Выбор зависит от риска функции.
\nЭта модель не выбирает конкретный SDK, TTL кэша, транспорт событий или процент rollout. Она не доказывает, что provider доступен, токен клиента ограничен или эксперимент статистически значим. Актуальная документация OpenFeature описывает provider, evaluation context и provider events как части API и жизненного цикла. Она не задаёт вашей команде SLA свежести, политику персональных данных или семантику product exposure.
\nУчебный код выше не является production-рецептом. В боевой системе отдельно проверяют авторизацию, валидацию context, обработку ошибок, наблюдаемость и совместимость версий. Не переносите строку checkout-banner-v3 в рабочую конфигурацию без определения владельца и пути удаления.
Критерий готовности проверяемый. Для одного реального flag возьмите один subject и один запрос. По логам и ответу назовите evaluator, targeting key, variant, config version и fallback. Затем покажите, где возникло evaluation, где произошёл render и как consumer обработал повтор события. Если любой ответ требует догадки или поиска правила в двух слоях, контракт ещё не готов.
\nНовый экран включили для десяти процентов пользователей. Часть из них увидела кнопку, но API продолжил выполнять старую ветку. У другой части браузер показал новый вариант после обновления страницы, хотя сервер ещё отдавал старый. В аналитике при этом появился один общий event exposure. По нему нельзя понять, было ли решение вычислено, дошёл ли ответ до браузера и отрендерился ли экран.
\nТакой сбой появляется, когда один флаг становится сразу тремя вещами: правилом доступа, состоянием интерфейса и событием аналитики. Браузер копирует серверное правило, кэш держит старую конфигурацию, а событие отправляется сразу после вычисления. В итоге один пользователь получает разные варианты на соседних запросах, а команда не может восстановить причину.
\nFeature flag — это контракт из входов, владельца решения, версии конфигурации, безопасного значения и пути удаления. В статье разберём границу между evaluation, render и business effect на учебном примере. Пример не подключается к реальному провайдеру и не выдаёт его за production-код: его задача — сделать проверяемым сам механизм.
\n| Наблюдение | Вероятная причина | Что проверить | Ограниченное действие |
|---|---|---|---|
| UI показывает candidate, API выполняет control | Два слоя независимо оценивают один ключ | Сверить evaluator, targeting key, variant и config version в одном trace | Оставить eligibility на сервере и передавать verdict клиенту |
| Вариант меняется после входа в аккаунт | До логина используется anonymous ID, после — account ID | Построить последовательность ключей одной сессии | Описать переход или закрепить вариант на операцию |
| Есть exposure, но экран не появился | Событие отправлено на evaluation | Сопоставить событие с точкой монтирования и ошибкой рендера | Развести evaluation и render confirmation |
| Отключение флага видно не всем | Кэш, refresh interval или edge отдают старую версию | Проверить effective config version и возраст ответа | Задать допустимое окно устаревания и fallback |
Первый вопрос должен звучать не «почему процент работает неправильно?», а «какой слой принял решение и какой факт мы сейчас наблюдаем?». Если ответ невозможно дать по логам и ответу API, сначала добавьте эти поля в контракт. Измерение процента без идентификатора субъекта и версии конфигурации не объясняет распределение.
\nEvaluator — компонент, который получает ключ флага и контекст и возвращает значение. Для одного бизнес-решения назначьте одного владельца eligibility (допустимости). Сервер может принять защищённые сведения о правах, тарифе или риске. Клиент после этого выбирает только разрешённое представление. Он не должен повторно решать, имеет ли пользователь право на функцию.
\nЭто не запрещает client-side flags вообще. Клиентский evaluator подходит для публичной настройки, например выбранной темы, если правило не защищает доступ и не влияет на операцию на сервере. Для смешанного правила сервер сначала возвращает безопасный verdict, а клиент может выбрать один из вариантов presentation внутри этого verdict.
\nВ ответе полезно различать минимум четыре поля:
\n| Поле | Смысл | Чего поле не доказывает |
|---|---|---|
flagKey | Какое решение вычисляли | Что ключ не изменится завтра |
variant | Какой вариант вернул evaluator | Что пользователь его увидел |
configVersion | С какой эффективной версией работал evaluator | Что все сервисы уже используют эту версию |
reason | Почему получено значение: targeting, split, cached, default, error и т. п. | Что провайдер поддерживает каждую причину одинаково |
fallback | Что делать при ошибке или отсутствии обязательного контекста | Что fallback безопасен для любого домена |
OpenFeature называет provider слоем, который разрешает значение, а подробный результат может содержать variant, reason, error code и metadata. Это удобный общий словарь, но не готовая политика вашей системы. Название configVersion в собственном API также не становится глобальной версией всех сервисов: его нужно сопоставлять с журналом публикации.
Ниже — самостоятельный пример для Node.js 18 и новее. Он не зависит от SDK: детерминированно распределяет subject по 100 корзинам и возвращает подробный результат. Запустите его в shell как есть. Хеш нужен только для учебного стабильного распределения; он не заменяет защиту персональных данных и не доказывает статистическую значимость эксперимента.
\nnode --input-type=module <<'NODE'\nimport { createHash } from 'node:crypto';\n\nconst flagKey = 'checkout-banner';\nconst configVersion = 'checkout-banner-v3';\nconst rolloutPercent = 10;\n\nfunction bucket(targetingKey) {\n const digest = createHash('sha256')\n .update(flagKey + ':' + targetingKey)\n .digest();\n return digest.readUInt32BE(0) % 100;\n}\n\nfunction evaluate({ targetingKey, providerReady }) {\n if (!providerReady || !targetingKey) {\n return {\n flagKey,\n variant: 'control',\n reason: providerReady ? 'missing-targeting-key' : 'error',\n configVersion,\n fallback: true,\n };\n }\n\n const inRollout = bucket(targetingKey) < rolloutPercent;\n return {\n flagKey,\n variant: inRollout ? 'candidate' : 'control',\n reason: 'split',\n configVersion,\n fallback: false,\n };\n}\n\nfor (const input of [\n { targetingKey: 'user-001', providerReady: true },\n { targetingKey: 'user-002', providerReady: true },\n { targetingKey: '', providerReady: true },\n { targetingKey: 'user-001', providerReady: false },\n]) {\n console.log(JSON.stringify({ input, result: evaluate(input) }));\n}\nNODE\nРезультат содержит два важных свойства. Один и тот же ключ даёт один и тот же bucket при неизменном flag key, а отсутствие ключа или отказ provider не запускает второй алгоритм: система явно возвращает control и помечает fallback. В рабочем коде fallback выбирают по риску операции. Для декоративного баннера обычно безопасен control, для платежа или миграции данных может понадобиться блокировка операции и ручная диагностика.
\nНе передавайте в браузер hasNewCheckoutAccess, внутренний fraud signal или правило вычисления только ради рендера. Браузеру достаточно результата, который разрешён для его доверенной границы. Если provider поддерживает подробную оценку, логируйте минимально нужные поля на сервере: не превращайте targeting context в копию профиля пользователя.
Процент rollout стабилен только относительно ключа субъекта. Сервер, который хеширует account ID, и браузер, который хеширует cookie ID, распределяют одного человека независимо. После логина ключ может измениться ещё раз. Это не «небольшая погрешность»: изменился субъект, которому принадлежит решение.
\nПеред запуском запишите четыре правила: кто является subject, когда появляется targeting key, как проходит anonymous → authenticated и может ли вариант измениться в активной операции. Для чтения новостей пересчёт после логина может быть приемлем. Для оформления заказа, миграции или многошаговой формы вариант часто фиксируют до завершения операции.
\nOpenFeature описывает targeting key как строковый идентификатор субъекта и предупреждает, что провайдеры могут требовать его для дробного распределения или адресных правил. Там же есть отдельное предупреждение о персональных данных в evaluation context: провайдер может сериализовать или сохранять контекст. Поэтому используйте устойчивый технический идентификатор или согласованный хеш только после проверки модели угроз и политики хранения. Хеш сам по себе не делает значение анонимным.
\nEvaluation означает, что система получила входы и вернула значение. Это ещё не exposure. Если сервер оценил флаг, но запрос завершился ошибкой, экран не показан. Если клиент получил HTML, но компонент не смонтировался, показ также не доказан. Даже подтверждённый рендер не говорит, что человек прочитал или использовал экран.
\nРазделите жизненный цикл на четыре факта:
\nOpenFeature предоставляет tracking API для связывания последующего действия или состояния приложения с контекстом оценки. Это помогает анализировать влияние флага, но tracking-вызов не превращается автоматически в доказательство рендера. Семантику «виден пользователю» задаёт ваше приложение и его критерий видимости.
\nОтключение флага не обязано мгновенно менять каждый процесс. Старое состояние может жить в памяти SDK, edge-кэше, service worker или ответе HTTP. Поэтому для каждого флага задайте допустимое окно stale: сколько времени старый verdict приемлем и что происходит после его истечения.
\nВ HTTP кэш не должен отдавать stale-ответ без разрешения протокола или явного контракта. Но из этого не следует, что бизнес-флаг обновится мгновенно: ваши refresh interval, локальный кэш и аварийный режим всё равно требуют проектного решения. Возвращайте effective config version, а при диагностике сохраняйте возраст значения и источник кэша.
\nПри сигнале изменения конфигурации можно запустить refresh или мониторинг. OpenFeature перечисляет provider events, включая configuration changed, error и stale. Эти события говорят о состоянии provider, а не о том, что конкретный пользователь увидел новый экран. При ошибке обновления заранее выберите одно из действий: сохранить последний допустимый вариант на короткое окно, перейти в control или остановить рискованную операцию.
\n# Пример ручной проверки контракта локального сервиса.\n# Ожидайте в JSON flagKey, variant, configVersion, reason и fallback.\nset -eu\ncurl --fail-with-body --silent --show-error \\\n -H 'X-Debug-Subject: user-001' \\\n 'http://localhost:3000/api/flags/checkout-banner' \\\n | jq '{flagKey, variant, configVersion, reason, fallback}'\nКоманда не предполагает конкретный продуктовый endpoint: URL и заголовок должны существовать в вашем сервисе. Она показывает воспроизводимый минимум ручной проверки. Для двух последовательных запросов одного subject сравните variant и configVersion; затем выключите provider и проверьте, что response явно перешёл в выбранный fallback.
\nСчастливый путь доказывает только то, что candidate когда-то вернулся. Надёжность видна на отказах. Тестируйте не только процент, но и владельца решения, отсутствие контекста, смену версии и повторную доставку события.
\nЭта модель не выбирает SDK, транспорт аналитики, TTL, процент rollout или способ статистической проверки эксперимента. OpenFeature — vendor-neutral API: его provider может получать данные из разных источников, а конкретная гарантия свежести, хранения контекста и доставки tracking зависит от реализации. RFC 9111 описывает HTTP-кэширование, но не задаёт вашей бизнес-семантике безопасный fallback.
\nУчебный SHA-256 evaluator не является доказательством равномерного эксперимента: он показывает детерминированную корзину, но не проверяет качество выборки, SRM, мощность теста или причинность метрики. Не используйте этот фрагмент как замену провайдеру с управлением конфигурацией. Не считайте render confirmation доказательством внимания пользователя и не называйте доставку exactly-once без подтверждённой семантики транспорта.
Flag готов к rollout, если для одного реального subject команда может без догадок назвать evaluator, targeting key, variant, config version, reason и fallback; показать точку render; воспроизвести отказ provider; увидеть границу stale; связать бизнес-эффект с evaluation. Если правило приходится искать одновременно в сервере, браузере и конфигурационном файле, сначала сократите число владельцев.
\nВ пятницу новый checkout включают для пяти процентов пользователей. В понедельник команда видит ошибки только на части запросов и выключает флаг. Но старый и новый код остаются в репозитории, тесты продолжают покрывать два пути, а конфигурация живёт без владельца. Через месяц никто не знает, можно ли удалить условие: новый путь мог записать данные в другом формате, а клиент мог закешировать старый вариант.
\nЦена такой ошибки растёт не вместе с процентом rollout. Она растёт с каждым новым условием, исключением и сервисом, который читает тот же ключ. Выключенный флаг уменьшает аудиторию, но не убирает ветки, не возвращает изменённые данные и не объясняет, почему система выбрала вариант. Тезис статьи простой: release-флаг — это временный контракт выпуска. Он должен описывать границу решения, безопасное значение, наблюдение и момент, когда код исчезнет.
\nЗначение true или false отвечает только на один вопрос: какой путь выбрать сейчас. Выпуск требует ответов на другие вопросы. Для кого действует правило? Где его вычисляют? Что произойдёт при ошибке провайдера или устаревшей конфигурации? Кто остановит rollout? Как проверить новый путь? Что удалить после выбора варианта?
Удобно разделить флаг на пять частей. Owner принимает решение на review. Audience задаёт стабильную когорту и окружение. Fallback определяет путь при недоступном или сомнительном решении. Signals показывают, что именно произошло. Cleanup связывает итоговый вариант с удалением условий, настроек и тестовых исключений.
\nЭти части не заменяют flag management system. Они задают контракт вокруг неё. Провайдер может вернуть default value, сообщить об ошибке или показать, что его состояние устарело. Приложение всё равно должно решить, какое значение безопасно для конкретной операции. Для цены, права доступа и другого защищённого решения fallback выбирает сервер. Клиент получает уже вычисленный результат, а не правило с закрытым контекстом.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| «Временный» флаг появляется без даты удаления | Boolean приняли за весь контракт выпуска | Найдите owner, review date и список веток | Оформите карточку и назначьте review до rollout |
| После выключения ошибки исчезли, но код не меняется | Disablement перепутали с cleanup | Сравните ветки, config keys, тесты и формат записанных данных | Откройте отдельное удаление и оставьте fallback до его завершения |
| Один пользователь видит разные варианты | Сервисы используют разные targeting keys или версии конфигурации | Запишите key, effective version и границу сессии в evaluation details | Назначьте один authoritative evaluator или явно опишите split decision |
| Событие exposure есть, а интерфейс не показался | Evaluation назвали эффектом | Разделите evaluation, response, render и user action | Переименуйте сигнал и не делайте вывод о результате по одной записи |
| Провайдер недоступен | Fallback не определён для ошибки или stale state | Воспроизведите provider error в изолированной проверке | Верните объявленный safe value и поднимите сигнал оператору |
Сначала сформулируйте один вопрос выпуска. «Включить новый checkout» слишком широко. Лучше: «Показать новую форму checkout синтетической внутренней когорте, оставив прежнюю форму доступной при любом сбое оценки». Такая формулировка задаёт аудиторию, вариант и отрицательный путь.
\nconst releaseFlagCard = {\n key: 'synthetic-checkout-copy-v1',\n class: 'release',\n owner: 'synthetic-checkout-owner',\n audience: 'synthetic-internal-beta-cohort',\n targetingKey: 'synthetic-subject-id',\n fallback: 'existing-checkout-copy',\n reviewAt: 'synthetic-2026-09-30',\n signals: ['evaluation-outcome', 'client-render-error'],\n cleanup: ['freeze-variant', 'remove-branches', 'remove-config'],\n};\n\n// Учебная запись. Она не создаёт флаг, не читает provider\n// и не отправляет telemetry или данные пользователя.\nВ примере все значения synthetic. Дата не запускает таймер, ключ не определяет настоящую когорту, а массив cleanup не удаляет файлы. Он показывает форму review. В реальной системе карточка должна ссылаться на конкретные владельца, окружение, конфигурацию и проверку, но не должна содержать секреты.
\nСервер должен владеть решением, если оно зависит от entitlement, цены, account state, fraud signal или другого защищённого входа. Сервер возвращает клиенту presentation value и, при необходимости, effective configuration version. Браузер не должен повторять правило по урезанному контексту: такое дублирование создаёт два источника истины.
\nКлиент может вычислять presentation-флаг, если все входы уже публичны: например, доступная локаль, опубликованный вариант оформления или локальная настройка layout. Это сокращает сетевой путь, но не отменяет проверку token, CORS, cache и refresh semantics конкретного провайдера.
\nРазделяйте evaluation и exposure. Evaluation означает, что evaluator вернул вариант для заданного контекста. Exposure можно использовать для записи попытки показать вариант, но запись не доказывает успешный render, пользовательское действие или бизнес-эффект. Если событие доставляется повторно или теряется, это нужно учитывать в consumer и в выводах из метрик.
\nВыключение и удаление проходят разными проверками. После выключения убедитесь, что новый путь не получает аудиторию и старый путь отвечает за заявленный контракт. Затем проверьте, не остались ли записи нового формата, client cache, server cache, отдельные разрешения и тестовые обходы. Только после этого удаляйте ветки. Иначе «cleanup» может убрать защитное условие раньше, чем система перестанет читать его последствия.
\nПростой критерий для pull request: поиск по ключу флага не находит production-условий после удаления, тесты больше не выбирают вариант через старую конфигурацию, а единственный оставшийся путь не зависит от временного default. Если данные менялись, добавьте отдельную проверку совместимости. Не объявляйте cleanup завершённым по одному зелёному unit-тесту.
\nКарточка не выбирает безопасный процент rollout и не заменяет threat model, approval, миграцию схемы, SLA или план отката данных. OpenFeature задаёт API evaluation context, targeting key, providers и provider events, но не навязывает поля owner, reviewAt или cleanup. Эти поля — локальная инженерная policy.
\nРазные провайдеры по-разному обновляют конфигурацию, кэшируют правила и обрабатывают ошибки. Нельзя переносить интервал refresh или семантику токена одного SDK в другой без проверки его документации. Нельзя считать targeting key доказательством единой когорты во всех сервисах, если они не используют общую версию конфигурации и один authoritative evaluator.
\nУчебный объект выше не является результатом production-эксперимента. Он не доказывает latency, delivery rate, безопасность данных или полезный эффект новой формы. Production-вывод требует реальных наблюдений, определённого окна измерения и заранее согласованного stop condition.
\nФлаг готов к включению, если другой инженер без устного контекста может назвать его вопрос, owner, audience boundary, authoritative evaluator, safe fallback, signals, stop condition и review date. Флаг готов к удалению, если выбран вариант зафиксирован, поиск не находит временных веток, тесты не зависят от старого ключа, а изменения данных проверены отдельно. Если хотя бы один ответ отсутствует, это не повод добавить ещё один boolean. Это сигнал вернуть контракт на review.
" + "contentHtml": "Проблема с фича-флагом обычно проявляется не в момент включения. В пятницу новую форму checkout включают для небольшой когорты, а в понедельник после ошибки возвращают старую. Ошибка исчезает, но в репозитории остаются две ветки, конфигурация живёт без владельца, а часть пользователей могла сохранить данные нового формата.
\nВыключенный флаг уменьшает аудиторию, но не удаляет код и не откатывает побочные эффекты. Поэтому release-флаг нужно рассматривать как временный контракт выпуска: он отвечает на вопрос, кто получает новый путь, какое значение безопасно при сбое, какие сигналы останавливают rollout и когда исчезнут условие, настройка и лишние тесты.
\nФича-флаг отделяет доставку кода от выбора поведения во время работы программы. Команда может выложить совместимый код, проверить его на внутренней когорте и только потом расширять аудиторию. Это полезно для релиза, но само по себе не делает новую функцию безопасной: флаг не заменяет миграцию данных, контроль прав, обратную совместимость или план отката.
\nДо первой строки кода зафиксируйте один вопрос. Формулировка «включить новый checkout» слишком широкая. Проверяемый вариант звучит так: «Показать новую форму внутренней когорте, а при ошибке оценки или рендера оставить прежнюю форму». В такой фразе уже видны субъект, граница решения и отрицательный путь.
\n| Часть контракта | Что зафиксировать | Как проверить |
|---|---|---|
| Вопрос | Какое поведение меняется и для кого | Другой инженер пересказывает условие одним предложением |
| Владелец | Кто остановит rollout и примет итоговый вариант | Имя или команда указаны в карточке изменения |
| Аудитория | Окружение, targeting key и способ удержания когорты | Один субъект получает ожидаемый вариант повторно |
| Fallback | Путь при ошибке, таймауте и устаревшем состоянии | Сбой провайдера воспроизведён в изолированной проверке |
| Удаление | Код, настройка, тесты и сигналы, которые исчезнут | После cleanup поиск по ключу не находит рабочих веток |
Boolean скрывает причину выбора. В стандарте OpenFeature провайдер разрешает типизированное значение по ключу, default value и контексту оценки, а результат может содержать причину, вариант, метаданные и код ошибки. Приложение должно сохранить эту информацию там, где она нужна для диагностики, но не выводить в логи персональные поля из контекста.
\nКонтекст оценки содержит сведения, по которым выбирается вариант. В нём есть необязательный targetingKey — строковый идентификатор субъекта. Он может быть идентификатором пользователя, сервиса или тестового субъекта. Если провайдер использует процентное распределение или адресные правила, отсутствие ключа может сделать результат непредсказуемым. Это свойство провайдера, а не гарантия всех систем фича-флагов.
Условие применимости здесь важнее названия библиотеки. OpenFeature описывает API и жизненный цикл интеграции, но не назначает владельца, срок удаления, процент rollout или безопасный fallback для бизнеса. Эти поля нужно определить в своей change-карточке и проверить средствами конкретного SDK.
\nconst flagCard = {
key: 'checkout-form-v2',
kind: 'release',
owner: 'checkout-team',
environment: 'staging',
targetingKey: 'test-subject-001',
defaultValue: false,
fallbackPath: 'checkout-form-v1',
stopConditions: ['5xx increase', 'render error', 'latency budget'],
reviewAfter: '2024-08-30',
cleanup: ['remove-branch', 'remove-config', 'keep-regression-test'],
};
if (flagCard.defaultValue !== false || !flagCard.cleanup.length) process.exit(1);
console.log(flagCard.key);\nСохраните фрагмент в файл flag-card.mjs, затем выполните node flag-card.mjs. Команда проверит, что fallback по умолчанию выключен и список удаления не пуст. Файл не обращается к настоящему провайдеру и не отправляет телеметрию. Поля owner, reviewAfter и cleanup — локальная политика команды, а не часть обязательного API OpenFeature.
Для рабочего проекта добавьте к такой проверке схему данных и проверку запрещённых значений. Секреты, email и необезличенные атрибуты не должны попадать в карточку или в контекст оценки без отдельного решения о хранении и доступе.
\nРешение, зависящее от тарифа, права доступа, состояния счёта, fraud-сигнала или другого защищённого входа, вычисляйте на сервере. Клиент должен получить уже разрешённый presentation value и обработать его как вход, а не повторять правило по урезанному контексту. Иначе сервер и браузер могут выбрать разные варианты.
\nКлиентская оценка подходит для публичного оформления, локали или другого решения, для которого все входы уже открыты и потеря точности не меняет права пользователя. Даже в этом случае проверьте семантику кэша, обновления и отсутствие чувствительных данных в evaluation context. Название «client-side» не означает, что конфигурация автоматически безопасна.
\nEvaluation означает, что evaluator вернул значение для контекста. Это ещё не показ интерфейса. Между оценкой и результатом могут произойти ошибка API, отказ рендера, закрытие страницы или повторная доставка события.
\nРазделите минимум четыре сигнала:
\nСобытия провайдера полезны для состояния самого источника: OpenFeature описывает готовность, ошибку, изменение конфигурации и устаревшее состояние. Событие PROVIDER_STALE говорит о свежести кэша, но не доказывает exposure. Аналогично, событие evaluation не доказывает успешный render. В каждом дашборде укажите, какой вывод разрешает сигнал и какой вывод из него делать нельзя.
Rollout — это последовательность проверяемых шагов, а не одно изменение процента. Сначала убедитесь, что старый путь по-прежнему поддерживается и что оба пути согласованы с форматом данных. Затем расширяйте аудиторию только при выполнении заранее заданных stop conditions.
\nВариант «внутренняя когорта» не означает автоматически безопасный вариант. Проверьте, что в неё не попадают реальные клиенты, что персональные данные не используются как необязательный targeting input и что оператор может быстро вернуть старое поведение.
\nПосле disablement новая аудитория перестаёт получать ветку, но зависимость от флага остаётся. Перед удалением проверьте три слоя: код, конфигурацию и данные. В коде ищите ключ, условные ветки, адаптеры и тестовые обходы. В конфигурации — правила, значения по умолчанию, окружения, кэш и разрешения. В данных — записи нового формата, фоновые задания и потребителей, которые ещё могут их читать.
\nrg -n --hidden --glob '!node_modules' 'checkout-form-v2' src test config
rg -n --hidden --glob '!node_modules' 'checkout-form-v1|checkout-form-v2' src test config
git diff --check
pnpm test --filter checkout\nКоманды предполагают Unix-подобную оболочку, установленный rg, рабочий скрипт тестов и каталог src. Подставьте имена своего репозитория. Первый поиск показывает остатки ключа, второй помогает найти обе ветки до удаления. Если новая ветка записывала данные, добавьте проверку чтения старого и нового формата; одного поиска и зелёного unit-теста недостаточно.
Надёжный cleanup оставляет тест поведения выбранного варианта, но удаляет временную развилку и её настройку. Отдельно обновите runbook и мониторинг. Если поиск всё ещё находит production-условие по ключу, удаление не закончено.
\nЭта схема рассчитана на release-флаг с обратным выбором поведения. Она не описывает полноценный эксперимент: для него нужны рандомизация, заранее выбранная метрика, длительность окна и статистический план. Она также не является планом отката базы данных и не решает проблему несовместимой схемы.
\nИнтервалы обновления, поведение при сетевой ошибке, кэш и формат resolution details зависят от провайдера и SDK. Нельзя переносить настройки одного продукта в другой по названию поля. Для OpenFeature гарантии относятся к контракту API; owner, review date, stop condition, redaction и cleanup команда должна определить и протестировать самостоятельно.
\nНе передавайте в контекст оценки больше данных, чем нужно правилу. Официальная документация OpenFeature отдельно предупреждает о том, что провайдер может сериализовать или сохранять контекст. Используйте стабильный обезличенный ключ, когда провайдер и требования к аудиту это допускают, и проверьте политику хранения у выбранного сервиса.
\nФлаг готов к включению, если инженер может без устного контекста назвать вопрос, старый и новый путь, owner, аудиторию, targeting key, серверную или клиентскую границу, fallback, stop conditions, сигналы и дату review. Флаг готов к удалению, если выбранный вариант зафиксирован, совместимость данных проверена, поиск не находит временных веток, а тесты больше не зависят от старого ключа.
\nЕсли не известен хотя бы один из этих ответов, добавление ещё одного boolean не уменьшит риск. Сначала верните изменение на review и заполните контракт.
" } diff --git a/editorial/agent-rewrites/124.json b/editorial/agent-rewrites/124.json index 7a7de62..f2c7425 100644 --- a/editorial/agent-rewrites/124.json +++ b/editorial/agent-rewrites/124.json @@ -2,6 +2,6 @@ "index": 124, "slug": "editorial-2024-07-field-release-engineering", "title": "Когда версия не доказывает релиз: проверка artifact, migration и rollback", - "excerpt": "Один номер релиза может скрывать разные commit, artifact и migration. Разбираем, где остановиться, какие связи проверить и почему возврат образа не отменяет изменения данных.", - "contentHtml": "В заявке на выпуск стоит 2024.07.0. Такой же номер виден у commit, container image, migration и rollout. После выкладки сервис отвечает кодом старой схемы: новый код ждёт поле, которого в базе нет. Команда повторяет deploy, потому что все карточки выглядят согласованными. Ошибка становится дороже с каждой попыткой: растёт окно недоступности, меняется состояние данных, а точку возврата уже трудно назвать.
Проблема не в самом номере версии. Проблема в том, что номер заменил связи между объектами. Он не доказывает, что artifact собран из нужного commit, что migration рассчитана на этот contract и что rollout ссылается на тот же digest. Выпуск готов только тогда, когда эти связи можно проверить по точным значениям, а отрицательный результат останавливает действие.
\nCommit описывает исходный revision. Artifact содержит собранное содержимое и immutable digest. Migration меняет схему или данные и должна назвать целевую версию и совместимость. Rollout intent говорит, какой digest команда собирается отправить. Return point хранит предыдущую версию и digest. Эти записи связаны, но не заменяют друг друга.
\nУ каждой связи есть проверяемое утверждение. Artifact должен ссылаться на exact commit id. Migration должна называть release version и совместимость с текущей схемой. Rollout должен содержать digest из artifact, а не только tag. Return point должен быть известен до approval. Если одно утверждение ложно или неизвестно, действие заканчивается на gate. Retry не исправляет неправильную запись.
\nНиже — ограниченный учебный пример. Значения вымышлены. Код не обращается к Git, registry, CI, Kubernetes API или базе данных. Он только сравнивает заранее заданные записи и возвращает решение для проверки человеком.
\nconst release={version:'2024.07.0'},commit={id:'commit-7f4a0c1'},artifact={digest:'sha256:release-070-a1',sourceCommitId:'commit-7f4a0c1'},migration={targetReleaseVersion:'2024.07.0',compatibleWith:'2024.06.3'},rollout={requestedArtifactDigest:'sha256:release-070-a1',migrationVersion:'2024.07.0'}; const checks={source:artifact.sourceCommitId===commit.id,migration:migration.targetReleaseVersion===release.version,artifact:rollout.requestedArtifactDigest===artifact.digest,rollout:rollout.migrationVersion===migration.targetReleaseVersion}; const ready=Object.values(checks).every(Boolean); if(!ready) throw new Error('stop: reconcile release records');\nПри ready === true пример говорит только о согласованности пяти записей. Он не говорит, что образ существует, подпись действительна, migration выполнена или сервис здоров. Если заменить sourceCommitId на другой id, результат должен стать отрицательным. То же относится к digest и target version. Это и есть полезный отрицательный путь: система не угадывает, какую запись считать правильной.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Номер версии совпадает, но artifact указывает на другой commit. | Tag используют вместо точной связи с исходным revision. | Сравнить artifact.sourceCommitId и commit id. | Остановить выпуск. Исправить запись или пересобрать artifact после решения владельца. |
| Код можно вернуть, но схема базы уже изменилась. | Rollback binary ошибочно считают rollback данных. | Проверить migration target, compatibility и обратную процедуру. | Вернуть только явно разрешённый artifact; вопрос данных передать отдельному владельцу. |
| Rollout прошёл с тем же tag, но другим digest. | Intent ссылается на mutable label, а не на immutable content. | Сравнить requested digest с digest artifact. | Не запускать rollout. Пересоздать intent после сверки. |
| После stop команда предлагает повторить deploy. | Retry используют как замену объяснению расхождения. | Найти первую ложную связь и назвать её источник. | Сначала reconcile records, затем повторить только проверку. |
Таблица разделяет четыре разных вопроса. Mismatch commit относится к происхождению artifact. Mismatch migration относится к совместимости contract. Mismatch digest относится к содержимому, выбранному для rollout. Повторная попытка без такой классификации стирает причину и оставляет команду без доказуемого решения.
\nRollback Deployment обычно возвращает предыдущую ревизию Pod template. Это полезно для кода и настроек, которые входят в template. Оно не отменяет произвольный SQL, удалённую запись, заполненное поле или изменение внешнего contract. Если migration уже прошла, старый image может не уметь читать новую схему.
\nReturn point должен содержать две границы. Первая — какую версию artifact можно запустить. Вторая — что разрешено делать с данными. Возможны обратная migration, совместимый промежуточный код, восстановление из резервной копии или запрет автоматического возврата. Пока путь не проверен, честная формулировка звучит так: «возврат artifact определён; откат данных не доказан».
\nТо же различие действует для provenance и attestations. Официальная документация SLSA описывает проверку provenance через сравнение с ожиданиями пакета. GitHub описывает artifact attestations как подписанные claims о происхождении и даёт команды для проверки. Ни один из этих механизмов сам по себе не утверждает, что migration совместима, rollout одобрен или production здоров.
\nОписанный подход ловит расхождения между названными записями. Он не доказывает, что значения правдивы. Он не проверяет историю Git, содержимое image, подпись, policy CI, права на кластер, runtime configuration, состояние базы, трафик, метрики или факт доставки. Для этих вопросов нужны разрешённые источники и отдельные проверки.
\nЕсли commit неизвестен, digest отсутствует, migration не имеет compatibility statement или return point не назван, результат должен быть отрицательным. Не подставляйте «последний main», не ищите image по tag и не объявляйте data rollback по факту отката Pod template. Остановитесь на первой неизвестной границе. Такое поведение медленнее одной зелёной кнопки, но дешевле расследования после повреждения данных.
\nМатериал готов к передаче на human review, если второй инженер без устных пояснений может показать: exact commit id, artifact digest, связь artifact с commit, migration target и compatibility, rollout digest, migration version и return point. Для каждой строки есть источник, владелец и действие при mismatch. Проверка должна дать либо все утверждения true, либо конкретный stop с названием ложной связи. В первом случае разрешение на deploy всё ещё принимает авторизованный процесс. Во втором случае deploy не начинается.
Учебные значения в примере не являются production-результатами. Их задача — показать форму проверки и сохранить отрицательный путь. Реальную оценку готовности нужно выполнять на доступных и разрешённых записях конкретного выпуска.
\nВ заявке на выпуск стоит 2024.07.0. Такое же имя указано у ветки, образа, миграции и выкладки. После запуска сервис отвечает ошибкой схемы: новый код обращается к полю, которого нет в базе. Команда повторяет deploy, потому что все записи выглядят согласованными. Это учебный сценарий, а не отчёт о конкретной production-системе. Его задача — показать, какие факты нужно связать до разрешения релиза.
Номер версии не доказывает происхождение и содержимое выпуска. Для решения нужны как минимум четыре независимые связи: artifact собран из нужного commit, rollout выбирает тот же digest, migration совместима с текущей схемой, а return point описан отдельно. Если связь неизвестна или ложна, проверка должна остановиться. Повторный deploy не превращает неизвестный факт в доказательство.
\nCommit — точная ревизия исходников. Artifact — результат сборки, например контейнерный образ, доступный по digest. Provenance — проверяемые сведения о том, где и как собран artifact. Migration — изменение схемы или данных с заявленной совместимостью. Rollout intent — запись о том, какой artifact и какую migration собираются применить. Return point — заранее известный вариант возврата к предыдущему коду и описание границы данных.
\nЭто не одна сущность с полем version. Для каждого объекта нужен источник и владелец. Поле artifact.sourceCommitId ниже — проектный контракт учебной модели, а не универсальное поле Docker или Kubernetes. Аналогично, migration.compatibleWith требует договорённости команды: инструменты миграций называют такие сведения по-разному.
Tag удобен для человека, но это изменяемая ссылка. Один и тот же tag может указывать на другой образ после следующей сборки. Digest адресует содержимое образа. Docker документирует загрузку образа по форме name@sha256:... и объясняет, что такой идентификатор фиксирует выбранную версию содержимого. Поэтому в rollout-записи храните digest, а tag оставляйте только как дополнительную подпись для чтения.
Digest отвечает лишь на вопрос «какое содержимое выбрано». Он не отвечает на вопросы «из какого commit оно собрано», «кто его собрал» и «совместима ли схема». Для этого нужен provenance и политика проверки. В SLSA v1.2 проверка включает сопоставление subject с digest artifact, доверенный builder, подпись и ожидаемые параметры сборки. Это отдельный gate, а не синоним успешной загрузки образа.
\nУ attestation тоже есть граница. Подписанное утверждение связывает metadata с artifact, но не утверждает, что migration выполнилась, сервис принимает трафик или решение о выкладке одобрил нужный человек. Эти факты должны появиться в собственных системах и проверках.
\nСледующая команда запускается в Node.js без зависимостей. Все значения синтетические: она не обращается к Git, registry, CI, Kubernetes API или базе. Код проверяет только заранее подготовленные записи и завершает процесс с ненулевым статусом при расхождении. Сохраните его как команду через heredoc или вставьте в локальный терминал.
\nnode --input-type=module <<'NODE'\nconst current = { schemaVersion: '2024.06.3' };\nconst release = { version: '2024.07.0' };\nconst commit = { id: 'commit-7f4a0c1' };\nconst artifact = {\n digest: 'sha256:release-070-a1',\n sourceCommitId: 'commit-7f4a0c1',\n};\nconst migration = {\n version: '2024.07.0',\n targetSchemaVersion: '2024.07.0',\n compatibleWith: '2024.06.3',\n};\nconst rollout = {\n artifactDigest: 'sha256:release-070-a1',\n migrationVersion: '2024.07.0',\n};\nconst returnPoint = {\n artifactDigest: 'sha256:release-069-z9',\n schemaVersion: '2024.06.3',\n};\n\nconst checks = {\n source: artifact.sourceCommitId === commit.id,\n release: migration.version === release.version,\n compatibility: migration.compatibleWith === current.schemaVersion,\n artifact: rollout.artifactDigest === artifact.digest,\n migration: rollout.migrationVersion === migration.version,\n returnPoint: Boolean(returnPoint.artifactDigest && returnPoint.schemaVersion),\n};\nconst ready = Object.values(checks).every(Boolean);\n\nconsole.log(JSON.stringify({ checks, ready }, null, 2));\nif (!ready) process.exitCode = 1;\nNODE\nПри исходных данных команда печатает \"ready\": true и завершается с кодом 0. Измените rollout.artifactDigest на sha256:release-070-other: поле artifact станет false, а процесс завершится с кодом 1. Такой отрицательный тест важнее красивого положительного fixture: он показывает, что gate не угадывает правильную запись.
Положительный результат означает только согласованность шести полей в памяти. Он не доказывает, что digest существует в registry, provenance подписан, миграция запущена, а старый artifact умеет работать с новой схемой. Эти вопросы нельзя «досчитать» из примера.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Tag совпадает, а digest другой. | Rollout использует изменяемую ссылку вместо содержимого. | Сверить digest в intent с digest из registry и записи сборки. | Остановить запуск; пересоздать intent после выбора exact artifact. |
| Artifact указывает на другой commit. | Образ собран из другой ревизии, либо provenance неполон. | Сопоставить commit, provenance subject и ожидаемый source repository. | Не объявлять образ релизом; пересобрать или исправить запись после расследования. |
| Новая схема не совместима со старым кодом. | Миграцию выполнили как необратимый шаг до перехода reader/writer. | Проверить порядок expand, переключение читателей и contract-шаг. | Остановить удаление/изменение данных; привлечь владельца схемы. |
| После stop предлагают повторить deploy. | Retry используют вместо объяснения первой ложной связи. | Найти первый mismatch и назвать источник каждого значения. | Сначала reconcile записей, затем повторить только проверку. |
Таблица разделяет происхождение, содержимое и совместимость. Это помогает не лечить ошибку схемы заменой образа и не считать зелёный rollout доказательством корректности данных. Если один источник недоступен, статус должен быть «не проверено», а не «вероятно совпадает».
\nregistry.example/app@sha256:....В Kubernetes новая revision Deployment появляется, когда меняется Pod template, например image или labels. Команда kubectl rollout undo возвращает предыдущую ревизию этого шаблона. Значит, такой rollback касается описания Pod: образа, переменных и других полей template. Он не является отменой произвольного SQL, удалённой строки, уже отправленного сообщения или изменения внешней системы.
Проблема особенно заметна при миграции. Если новая версия добавила поле и старый код его не ожидает, возврат image может вернуть работоспособность. Если новая версия удалила или изменила смысл поля, старый код может не запуститься на текущей схеме. До релиза нужен либо совместимый expand/contract-переход, либо проверенная обратная миграция, либо восстановление из резервной копии. Выбор зависит от базы, инструмента и договора владельцев данных.
\nПрактичная формулировка return point состоит из двух утверждений: «этот artifact можно снова запустить» и «для этой схемы есть разрешённое действие». Первое не даёт права утверждать второе. Если доказан только возврат Pod template, так и пишите в release record: «rollback кода определён; rollback данных не подтверждён».
\nМетод подходит как предварительная проверка связей в release record и как шаблон для CI-gate. Он не заменяет security review, проверку подписи, тест совместимости, backup/restore drill, smoke-тест, анализ метрик или approval согласно правилам организации. Названия полей и формат provenance в вашем toolchain могут отличаться; переносите инварианты, а не имена из учебного примера.
\nSLSA проверяет provenance относительно заданных ожиданий, но сами ожидания должны быть сформированы и защищены командой. GitHub artifact attestations доступны только при соответствующей настройке workflow и permissions; команда gh attestation verify требует доступного GitHub-контекста и не проверяет вашу migration. Kubernetes хранит историю Deployment с ограничениями revision history, поэтому старый Pod template может быть недоступен, если историю сократили или образ удалён.
Статус должен быть отрицательным, если отсутствует полный commit id, digest, источник provenance, compatibility statement или return point. Не подставляйте «последний main», не ищите образ по tag и не объявляйте rollback данных по факту kubectl rollout undo. Остановка на первой неизвестной границе дешевле расследования после повреждения данных.
Перед human approval второй инженер без устных пояснений должен найти exact commit id, source repository, artifact digest, связь artifact с commit, результат provenance verification, текущую и целевую схему, compatibility statement, rollout digest, migration version и return point. Для каждого значения указаны источник, владелец и действие при mismatch.
\nГотовность здесь — не одно зелёное число. Это воспроизводимый набор утверждений: все обязательные связи истинны, неизвестные значения не замаскированы, а возврат коду не выдан за возврат данным. Если хотя бы одно утверждение нельзя показать, релиз остаётся на проверке с конкретной причиной остановки.
\n