ADR-0014: Graceful degradation без прогрева

Status: accepted Date: 2026-04-17 Deciders: команда проекта

Контекст

ADR-0011 установил no-proxy архитектуру: клиентские запросы получают данные из нашего хранилища, не транслируясь к поставщику синхронно. Уточнение требуется по поведению в случаях:

  • Холодный старт: данных вообще нет.
  • Прогретая система, но устаревшие данные.
  • Частичные данные (одни поля известны, другие нет).
  • Несколько источников с разной свежестью.

Главное требование: клиентский запрос никогда не должен блокироваться ожиданием поставщика.

Решение

Двухконтурная модель: SLA на ответ + SLA на свежесть, независимые.

  • Ответ всегда возвращается на основе имеющихся данных, без блокировки.
  • Если данных нет совсем → “не найдено” + enqueue async DiscoveryJob.
  • Если данные устарели → возвращаем с пометкой stale=true + enqueue RefreshJob.
  • Если данные частичные → возвращаем что есть с пометкой “incomplete”.

Численные SLA

SLA на ответ (синхронный, обязательный, нарушение = алерт):

Точка выдачиp50p95p99
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)WarmColdHard limit (then “incomplete”)
Цены customer credential6 ч24 ч72 ч7 дней
Цены system credential / B2C6 ч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.

Ссылки