Питання 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. Як правильно виконати декілька паралельних запитів до мікросервісів]]