Питання 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:

// Останній таймаут перемагає
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. Як правильно виконати декілька паралельних запитів до мікросервісів]]