8 lines
16 KiB
JSON
8 lines
16 KiB
JSON
{
|
||
"index": 113,
|
||
"slug": "editorial-2024-11-mechanism-deprecation",
|
||
"title": "Deprecation API: почему пустая telemetry не разрешает удаление",
|
||
"excerpt": "Пустой график вызовов не доказывает отсутствие потребителей API. Разбираем пять типов evidence, границы deprecation и removal gate на ограниченном примере.",
|
||
"contentHtml": "<p>Команда собирается удалить старый endpoint. За последние две недели в dashboard нет вызовов. В access log видны только запросы к новой версии. Кто-то делает вывод: потребителей больше нет.</p>\n<p>Симптом выглядит убедительно, но он описывает только выбранный источник наблюдения. В него могли не попасть редкие клиенты, другой gateway, кэш, batch-задача или credential с отдельной политикой доступа. Цена ошибки — несовместимый релиз для неизвестного caller. Ошибка проявится после удаления, когда старый контракт уже не с чем сравнивать.</p>\n<p>Тезис статьи простой: deprecation — это управляемый переход, а не доказательство отсутствия пользователей. Сначала нужно разделить типы сведений и их границы. Затем объявить замену, предупредить потребителей, определить sunset boundary и только после отдельной проверки открыть removal gate. Ни один сигнал сам по себе не превращает пустую telemetry в полный список клиентов.</p>\n<h2>Что именно нужно доказать</h2>\n<p>Начните с одной операции: method, URI template, operationId и версия контракта. Формулировка «удаляем старый API» слишком широка. Для <code>POST /v1/ledger/entries</code> вопрос звучит точнее: какие callers ещё зависят от поведения этой операции, кто отвечает за замену и какие наблюдения покрывают маршрут?</p>\n<p>Source usage показывает вызов в заданном дереве исходников. Он помогает найти известный сервис, но не видит закрытый репозиторий, скомпилированный клиент или deployed версию, которая не совпала с локальной веткой. Declared dependency показывает объявленный SDK, schema или contract. Зависимость может остаться в manifest после миграции и не доказывает runtime-вызов.</p>\n<p>Exposure or traffic показывает observed requests в конкретном инструменте, маршруте, периоде и sampling policy. Запрос не равен пользователю. Один сервис может отправить тысячу запросов, а редкий клиент — один запрос за месяц. Нулевое значение означает «в этом scope сигнал не найден», а не «caller отсутствует».</p>\n<p>Authorization показывает, какой credential class или permission способен обратиться к resource. Способность не равна активности. Если политика допускает партнёрский ключ, это ещё не доказывает, что партнёр вызывает операцию. Но если такой класс не учтён, removal имеет слепую зону.</p>\n<p>Unknown consumer — не ошибка заполнения таблицы. Это честное состояние, когда scope нельзя замкнуть. Его нужно хранить отдельно от zero и migrated. Именно unknown должен блокировать автоматическое решение об удалении.</p>\n<table><caption>Симптом → причина → проверка → действие</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>График вызовов равен нулю</td><td>Окно или route не покрывает всех callers</td><td>Записать инструмент, период, sampling, cache и auth boundary</td><td>Оставить consumer unknown и расширить разрешённую проверку</td></tr><tr><td>В manifest осталась зависимость</td><td>Declared dependency приняли за runtime usage</td><td>Сверить версию, owner и фактический call path</td><td>Назначить migration owner, не удалять endpoint по одной записи</td></tr><tr><td>В OpenAPI стоит deprecated</td><td>Декларацию приняли за завершённую миграцию</td><td>Найти replacement и проверить его контракт</td><td>Опубликовать warning и сохранить совместимое поведение</td></tr><tr><td>Наступила дата Sunset</td><td>Дату приняли за доказательство, что все ушли</td><td>Провести removal review по отдельному scope</td><td>Остановить change при active или unknown row</td></tr></tbody></table>\n<h2>Четыре границы lifecycle</h2>\n<p><strong>Warning</strong> делает замену видимой. Документация должна назвать replacement, owner, область действия и способ задать вопрос. Warning не подтверждает, что клиент его прочитал. Поэтому он не меняет функциональное поведение endpoint.</p>\n<p><strong>Deprecation</strong> сообщает, что операция больше не является предпочтительной. В OpenAPI это поле <code>deprecated: true</code>. В HTTP можно использовать Deprecation header и ссылку на документацию, если такой сигнал поддерживают ваши клиенты. Эти механизмы помогают обнаружить новые зависимости, но не выключают ресурс и не перечисляют callers.</p>\n<p><strong>Sunset</strong> задаёт планируемую границу возможной недоступности. RFC 8594 описывает её как сигнал о том, что конкретный URI, вероятно, станет недоступен в указанное время. Это hint, а не доказательство миграции и не гарантия, что сервер исчезнет ровно в timestamp. До этой границы всё равно нужна проверка остаточного риска.</p>\n<p><strong>Removal</strong> — отдельное несовместимое изменение. Оно должно иметь узкий scope, owner, stop condition и restore boundary. Если одна строка consumer map имеет состояние <code>active</code> или <code>unknown</code>, автоматическое удаление нельзя считать безопасным. Состояние <code>migrated</code> разрешает только review: нужно проверить замену, данные и поведение, а не просто закрыть старый маршрут.</p>\n<figure><img src=\"/assets/editorial/2024/deprecation-2024-timeline.svg\" alt=\"Временная шкала deprecation: warning, deprecated, sunset и removal gate с replacement, owner, evidence scope и stop condition\" loading=\"lazy\" /><figcaption>Сигналы идут последовательно, но не заменяют друг друга. Иллюстрация показывает порядок решений; она не содержит telemetry и не задаёт реальную календарную дату.</figcaption></figure>\n<h2>Учебный пример: одна операция и три состояния</h2>\n<p>Ниже приведён синтетический пример. Имена, даты и строки не получены из production, telemetry или списка клиентов. Они нужны, чтобы показать логику решения на одном endpoint.</p>\n<pre><code>operation: POST /v1/ledger/entries\nreplacement: POST /v2/ledger/entries\nowner: ledger-team\nsunset: 2025-02-03\n\nconsumer evidence state\ncheckout-service source usage active\nmobile-sdk declared dependency migrated\npartner-gateway authorization unknown\n\nremoval: block\nreason: active and unknown consumers remain</code></pre>\n<p>Первая строка содержит наблюдаемый вызов. Она блокирует removal. Вторая подтверждает план перехода, но сама по себе не доказывает, что старый endpoint больше не вызывается. Третья показывает capability boundary: gateway может иметь доступ, однако его активность не установлена. Вердикт должен остаться <code>block</code>.</p>\n<p>Если заменить значение <code>unknown</code> на <code>zero</code>, факты не изменятся. Изменится только видимость риска. Поэтому consumer map должна хранить не только state, но и evidence scope: tool, period, route, sampling, auth boundary, owner и blind zone. Без этих полей строка не воспроизводится.</p>\n<h2>Порядок действий</h2>\n<ol><li>Зафиксируйте одну операцию и точный replacement. Не объединяйте в одну карту разные URI, версии и semantics.</li><li>Назначьте owner старого контракта и owner замены. Запишите, кто может остановить change.</li><li>Добавьте warning в документацию и migration guide. Опишите совместимость, различия ответов и путь поддержки.</li><li>Объявите deprecation в OpenAPI или согласованным HTTP-сигналом. Не меняйте поведение endpoint только из-за notice.</li><li>Соберите evidence по отдельным типам: source usage, declared dependency, exposure or traffic, authorization и unknown. Для каждой записи укажите scope и слепую зону.</li><li>Проверьте replacement на тех же входах и ошибках, которые важны для старого контракта. «Клиент обновился» недостаточно без проверки поведения.</li><li>Назначьте sunset boundary как плановую дату и заранее определите stop condition. Active row, unknown row или несовместимый replacement должны останавливать удаление.</li><li>Проведите human review removal. Решение должно содержать residual risk, restore boundary и ссылку на evidence. Только затем выполняйте отдельный change.</li></ol>\n<h2>Отрицательный путь</h2>\n<p>Иногда все named consumers мигрировали, а неизвестный класс доступа остался. Это не повод объявить карту полной. Сохраните старый контракт совместимым, сузьте следующую проверку до разрешённой auth boundary и назначьте срок пересмотра. Если нужное наблюдение нельзя получить законно или технически, риск остаётся unknown. Дата Sunset не превращает его в zero.</p>\n<p>Другой отрицательный путь — replacement меняет semantics: новые обязательные поля, другой порядок побочных эффектов или иные коды ошибок. Даже полный список callers не делает такое удаление безопасным. Сначала нужно сравнить контракты и решить, как клиент переживёт несовместимость. Если restore невозможен после записи новых данных, rollback старого route не восстановит прежнее состояние.</p>\n<h2>Ограничения</h2>\n<p>Эта схема не создаёт универсальный consumer map. Она не отменяет sampling, кэширование, batch-вызовы, закрытые сети, задержку доставки логов и различия между deployed и исходным кодом. Она также не говорит, сколько дней нужно наблюдать. Окно зависит от частоты вызовов и допустимого риска, а его границы нужно обосновать.</p>\n<p>Учебный код выше не вызывает сеть, не читает файлы и не утверждает production-результаты. Официальные спецификации описывают смысл сигналов, но не проверяют ваш API. Реальную готовность устанавливает только evidence с понятным scope и ответственным владельцем.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Removal готов к отдельному рассмотрению, если выполнены все условия: одна операция имеет названный replacement; у старого и нового контрактов есть owners; warning и deprecation опубликованы; каждая строка consumer map содержит тип evidence, scope, период и blind zone; active и unknown строки либо закрыты разрешённой проверкой, либо явно приняты владельцем как residual risk; replacement проверен на критичных входах; определены stop condition и restore boundary. Если хотя бы одно условие неизвестно, вердикт — <code>block</code>, а не «пользователей нет».</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://datatracker.ietf.org/doc/rfc8594/\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 8594: The Sunset HTTP Header Field</a> — назначение Sunset и его граница как lifecycle hint.</li><li><a href=\"https://spec.openapis.org/oas/v3.1.0.html\" target=\"_blank\" rel=\"noopener noreferrer\">OpenAPI Specification 3.1.0, Operation Object</a> — поле <code>deprecated</code> в описании операции.</li><li><a href=\"https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-deprecation-header-09\" target=\"_blank\" rel=\"noopener noreferrer\">IETF draft-ietf-httpapi-deprecation-header-09</a> — историческая для ноября 2024 спецификация HTTP-сигнала deprecation; это Internet-Draft, а не RFC.</li></ul>"
|
||
}
|