From da9c232618961335535294a3835be74f50b118d1 Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Thu, 3 Sep 2026 21:35:58 +0300 Subject: [PATCH] =?UTF-8?q?editorial-254:=20=D0=BE=D1=82=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D0=B1=D0=BE=D1=80=20=D0=B8=D0=BD=D1=86=D0=B8?= =?UTF-8?q?=D0=B4=D0=B5=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- editorial/agent-rewrites/254.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/editorial/agent-rewrites/254.json b/editorial/agent-rewrites/254.json index 1208f29..89ebda7 100644 --- a/editorial/agent-rewrites/254.json +++ b/editorial/agent-rewrites/254.json @@ -2,6 +2,6 @@ "index": 254, "slug": "editorial-2020-12-mechanism-incident-review", "title": "Разбор инцидента: как не перепутать симптом с причиной", - "excerpt": "После восстановления сервиса команда часто фиксирует обход как окончательное решение. Разбираем цепочку от наблюдения до проверки и показываем, как сохранить отрицательный результат и превратить вывод в проверяемое действие.", - "contentHtml": "

Сервис вернул ошибку, очередь выросла, а после отката показатели снова стали нормальными. На этом месте разбор часто заканчивается фразой: «вернули старую настройку и добавили тест». Через месяц тот же симптом появляется после другого изменения. Команда повторяет обход, но не знает, что именно сломалось и какой сигнал подтверждает исправление.

\n

Цена ошибки — не только повторный сбой. Временное действие становится частью обычного пути. Оно может скрывать потерю данных, увеличивать задержку или переносить отказ на следующую границу. Отчёт сохраняет уверенный рассказ, но не сохраняет ход проверки. Следующий инженер восстанавливает историю по памяти.

\n

Надёжный разбор разделяет пять состояний: наблюдение, гипотезу, действие, проверку и профилактику. Наблюдение описывает факт. Гипотеза связывает факты и остаётся опровержимой. Действие меняет одну ограниченную часть системы. Проверка измеряет результат этого действия. Профилактика меняет будущий путь и считается выполненной только после отдельной проверки.

\n

Сначала фиксируем границу и симптом

\n

Разбор начинается не со слова «причина». Сначала нужно назвать границу, на которой появился эффект. Это может быть переход между mapper и адаптером, запись в базу, публикация сообщения или вызов внешнего API. Затем нужно записать наблюдаемый результат и вход, на котором он возник.

\n

Рассмотрим учебный пример. Объект заказа проходит через mapper и приходит в pricing-adapter. Адаптер ожидает обязательное поле currency. После изменения mapper поле исчезает, а учебная операция preview-order возвращает blocked. Эти два факта не доказывают, что mapper — единственная причина. Они задают узкую ветку для проверки.

\n

Учебный пример ниже синтетический. Он не описывает production-систему, реальный трафик, время восстановления или фактический инцидент. Его задача — показать форму рассуждения на маленьком входе.

\n

Механизм: пять состояний не смешиваются

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

В первой строке есть симптом, но нет диагноза. Во второй появляется гипотеза. В третьей проверка подтверждает только выбранное действие для конкретного входа. Она не доказывает, что исправлены все пути заказа. В четвёртой появляется профилактика. Такая граница не даёт назвать совпадение устранением причины.

\n

Наблюдение должно пережить неудачную гипотезу. Запись «после преобразования поле отсутствует» останется верной, даже если поле пропало раньше mapper. Запись «mapper отбросил поле» уже утверждает больше, чем показал сигнал. Если проверка опровергнет эту гипотезу, меняется гипотеза, а не прошлое наблюдение.

\n

Конкретный пример

\n

Минимальный код должен показывать только учебный контракт. Он не имитирует сеть и не создаёт видимость реального журнала:

\n
const input = { amount: 1250, currency: 'RUB' };\n\nfunction mapOrder(order) {\n  return { amount: order.amount };\n}\n\nfunction validateForPricing(order) {\n  return order.currency ? 'allowed' : 'blocked';\n}\n\nconst mapped = mapOrder(input);\nconst symptom = validateForPricing(mapped);\n\nif (symptom !== 'blocked') throw new Error('expected the exercise symptom');\nif ('currency' in mapped) throw new Error('the field should be absent here');\n\n// Учебный обÑ\nод: используем известное преобразование.\nconst restored = { amount: input.amount, currency: input.currency };\nconst check = validateForPricing(restored);\n\nif (check !== 'allowed') throw new Error('the bounded check failed');
\n

Этот код показывает две разные вещи. Сначала он воспроизводит симптом: mapper возвращает объект без currency. Затем он проверяет обратимый обход на том же входе. Успешный allowed подтверждает только действие для учебного сценария. Он не подтверждает новую реализацию, все валюты, другие версии контракта или production-эффект.

\n

Не стоит добавлять к этому же действию повторные попытки, увеличенный таймаут и новый кеш. Каждая мера меняет другую переменную. Если результат улучшится, команда не узнает, что повлияло на него. Retry уместен, когда наблюдение указывает на временную ошибку и есть защита от дублей. Таймаут уместен, когда проверена задержка, а не потеря поля. Для каждой меры нужна отдельная гипотеза и отдельная проверка.

\n
\"Схема
Проверяемая цепочка отделяет временный обход от профилактики и не назначает виноватого.
\n

Решение должно иметь условие и ожидаемый сигнал

\n

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

\n

В учебном случае решение выглядит так: если на границе адаптера нет currency, не расширять retry и не увеличивать timeout; временно использовать известное преобразование; проверить наличие поля и статус allowed на том же входе; при отрицательном результате вернуться к новой гипотезе. Это не утверждает, что retry или timeout вредны всегда. Они просто не проверяют данную гипотезу.

\n

Проверка обязана иметь отрицательный путь. Если старый mapper тоже возвращает blocked, обход не подтвердился. Нельзя переписать это как «система всё равно восстановилась». Нужно сохранить новый факт: действие не объяснило симптом. Затем проверить вход до mapper, версию контракта и другую ветку обработки. Отрицательный результат сокращает пространство поиска.

\n

Профилактика меняет будущий путь

\n

Фраза «добавить тест» не описывает работу. Назовите защищаемый контракт, место проверки и ожидаемый отказ. Например: преобразование должно отклоняться, если не передало currency; проверка должна выполняться на границе pricing-adapter; минимальный вход должен приводить к явной ошибке до вызова адаптера.

\n

Профилактика не закрыта в момент, когда её записали. Она готова после того, как изменение появилось в коде, проверка запускается в нужном пути и отрицательный сценарий действительно ломает сборку или тест. До этого состояние нужно назвать планом. Иначе отчёт приписывает системе защиту, которой ещё нет.

\n

Порядок действий

\n
  1. Назовите одну границу и один наблюдаемый симптом. Запишите вход и время наблюдения.
  2. Снимите данные до изменения: поля объекта, ответ, код ошибки, версию и доступный контекст.
  3. Сформулируйте одну гипотезу, которая объясняет конкретное наблюдение и допускает маленький эксперимент.
  4. Выберите обратимое действие с ограниченным радиусом. Запишите trigger, ожидаемый сигнал и условие возврата.
  5. Проверьте действие на том же входе. Не расширяйте результат на неизвестные пути.
  6. Если проверка отрицательна, сохраните этот факт и вернитесь к следующей гипотезе. Не объявляйте обход успешным задним числом.
  7. Сформулируйте профилактику как изменение контракта, теста или наблюдения. Укажите отдельный критерий её выполнения.
\n

Ограничения модели

\n

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

\n

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

\n

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

\n

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

\n

Практическая финальная проверка проста: удалите из текста слова «исправили» и «добавили тест» и замените их конкретными условиями. Если после этого остаются вход, граница, действие, сигнал, откат и критерий профилактики, запись пригодна для работы. Если остаётся только уверенный пересказ, расследование ещё не закончено.

\n

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

\n" + "excerpt": "После восстановления сервиса обход легко принять за окончательное решение. Разбираем цепочку от наблюдения до проверки и показываем, как сохранить отрицательный результат и превратить вывод в проверяемое действие.", + "contentHtml": "

Синтетический сценарий выглядит знакомо: операция вернула ошибку, после отката проверка снова прошла, и команда записала результат как «исправили». Через месяц похожий симптом появился после другого изменения. Повторный обход сработал на одном входе, но никто не мог показать, какая граница нарушилась и какой сигнал подтвердил решение.

\n

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

\n

Разбор полезен, когда разделяет пять состояний: наблюдение, гипотезу, действие, проверку и профилактику. Наблюдение фиксирует факт. Гипотеза объясняет факт и допускает опровержение. Действие меняет одну ограниченную часть системы. Проверка измеряет результат именно этого действия. Профилактика меняет будущий путь и считается выполненной только после отдельной проверки.

\n

Сначала фиксируем границу и симптом

\n

Разбор начинается с границы, а не со слова «причина». Граница — место, где можно сравнить вход и выход: переход между mapper и адаптером, запись в базу, публикация сообщения или вызов внешнего API. Рядом с ней запишите фактический вход, ожидаемое поведение и наблюдаемый результат.

\n

В учебном примере объект заказа проходит через order-mapper и попадает в pricing-adapter. Адаптер принимает заказ только с непустым полем currency. После изменения mapper поле исчезает, а операция preview-order получает статус blocked. Это два наблюдения. Они ещё не доказывают, что mapper — единственная причина отказа.

\n

Сценарий синтетический: он не описывает production-систему, реальный трафик, время восстановления или фактический инцидент. Маленький вход нужен для воспроизводимой проверки формы рассуждения. В рабочем расследовании те же места заполняются данными из запроса, журнала, трассировки и версии контракта.

\n

Механизм: пять состояний не смешиваются

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

В первой строке есть только симптом. Во второй появляется проверяемая гипотеза. В третьей действие меняет одну переменную. В четвёртой измеряется ожидаемый сигнал. Если добавить к откату повторные попытки и новый таймаут, улучшение результата уже нельзя будет связать с одной причиной.

\n

Наблюдение должно пережить неудачную гипотезу. Запись «после преобразования поле отсутствует» останется верной, даже если поле пропало до mapper. Запись «mapper отбросил поле» утверждает больше, чем показывает сигнал. Если эксперимент опровергнет эту версию, меняется гипотеза, а не прошлое наблюдение.

\n

Конкретный пример с двумя преобразованиями

\n

Код ниже воспроизводит симптом и отдельно проверяет ограниченный обход. В нём нет сети, базы и скрытого состояния, поэтому результат можно получить на любом запуске JavaScript:

\n
const input = { amount: 1250, currency: 'RUB' };\n\nfunction mapOrderV2(order) {\n  return { amount: order.amount };\n}\n\nfunction mapOrderV1(order) {\n  return { amount: order.amount, currency: order.currency };\n}\n\nfunction validateForPricing(order) {\n  return typeof order.currency === 'string' && order.currency.length > 0\n    ? 'allowed'\n    : 'blocked';\n}\n\nconst mapped = mapOrderV2(input);\nconst symptom = validateForPricing(mapped);\n\nif (symptom !== 'blocked') throw new Error('expected the exercise symptom');\nif ('currency' in mapped) throw new Error('currency must be absent');\n\nconst restored = mapOrderV1(input);\nconst check = validateForPricing(restored);\n\nif (check !== 'allowed') throw new Error('the bounded check failed');
\n

Первый вызов подтверждает воспроизводимость симптома: версия V2 теряет поле до адаптера. Второй вызов проверяет только действие A1 — выбор известного преобразования для того же объекта. Статус allowed не доказывает, что исправлены новая версия, все валюты, пустые значения, другие ветки или рабочая система.

\n

Важна и отрицательная проверка. Если mapOrderV1 тоже вернёт blocked, обход не подтвердился. Нельзя переписать результат как «система всё равно восстановилась»: новый факт сужает поиск. Следующий шаг — сравнить вход до mapper, версию схемы, ветку маршрутизации и контракт адаптера.

\n
\"Схема
Проверяемая цепочка отделяет временный обход от профилактики и не назначает виноватого.
\n

Действие должно иметь trigger и сигнал

\n

Во время инцидента решение выбирают быстро, но его границы можно записать сразу. Зафиксируйте trigger, изменение и ожидаемый сигнал. Добавьте условие возврата к диагностике. Тогда результат действия не смешается со случайным совпадением.

\n

В учебном случае trigger таков: на границе адаптера отсутствует currency. Изменение одно: для этого входа выбрать известный mapper. Ожидаемый сигнал — поле присутствует, а валидатор возвращает allowed. Условие возврата — любой другой ответ, отсутствие поля или расхождение входа; при нём действие считают неподтверждённым.

\n

Этот порядок не делает retry или timeout вредными сами по себе. Они проверяют другие гипотезы. Повторная попытка уместна при обоснованной временной ошибке и защите от дублей. Таймаут проверяет задержку, а не потерю поля. Каждой мере нужен собственный trigger, ожидаемый сигнал и отрицательный путь.

\n

Профилактика меняет будущий путь

\n

Фраза «добавить тест» слишком коротка для action item. Назовите контракт, место проверки и ожидаемый отказ. Например: mapper обязан сохранить непустой currency; проверка должна стоять перед вызовом pricing-adapter; минимальный вход без валюты должен приводить к явной ошибке до внешнего вызова.

\n

Профилактика не завершена в момент, когда её записали в отчёт. Она готова, когда изменение появилось в нужном коде, проверка запускается в этом пути, а отрицательный сценарий действительно обнаруживает нарушение. До этого состояние следует назвать планом. Иначе отчёт приписывает системе защиту, которой в ней ещё нет.

\n

Для рабочего postmortem полезно сохранить не только действие, но и влияние, исходные данные, contributing causes и follow-up. При этом техническая запись может быть blameless: она называет поле, границу и отсутствующую защиту, но не приписывает человеку намерение, которого нет в evidence.

\n

Как проверить причинную связь

\n

Сначала сохраните базовый результат. Затем измените только одну переменную и повторите проверку на сопоставимом входе. Сравните объект на границе, ответ валидатора и побочные сигналы. Если результат изменился, это evidence в пользу гипотезы, но не доказательство для всех вариантов.

\n

Чтобы не расширить вывод случайно, перечислите непроверенные ветки: другая версия схемы, пустая строка, неизвестная валюта, повторная доставка, параллельный маршрут и ошибка внешнего сервиса. Для каждой ветки укажите отдельный тест или наблюдение. Успех одного минимального примера остаётся успехом одного минимального примера.

\n

Порядок действий

\n
  1. Назовите одну границу и один наблюдаемый симптом. Сохраните вход, ответ, версию и время наблюдения.
  2. Отделите факт от гипотезы: «поле отсутствует после преобразования» — наблюдение, «mapper его потерял» — версия для проверки.
  3. Выберите один эксперимент с ограниченным радиусом и запишите trigger, ожидаемый сигнал и условие возврата.
  4. Проверьте действие на том же входе. Не меняйте одновременно retry, timeout, кеш и формат данных.
  5. Сохраните отрицательный результат, если сигнал не появился. Он меняет направление поиска, но не исправляет историю.
  6. Проверьте соседние ветки и версии контракта отдельными входами, не распространяя вывод без evidence.
  7. Опишите профилактику как изменение контракта, кода, теста или наблюдения и укажите критерий её выполнения.
\n

Ограничения модели

\n

Разделение состояний не вычисляет корневую причину автоматически и не заменяет мониторинг, резервирование, контроль доступа, откат или техническое расследование. У одного отказа может быть несколько contributing causes: потеря поля, отсутствие проверки и слишком широкий обход. Для каждого условия нужны свои наблюдения.

\n

Код выше не измеряет доступность, задержку, нагрузку, восстановление или влияние на пользователей. Он не подтверждает, что конкретный mapper вызвал реальный отказ. В рабочей системе проверьте фактический контракт адаптера, версию схемы, трассировку поля, права, данные и внешние зависимости. Если данных нет, вывод ограничивается учебным сценарием.

\n

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

\n

Разбор готов, когда другой инженер без устного пересказа может назвать симптом и границу, увидеть исходный evidence, повторить ограниченное действие, получить ожидаемый сигнал и найти отрицательный путь. Он также должен видеть, что осталось непроверенным и какая профилактика защищает контракт в будущем.

\n

Финальная проверка должна отвечать на пять вопросов: что произошло; где это наблюдалось; какая гипотеза проверялась; что именно изменили; какой результат получен. Если на любой вопрос приходится отвечать «так принято» или «сервис восстановился», в записи не хватает evidence.

\n

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

\n" }