{ "index": 113, "slug": "editorial-2024-11-mechanism-deprecation", "title": "Deprecation API: как доказать готовность к удалению", "excerpt": "Пустой график вызовов не доказывает, что API никому не нужен. Разбираем границы deprecation, Sunset и removal gate на воспроизводимом примере.", "contentHtml": "
Команда хочет удалить POST /v1/ledger/entries: за последние две недели на одном графике нет вызовов, а в access log видна только версия /v2. Такое наблюдение легко превратить в фразу «потребителей больше нет». Но график описывает не API, а выбранный инструмент, маршрут и окно времени. Редкий партнёр, batch-задача, другой gateway или закэшированный запрос могли в него не попасть.
Цена ошибки — несовместимый релиз для caller, о котором команда не знает. После удаления уже нельзя сравнить старое поведение с новым, а восстановление маршрута не исправит побочный эффект, если запрос успел записать данные. Поэтому deprecation — не команда «удалить позже», а переход с отдельными сигналами, владельцами, evidence и условием остановки.
\nНиже используется синтетический пример: имена, дата и строки таблицы придуманы для объяснения метода. Они не сообщают о реальных клиентах или production telemetry. Цель — показать, как из пустого сигнала получить проверяемое решение, а не ложное доказательство отсутствия пользователей.
\nФраза «устарел старый API» слишком широкая. В карту нужно занести method, URI-шаблон, operationId, версию контракта и replacement. Например: POST /v1/ledger/entries заменяется на POST /v2/ledger/entries. Если в одной строке смешать ещё GET, другой ресурс и несколько версий, результат нельзя будет связать с конкретным запросом.
Затем назначьте две ответственности. Владелец старой операции отвечает за совместимость и решение о её отключении. Владелец replacement отвечает за различия входных полей, кодов ошибок и побочных эффектов. У одного человека или команды может быть обе роли, но это должно быть явно записано.
\n| Тип evidence | Что он подтверждает | Чего он не подтверждает | Следующий шаг |
|---|---|---|---|
| Source usage | В доступном дереве исходников найден call path | Закрытые репозитории и deployed-версия | Назвать owner и проверить фактический релиз caller |
| Declared dependency | SDK, схема или контракт объявлены зависимостью | Что библиотека действительно вызывает старую операцию | Сверить версию и runtime call path |
| Traffic | Запросы видны в конкретном маршруте, окне и sampling policy | Редкие, кэшированные и неохваченные запросы | Записать blind zone и расширить наблюдение |
| Authorization | Credential class имеет право обратиться к resource | Что этот класс обращается к нему сейчас | Проверить активность в разрешённом auth scope |
| Unknown | Граница наблюдения не позволила классифицировать caller | Что caller отсутствует | Остановить автоматическое удаление |
Состояния zero и unknown нельзя склеивать. Zero означает ноль найденных событий внутри заранее описанного scope. Unknown означает, что scope недостаточен, недоступен или не связывает событие с caller. Второе состояние не является отрицательным результатом поиска.
В OpenAPI 3.1 у Operation Object есть поле deprecated. Значение true сообщает потребителям описания, что операцию следует перестать использовать. Это полезно для документации, генераторов клиента и review контракта. Оно не удаляет endpoint, не проверяет обновление сгенерированного SDK и не показывает фактический трафик.
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\nТакой фрагмент можно проверить в локальном файле без доступа к реальному API:
\njq -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:'\nПервая команда ожидает JSON-документ; для YAML нужен валидатор или конвертер, принятый в проекте. Во второй замените домен на тестовый endpoint. Если тестовый endpoint требует авторизацию, добавьте её безопасным способом и не помещайте секрет в shell history.
\nRFC 8594 определяет Sunset как response header, который указывает, что URI вероятно станет недоступен в заданный момент. «Вероятно» здесь существенно: это объявление жизненного цикла, а не гарантия доступности до даты, не расписание миграции и не список callers. В RFC также описан link relation sunset для документации политики или вариантов смягчения.
На дату этой статьи отдельный HTTP-сигнал deprecation ещё нельзя выдавать за устоявшийся RFC-контракт: опубликованный документ был Internet-Draft, поэтому команда может использовать только этот draft или внутреннюю policy, которую реально поддерживают её клиенты. Безопаснее считать OpenAPI-декларацию и документацию основными каналами, а response headers вводить после проверки совместимости gateway, SDK и кэшей.
\nWarning делает replacement заметным. В документации должны быть ссылка, owner, различия контрактов и канал поддержки. Warning не меняет поведение операции: клиент может его не прочитать.
\nDeprecation фиксирует, что операция больше не является предпочтительной. Это момент обновления контракта и инструкций, а не момент отключения. Если проект применяет SemVer к публичному API, спецификация рекомендует выпустить minor-версию при объявлении deprecated-функциональности.
\nSunset задаёт плановую границу возможной недоступности. Дату нужно согласовать с владельцами callers и явно связать с часовым поясом. Наличие даты не превращает unknown в zero: редкий ежемесячный вызов не становится безопасным только потому, что календарь прошёл.
\nRemoval — отдельное несовместимое изменение. Оно требует узкого change scope, human review, stop condition и понятной границы восстановления. В SemVer удаление функциональности после её deprecation обычно относится к major-изменению, но это правило действует только для проектов, которые действительно следуют SemVer и имеют объявленный public API.
\nНиже тот же endpoint с пятью типами evidence. Обозначение active значит, что вызов подтверждён в установленном scope. migrated значит, что конкретный caller переведён и его replacement проверен. unknown оставляет вопрос открытым.
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\nДля checkout-service removal блокируется сразу: активный вызов важнее пустого среднего графика. mobile-sdk не блокирует решение только после проверки, что новая операция принимает нужные поля и сохраняет требуемые ошибки. Две строки unknown нельзя заменить на zero без нового evidence с описанной границей.
Практическая форма карты должна хранить не только state, но и инструмент, период, маршрут, sampling, auth boundary, ссылку на артефакт и owner. Например: «traffic, gateway A, 2024-10-01—2024-10-31, sampled 1:100, без batch-сети». Такая запись не доказывает отсутствие callers, зато позволяет увидеть, какую именно дыру нужно закрыть.
\nДаже полный список callers не спасает несовместимую замену. Сравните обязательные поля, формат идентификатора, коды ответа, идемпотентность и порядок побочных эффектов. Для записи в ledger особенно важны повтор запроса, таймаут после записи и поведение при частичном отказе.
\nblock.Первый сценарий — «все named consumers мигрировали». Это хороший результат инвентаризации, но не доказательство полной видимости. Если партнёрский ключ всё ещё имеет право на маршрут, capability нужно либо сузить, либо проверить активность в разрешённой системе. Пока ни одно действие не выполнено, состояние остаётся unknown.
Второй сценарий — replacement отвечает быстро, но иначе обрабатывает повторный запрос. Клиент мог рассчитывать на идемпотентный ключ старой операции, а новая версия создаёт вторую запись. В этом случае статистика перехода и отсутствие вызовов старого маршрута не компенсируют разницу semantics. Сначала исправляется контракт или адаптер, затем пересматривается gate.
\nМетод не даёт универсального числа дней для наблюдения. Окно зависит от частоты вызовов, сезонности, SLA и допустимого риска. Для ежемесячного batch две недели явно не покрывают полный цикл. Sampling, кэш, offline-клиенты, задержка логов и несколько gateway могут скрывать обращения.
\nМетод также не разрешает искать данные там, где у команды нет права доступа. Если нужный auth или traffic source недоступен, это ограничение нужно записать как blind zone, а не замаскировать уверенным нулём. Синтетический пример выше не делает сетевых запросов и не описывает реальный релиз; его можно воспроизвести только как шаблон карты и проверки собственного API.
\nRemoval можно выносить на отдельное рассмотрение, когда у операции есть названный replacement и owners, опубликованы warning и deprecation, карта callers содержит тип evidence и его границы, а replacement проверен на критичных входах и ошибках. Active и unknown строки должны быть закрыты разрешённым наблюдением или явно принятым residual risk. Должны быть определены stop condition и граница восстановления.
\nЕсли хотя бы одно из этих условий неизвестно, честный результат — block. Это не означает, что endpoint нужно поддерживать навсегда. Это означает, что следующий шаг должен уменьшить конкретную blind zone: найти batch inventory, связать credential с caller, сравнить контракт или назначить владельца.
Sunset и link relation sunset. Документ информационный: он не задаёт ваш процесс миграции и не перечисляет потребителей.deprecated и его значение для потребителей описания операции.