8 lines
20 KiB
JSON
8 lines
20 KiB
JSON
{
|
||
"index": 112,
|
||
"slug": "editorial-2024-11-field-deprecation",
|
||
"title": "Как удалить устаревший API и не сломать последнего клиента",
|
||
"excerpt": "Депрекация не доказывает, что endpoint больше не используют. Разбираем removal gate: как отделить активного клиента от неизвестной зоны, выбрать проверку, остановить удаление и определить готовность изменения.",
|
||
"contentHtml": "<p>Команда пометила <code>GET /v1/orders/{id}</code> устаревшим, выпустила замену и увидела ноль вызовов в выбранном графике. Удаление кажется безопасным, пока внешний интегратор не получает 404 или 410. Такой сбой начинается не с плохого HTTP-кода, а с неверного вывода: отсутствие записей в одном источнике приняли за отсутствие всех потребителей.</p>\n<p>Удаление API нужно рассматривать как доказательство с ограниченной областью, а не как дату в календаре. Сначала зафиксируйте одну операцию, затем свяжите известные вызовы с владельцами, явно запишите слепые зоны и только после этого откройте отдельный removal review. Если остался активный или неизвестный потребитель, автоматическое удаление должно остановиться.</p>\n<h2>Депрекация не равна удалению</h2>\n<p>В OpenAPI 3.1.0 поле <code>deprecated: true</code> объявляет операцию устаревшей и рекомендует потребителям перейти с неё. Это часть описания контракта. Поле не читает access log, не знает о старых версиях SDK и не подтверждает, что replacement поддерживает тот же сценарий. Если генератор документации не публикует это поле или использует другую версию OpenAPI, поведение нужно проверить в конкретном toolchain.</p>\n<p>Заголовок <code>Sunset</code> решает другую задачу. RFC 8594 описывает его как сигнал о том, что URI, вероятно, станет недоступен в указанный момент. Документ прямо называет timestamp подсказкой: после этой даты возможны ошибки, редирект или полная недоступность, но точное поведение не обещано. Поэтому <code>Sunset</code> помогает потребителю спланировать миграцию, но не заменяет inventory и проверку владельца.</p>\n<p>После удаления ответы тоже нужно трактовать аккуратно. RFC 9110 описывает 404 как отсутствие текущего представления или нежелание раскрывать его существование. 410 означает, что доступ больше недоступен и состояние, вероятно, постоянно. Если сервер не знает, что удаление окончательно, RFC рекомендует 404. Ни один статус сам по себе не доказывает, что все клиенты перешли на новый маршрут.</p>\n<h2>Сначала сузьте предмет проверки</h2>\n<p>Фраза «удаляем v1» слишком широкая для решения. В одной версии могут жить разные методы, форматы ошибок и права доступа. Для removal gate запишите method, URI template, <code>operationId</code>, версию схемы, replacement, владельца и дату, после которой разрешён отдельный change. Если меняется только поле ответа, проверяйте поле, а не весь маршрут.</p>\n<p>Сравните не только URL. Для каждого обязательного сценария зафиксируйте authentication boundary, коды ошибок, pagination, идемпотентность, лимиты и правила повторной отправки. Клиент может успешно получить <code>200</code> от нового маршрута и всё равно сломаться, если новый контракт иначе трактует пустой результат или отказ в доступе.</p>\n<table><caption>Быстрый разбор перед удалением одной операции</caption><thead><tr><th>Наблюдение</th><th>Что оно доказывает</th><th>Чего не доказывает</th><th>Следующее действие</th></tr></thead><tbody><tr><td><code>deprecated: true</code> в OpenAPI</td><td>Операция объявлена устаревшей</td><td>Потребители мигрировали</td><td>Опубликовать replacement и начать consumer map</td></tr><tr><td>В графике нет запросов</td><td>В выбранном scope вызовы не наблюдались</td><td>Вызовов нет вообще</td><td>Записать период, sampling, регион и credential scope</td></tr><tr><td>Все найденные rows migrated</td><td>Названные потребители имеют путь перехода</td><td>Список потребителей полный</td><td>Проверить unknown-зону и назначить остаточный риск</td></tr><tr><td>Новая операция отвечает 200</td><td>Один проверенный запрос прошёл</td><td>Совместимы ошибки, права и редкие сценарии</td><td>Сравнить фиксированный набор contract cases</td></tr><tr><td>Назначена дата Sunset</td><td>Есть объявленная граница планирования</td><td>Ресурс станет недоступен точно в эту дату</td><td>Согласовать отдельный removal change и stop condition</td></tr></tbody></table>\n<h2>Consumer map хранит найденное и неизвестное</h2>\n<p>Строка consumer map должна связывать потребителя не с догадкой, а с evidence. Минимальный набор полей: имя потребителя, состояние, источник, период, охват, владелец, replacement и следующий шаг. Состояние <code>active</code> означает наблюдаемый вызов старого контракта. <code>migrated</code> означает, что названный потребитель перешёл и это подтверждено проверкой. <code>unknown</code> означает, что область не наблюдается или не проверена.</p>\n<p>Unknown — не мягкая форма migrated. Если telemetry не включает внешний tenant, старые credentials, редкий cron или трафик через общий gateway, строка должна остаться неизвестной. Запись «usage = 0» без периода и границы выглядит точной, но не позволяет воспроизвести вывод. За unknown назначают владельца остаточного риска: он либо расширяет разрешённую проверку, либо оставляет совместимость, либо выносит принятие риска на явное human review.</p>\n<figure><img src=\"/assets/editorial/2024/deprecation-2024-removal-gate.svg\" alt=\"Схема решения по устаревшему API: active и unknown блокируют удаление, migrated ведёт к ручному review и отдельному change\"><figcaption>Removal gate разделяет активный, неизвестный и мигрированный потребитель. Иллюстрация показывает логику решения, но не является телеметрией конкретного API.</figcaption></figure>\n<h2>Правило removal gate</h2>\n<p>Разделите решение на три ветки. Любой <code>active</code> блокирует удаление: сначала нужен владелец и согласованный путь миграции. Любой <code>unknown</code> блокирует автоматическое удаление: неизвестность не равна нулевой активности. Если все названные строки <code>migrated</code>, можно открыть ручной review, но это ещё не разрешение удалить маршрут. В review должны попасть контракт, owner, notice, residual risk и план проверки после релиза.</p>\n<p>Простейшее правило можно повторить локально на фиксированном наборе состояний. Команда ниже ничего не читает из production и не делает сетевых запросов: она только показывает, что unknown должен привести к блокировке.</p>\n<pre><code>node --input-type=module -e \"const evidence = [{ state: 'migrated' }, { state: 'unknown' }]; const blocked = evidence.some(function (row) { return row.state === 'active' || row.state === 'unknown'; }); console.log(blocked ? 'block-removal' : 'human-review');\"\n\n// Ожидаемый вывод:\n// block-removal\n</code></pre>\n<p>В учебной записи один известный сервис мигрировал, но внешняя зона не проверена. Поэтому автоматический результат — <code>block-removal</code>. Поле <code>blindZone</code> объясняет причину отказа. В реальном проекте замените фиктивные rows на записи из разрешённых источников и сохраните ссылку на запрос, dashboard или commit, который можно открыть повторно.</p>\n<h2>Проверка по слоям</h2>\n<ol><li><strong>Контракт.</strong> Найдите операцию по <code>operationId</code> и URI, затем проверьте request, response, ошибки, auth и replacement. Для локального репозитория начните с команды <code>rg -n 'getOrderV1|/v1/orders' .</code>. Поиск показывает строки, но не доказывает runtime-вызов.</li><li><strong>Зависимости.</strong> Проверьте исходники, lock-файлы, сгенерированные клиенты, документацию и конфигурацию gateway. Отдельно ищите динамически собранные URL и старые версии пакетов.</li><li><strong>Наблюдаемость.</strong> Возьмите запросы за заранее выбранный период и запишите route, client identity, регион, tenant, sampling, кеши и очереди. SQL-шаблон <code>SELECT client_id, count(*) FROM api_access WHERE route = '/v1/orders/{id}' AND ts >= :from AND ts < :to GROUP BY client_id;</code> нужно адаптировать к своей схеме; результат без описания охвата остаётся частичным.</li><li><strong>Владельцы.</strong> Для каждой найденной строки назначьте человека или команду, срок миграции, replacement и способ связаться. Внешнего потребителя нельзя считать migrated по тому, что внутренний сервис собрался.</li><li><strong>Совместимость.</strong> Прогоните одинаковый фиксированный набор сценариев против v1 и v2 в тестовой среде. Сравните обязательные поля, коды ошибок, права, pagination, ретраи и побочные эффекты. Один успешный happy path не закрывает контракт.</li><li><strong>Уведомление.</strong> Обновите OpenAPI, migration guide и changelog. Если используете <code>Sunset</code>, явно укажите scope и трактуйте дату как сигнал, а не гарантию доступности или удаления.</li><li><strong>Stop condition.</strong> Заранее запишите факты, при которых review прекращается: active row, unresolved unknown, несовместимая ошибка, неподтверждённый owner или отсутствие способа восстановить старый контракт.</li><li><strong>Отдельное изменение.</strong> Удаление оформите самостоятельным change с наблюдением после релиза. Не прячьте его внутри миграции, чтобы зелёные тесты нового клиента не замаскировали старого.</li></ol>\n<h2>Restore boundary важнее обещания отката</h2>\n<p>До удаления назовите последний совместимый контракт и способ временно вернуть обработчик. Если новый код уже изменяет данные, восстановление HTTP-маршрута не вернёт прежнее состояние. Тогда граница восстановления должна включать совместимый read path, миграцию данных или заранее подготовленный обратный план.</p>\n<p>Согласуйте, кто может остановить rollout и какой сигнал это делает. Например, рост ответов 4xx от известного клиента после удаления — достаточное основание остановить change, но не доказательство, что причина именно в API. Нужны correlation id, журнал изменения и сравнение с baseline. Откат должен вернуть систему в проверяемое состояние, а не просто скрыть симптом.</p>\n<h2>После удаления проверяется не только статус</h2>\n<p>Проверка после релиза должна охватить старый и новый путь в заявленной области. Для старого маршрута проверьте ожидаемый статус и тело ошибки тестовым клиентом, не используя реальные секреты. Для нового — повторите контрактные сценарии и убедитесь, что авторизация, лимиты и ответы соответствуют согласованному replacement.</p>\n<p>Затем посмотрите на реальные сигналы: ошибки по client identity, retry rate, latency, gateway responses, очереди и обращения владельцев. Учитывайте задержку доставки логов и кэш. Если removal change коснулся только одного региона или credential scope, честный итог звучит так: «в заявленной области старый путь недоступен». Формулировка «клиентов не осталось» требует более сильного доказательства, чем обычно может дать один график.</p>\n<h2>Ограничения применимости</h2>\n<p>Полного доказательства отсутствия всех consumers обычно не существует. Редкий cron не попадёт в короткое окно. Динамический URL может исчезнуть из статического поиска. Общий gateway скроет исходного клиента. Другой tenant, регион, credential или sampling оставит blind zone. Кэш, retry и очередь сдвинут вызов за пределы момента проверки.</p>\n<p>Поэтому removal gate — это контроль риска, а не математическое доказательство. Он работает, когда у команды есть право читать нужные источники и назначать владельцев. При отсутствии доступа состояние остаётся <code>unknown</code>. Учебные rows и команда выше не являются фактическими данными, incident report или результатом запроса к вашему API.</p>\n<h2>Критерий готовности</h2>\n<p>Операция готова к отдельному removal change, если одновременно выполнены пять условий: scope операции и replacement однозначны; OpenAPI и уведомление опубликованы; каждая известная зависимость имеет owner и состояние; неизвестные области перечислены с периодом и stop condition; restore boundary проверена на совместимом контракте. Для каждого пункта оставьте ссылку на evidence.</p>\n<p>Если хотя бы один пункт не выполнен, решение не должно превращаться в «удалим и посмотрим». Оставьте старую операцию, ограничьте новые зависимости и назначьте следующий проверяемый шаг. Депрекация тогда становится управляемым переходом, а не бессрочной пометкой, которая однажды внезапно превращается в 404.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://spec.openapis.org/oas/v3.1.0.html\" target=\"_blank\" rel=\"noopener noreferrer\">OpenAPI Specification v3.1.0</a> — описание поля <code>deprecated</code> у Operation Object; поле объявляет операцию устаревшей, но не предоставляет список её потребителей.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc8594.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 8594: The Sunset HTTP Header Field</a> — смысл заголовка <code>Sunset</code>, его scope и ограничение: timestamp является подсказкой о возможной недоступности, а не гарантией.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc9110.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9110: HTTP Semantics</a> — семантика 404 Not Found и 410 Gone; используется для точной формулировки результата после удаления.</li></ul>"
|
||
}
|