DarkRiDDeR13 мин

Выключить флаг — не значит удалить риск: rollout, rollback и cleanup

РелизыКейсы

После неудачного шага rollout команда выключает флаг. Новый путь перестаёт получать аудиторию, тревога стихает, change закрывают. Но проблема остаётся: через два релиза тот же код всё ещё хранит обе ветки, config содержит старый ключ, тесты знают два ответа, а developer не уверен, какой из них должен быть единственным. Риск не исчез: он просто перестал проявляться у пользователей. Любая следующая доработка может снова активировать старый путь случайной конфигурацией или оставить несовместимое состояние в коде.

Другой частый сценарий обратный. Rollout дошёл до 100 процентов, и это называют завершением. Но 100 процентов означает лишь, что правило сейчас выбирает candidate для объявленной аудитории. Это не доказательство, что fallback больше не нужен, что данные совместимы или что безопасно удалить ключ. Пока не зафиксирован final variant, не собраны разрешённые evidence и не удалены runtime branches, флаг продолжает расширять поверхность ошибки. Поэтому rollout, rollback и cleanup нужно держать как три разные операции.

Симптом → причина → проверка → действие

  1. Симптом. Флаг выключен или включён на 100 процентов, но в коде остаются две ветки и никто не может назвать дату удаления.
  2. Причина. Аудитория, fallback, наблюдение и cleanup смешаны в одной операции enable/disable.
  3. Проверка. Для каждого stage запишите cohort key, observation question, stop condition, fallback action и отдельно cleanup gate с доказательствами удаления.
  4. Действие. При stop condition сначала верните аудиторию к fallback. После выбора final variant начните новый change: удалить branches, config references, флаговые tests и документацию, сохранив обычную behaviour-проверку.

Четыре состояния вместо одной шкалы процента

Процент rollout — только один параметр delivery. Он не сообщает, применился ли новый путь к конкретной сессии, пришёл ли refresh к клиенту, завершился ли запрос и безопасно ли удалять старый код. Поэтому у шага должны быть как минимум пять полей: stage, cohort key, окно наблюдения, stop condition и действие на остановке. Числа 0, 5, 25 и 100 ниже — не рекомендованная лестница и не данные реальной команды. Это fixed synthetic объект fixture, удобный для обсуждения переходов.

Учебная лестница rollout и действия
Synthetic stageДопустимый вопросStop conditionНемедленное действиеЧто остаётся открытым
0%fallback path существует и его контракт ещё можно проверитьfallback не выполняет согласованный smoke scenarioне начинать rollout; вернуть change в designвсе candidate branches и data assumptions
5%одна стабильная synthetic cohort получает candidate согласно declared keyсогласованный synthetic signal нарушает заранее записанную границувернуть audience к 0%, сохранить change recordroot cause и решение о повторном запуске
25%candidate и fallback можно сопоставить в одной разрешённой модели наблюденияsignal не интерпретируется или config version неизвестнаостановить увеличение аудитории, не менять одновременно rule и кодкачество evidence и consistency boundary
100%вся объявленная аудитория получает selected variant в текущей config versionfinal variant ещё не утверждён либо fallback нужен для recoveryне удалять флаг; открыть final decision reviewcleanup proof и сохранность данных
Cleanup gatecode и config больше не требуют alternate branchнайдена хотя бы одна runtime reference или необходимый fallbackоставить key и вернуть cleanup в доработкуновый independent release удаления
Временная схема отделяет rollout 0–5–25–100 процентов от rollback к fallback и от cleanup gate. После 100 процентов показан отдельный review, затем удаление веток, конфигурации и флаговых tests. Проценты — fixed synthetic model, не реальные показатели.
Выключение флага ведёт к mitigation и расследованию. Cleanup начинается только после выбора итогового поведения и проверки, что alternate path действительно удалён.

Rollback сначала возвращает поведение, а не переписывает историю

Rollback нужен для короткого и понятного возврата к fallback. В момент stop condition не надо одновременно менять процент, evaluator, data migration и код. Иначе команда теряет причинную связь: неизвестно, что именно изменило наблюдение. Минимальная операция — вернуть объявленную аудиторию к нулю или к заранее выбранному safe value, оставить старый путь выполнимым и записать effective configuration version, на которой остановились. Этот action не лечит данные и не удаляет риск. Он только прекращает дальнейшее расширение candidate.

Если новый путь успел записать несовместимое состояние, rollback должен опираться на отдельный migration contract. Фича-флаг не заменяет backward compatibility. Нельзя обещать, что достаточно поставить false, если API response, database schema или side effect уже сменились. До первого rollout полезно спросить: может ли fallback прочитать состояние, созданное candidate, и какая команда владеет исправлением, если ответ нет. Если ответ не известен, это blocker дизайна, а не причина ускорить rollout маленьким процентом.

Cleanup начинается с решения о единственном варианте

Флаг можно удалять только после того, как owner зафиксировал final variant. Это кажется очевидным, но без такого шага cleanup превращается в спор «оставим на всякий случай». Сначала замораживают правило: больше не добавляют conditions, segments, variants или новые client checks. Затем выносят отдельный change с областью удаления. В нём есть source files, server branches, client branches, configuration references, tests, docs и migration notes. Удалять всё одним глобальным search-and-replace рискованно: похожий ключ может быть частью другого текста, а feature flag может иметь несколько evaluator.

Проверка cleanup состоит из негативных утверждений. Runtime code больше не должен ветвиться по ключу. Конфигурация больше не должна содержать key, targeting rule или token permission, созданные только для него. Tests больше не должны тестировать два варианта, но обязаны сохранять проверку выбранного итогового поведения. Документация не должна приглашать новую команду включить уже отсутствующий путь. Если какая-то reference нужна для reversible migration, это не «почти удалено»: флаг всё ещё имеет ownership и review.

Cleanup gate: что ищем до удаления configuration
ОбластьПроверяемый артефактПричина отказа cleanupДействие
Server codeнет evaluator call и alternate branch для ключазащищённое решение всё ещё зависит от flag resultудалить branch или разделить change на migration и cleanup
Client codeнет presentation toggle, stale cache key или exposure schema, привязанной только к флагуUI может снова трактовать старый resultсохранить только итоговый UI contract и проверить fallback assumptions
Flag configurationнет targeting rules, variants, environment overrides и токенов, нужных только для ключакод ещё ожидает result либо нужен rollbackне удалять configuration до устранения references
Testsостался test итогового поведения и удалены flag-specific forkstest matrix по-прежнему требует оба путипереписать test вокруг business contract, не вокруг boolean
Документациякарточка выпуска закрыта решением и ссылкой на cleanup changeследующая команда не понимает, почему key исчезоставить короткую decision запись без инструкции вернуть флаг

Воспроизводимая модель не притворяется production-runbook

Учебная фикстура хранит stages 0, 5, 25 и 100 как плотный массив fixed synthetic object. Она не читает dashboard, feature store, Git, CI, event broker или clock. Поэтому прогон может проверить только дисциплину модели: stage имеет rollback, cleanup gate требует удалённых веток и config, а input с URL, реальным ключом, телеметрией или production marker отвергается. Это полезнее, чем код, который выглядит как оператор и молча скрывает доступ к реальному флагу.

import {
  createFixedSyntheticFeatureFlagInput,
  inspectSyntheticFeatureFlag,
  planSyntheticFeatureFlagReview,
  runFeatureFlagsFixture,
} from './upgrade-2024-08.mjs';

const input = createFixedSyntheticFeatureFlagInput('fixed-server-decision-v1');
const report = inspectSyntheticFeatureFlag(input);
const review = planSyntheticFeatureFlagReview(report);

if (!Object.values(runFeatureFlagsFixture().assertions).every(Boolean)) {
  throw new Error('fixed synthetic fixture failed');
}

console.log({ decision: report.decision.code, actions: review.actions });

// Только fixed synthetic objects в памяти.
// Не читаются flag store, файлы, сеть, часы, CI, telemetry, user data или production.
// Exposure boundary не означает exactly-once delivery и не доказывает effect.

node web/scripts/upgrade-2024-08.mjs --verify-fixture

# PASS подтверждает только согласованность fixed synthetic records и отрицательных веток.

В case fixed-rollout-cleanup-v1 arrays stagesPercent, requiredSignals, rollback и cleanupGate намеренно фиксированы. Если заменить stages на разрежённый массив, подменить stop action или добавить output от настоящего API, plan отказывает. Если сделать decision cyclic, проверка завершается отказом, а не исключением. PASS не означает, что 5 процентов безопасны или cleanup завершён. Он означает, что учебный объект не потерял свою границу и не стал каналом управления production.

Как пройти от rollout к удалению

  1. До первого включения. Проверьте fallback contract и data compatibility. Зафиксируйте owner, cohort key, stop condition и один разрешённый source evidence.
  2. На каждом stage. Меняйте только audience или только rule в одном change. Записывайте effective config version и вопрос наблюдения; не выдавайте случайный event за эффект.
  3. При остановке. Сначала верните audience к fallback. Не удаляйте code, пока не известно, что candidate больше не нужен для diagnosis или migration.
  4. После 100%. Проведите final decision review. «Все получают candidate» не является решением удалить fallback.
  5. Откройте cleanup change. Заморозьте flag policy, перечислите references и разделите удаление кода, config, tests и документации на проверяемые шаги.
  6. Проверьте отрицательные условия. Search, unit/integration tests и config review должны показать отсутствие runtime dependence. Затем deploy cleanup как отдельное изменение с обычным rollback plan.
  7. Закройте карточку. Сохраните final variant, причину удаления, owner и ссылку на cleanup evidence. Не сохраняйте флаг «для памяти»: память хранится в decision record, не в выполняемой ветке.

Почему disabled flag не является доказательством cleanup

Immutable commit Feature Toggles Unleash от 21 августа 2024 говорит, что disabled toggle в environment evaluates false. Это ценная механическая гарантия конкретного продукта: включённая стратегия не будет случайно выбрать true, пока флаг disabled в этом environment. Но из неё не следует, что server code перестал вычислять флаг, клиент перестал знать ключ, тесты перестали держать две ветки или секреты/permissions уже удалены. Даже false evaluation остаётся dependency, если приложение продолжает спрашивать ответ.

Именно поэтому policy cleanup намеренно сильнее disablement. Сначала нужно доказать, что выбранный final behavior существует без evaluator. Потом убрать configuration, чтобы будущая случайная смена не могла оживить старый путь. И только затем закроется долг. В некоторых системах порядок config/code может отличаться из-за deployment topology; это надо явно описать в change. Универсального «сначала delete flag» нет. Есть только правило: ни один шаг не должен оставлять исполняемый code без нужного ему contract.

Ограничения и следующий проверяемый шаг

Материал не задаёт rollout percent, SLO, error budget, retention периода, способ миграции или команду для feature platform. Синтетическая шкала не описывает трафик, conversion, latency, incident или real cohort. Источники OpenFeature и Unleash задают узкие API/configuration facts, но не дают готового cleanup process вашей организации. Отдельно нужно проверить provider semantics, token scopes, cache, legal boundary контекста и совместимость данных конкретного изменения.

Следующий проверяемый шаг — выберите один отключённый флаг и проведите короткий cleanup gate из второй таблицы. Не удаляйте key сразу. Сначала найдите одну живую reference: server branch, client branch, config, test или документ. Назначьте её owner и откройте отдельный change удаления. Ожидаемый результат: флаг перестаёт быть тёмным углом репозитория, потому что есть одно решение, один список оставшихся references и путь к единственному поведению.

Проверяемые источники

  • OpenFeature Specification v0.6.0: release, 16.05.2023 — Первичный versioned release OpenFeature, опубликованный 16 мая 2023 и уже доступный к августу 2024. В выпуск вошли provider events, initialization и shutdown; это не доказательство одинаковой реализации у каждого provider или exactly-once доставки какого-либо события.
  • OpenFeature v0.6.0: Flag Evaluation API — Первичная спецификация typed evaluation: flag key, default value и optional evaluation context; при abnormal execution возвращается default value. Раздел помечен hardening и задаёт API-контракт, а не policy хранения секретов, TTL или жизненный цикл конкретного флага.
  • OpenFeature v0.6.0: Evaluation Context — Первичная спецификация context: targeting key идентифицирует subject, а global, client и invocation context имеют порядок merge. Раздел experimental; он не обещает непрерывную консистентность когорты между сервисами, браузерами или версиями конфигурации.
  • OpenFeature v0.6.0: Events — Первичная experimental-спецификация provider events: readiness, error, configuration changed и stale. Она описывает обработчики состояния provider, но не является семантикой бизнес-exposure, не определяет delivery transport и не даёт exactly-once гарантию.
  • Unleash: Feature Toggles, immutable Git commit 3417039 (21.08.2024) — Первичный неизменяемый Git-снимок Unleash от 21 августа 2024: флаг имеет type и description, а activation strategies настраиваются по environment; disabled toggle в environment evaluates false. Этот commit не утверждает, что у произвольной команды есть owner, срок удаления, метрика или единая стратегия cleanup.
  • Unleash: Front-end API access, immutable Git commit 4ae65d6 (24.05.2024) — Первичный неизменяемый Git-снимок Unleash от 24 мая 2024: отдельный front-end API, FRONTEND token, CORS boundary и refresh interval с random offset. Это конкретный механизм Unleash 4.18+, а не универсальная модель client-side evaluation или обещание моментальной смены всех клиентов.