Проблема каскадного сбоя на review часто начинается с вопроса, сколько реплик добавить. При каскаде это поздний вопрос. Сначала нужно показать, где лишняя работа перестаёт рождаться. Retry, fallback и репликация могут быть уместны по отдельности, но без единой точки ограничения они создают конкурирующие версии «ещё одной попытки». Цена такой неопределённости — невозможность быстро понять trace, лишняя нагрузка на ослабленный маршрут и спор о спасении, который начинается после того, как budget уже перестал быть видимым.
Это учебный field-проход над fixed literals. В нём нет реального клиента, организации, endpoint, метрик, telemetry, сети, файлов, Git, CI, clock или PII. Failure injection задан как строка, latency как units, trace как массив строк. Функции не производят side effect. Результат accepted ограничен фразой synthetic resilience review hand-off; он не означает, что реальная система выдержит отказ, что fallback полезен пользователю или что репликация безопасна для данных.
Review начинается с вопроса о праве сравнивать
Сценарии похожи, пока их не положили рядом. Один может иметь два logical request и temporary-timeout на primary-a, другой — семь запросов и permanent-contract-error на другом имени. Оба могут содержать retry, fallback и две реплики, но сравнение их итогов не скажет ничего о выбранной защите. Оно перемешает различную базовую работу и разные failure modes. Поэтому в карточке есть comparison key. Он фиксирует, что baseline имеет тот же logical load и тот же named injection. Если ключ не совпал, review не строит рейтинг и не выбирает лучший вариант.
| Поле | Вопрос к записи | Допустимый fixed ответ | Причина stop |
|---|---|---|---|
| failureInjection | какой исход введён? | один названный temporary timeout | исход без имени или со скрытой областью |
| retryPlan | кто создаёт повтор? | edge, две попытки | два enabled owner |
| replication | какая ширина маршрута? | одно имя на попытку | any number или maxFanout без числа |
| fallback | что меняется в результате? | named-summary, один work unit | пустое имя либо новый retry |
| limit | когда прекращается расширение? | до route expansion, fixed terminal action | нет места, бюджета или выхода |
| comparison | с чем сопоставляют? | тот же fixed baseline key | другая нагрузка или injection |
Trace полезен, если из него можно восстановить решение
В accepted карточке trace короткий: logical-01 получает timeout на primary-a, edge списывает token, следующая попытка идёт на primary-b; logical-02 получает fixed optional result unavailable и переходит в named-summary. Этот список не является метрикой и не показывает время. Его назначение скромнее: reviewer может связать каждый переход с одним разрешением из модели. Если в trace появляется переход, которому нет места в retry plan, fallback или replication, это не «деталь реализации», а повод остановить hand-off.
Особенно важен момент terminal action. После исчерпания maxExtraAttempts модель возвращает fixed degraded result. Она не ищет очередную реплику, не переключает fallback на «что-нибудь другое» и не просит человека вручную продолжить цепочку. Такое завершение может выглядеть менее героическим, чем бесконечная попытка восстановить полный ответ, но именно оно делает цену отказа ограниченной. В настоящем продукте выбор результата потребует отдельного решения. Здесь мы проверяем лишь наличие конечной ветки, а не её ценность для пользователя.
Что именно вернуть после stop
Stop без результата часто только переносит каскад на другой слой. Поэтому field-record хранит terminal action вместе с limit: return-fixed-degraded-result. Это не обещание, что пользователь доволен деградацией, и не типовой ответ для любой операции. Это маркировка завершения модели: после этой строки больше не разрешены retry, другой выбор реплики или новая безымянная альтернатива. Reviewer может сопоставить action с trigger и сказать, что на конкретной ветке добавочная synthetic работа закончилась. Без такого выхода budget выглядит как наблюдение, которое забыли применить.
Важно не превращать terminal output в ловушку для данных. Если реальная операция меняет состояние, требует строгого подтверждения или несёт финансовый риск, fixed degraded result из этой статьи неприменим. Для неё потребуется другой контракт и возможно отказ без результата. Учебная карточка намеренно не скрывает эту неопределённость: она не знает предметной области и не пытается выдать универсальный fallback. Зато она заставляет явно назвать последствия исчерпания лимита вместо того, чтобы незаметно расширять маршрут под видом заботы о доступности.
Исполняемый отказ от ложного сравнения
import { compareFixedResilienceScenarios } from './upgrade-2026-02.mjs';
const comparison = compareFixedResilienceScenarios(
'bounded-cascade-v1',
'incomparable-scenario-v1',
);
console.log({ decision: comparison.decision, reasons: comparison.reasons });
// { decision: 'stop-incomparable-scenario', reasons: ['stop-incomparable-scenario'] }
Здесь нет скрытого бенчмарка. Первая карточка и вторая заданы в памяти, а вторая заранее сообщает, что comparison key другой. Public function сначала оценивает обе записи и только затем отказывает в сравнении. Это намеренно строже, чем попытка нормализовать разные числа и исходы красивой формулой. Пока команда не выровняла logical work и injection, любая разница в trace может происходить от базового сценария, а не от механизма устойчивости.
Проверка в шесть коротких остановок
- Сверьте id с known fixture; произвольный объект не получает частично положительный статус.
- Прочитайте failure injection как контракт области, а не как доказательство настоящего инцидента.
- Убедитесь, что retry имеет одного named owner и фиксированное число попыток.
- Проверьте, что fallback именован, имеет одну synthetic стоимость и не запускает собственный retry.
- Потребуйте целочисленный fan-out и limit point, который расположен до расширения маршрута.
- Сверьте comparison key; несовпадение ведёт в stop-incomparable-scenario, а не в обсуждение среднего результата.
Репликация — не замена границе
Список из трёх реплик полезен только после ответа, сколько из них может участвовать в одной попытке. Без maxFanout слово «репликация» маскирует ширину поиска. В accepted record три имена существуют, однако selectedPerAttempt равен одному. Это делает trace читаемым: каждая дополнительная попытка получает ровно одно направление, которое можно связать с расходом retry budget. Если maxFanout равен null или правило говорит «любое число», fixture останавливается. Не потому что широкое распределение всегда неверно, а потому что карточка не умеет доказать его предел.
То же относится к fallback. В review нельзя считать его пустой стрелкой после primary. Нужны trigger, name, workUnits и output. Это превращает «попробуем запасное» в наблюдаемое решение: переход с fixed optional result unavailable на fixed degraded summary. Если для альтернативы нужно несколько шагов, они должны быть отдельной карточкой с собственным limit и trace, а не быть спрятаны под общим словом. Иначе reviewer рассматривает одну цепочку, а система получает две.
Откуда брать язык, но не гарантию
Архивная глава Google SRE описывает каскад как положительную обратную связь и прямо предупреждает о retry на нескольких уровнях. Это сильный исторический контекст для вопроса «где умножаются попытки». RFC 9110 даёт более узкую опору: 503 обозначает временную неспособность обработать запрос, а Retry-After может сообщить ожидаемую задержку. Ни один источник не знает names и numbers этого fixture. Поэтому ссылки помогают сформулировать границу, но не могут заменить evidence из реальной системы или сделать synthetic trace фактом об эксплуатации.
Проверяющий должен отделить три предложения: что закреплено внешним источником; что заранее задано в учебной карточке; что ещё неизвестно. Например, «503 может сопровождаться Retry-After» относится к RFC, «maxExtraAttempts равно двум» — к fixed literal, а «этого достаточно для восстановления конкретного маршрута» остаётся неизвестным. Такая маркировка кажется педантичной, пока не приходит incident review. Тогда она экономит время: люди видят, что можно проверить сейчас, что нужно измерить отдельно и где нельзя честно делать вывод.
Граница field-прохода и следующий шаг
Пакет не является runbook, тестом отказоустойчивости, анализом capacity или шаблоном для production. Он не умеет отправлять запросы, отменять in-flight работу, учитывать финансовую цену, определять важность данных или принимать решение за владельца продукта. Нельзя переносить fixed units, имена или terminal output в настоящую конфигурацию. Даже положительный fixture не избавляет от угрозы, что реальный retry небезопасен для операции, а fallback меняет семантику сильнее, чем допустимо.
Следующий шаг — подготовить отдельный, авторизованный и наблюдаемый review для одного реального класса операции. Его входом будут явно разрешённые данные и источник trace, а не этот модуль. В нём надо проверить idempotency, контракт деградации, владельца limit и критерий остановки. Если эти факты не готовы, удержите работу на уровне synthetic hand-off. Честный stop дешевле, чем необъяснимое расширение маршрута во время сбоя.
Проверяемые источники
- Google SRE Book, глава 22 в историческом захвате — версия: Wayback immutable capture from 23 January 2025 of the official 2017 book chapter. Глава служит контекстом для положительной обратной связи, reduced capacity и опасности многоуровневых попыток. Граница: Текст не является trace этого пакета и не устанавливает допустимый budget для чужого маршрута.
- HTTP 503 и Retry-After в закреплённой RFC 9110 — версия: static RFC 9110 publication, June 2022, sections 10.2.3 and 15.6.4. Документ отделяет временную недоступность сообщения от успешного результата и называет Retry-After. Граница: Protocol semantics не подтверждает, что повтор конкретной операции безопасен, полезен или приведёт к recovery.