{ "index": 114, "slug": "editorial-2024-11-practice-deprecation", "title": "Депрекация API: как не удалить контракт вместе с неизвестным потребителем", "excerpt": "Дата Sunset не доказывает, что API больше никто не вызывает. Разбираем consumer map, границы доказательств и removal gate, который останавливает опасное удаление.", "contentHtml": "
В задаче стоит дата: после 3 февраля путь /v1/posting удалят. Наступает день релиза. В графике вызовов пусто, в OpenAPI уже стоит deprecated: true, а команда не видит владельца старого клиента. Кто-то открывает pull request и удаляет обработчик. Через час внешний интегратор получает 404 или 410. У команды нет ответа на три вопроса: кому сообщили, какой контракт предложили взамен и что именно доказало безопасность удаления.
Это не редкий сбой календаря. Дата задаёт границу планирования, но не подтверждает отсутствие потребителей. Пустой график описывает только выбранный инструмент, маршрут, период и набор сигналов. Пометка в схеме сообщает о жизненном цикле операции, но не мигрирует SDK. Реальная депрекация должна разделять объявление, миграцию и удаление.
\nТезис: не удаляйте устаревший API по дате или одному зелёному индикатору. Сначала ограничьте одну операцию, составьте consumer map и укажите границу каждого доказательства. Если остался активный или неизвестный потребитель, автоматическое удаление запрещено. Для полностью подготовленного случая открывают отдельный human review, а не превращают ревью депрекации в незаметный production change.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Есть дата удаления, но нет списка владельцев | Дедлайн приняли за доказательство последнего consumer | Проверить contract, scope, owner и evidence boundary | Заблокировать removal и открыть consumer map |
| В telemetry нет запросов | Пустой сигнал ограничен периодом, sampling, proxy или credential boundary | Записать инструмент, окно, маршрут и слепые зоны | Назвать результат not observed in this scope, а не «никто не использует» |
В OpenAPI стоит deprecated: true | Декларацию смешали с миграцией | Найти replacement, владельца и путь перехода | Опубликовать notice и migration guide |
| Все известные клиенты мигрировали | Named rows приняли за полную population | Проверить unknown scope и остаточный риск | Разрешить только отдельный human review |
Фраза «удаляем v1» слишком широкая. Она может означать один HTTP route, несколько методов, SDK-функцию, callback или весь набор ресурсов. Выберите одну operation. Запишите HTTP method, URI template, operationId, request и response shape, authentication boundary и replacement. Если новый путь меняет семантику, сравните не только URL. Проверьте idempotency key, коды ошибок, pagination, ретраи и правила авторизации.
\nТакой scope снижает риск ложного согласия. Владелец сервиса может подтвердить удаление одного метода, но не всего API. Владелец SDK может выпустить новую функцию, но не контролировать старые бинарные клиенты. Внешний потребитель может получать notice через документацию, но не читать вашу схему. Контракт, владелец и канал объявления должны совпадать по scope.
\nOpenAPI помогает объявить операцию устаревшей. Поле deprecated отвечает на вопрос «рекомендуется ли эта операция дальше?». Оно не отвечает на вопросы «кто вызывает её сейчас?» и «может ли replacement принять тот же сценарий?». Поэтому schema и consumer map — разные артефакты. Один сообщает о намерении. Второй связывает намерение с людьми, зависимостями и проверками.
Минимальная строка карты содержит consumer, класс сведения, contract, owner, migration path, deadline, announcement и evidence boundary. Не скрывайте неизвестность. Строка unknown consumer честнее, чем пустая таблица. Она означает, что область ещё не замкнута и автоматический removal нельзя считать безопасным.
| Потребитель | Сведение | Владелец и переход | Граница доказательства |
|---|---|---|---|
synthetic-web-checkout | source usage | synthetic-checkout-owner; fixed v1 call → fixed v2 contract | Учебная строка, не результат поиска кода |
synthetic-sdk-package | declared dependency | synthetic-sdk-owner; выпустить v2 SDK surface | Учебная запись зависимости, не package inventory |
synthetic-unknown-integrator | unknown consumer | synthetic-api-owner; сохранить notice и ограничить scope | Не customer list и не доказательство отсутствия вызовов |
Разные классы evidence нельзя складывать в один count. Source usage показывает вызов в разрешённой области исходного кода. Declared dependency показывает объявленную связь пакета, схемы или SDK. Observed traffic показывает запросы в конкретном маршруте и окне. Authorization показывает, какой credential class имеет право обратиться. Ни один класс сам по себе не доказывает полную population клиентов.
\nНапример, пустой access report может не видеть запросы через gateway, другой hostname, старый credential или редкий batch. Кодовый поиск может не найти вызов, спрятанный в сгенерированном клиенте или внешнем binary. Manifest может хранить уже неиспользуемую зависимость. Поэтому рядом с каждой строкой пишите не только результат, но и то, чего он не доказывает.
\nУведомление должно дать потребителю понятный replacement, владельца и ссылку на инструкцию. HTTP-заголовок Deprecation может сообщить, что ресурс устарел или станет устаревшим. Sunset сообщает ожидаемую будущую недоступность ресурса. Оба сигнала улучшают обнаруживаемость решения. Они не заставляют клиент мигрировать и не подтверждают, что клиент получил, понял или применил notice.
\nСохраните в записи lifecycle отдельные поля. Дата депрекации описывает статус контракта. Sunset boundary описывает ожидаемое изменение доступности. Removal gate описывает условия допуска к отдельному изменению. Не называйте Sunset жёсткой гарантией: RFC 8594 формулирует его как указание на ожидаемую недоступность, а не как доказательство фактического поведения каждого клиента.
\n{\n \"contract\": \"synthetic-ledger-v1-posting-path\",\n \"owner\": \"synthetic-ledger-owner\",\n \"replacement\": \"synthetic-ledger-v2-posting-path\",\n \"deprecationDate\": \"synthetic-2024-11-04\",\n \"sunsetBoundary\": \"synthetic-2025-02-03\",\n \"announcement\": \"synthetic-public-deprecation-page\",\n \"unknownConsumerRule\": \"unknown blocks automatic removal\",\n \"restoreBoundary\": \"stop proposal before a real removal change\"\n}\nКод выше — учебный объект. Он не является production-конфигурацией, не содержит реальную дату, токен, route table или список клиентов. Его задача — показать обязательные связи. Если у replacement нет владельца или у unknown нет границы проверки, объект не готов к removal review.
\nХороший gate формулируют как список причин не удалять. Есть active consumer — остановиться. Есть unknown scope без принятого остаточного риска — остановиться. Replacement не сохраняет важную семантику — остановиться. Notice не связан с affected scope — остановиться. Restore boundary не описана — остановиться. Ранняя дата не компенсирует ни один из этих пробелов.
\nСлово «мигрировал» тоже требует проверки. Оно должно означать, что конкретный consumer получил replacement, проверил совместимость и больше не зависит от старой операции в согласованной границе. Если строка лишь помечена как migrated в таблице, это статус документа, а не runtime evidence. Оставьте исходный тип сведения и ссылку на проверку.
\nЕсли перед удалением найден active consumer, не меняйте одновременно route, authorization, документацию и fallback. Остановите proposal. Зафиксируйте конфликт, сохраните текущую compatibility boundary и назначьте владельца миграции. Если неизвестный потребитель остаётся, владелец должен выбрать одно из трёх действий: сузить разрешённую проверку, продлить совместимость или принять residual risk отдельным решением.
\nЭта схема не делает неизвестных потребителей видимыми автоматически. Она не заменяет разрешённый source search, анализ access logs, inventory клиентов, review авторизации или проверку replacement в реальной среде. Учебные строки в статье не доказывают наличие или отсутствие клиентов. Они показывают, как сохранить класс риска в модели.
\nНе пытайтесь получить «нулевой риск» из одного сигнала. Даже полный на вид отчёт имеет scope: конкретный период, route, credential, sampling и доступность данных. Отрицательный путь должен быть первым классом результата. Если проверка не может замкнуть область, ответ — unknown, а действие — сохранить совместимость или провести отдельное решение с владельцем остаточного риска.
Restore boundary также ограничивает обещание. Для draft достаточно сказать: proposal остановлен, старый контракт не изменён, черновик удалён. После реального удаления нужен другой план: кто возвращает route, какие schema и credentials ещё совместимы и как проверяется восстановление. Фраза «rollback available» без этих условий не является проверяемым планом.
\nДепрекация готова к отдельному removal review, когда одна operation однозначно названа; replacement имеет владельца и описанную compatibility boundary; каждая известная строка consumer map содержит migration path; unknown scope либо ограничен разрешённой проверкой, либо явно принят владельцем; notice и дедлайн доступны; stop condition и restore boundary записаны. Итоговый вердикт должен быть одним из трёх: block: active, block: unknown или allow human review only. Ни один из них не означает, что endpoint уже можно удалить.
Проверяемый результат — не пустой график и не дата в календаре. Это воспроизводимая запись, в которой другой инженер видит scope, доказательство, его границу, владельца и причину следующего действия. Если он не может повторить проверку или назвать условие остановки, контракт ещё не готов к удалению.
\ndeprecated объявляет операцию устаревшей, но не описывает её потребителей.