Вопрос 28 · Раздел 19

Как реализовать retry логику с помощью CompletableFuture

Structured Java interview answer with junior, middle, and senior-level explanation.

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Retry — повторить операцию при ошибке. В CompletableFuture это можно сделать через exceptionally():

public CompletableFuture<String> withRetry(Supplier<CompletableFuture<String>> operation, int maxRetries) {
    return operation.get()
        .exceptionallyCompose(ex -> {
            if (maxRetries > 0) {
                return withRetry(operation, maxRetries - 1);
            }
            return CompletableFuture.failedFuture(new RuntimeException("All retries failed", ex));
        });
}
// Важно: .join() внутри exceptionally блокирует поток callback-а.
// exceptionallyCompose (Java 12+) сохраняет неблокирующую природу.

// Использование
withRetry(() -> fetchDataAsync(), 3)
    .thenAccept(result -> System.out.println(result));

🟡 Middle Level

С экспоненциальной задержкой

// CompletableFuture.delayedExecutor доступен с Java 9.
// exceptionallyCompose доступен с Java 12.
// Для Java 8 используйте ScheduledExecutorService.
public CompletableFuture<String> withRetryAndDelay(
    Supplier<CompletableFuture<String>> operation, 
    int maxRetries,
    long delayMs
) {
    return operation.get()
        .exceptionallyCompose(ex -> {
            if (maxRetries > 0) {
                // Ждём и повторяем
                return CompletableFuture.delayedExecutor(delayMs, TimeUnit.MILLISECONDS)
                    .thenCompose(v -> withRetryAndDelay(operation, maxRetries - 1, delayMs * 2));
            }
            return CompletableFuture.failedFuture(ex);
        });
}

// Использование
withRetryAndDelay(() -> httpClient.getAsync(url), 3, 1000)
    .thenAccept(System.out::println);

С проверкой типа ошибки

public CompletableFuture<String> withRetryIf(
    Supplier<CompletableFuture<String>> operation,
    int maxRetries,
    Predicate<Throwable> shouldRetry
) {
    return operation.get()
        .exceptionallyCompose(ex -> {
            if (maxRetries > 0 && shouldRetry.test(ex)) {
                return withRetryIf(operation, maxRetries - 1, shouldRetry);
            }
            return CompletableFuture.failedFuture(ex);
        });
}

// Только для timeout
withRetryIf(() -> fetchDataAsync(), 3, 
    ex -> ex.getCause() instanceof TimeoutException);

Типичные ошибки

  1. Бесконечный retry: ```java // ❌ Нет уменьшения maxRetries .exceptionallyCompose(ex -> withRetry(operation, maxRetries));

// ✅ Уменьшаем .exceptionallyCompose(ex -> withRetry(operation, maxRetries - 1));


---

## 🔴 Senior Level

### Resilience4j интеграция

```java
import io.github.resilience4j.retry.Retry;

Retry retry = Retry.of("fetchData", RetryConfig.custom()
    .maxAttempts(3)
    .waitDuration(Duration.ofSeconds(1))
    .retryOnException(ex -> ex instanceof TimeoutException)
    .build());

CompletableFuture<String> result = Retry.decorateCompletionStage(
    retry,
    () -> fetchDataAsync()
);

Java 12+ exceptionallyCompose

cf.exceptionallyCompose(ex -> {
    if (shouldRetry(ex)) {
        return retryOperation();
    }
    return CompletableFuture.failedFuture(ex);
});

Production Experience

Generic retry utility:

public class RetryUtil {
    public static <T> CompletableFuture<T> withRetry(
        Supplier<CompletableFuture<T>> operation,
        int maxRetries,
        long initialDelayMs,
        double backoffMultiplier
    ) {
        return attempt(operation, maxRetries, initialDelayMs, backoffMultiplier);
    }
    
    private static <T> CompletableFuture<T> attempt(
        Supplier<CompletableFuture<T>> operation,
        int remaining,
        long delayMs,
        double multiplier
    ) {
        return operation.get()
            .exceptionallyCompose(ex -> {
                if (remaining <= 0) {
                    return CompletableFuture.failedFuture(ex);
                }
                
                log.warn("Retry, {} attempts left", remaining - 1);
                
                return CompletableFuture.delayedExecutor(delayMs, TimeUnit.MILLISECONDS)
                    .thenCompose(v -> 
                        attempt(operation, remaining - 1, 
                            (long)(delayMs * multiplier), multiplier)
                    );
            });
    }
}

// Использование
RetryUtil.withRetry(
    () -> httpClient.getAsync(url),
    3,
    1000,
    2.0  // exponential backoff
    // Чистый exponential backoff вызывает "thundering herd" — все повторные
    // запросы приходят одновременно. В production добавляйте jitter:
    // delayMs * (1 + random.nextDouble() * 0.5)
)
.thenAccept(System.out::println);

Best Practices

// ✅ Ограничивайте число попыток
withRetry(operation, 3);

// ✅ Экспоненциальная задержка
delayMs * Math.pow(2, attempt);

// ✅ Проверка типа ошибки
ex -> ex.getCause() instanceof TimeoutException;

// ❌ Бесконечный retry
// ❌ Retry для всех ошибок (не для всех имеет смысл)

🎯 Шпаргалка для интервью

Обязательно знать:

  • exceptionallyCompose (Java 12+) — возврат другого CF при ошибке, основа для retry
  • Экспоненциальная задержка: delayMs * multiplier^attempt, с jitter для production
  • Ограничение попыток: maxRetries уменьшается на 1 каждый раз
  • Проверка типа ошибки: retry только для retryable (Timeout, 5xx), не для всех
  • Resilience4j — production-ready решение с Circuit Breaker + Retry + Bulkhead

Частые уточняющие вопросы:

  • exceptionally vs exceptionallyCompose для retry? — exceptionally возвращает значение, compose возвращает CF (async retry)
  • Зачем jitter в backoff? — Без jitter все повторные запросы приходят одновременно (thundering herd)
  • delayedExecutor блокирует поток? — Нет, это ScheduledExecutor. Поток не блокируется во время ожидания
  • Java 8 retry без exceptionallyCompose? — Рекурсия в exceptionally: return withRetry(operation, retries - 1)

Красные флаги (НЕ говорить):

  • «Бесконечный retry — ок» — cascade failure при permanent ошибке
  • «Retry для всех ошибок» — 4xx не нужно retry, только 5xx и timeout
  • «delayedExecutor блокирует поток callback-а» — это scheduler, не блокирует

Связанные темы:

  • [[6. Как обрабатывать исключения в цепочке CompletableFuture]]
  • [[7. В чём разница между handle(), exceptionally() и whenComplete()]]
  • [[20. Как реализовать timeout для CompletableFuture]]
  • [[16. Как правильно выполнить несколько параллельных запросов к микросервисам]]