Files
progcode/editorial/agent-rewrites/113.json
T

8 lines
21 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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>"
}