{ "index": 253, "slug": "editorial-2020-12-field-incident-review", "title": "Разбор инцидента: как не принять обход за исправление", "excerpt": "Запрос возвращает blocked, потому что обязательное поле теряется на границе mapper. Разбираем, как отделить факт от гипотезы, проверить обратимый обход и оставить защиту от повторения.", "contentHtml": "

Представим разбор инцидента: запрос preview-order возвращает blocked, хотя вход выглядит допустимым. После преобразования объекта пропадает обязательное поле currency. Возврат к прежнему mapper даёт allowed, и команда закрывает задачу. Цена ошибки проявится позже: новый mapper снова попадёт в путь, поле снова исчезнет, а временный обход уже будут считать исправлением.

\n

Восстановление сервиса и устранение причины — разные события. Временное действие должно иметь условие запуска, ожидаемый результат и путь отмены. Отдельно нужна проверка, которая не даст известному нарушению контракта вернуться. Ниже — учебный сценарий с воспроизводимой fixture, а не заявление о конкретном production-инциденте.

\n

Граница примера

\n

Рассмотрим только переход от mapper к локальной функции previewOrder, которая имитирует границу pricing-adapter. Стенд не обращается к сети, базе, очереди или внешнему API. Вход содержит сумму, идентификатор товара и валюту. Новый mapper намеренно теряет currency, старый сохраняет все поля. Так мы проверяем одну гипотезу и не выдаём результат fixture за результат системы.

\n
const input = { amount: 1000, itemId: 'demo-1', currency: 'RUB' };\n\nfunction newMapper(order) {\n  return { amount: order.amount, itemId: order.itemId };\n}\n\nfunction oldMapper(order) {\n  return { ...order };\n}\n\nfunction previewOrder(mapped) {\n  if (!mapped.currency) {\n    return { status: 'blocked', reason: 'currency_missing' };\n  }\n  return { status: 'allowed', currency: mapped.currency };\n}\n\nconst broken = previewOrder(newMapper(input));\nconst fallback = previewOrder(oldMapper(input));\n\nif (broken.status !== 'blocked' || fallback.status !== 'allowed') {\n  throw new Error('fixture contract failed');\n}\n\nconsole.log({ broken, fallback });
\n

Ожидаемый вывод — broken.status === 'blocked' с причиной currency_missing и fallback.status === 'allowed' с валютой RUB. Важна не удачная строка в консоли, а контраст двух результатов на одном входе. Если изменить вход или правило в previewOrder, вывод уже нельзя переносить на прежний сценарий.

\n

Функции в примере определены прямо в fixture, поэтому её можно сохранить как небольшой файл Node.js и запустить без зависимостей. Это не интеграционный тест: он не проверяет сериализацию, сеть, базу или реальный код mapper. Эти границы нужно проверять отдельно.

\n

Сначала факт, потом гипотеза

\n

Наблюдение описывает то, что можно увидеть: один и тот же вход дошёл до границы, после mapper поле отсутствует, а previewOrder вернул blocked. Гипотеза связывает наблюдения: новый mapper не переносит обязательное поле. Она правдоподобна, но не доказана, пока не сравнены вход и результат на этой границе.

\n

До изменения сохраните четыре значения: входной объект, результат mapper, ответ проверки и идентификатор попытки. Если в настоящем сервисе есть лог или trace, запишите ссылку на него; если есть только локальная fixture, назовите её fixture. Не заменяйте отсутствующее наблюдение уверенной фразой «причина найдена».

\n

Проверка должна менять одну переменную. В нашем случае новая версия mapper заменяется старой только на ограниченном пути. Если тот же вход даёт allowed и возвращает currency, гипотеза получает поддержку. Но это ещё не доказывает, что она объясняет все отказы: нужно проверить другие обязательные поля и реальные точки сериализации.

\n
Связь наблюдения, проверки и решения
НаблюдениеГипотезаПроверкаВладелецРешение
preview-order вернул blockedНарушен контракт обязательного поля или сработала другая ветка отказаСохранить ответ и проверить вход на границе mapperИнженер, ведущий диагностикуНе менять retry и timeout до локализации причины
После нового mapper нет currencyMapper мог отбросить поле при сборке объектаСравнить вход, результат нового mapper и результат старогоВладелец адаптераСформулировать узкую гипотезу H1
Старый mapper даёт allowedОбход возвращает нужный контракт, но причина не устраненаПовторить fixture на том же входе и проверить сохранение поляВладелец релизаОставить обход ограниченным и записать условие отмены
Проверка обхода не прошлаГипотеза неполна или отказ возник на другой границеСобрать новое наблюдение до следующего измененияВедущий диагностикиОтменить вывод и проверить альтернативную ветку
Ошибка вернулась после изменения mapperДля контракта нет автоматической защитыЗапустить тест на отсутствие обязательного поляВладелец компонентаДобавить контрактную проверку и связать её с релизом
\n

Восстановление не равно исправлению

\n

Возврат к старому mapper может быть правильным способом уменьшить влияние ошибки. У него должны быть границы: какой маршрут переключается, какой сигнал включает обход, кто его подтверждает и по какому условию он снимается. Без этого временное решение превращается в новый постоянный путь, который никто не проверяет.

\n

После восстановления отдельно зафиксируйте причинный вывод. В нашем примере он звучит узко: при входе с currency новый mapper возвращает объект без этого поля, а старый mapper поле сохраняет. Формулировка не обещает, что найден единственный дефект во всей системе. Она указывает конкретную границу, которую можно проверить.

\n

Такое разделение совпадает с практикой управления инцидентами: во время сбоя нужны отдельные роли для операционной работы, коммуникации и координации, а после восстановления — живой документ с действиями и состоянием. Это снижает риск, что несколько инженеров одновременно изменят систему и сотрут причинную связь между изменением и результатом.

\n

Почему нельзя лечить все симптомы сразу

\n

Увеличение timeout скрывает медленный ответ, но не возвращает пропавшее поле. Дополнительный retry повторяет тот же неверный объект и может умножить побочный эффект. Одновременная правка mapper, retry и timeout стирает причинную связь: если результат изменится, станет непонятно, какая правка помогла.

\n

Это не запрет на retry или timeout. Они уместны, когда наблюдение указывает на временную сетевую ошибку или ограничение времени ответа. Но у такого решения должны быть собственный trigger, лимит повторов и собственная проверка. Инструмент выбирают по наблюдаемому механизму, а не по привычке.

\n
Цикл разбора инцидента: факт, гипотеза, ограниченное действие, проверка и профилактика
Ограниченное действие снижает влияние сейчас. Контрактная проверка снижает риск повторения позже.
\n

Отрицательный путь обязателен

\n

Предположим, возврат к старому mapper не дал allowed. Это не повод объявить старый код неисправным. Новый факт говорит только о том, что выбранный обход не подтвердил H1. Поле могло отсутствовать уже во входе. Отказ мог зависеть от другой обязательной величины. Следующая проверка должна отличить эти варианты.

\n

Проверим первый вариант: уберём currency из самого входа и запустим старый mapper. Поле останется отсутствующим, поэтому результат blocked больше не подтверждает гипотезу о новом mapper. Проверим второй вариант: оставим валюту, но уберём другой обязательный атрибут. Если отказ сохранится, причина находится не в потерянной валюте.

\n

Хороший review не прячет отрицательный результат. Запись «A1 не подтвердил H1 на входе E1» полезнее, чем «фикс не сработал»: первая формулировка указывает границу нового исследования. Она также не обвиняет автора mapper и оставляет следующий шаг проверяемым.

\n

Порядок проверки

\n
  1. Назвать один симптом и одну границу. В примере это blocked на переходе к pricing-adapter.
  2. Собрать факты до изменения: вход, результат mapper, ответ и идентификатор попытки.
  3. Разделить факт и гипотезу. Возможную причину не записывать как доказанное наблюдение.
  4. Выбрать одно обратимое действие. Указать trigger, ожидаемый результат и условие отмены.
  5. Повторить тот же сценарий на том же входе или выполнить безопасный контролируемый запрос.
  6. Проверить результат только в пределах входа и среды. Не переносить fixture на неизвестные пути.
  7. Прогнать отрицательные варианты: поле отсутствует во входе, отказ вызван другим полем, обход не меняет ответ.
  8. Добавить контрактный тест: он должен падать на результате mapper без currency и проходить на корректном объекте.
  9. Закрыть работу только после проверки действия и профилактики. Если проверка отрицательна, начать новый цикл с наблюдения.
\n

Ограничения

\n

Fixture не измеряет доступность, нагрузку, время восстановления, права доступа или поведение внешней зависимости. Она не заменяет журнал, мониторинг, резервирование и процедуру отката. Схема поясняет последовательность, но не является доказательством результата. Учебный код ограничен одним объектом, двумя mapper и одним обязательным полем.

\n

Нельзя объявлять production-инцидент исправленным по одному успешному примеру. В реальной системе нужно подтвердить границу на фактическом запросе, проверить безопасный rollout и посмотреть на отрицательные случаи. Нельзя также превращать разбор в поиск виноватого: смена автора mapper не объясняет, почему контракт оказался без защиты.

\n

Источники ниже подтверждают практики incident management и postmortem, но не подтверждают вымышленный запрос preview-order или состояние конкретного сервиса. Факты такого инцидента должны подтверждаться собственными логами, изменениями и конфигурацией мониторинга.

\n

Критерий готовности

\n

Работа готова, когда симптом воспроизводится на известном входе, действие меняет только заявленную границу и проходит свою проверку, а отрицательный путь приводит к новой гипотезе. Дополнительно автоматическая проверка должна падать на результате mapper без currency и проходить на корректном результате.

\n

Этот критерий не обещает, что система больше никогда не откажет. Он делает более узкое утверждение: конкретный контракт виден, действие проверено, а известный способ регрессии получает автоматическую защиту. Такой результат можно связать с последующим изменением и проверить повторно.

\n

Проверяемые источники

" }