ADR-0014: Graceful degradation без прогрева
Status: accepted Date: 2026-04-17 Deciders: команда проекта
Контекст
ADR-0011 установил no-proxy архитектуру: клиентские запросы получают данные из нашего хранилища, не транслируясь к поставщику синхронно. Уточнение требуется по поведению в случаях:
- Холодный старт: данных вообще нет.
- Прогретая система, но устаревшие данные.
- Частичные данные (одни поля известны, другие нет).
- Несколько источников с разной свежестью.
Главное требование: клиентский запрос никогда не должен блокироваться ожиданием поставщика.
Решение
Двухконтурная модель: SLA на ответ + SLA на свежесть, независимые.
- Ответ всегда возвращается на основе имеющихся данных, без блокировки.
- Если данных нет совсем → “не найдено” + enqueue async
DiscoveryJob. - Если данные устарели → возвращаем с пометкой
stale=true+ enqueueRefreshJob. - Если данные частичные → возвращаем что есть с пометкой “incomplete”.
Численные SLA
SLA на ответ (синхронный, обязательный, нарушение = алерт):
| Точка выдачи | p50 | p95 | p99 |
|---|---|---|---|
GET /v1/products/{id} | 100 мс | 300 мс | 1 с |
GET /v1/products (search) | 150 мс | 400 мс | 1.5 с |
POST /v1/pricing (single offer) | 100 мс | 300 мс | 1 с |
POST /v1/pricing/bulk (≤ 50 offers) | 250 мс | 800 мс | 2 с |
POST /v1/estimates (≤ 200 lines) | 2 с | 10 с | 30 с |
Эти SLA измеряются на наших данных, без ожидания поставщика.
SLA на свежесть (max_staleness, нарушение = stale=true в ответе + RefreshJob):
| Тип данных | Hot (запрашивалось < 24h) | Warm | Cold | Hard limit (then “incomplete”) |
|---|---|---|---|---|
| Цены customer credential | 6 ч | 24 ч | 72 ч | 7 дней |
| Цены system credential / B2C | 6 ч | 24 ч | 72 ч | 7 дней |
| Остатки | 2 ч | 12 ч | 24 ч | 72 ч |
| Характеристики товара | 7 дней | 14 дней | 30 дней | бессрочно |
| Lifecycle (discontinued) | 24 ч | 7 дней | 30 дней | бессрочно |
Превышение max_staleness → метрика freshness_violations_total{data_type, tier},
дашборд по затронутым SKU. Превышение Hard limit → данные помечаются incomplete с явным
текстом для UI.
API контракт всегда возвращает: { "freshness_status": "fresh|stale|incomplete|empty", "observed_at": "...", "stale_since": "...", "refresh_eta": "..." }.
Enrichment jobs:
- Дедуплицируются: если на тот же target уже есть pending job — присоединяемся.
- Троттлятся: для одного target создаём не чаще раза в минуту.
- Имеют priority: high (по запросу клиента) | normal (расписание) | low (background).
UX-индикаторы: явные иконки/badges свежести данных в клиентском UI.
Последствия
Плюсы
- Предсказуемая latency: никогда не висим на поставщике.
- Холодный старт работает (отвечает “пусто”, показывает прогресс заполнения).
- Деградация поставщика не уроняет наш сервис.
- Кэшируемость остаётся (мы возвращаем то, что у нас есть).
Минусы
- Клиент может видеть устаревшие данные (но с явной пометкой).
- Нужна дисциплина TTL и алертов на массовую stale.
- Сложнее объяснить пользователю: “почему цена менялась” → потому что было stale, обновилось.
Нейтральные последствия
- UI должен явно дифференцировать состояния свежести.
- API контракт обогащается полями
freshness_status,stale_since,refresh_eta.
Рассмотренные альтернативы
Синхронный fetch при отсутствии данных
Нарушает no-proxy. Непредсказуемая latency. Уроняет систему при падении поставщика.
Промежуточная задержка ответа на N секунд (вдруг enrichment завершится)
Усложняет latency profile, не даёт гарантий. Лучше отвечать сразу + enqueue.
Ссылки
- ADR-0011 (no-proxy).
../../20-architecture/principles.md