Gymterview
middle

Что такое паттерны Retry и Timeout?

Retry и Timeout — базовые паттерны отказоустойчивости при межсервисном взаимодействии. Timeout ограничивает время ожидания ответа, Retry автоматически повторяет запрос при transient-ошибках.

Timeout

Без таймаута один зависший сервис может «повесить» всю цепочку вызовов.

Пример
// Таймаут через WebClient
WebClient webClient = WebClient.builder()
    .baseUrl("http://customer-service")
    .clientConnector(new ReactorClientHttpConnector(
        HttpClient.create()
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
            .responseTimeout(Duration.ofSeconds(5))
    ))
    .build();

Retry

Автоматический повтор запроса при сбое. Повторять только идемпотентные операции и только при transient (временных) ошибках.

Resilience4j Retry
@Service
public class CustomerService {

    @Retry(name = "customerService", fallbackMethod = "getCustomerFallback")
    public CustomerDto getCustomer(Long id) {
        return customerClient.getCustomer(id);
    }

    private CustomerDto getCustomerFallback(Long id, Exception e) {
        log.warn("Все попытки вызова customer-service исчерпаны: {}", e.getMessage());
        return cachedCustomerRepository.findById(id)
            .orElseThrow(() -> new ServiceUnavailableException("Customer service недоступен"));
    }
}
resilience4j:
  retry:
    instances:
      customerService:
        max-attempts: 3
        wait-duration: 500ms
        exponential-backoff-multiplier: 2  # 500ms, 1s, 2s
        retry-exceptions:
          - java.io.IOException
          - java.net.SocketTimeoutException
          - org.springframework.web.client.HttpServerErrorException
        ignore-exceptions:
          - org.springframework.web.client.HttpClientErrorException  # 4xx — не повторять!

Exponential Backoff с Jitter

Пример
Попытка 1: ошибка → ждём 500ms + random(0-200ms)
Попытка 2: ошибка → ждём 1000ms + random(0-400ms)
Попытка 3: ошибка → ждём 2000ms + random(0-800ms)
Попытка 4: ошибка → fallback

Без jitter все клиенты, получившие ошибку одновременно, будут повторять запросы в одно и то же время, создавая пиковую нагрузку («грозовое стадо»).

Spring Retry
@Configuration
@EnableRetry
public class RetryConfig {}

@Service
public class PaymentGatewayService {

    @Retryable(
        retryFor = {SocketTimeoutException.class, HttpServerErrorException.class},
        noRetryFor = {HttpClientErrorException.class},
        maxAttempts = 3,
        backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 10000)
    )
    public PaymentResult sendPayment(PaymentRequest request) {
        return paymentGateway.execute(request);
    }

    @Recover
    public PaymentResult recoverSendPayment(Exception e, PaymentRequest request) {
        log.error("Платёжный шлюз недоступен после всех попыток: {}", e.getMessage());
        return PaymentResult.deferred(request.getId());
    }
}

Комбинация паттернов (порядок имеет значение!)

Пример
Запрос → Retry → CircuitBreaker → Timeout → Вызов сервиса
Пример
resilience4j:
  retry:
    instances:
      paymentGateway:
        max-attempts: 3
        wait-duration: 500ms
  circuitbreaker:
    instances:
      paymentGateway:
        failure-rate-threshold: 50
        wait-duration-in-open-state: 30s
  timelimiter:
    instances:
      paymentGateway:
        timeout-duration: 5s

На собеседовании: обязательно упомяните exponential backoff с jitter — это показывает понимание реальных проблем. Ключевое правило: retry только для идемпотентных операций и только для 5xx, не для 4xx. Частая ошибка — ретраить бизнес-ошибки (400 Bad Request).