Як реалізувати retry логіку за допомогою CompletableFuture
Structured Java interview answer with junior, middle, and senior-level explanation.
🟢 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);
Типові помилки
- Нескінченний 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. Як правильно виконати декілька паралельних запитів до мікросервісів]]