Files
progcode/editorial/agent-rewrites/143.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
17 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": 143,
"slug": "editorial-2024-01-mechanism-legacy-modernization",
"title": "Модернизация legacy без ложной совместимости: как проверять поведение",
"excerpt": "Одинаковый HTTP-ответ не доказывает совместимость старой и новой реализации. Разбираем контракт, побочные эффекты, повтор запроса и минимальную проверку перед переключением маршрута.",
"contentHtml": "<p>После замены старого обработчика команда получает знакомый симптом: новый endpoint отвечает тем же статусом и похожим JSON, но клиент ломается на повторном запросе. В одном случае validation-ошибка превращается в 500. В другом preview начинает публиковать событие. В третьем повтор той же операции создаёт вторую запись. Цена ошибки — не только откат релиза. Команда уже меняет данные, отправляет уведомления или теряет доверие к ответу, а по snapshot это не видно.</p>\n<p>Проблема начинается раньше кода. Команда называет совместимостью сходство сериализованного ответа. Но legacy-контракт включает больше: вход, категорию ошибки, обязательные поля, момент чтения состояния, запись, публикацию и правила повтора. Если эти свойства не названы до миграции, проверка после переключения превращается в спор о том, что считать регрессией.</p>\n<h2>Тезис: переносить нужно договор, а не форму ответа</h2>\n<p>Модернизация legacy безопаснее, когда команда выбирает один наблюдаемый шов и описывает его до замены. Шов — это конкретная операция между потребителем и старой реализацией. У него есть вход, выход, состояние, побочный эффект и владелец решения. Новый adapter может менять язык, библиотеку и внутреннюю структуру. Он не должен молча менять свойства, на которые опирается consumer.</p>\n<p>Parity здесь означает не побайтное равенство. Она означает совпадение заранее выбранных инвариантов. Для read-only preview важны статус, обязательные поля и отсутствие публикации. Для записи важны idempotency key, порядок эффекта и состояние после повтора. Для платежа добавляются денежная точность, авторизация и reconciliation. Один общий список проверок не подходит всем операциям.</p>\n<h2>Механизм совместимости</h2>\n<p>Разделите контракт на четыре слоя. Transport описывает метод, путь, статус и значимые заголовки. Payload описывает типы, обязательные поля, значение null и отсутствие поля. Effect описывает запись, публикацию, очистку cache и запрет повторного действия. Time описывает, в каком состоянии читаются данные и что означает «тот же запрос»: тот же input, business key или idempotency key.</p>\n<p>OpenAPI помогает зафиксировать первый и часть второго слоя. Он делает видимыми paths, operations, схемы и ответы. Но одинаковая схема не говорит, записал ли обработчик событие, когда он прочитал баланс и что произойдёт при повторе. Поэтому schema — это граница формы, а не сертификат поведенческой совместимости.</p>\n<pre><code>type CompatibilityCase = {\n name: 'valid' | 'invalid' | 'repeat';\n precondition: string;\n request: unknown;\n expected: {\n statusCategory: string;\n fields: string[];\n effect: 'none' | 'one' | 'same-key-no-duplicate';\n };\n evidence: string;\n};\n\nconst repeatCase: CompatibilityCase = {\n name: 'repeat',\n precondition: 'same idempotency key, known initial state',\n request: { amount: 1000, idempotencyKey: 'case-42' },\n expected: {\n statusCategory: 'success-or-replayed-success',\n fields: ['operationId', 'status'],\n effect: 'same-key-no-duplicate'\n },\n evidence: 'response plus effect log in the test environment'\n};</code></pre>\n<p>Это учебный TypeScript-пример. Он показывает форму записи, но не вызывает endpoint и не доказывает, что повтор безопасен. Значение поля <code>evidence</code> должно ссылаться на реально доступный журнал, тестовую базу или другой наблюдаемый источник. Если источник ещё не подключён, результат нельзя помечать как parity passed.</p>\n<div class=\"table-scroll\"><table><caption>Диагностика совместимости одного legacy-шва</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Одинаковый JSON, но повтор создаёт запись</td><td>Сравнили payload и не проверили effect</td><td>Повторить запрос с тем же ключом и проверить журнал эффекта</td><td>Добавить idempotency rule или оставить операцию на legacy</td></tr><tr><td>Невалидный ввод получил 500</td><td>Новая реализация потеряла категорию ошибки</td><td>Сопоставить status category и поля ошибки для invalid case</td><td>Сохранить внешний error contract либо версионировать API</td></tr><tr><td>Результаты расходятся только утром</td><td>Различается время чтения состояния или часовой пояс</td><td>Повторить case на фиксированном состоянии и записать timestamp</td><td>Явно определить time boundary и источник времени</td></tr><tr><td>Новый ответ содержит дополнительное поле</td><td>Совместимое расширение принято без проверки consumer</td><td>Проверить парсеры старых клиентов и правило unknown fields</td><td>Оставить поле optional или подготовить migration path</td></tr><tr><td>Команда не знает, какое отличие важно</td><td>У контракта нет владельца и инвариантов</td><td>Назначить owner и классифицировать каждое расхождение</td><td>Остановить расширение шва до решения владельца</td></tr></tbody></table></div>\n<figure><img src=\"/assets/editorial/2024/legacy-modernization-2024-compatibility-matrix.svg\" alt=\"Матрица совместимости старого и нового пути по слоям transport, payload, effect и time\" loading=\"lazy\" /><figcaption>Матрица помогает разделить свойства операции. Она не содержит ответы реальных сервисов и не заменяет parity-проверку в доступном контуре.</figcaption></figure>\n<h2>Как читать различия</h2>\n<p>Сначала присвойте различию класс. Cosmetic — изменение, от которого не зависит consumer: например, пробел в сообщении или порядок незначимых ключей. Compatible extension — дополнительное optional-поле, которое старый клиент по договору игнорирует. Behavioral mismatch — другой статус, обязательное поле, значение, момент чтения или эффект. Unknown — различие обнаружено, но его значение ещё не установлено.</p>\n<p>Unknown нельзя считать совместимым по умолчанию. Если неизвестное поле влияет на сумму, доступ, уведомление или повтор, новый путь не готов. Нужна проверка или решение владельца. Иногда старое поведение выглядит как ошибка, но на него уже опирается клиент. Тогда есть три честных варианта: временно сохранить поведение, выпустить новый контракт с миграцией или отложить замену. Нельзя назвать bugfix совместимостью только потому, что он кажется правильнее.</p>\n<h2>Три минимальных case</h2>\n<p>Начните с трёх случаев, но не принимайте их за полное покрытие. Valid показывает основной результат и обязательные поля. Invalid проверяет категорию отказа и отсутствие недопустимого эффекта. Repeat проверяет одинаковый ключ или вход и ожидаемое действие. Для каждого случая запишите precondition, request, expected result, место наблюдения и owner.</p>\n<p>Если операция зависит от внешнего provider, курса, очереди или времени, это часть case. Зафиксируйте состояние, которое можно воспроизвести, или пометьте свойство неизвестным. Snapshot ответа подходит для payload. Для effect он недостаточен: два пути могут вернуть одинаковый JSON, но только один отправить сообщение. Здесь нужен журнал, счётчик, тестовая запись или иной источник, который действительно видит эффект.</p>\n<h2>Порядок проверки перед переключением</h2>\n<ol><li>Выберите одну операцию и назовите потребителя. Не начинайте с «переписать модуль».</li><li>Запишите transport, payload, effect и time. Отдельно отметьте свойства, которые не входят в обещание.</li><li>Назначьте владельца контракта. Он решает, что сохранять, что версионировать и что считать неизвестным.</li><li>Подготовьте valid, invalid и repeat case с фиксированными precondition и expected result.</li><li>Проверьте старый путь и сохраните evidence в одном сопоставимом формате. Не сравнивайте результаты из разных состояний.</li><li>Запустите новый путь на тех же case. Для effect используйте источник, который видит запись или публикацию, а не только response body.</li><li>Классифицируйте каждое отличие. Behavioral mismatch блокирует расширение маршрута; compatible extension требует проверки consumer.</li><li>Определите обратное действие. Возврат маршрута к legacy не означает восстановление уже изменённых данных.</li><li>Переключайте только выбранный шов. Остальной трафик остаётся на известном пути до отдельного решения.</li></ol>\n<h2>Отрицательный путь: когда модернизацию нужно остановить</h2>\n<p>Не каждый шов следует переносить первым. Остановите замену, если неизвестен владелец эффекта, нельзя получить начальное состояние, consumer скрыт, а различие касается денег, доступа или публикации. Остановите её также, если rollback возвращает маршрут, но не объясняет судьбу уже созданных данных. В таком случае проблема не в недостатке тестов. Сначала нужна граница данных и решение о reconciliation.</p>\n<p>Не пытайтесь закрыть неизвестность большим snapshot или процентом «покрытия parity». Одно число смешивает критичный платёж и косметическую подпись. Полезнее список незакрытых классов: time-dependent, effectful, external-provider, access-denied. Для каждого выберите действие: проверить следующим, оставить на legacy или изменить контракт с версией.</p>\n<h2>Ограничения и критерий готовности</h2>\n<p>OpenAPI описывает интерфейс, но не бизнес-смысл. Три case задают стартовую границу, но не покрывают всю систему. Учебный код выше не читает legacy, не запускает трафик, не сравнивает базу и не измеряет производительность. Поэтому статья не утверждает сохранение поведения какой-либо production-системы. Реальная готовность требует evidence из конкретного тестового или staging-контура.</p>\n<p>Шов готов к ограниченному переключению, когда owner назван, четыре слоя контракта заполнены, valid/invalid/repeat воспроизводимы, каждое обязательное различие классифицировано, effect наблюдаем, а возврат маршрута проверен отдельно от восстановления данных. Критический unknown должен отсутствовать или иметь явно принятое решение оставить операцию на legacy. Только тогда команда может объяснить, что именно она перенесла и какую цену изменения согласовала.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://martinfowler.com/bliki/OriginalStranglerFigApplication.html\" target=\"_blank\" rel=\"noopener noreferrer\">Martin Fowler: Original Strangler Fig Application</a> — исходное описание постепенного вытеснения старой системы и риска одномоментного cut-over.</li><li><a href=\"https://spec.openapis.org/oas/v3.1.0.html\" target=\"_blank\" rel=\"noopener noreferrer\">OpenAPI Specification 3.1.0</a> — официальное описание paths, operations и схем HTTP-интерфейса; спецификация не заменяет проверку бизнес-эффектов.</li><li><a href=\"https://sre.google/workbook/canarying-releases/\" target=\"_blank\" rel=\"noopener noreferrer\">Google SRE Workbook: Canarying Releases</a> — официальный материал об ограниченном rollout, сравнении control и canary и границах такого сигнала.</li></ul>"
}