Настройка timeout без модели работы часто создаёт ложное чувство контроля. При частичном отказе система может быстрее сдаться, но всё ещё отправить несколько копий одной логической задачи в разные ветви. Цена ошибки — не только задержка в карточке. Это ширина незавершённой работы: сколько разрешённых попыток одновременно и последовательно появится до terminal result. Если считать лишь успешные ответы, каскад остаётся невидимым до момента, когда оставшиеся реплики уже заняты лишними действиями.
В этой статье числа, outcomes, trace и latency units существуют только как fixed JS literals. Они не описывают сервис, сетевой протокол, реальный deadline или телеметрию. Функция fixture не моделирует очередь и не ждёт времени. Она проверяет форму: один owner retry, конечный выбор реплики, конкретный fallback, limit до расширения и сопоставимый baseline. Статус ready означает лишь право передать synthetic карточку человеку; он не означает, что какая-либо система стала устойчивой.
Четыре оси, которые нельзя складывать в одно слово retry
Первое различие — между числом попыток и числом направлений. Последовательный retry добавляет новый запуск после определённого исхода. Fan-out создаёт несколько направлений одной попытки. Репликация в такой модели — не магическое слово про запас, а правило выбора: сколько имён может получить один запуск. Fallback — не третий вид retry, а смена результата и, возможно, работы. Limit point — отдельная ось: он должен прекратить расширение до того, как следующая защита начнёт действовать.
| Величина | Fixed значение | Что запрещает | Почему отдельно |
|---|---|---|---|
| logicalRequests | 2 | сравнение с другой базой нагрузки | задаёт единицу расчёта |
| maxAttemptsPerLogicalRequest | 2 | бесконечный retry | принадлежит одному owner |
| maxFanout | 1 | широкий поиск всех реплик | не равен числу реплик в списке |
| fallback workUnits | 1 | скрытый дорогой обход | описует цену смены режима |
| maxExtraAttempts | 2 | расширение после stop | принадлежит limit point |
| comparison key | same logical load and injection | ложную причинную аналогию | фиксирует допустимый baseline |
Граница выше, чем отдельная настройка
У bounded-cascade-v1 есть три реплики, но это не означает fan-out три. На одну попытку выбирается одно имя, и максимум запуска ограничен ещё раньше. В допустимом верхнем приближении можно прочитать модель так: два logical request умножаются максимум на две попытки и на fan-out один. Получается не прогноз нагрузки, а видимый предел synthetic work: четыре route attempts до учета named-summary. Summary описан отдельно, потому что смена ответа не должна тайно наследовать retry-политику первичного маршрута.
В реальной инженерной схеме формула была бы недостаточна: появились бы очереди, дедлайны, отмена, side effects, разные классы запросов, idempotency и распределённое состояние. Здесь их нет специально. Узкая арифметика полезна не тем, что подменяет систему, а тем, что ловит продукт настроек: два retry owner уже не складываются, а потенциально перемножают разрешённые пути. Поэтому fixture отвергает даже красивый сценарий, если edge и adapter оба считают себя владельцем повторной попытки.
import { createFixedResilienceScenario, assessFixedResilienceScenario } from './upgrade-2026-02.mjs';
const candidate = createFixedResilienceScenario('retry-amplification-v1');
const result = assessFixedResilienceScenario(candidate);
console.log({ status: result.status, reason: result.reasons[0], handoff: result.handoff });
// { status: 'stop-retry-amplification', reason: 'retry-must-have-one-owner-and-a-fixed-two-attempt-bound', handoff: 'not-eligible-for-synthetic-resilience-review' }
Этот негативный фрагмент выполняется буквально и останавливается до любых внешних действий. В candidate два enabled retry plan: edge и adapter. Проверка не пытается угадать, какой из них «лучше», и не симулирует задержку. Она отказывает по более сильному правилу: в карточке должен остаться ровно один владелец повтора с конечным числом попыток. Такое fail-closed решение полезно для review, потому что следующее действие ясно — убрать дублирование или вернуть сценарий на уточнение.
Как отличить ограничение от наблюдения
Trace сам по себе не является ограничением. Строка edge retry budget: 2 -> 1 полезна только потому, что рядом есть правило: после нуля limit point возвращает fixed degraded result и не открывает следующий маршрут. Аналогично, latency units не доказывают быстроту. Они помогают различить шаги в fixed sequence, но не заменяют измерение. Наблюдение говорит, что в карточке записано; ограничение разрешает или запрещает дальнейший переход. Смешение этих двух уровней рождает отчёты, где есть красивые следы, но нет механизма остановки.
Смысл named fallback также проверяется не его наличием, а тем, что он не создаёт вторую политику попыток. В fixture fallback активируется по fixed optional result unavailable, имеет workUnits ровно один и выдаёт fixed degraded summary. Если имя пустое, outcome становится неаудируемым: невозможно понять, что именно разрешили вернуть и какую работу добавили. Если fallback добавляет retry layer, он превращается в скрытый amplifier. Обе формы выполняются как отдельные отрицательные records, а не объясняются словами в review.
Консервативная верхняя граница без имитации нагрузки
Даже учебной модели полезно выписать предел, но нельзя выдавать его за прогноз. Для bounded-cascade-v1 он получается из трёх фиксированных множителей: два logical request, максимум две попытки на каждый и fan-out один. Это даёт четыре допустимых route attempts как верхнюю границу записи, а не QPS, latency или расход CPU. named-summary расположен рядом с этой формулой, а не внутри неё: он меняет вид результата и имеет собственный workUnits. Такое разделение удерживает разговор от привычной ошибки, когда всё, что не primary, превращают в одну неразличимую «дополнительную нагрузку».
Предел становится полезным только рядом с отрицательным примером. В retry-amplification-v1 те же два logical request встречают два owners. Если просто сложить их настройки, можно не увидеть проблему. Если проследить право каждого владельца запускать следующую попытку, возникают вложенные пути, и прежний верхний предел больше не применим. Fixture не пытается вычислить все комбинации: он отказывает раньше. Для design review это правильная экономия точности. Сначала сохраните однозначное право расширять работу, потом решайте, нужна ли более богатая модель с очередями и отменой.
Последовательность механической проверки
- Проверьте, что input совпадает с одним named fixed scenario, иначе остановите разбор как unknown.
- Отделите enabled retry plan от всех остальных действий и потребуйте одного owner с конечной попыткой.
- Проверьте имя fallback, его единичную synthetic стоимость и запрет на собственный retry.
- Потребуйте integer maxFanout и убедитесь, что selectedPerAttempt не шире этого предела.
- Найдите limit point до fallback и route expansion, затем проверьте terminal action.
- Сопоставьте logical load и injection с baseline; если ключ различается, не усредняйте результаты.
Почему 503 и policy не заменяют решение о каскаде
RFC 9110 фиксирует смысл 503 как временную неспособность обработать запрос и допускает Retry-After как подсказку ожидания. Это полезная граница протокольного сообщения: она помогает не путать overload с успешным ответом. Но HTTP-статус не выбирает владельца retry за архитектуру и не подтверждает безопасность повтора любого действия. Так же gRPC A6 описывает maxAttempts, throttling и pushback в конкретном дизайне. Эти документы дают точные имена полям, а не теорему о поведении произвольной системы.
Поэтому модель не читает 503 и не реализует gRPC. Она использует fixed outcome temporary-timeout и явное разрешение retryableOutcomes. Это ограничение устраняет ложную точность. Если реальная интеграция получит другой код или иной контракт, она не может «пройти» этот fixture за счёт похожего слова. Ей потребуется другой record, другой источник и отдельная проверка того, безопасна ли работа для повторения.
Граница модели и следующий шаг
Этот механизм не ищет оптимальный budget, не выбирает число реплик, не измеряет утилизацию и не отвечает, какой режим деградации приемлем для продукта. Порог два здесь не рекомендация, а literal, сделанный достаточно маленьким, чтобы review увидел все переходы. Также модель ничего не знает о попытках, уже начатых вне её области; у неё нет сети, clock, cancellation, clients или telemetry. Поэтому любой положительный output остаётся synthetic resilience review hand-off, а не рекомендацией выкатывать конфигурацию.
Следующий безопасный шаг — взять один настоящий класс работы отдельно от этого пакета и описать его contract: какие outcomes retryable, что считается side effect, где находится владелец retry и какое наблюдение докажет расход budget. Не переносите значения из fixture. Сначала сохраните сравнимую базу и способ получить trace законно и безопасно. Если хотя бы один элемент не определён, правильное решение не расширять модель, а оставить stop и сформулировать недостающий факт как самостоятельный вопрос.
Проверяемые источники
- gRPC proposal A6 с закреплёнными правилами retry — версия: raw commit dc9fd4fe5b94b90b82fe2833ad1d80938e6a49c1, publication timestamp 29 August 2024. Первичный design text различает maxAttempts, retry throttling, hedging и server pushback. Граница: Его параметры не запускаются в JavaScript fixture и не превращаются в доказательство устойчивости.
- RFC 9110, HTTP Semantics, разделы Retry-After и 503 — версия: RFC publication dated June 2022, immutable RFC text. Стандарт ограничивает трактовку 503 и Retry-After как семантику сообщения о временной недоступности. Граница: RFC не определяет retry owner, idempotency конкретного действия или предел fan-out в этом сценарии.