8 lines
21 KiB
JSON
8 lines
21 KiB
JSON
{
|
||
"index": 113,
|
||
"slug": "editorial-2024-11-mechanism-deprecation",
|
||
"title": "Deprecation API: как доказать готовность к удалению",
|
||
"excerpt": "Пустой график вызовов не доказывает, что API никому не нужен. Разбираем границы deprecation, Sunset и removal gate на воспроизводимом примере.",
|
||
"contentHtml": "<p>Команда хочет удалить <code>POST /v1/ledger/entries</code>: за последние две недели на одном графике нет вызовов, а в access log видна только версия <code>/v2</code>. Такое наблюдение легко превратить в фразу «потребителей больше нет». Но график описывает не API, а выбранный инструмент, маршрут и окно времени. Редкий партнёр, batch-задача, другой gateway или закэшированный запрос могли в него не попасть.</p>\n<p>Цена ошибки — несовместимый релиз для caller, о котором команда не знает. После удаления уже нельзя сравнить старое поведение с новым, а восстановление маршрута не исправит побочный эффект, если запрос успел записать данные. Поэтому deprecation — не команда «удалить позже», а переход с отдельными сигналами, владельцами, evidence и условием остановки.</p>\n<p>Ниже используется синтетический пример: имена, дата и строки таблицы придуманы для объяснения метода. Они не сообщают о реальных клиентах или production telemetry. Цель — показать, как из пустого сигнала получить проверяемое решение, а не ложное доказательство отсутствия пользователей.</p>\n<h2>Сначала зафиксируйте одну операцию</h2>\n<p>Фраза «устарел старый API» слишком широкая. В карту нужно занести method, URI-шаблон, <code>operationId</code>, версию контракта и replacement. Например: <code>POST /v1/ledger/entries</code> заменяется на <code>POST /v2/ledger/entries</code>. Если в одной строке смешать ещё <code>GET</code>, другой ресурс и несколько версий, результат нельзя будет связать с конкретным запросом.</p>\n<p>Затем назначьте две ответственности. Владелец старой операции отвечает за совместимость и решение о её отключении. Владелец replacement отвечает за различия входных полей, кодов ошибок и побочных эффектов. У одного человека или команды может быть обе роли, но это должно быть явно записано.</p>\n<table><caption>Как читать сигнал о потребителе</caption><thead><tr><th scope='col'>Тип evidence</th><th scope='col'>Что он подтверждает</th><th scope='col'>Чего он не подтверждает</th><th scope='col'>Следующий шаг</th></tr></thead><tbody><tr><td>Source usage</td><td>В доступном дереве исходников найден call path</td><td>Закрытые репозитории и deployed-версия</td><td>Назвать owner и проверить фактический релиз caller</td></tr><tr><td>Declared dependency</td><td>SDK, схема или контракт объявлены зависимостью</td><td>Что библиотека действительно вызывает старую операцию</td><td>Сверить версию и runtime call path</td></tr><tr><td>Traffic</td><td>Запросы видны в конкретном маршруте, окне и sampling policy</td><td>Редкие, кэшированные и неохваченные запросы</td><td>Записать blind zone и расширить наблюдение</td></tr><tr><td>Authorization</td><td>Credential class имеет право обратиться к resource</td><td>Что этот класс обращается к нему сейчас</td><td>Проверить активность в разрешённом auth scope</td></tr><tr><td>Unknown</td><td>Граница наблюдения не позволила классифицировать caller</td><td>Что caller отсутствует</td><td>Остановить автоматическое удаление</td></tr></tbody></table>\n<p>Состояния <code>zero</code> и <code>unknown</code> нельзя склеивать. <code>Zero</code> означает ноль найденных событий внутри заранее описанного scope. <code>Unknown</code> означает, что scope недостаточен, недоступен или не связывает событие с caller. Второе состояние не является отрицательным результатом поиска.</p>\n<h2>Что объявляют OpenAPI и HTTP</h2>\n<p>В OpenAPI 3.1 у Operation Object есть поле <code>deprecated</code>. Значение <code>true</code> сообщает потребителям описания, что операцию следует перестать использовать. Это полезно для документации, генераторов клиента и review контракта. Оно не удаляет endpoint, не проверяет обновление сгенерированного SDK и не показывает фактический трафик.</p>\n<pre><code>openapi: 3.1.0\ninfo:\n title: Ledger API\n version: 2.4.0\npaths:\n /v1/ledger/entries:\n post:\n operationId: createLedgerEntryV1\n deprecated: true\n description: Use POST /v2/ledger/entries. The v1 contract remains available until the removal review.\n responses:\n '201':\n description: Entry created\n /v2/ledger/entries:\n post:\n operationId: createLedgerEntryV2\n responses:\n '201':\n description: Entry created</code></pre>\n<p>Такой фрагмент можно проверить в локальном файле без доступа к реальному API:</p>\n<pre><code>jq -e '.paths[\"/v1/ledger/entries\"].post.deprecated == true' openapi.json\n# Для YAML сначала преобразуйте файл вашим валидатором OpenAPI.\ncurl -fsSI https://api.example.test/v1/ledger/entries | grep -i '^Sunset:'</code></pre>\n<p>Первая команда ожидает JSON-документ; для YAML нужен валидатор или конвертер, принятый в проекте. Во второй замените домен на тестовый endpoint. Если тестовый endpoint требует авторизацию, добавьте её безопасным способом и не помещайте секрет в shell history.</p>\n<p>RFC 8594 определяет <code>Sunset</code> как response header, который указывает, что URI вероятно станет недоступен в заданный момент. «Вероятно» здесь существенно: это объявление жизненного цикла, а не гарантия доступности до даты, не расписание миграции и не список callers. В RFC также описан link relation <code>sunset</code> для документации политики или вариантов смягчения.</p>\n<p>На дату этой статьи отдельный HTTP-сигнал deprecation ещё нельзя выдавать за устоявшийся RFC-контракт: опубликованный документ был Internet-Draft, поэтому команда может использовать только этот draft или внутреннюю policy, которую реально поддерживают её клиенты. Безопаснее считать OpenAPI-декларацию и документацию основными каналами, а response headers вводить после проверки совместимости gateway, SDK и кэшей.</p>\n<h2>Четыре границы перехода</h2>\n<p><strong>Warning</strong> делает replacement заметным. В документации должны быть ссылка, owner, различия контрактов и канал поддержки. Warning не меняет поведение операции: клиент может его не прочитать.</p>\n<p><strong>Deprecation</strong> фиксирует, что операция больше не является предпочтительной. Это момент обновления контракта и инструкций, а не момент отключения. Если проект применяет SemVer к публичному API, спецификация рекомендует выпустить minor-версию при объявлении deprecated-функциональности.</p>\n<p><strong>Sunset</strong> задаёт плановую границу возможной недоступности. Дату нужно согласовать с владельцами callers и явно связать с часовым поясом. Наличие даты не превращает unknown в zero: редкий ежемесячный вызов не становится безопасным только потому, что календарь прошёл.</p>\n<p><strong>Removal</strong> — отдельное несовместимое изменение. Оно требует узкого change scope, human review, stop condition и понятной границы восстановления. В SemVer удаление функциональности после её deprecation обычно относится к major-изменению, но это правило действует только для проектов, которые действительно следуют SemVer и имеют объявленный public API.</p>\n<figure><img src='/assets/editorial/2024/deprecation-2024-timeline.svg' alt='Последовательность жизненного цикла API: warning и replacement, deprecated в контракте, Sunset как плановая граница и отдельный removal gate' loading='lazy' /><figcaption>Сигналы образуют порядок решений, но не доказывают переход сами по себе. Временная шкала показывает lifecycle, а не реальную дату или состояние конкретного сервиса.</figcaption></figure>\n<h2>Синтетический removal gate</h2>\n<p>Ниже тот же endpoint с пятью типами evidence. Обозначение <code>active</code> значит, что вызов подтверждён в установленном scope. <code>migrated</code> значит, что конкретный caller переведён и его replacement проверен. <code>unknown</code> оставляет вопрос открытым.</p>\n<pre><code>operation: POST /v1/ledger/entries\nreplacement: POST /v2/ledger/entries\nowner: ledger-team\nsunset: 2025-02-03T00:00:00Z\n\nconsumer evidence state\ncheckout-service source + traffic active\nmobile-sdk declared dependency migrated\npartner-gateway authorization unknown\nnightly-export batch inventory unknown\n\nremoval: block\nstop_condition: any active or unknown row\nreason: replacement and consumer scope are not closed</code></pre>\n<p>Для <code>checkout-service</code> removal блокируется сразу: активный вызов важнее пустого среднего графика. <code>mobile-sdk</code> не блокирует решение только после проверки, что новая операция принимает нужные поля и сохраняет требуемые ошибки. Две строки <code>unknown</code> нельзя заменить на <code>zero</code> без нового evidence с описанной границей.</p>\n<p>Практическая форма карты должна хранить не только state, но и инструмент, период, маршрут, sampling, auth boundary, ссылку на артефакт и owner. Например: «traffic, gateway A, 2024-10-01—2024-10-31, sampled 1:100, без batch-сети». Такая запись не доказывает отсутствие callers, зато позволяет увидеть, какую именно дыру нужно закрыть.</p>\n<h2>Проверка replacement, а не только списка клиентов</h2>\n<p>Даже полный список callers не спасает несовместимую замену. Сравните обязательные поля, формат идентификатора, коды ответа, идемпотентность и порядок побочных эффектов. Для записи в ledger особенно важны повтор запроса, таймаут после записи и поведение при частичном отказе.</p>\n<ol><li>Запишите одну operation, её replacement, owners и ответственную за остановку change команду.</li><li>Добавьте deprecation в OpenAPI и migration guide; рядом перечислите различия входов, ответов и ошибок.</li><li>Соберите source usage, declared dependency, traffic, authorization и batch inventory отдельно. Для каждого результата укажите scope, период и blind zone.</li><li>Выполните одинаковый набор позитивных и отрицательных запросов против старой и новой операции в тестовом окружении. Сравните статус, тело ответа и побочные эффекты.</li><li>Назначьте Sunset только после согласования окна миграции и способа поддержки. Дата должна вести к review, а не автоматически запускать удаление.</li><li>Перед removal пересмотрите каждую строку карты. Active, unknown или несовместимый replacement переводят решение в <code>block</code>.</li><li>Если gate закрыт, оформите отдельный change с планом наблюдения после релиза и проверенной границей восстановления. Не удаляйте endpoint в том же изменении, которое впервые объявляет deprecation.</li></ol>\n<h2>Два отрицательных сценария</h2>\n<p>Первый сценарий — «все named consumers мигрировали». Это хороший результат инвентаризации, но не доказательство полной видимости. Если партнёрский ключ всё ещё имеет право на маршрут, capability нужно либо сузить, либо проверить активность в разрешённой системе. Пока ни одно действие не выполнено, состояние остаётся <code>unknown</code>.</p>\n<p>Второй сценарий — replacement отвечает быстро, но иначе обрабатывает повторный запрос. Клиент мог рассчитывать на идемпотентный ключ старой операции, а новая версия создаёт вторую запись. В этом случае статистика перехода и отсутствие вызовов старого маршрута не компенсируют разницу semantics. Сначала исправляется контракт или адаптер, затем пересматривается gate.</p>\n<h2>Ограничения применимости</h2>\n<p>Метод не даёт универсального числа дней для наблюдения. Окно зависит от частоты вызовов, сезонности, SLA и допустимого риска. Для ежемесячного batch две недели явно не покрывают полный цикл. Sampling, кэш, offline-клиенты, задержка логов и несколько gateway могут скрывать обращения.</p>\n<p>Метод также не разрешает искать данные там, где у команды нет права доступа. Если нужный auth или traffic source недоступен, это ограничение нужно записать как blind zone, а не замаскировать уверенным нулём. Синтетический пример выше не делает сетевых запросов и не описывает реальный релиз; его можно воспроизвести только как шаблон карты и проверки собственного API.</p>\n<h2>Критерий готовности</h2>\n<p>Removal можно выносить на отдельное рассмотрение, когда у операции есть названный replacement и owners, опубликованы warning и deprecation, карта callers содержит тип evidence и его границы, а replacement проверен на критичных входах и ошибках. Active и unknown строки должны быть закрыты разрешённым наблюдением или явно принятым residual risk. Должны быть определены stop condition и граница восстановления.</p>\n<p>Если хотя бы одно из этих условий неизвестно, честный результат — <code>block</code>. Это не означает, что endpoint нужно поддерживать навсегда. Это означает, что следующий шаг должен уменьшить конкретную blind zone: найти batch inventory, связать credential с caller, сравнить контракт или назначить владельца.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href='https://datatracker.ietf.org/doc/html/rfc8594' target='_blank' rel='noopener noreferrer'>IETF RFC 8594: The Sunset HTTP Header Field</a> — смысл заголовка <code>Sunset</code> и link relation <code>sunset</code>. Документ информационный: он не задаёт ваш процесс миграции и не перечисляет потребителей.</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://semver.org/spec/v2.0.0.html' target='_blank' rel='noopener noreferrer'>Semantic Versioning 2.0.0</a> — правила minor-версии для deprecation и major-версии для несовместимого изменения; применимы только при принятой в проекте SemVer-модели.</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> — историческое состояние HTTP-сигнала deprecation на сентябрь 2024 года; это Internet-Draft, а не RFC, и он не является списком callers.</li></ul>"
|
||
}
|