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

Как реализовать timeout для CompletableFuture

В Java 9+ есть встроенный метод orTimeout():

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

🟢 Junior Level

В Java 9+ есть встроенный метод orTimeout():

CompletableFuture<String> cf = fetchDataAsync();

// Таймаут 5 секунд
cf.orTimeout(5, TimeUnit.SECONDS)
  .thenAccept(result -> System.out.println(result))
  .exceptionally(ex -> {
      if (ex instanceof TimeoutException) {
          System.out.println("Timeout!");
      }
      return "fallback";
  });

Для Java 8 — можно реализовать вручную:

public static <T> CompletableFuture<T> withTimeout(
    CompletableFuture<T> cf, long timeout, TimeUnit unit
) {
    CompletableFuture<T> timeoutCF = new CompletableFuture<>();

    // ⚠️ Внимание: этот пример создаёт новый ScheduledThreadPool на каждый вызов.
    // В production используйте один общий scheduler или не забудьте scheduler.shutdown().
    ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    scheduler.schedule(() ->
        timeoutCF.completeExceptionally(new TimeoutException()),
        timeout, unit
    );
    
    return CompletableFuture.anyOf(cf, timeoutCF)
        .thenApply(result -> (T) result);
}

🟡 Middle Level

Java 9+ methods

orTimeout:

cf.orTimeout(5, TimeUnit.SECONDS)
  .exceptionally(ex -> {
      // TimeoutException
      return fallback;
  });

completeOnTimeout:

// При таймауте — fallback значение
cf.completeOnTimeout("default", 5, TimeUnit.SECONDS)
  .thenAccept(result -> System.out.println(result));

Java 8 implementation

public static <T> CompletableFuture<T> withTimeout(
    CompletableFuture<T> cf, long timeout, TimeUnit unit
) {
    CompletableFuture<T> result = new CompletableFuture<>();
    
    ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    
    // Schedule timeout
    ScheduledFuture<?> timeoutFuture = scheduler.schedule(() -> {
        result.completeExceptionally(new TimeoutException());
    }, timeout, unit);
    
    // Complete normally
    cf.whenComplete((value, ex) -> {
        timeoutFuture.cancel(false);
        if (ex != null) {
            result.completeExceptionally(ex);
        } else {
            result.complete(value);
        }
    });
    
    return result;
}

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

  1. Не закрыть scheduler: ```java // ❌ Scheduler не закрыт — memory leak ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

// ✅ Закрыть после использования scheduler.shutdown();


---

## 🔴 Senior Level

### Internal Implementation

**orTimeout internals:**
```java
// orTimeout регистрирует whenComplete обработчик, который при нормальном
// завершении вызывает scheduledFuture.cancel(false), отменяя отложенную задачу.
// Если таймаут срабатывает первым — completeExceptionally(TimeoutException)
// завершает CF, и цепочка коллбэков больше не выполняется.

Edge Cases

1. Timeout vs cancellation:

cf.orTimeout(5, TimeUnit.SECONDS);

// Timeout НЕ отменяет выполнение задачи!
// Задача продолжает выполняться в фоне
// Просто CF завершается с TimeoutException

2. Multiple timeouts:

// Последний timeout побеждает
cf.orTimeout(10, TimeUnit.SECONDS)
  .orTimeout(5, TimeUnit.SECONDS);  // этот сработает первым

Production Experience

Resilient service:

@Service
public class ResilientService {
    public CompletableFuture<Data> getData(String key) {
        return fetchDataAsync(key)
            .orTimeout(5, TimeUnit.SECONDS)
            .exceptionallyCompose(ex -> {
                if (ex instanceof TimeoutException) {
                    return getFromCacheAsync(key);
                }
                return CompletableFuture.failedFuture(ex);
            });
    }
}

Best Practices

// ✅ orTimeout для Java 9+
cf.orTimeout(5, TimeUnit.SECONDS);

// ✅ completeOnTimeout для fallback
cf.completeOnTimeout(defaultValue, 5, TimeUnit.SECONDS);

// ✅ Custom timeout для Java 8
withTimeout(cf, 5, TimeUnit.SECONDS);

// ❌ Забывать закрыть scheduler
// ❌ Игнорирование TimeoutException

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

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

  • orTimeout(timeout, unit) — Java 9+, завершает CF с TimeoutException
  • completeOnTimeout(value, timeout, unit) — Java 9+, fallback значение при таймауте
  • Таймаут НЕ отменяет задачу — она продолжает выполняться в фоне
  • Java 8: ScheduledExecutorService + anyOf(cf, timeoutCF)
  • Один общий scheduler для всех таймаутов, не новый на каждый вызов

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

  • orTimeout отменяет задачу? — Нет, CF завершается с TimeoutException, задача продолжает работать
  • completeOnTimeout vs orTimeout? — completeOnTimeout возвращает fallback, orTimeout бросает исключение
  • Java 8 таймаут без ScheduledExecutor? — Нет альтернативы. ScheduledExecutorService — единственный вариант
  • Multiple timeouts на один CF? — Последний (самый короткий) побеждает

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

  • «orTimeout отменяет выполнение задачи» — задача продолжает работать
  • «Создаю новый ScheduledThreadPool на каждый timeout» — memory leak, нужен один общий
  • «TimeoutException не нужно обрабатывать» — нужно fallback или retry

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

  • [[21. Что делает метод orTimeout() в Java 9+]]
  • [[10. Что делает метод anyOf() и в каких случаях он полезен]]
  • [[27. Как реализовать retry логику с помощью CompletableFuture]]
  • [[16. Как правильно выполнить несколько параллельных запросов к микросервисам]]