Как реализовать 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. Как правильно выполнить несколько параллельных запросов к микросервисам]]