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

8 lines
20 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": 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 &gt;= :from AND ts &lt; :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>"
}