How To Manage Timeout for CompletableFuture – 如何管理 CompletableFuture 的超时

最后修改: 2023年 10月 27日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

When we’re building a service that depends on other services, we often need to handle cases where a dependent service responds too slowly.

在构建依赖于其他服务的服务时,我们经常需要处理依赖服务响应过慢的情况。

If we use CompletableFuture to manage the calls to our dependencies asynchronously, its timeout capabilities enable us to set a maximum waiting time for the result. If the expected result doesn’t arrive within the specified time, we can take action, such as providing a default value, to prevent our application from getting stuck in a lengthy process.

如果我们使用 CompletableFuture 来异步管理对依赖关系的调用,其超时功能可让我们设置结果的最长等待时间。如果预期结果未在指定时间内到达,我们可以采取措施(如提供默认值),以防止应用程序陷入冗长的进程。

In this article, we’ll discuss three different ways to manage timeouts in CompletableFuture.

在本文中,我们将讨论在 CompletableFuture 中管理超时的三种不同方法。

2. Manage Timeout

2.管理超时

Imagine an e-commerce app that requires calls to external services for special product offers. We can use CompletableFuture with timeout settings to maintain responsiveness. This can throw errors or provide a default value if a service fails to respond quickly enough.

试想一下,电子商务应用程序需要调用外部服务来提供特殊产品优惠。我们可以使用带有超时设置的 CompletableFuture 来保持响应速度。如果服务未能足够快地响应,则可以抛出错误或提供默认值。

For example, in this case, let’s say we are going to make a request to an API that returns PRODUCT_OFFERS. Let’s call it fetchProductData(), which we can wrap with a CompletableFuture so we can handle the timeout:

例如,在本例中,假设我们将向返回 PRODUCT_OFFERS 的 API 发起请求。让我们调用 fetchProductData(),我们可以用 CompletableFuture 对其进行封装,这样就可以处理超时:

private CompletableFuture<String> fetchProductData() {
    return CompletableFuture.supplyAsync(() -> {
        try {
            URL url = new URL("http://localhost:8080/api/dummy");
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();

            try (BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
                String inputLine;
                StringBuffer response = new StringBuffer();

                while ((inputLine = in.readLine()) != null) {
                    response.append(inputLine);
                }

                return response.toString();
            } finally {
                connection.disconnect();
            }
        } catch (IOException e) {
            return "";
        }
    });
}

To test timeouts using WireMock, we can easily configure a mock server for timeouts by following the WireMock usage guide. Let’s assume a reasonable web page load time on a typical internet connection is 1000 milliseconds, so we set a DEFAULT_TIMEOUT to that value:

要使用 WireMock 测试超时,我们可以按照 WireMock 使用指南轻松配置一个用于超时的模拟服务器。让我们假设典型 Internet 连接上合理的网页加载时间为 1000 毫秒,因此我们将 DEFAULT_TIMEOUT 设置为该值:

private static final int DEFAULT_TIMEOUT = 1000; // 1 seconds

Then, we’ll create a wireMockServer that gives a body response of PRODUCT_OFFERS and set a delay of 5000 milliseconds or 5 seconds, ensuring that this value exceeds DEFAULT_TIMEOUT to ensure a timeout occurs:

然后,我们将创建一个 wireMockServer,给出 PRODUCT_OFFERS 的正文响应,并设置 5000 毫秒或 5 秒的延迟,确保该值超过 DEFAULT_TIMEOUT 以确保超时发生:

stubFor(get(urlEqualTo("/api/dummy"))
  .willReturn(aResponse()
    .withFixedDelay(5000) // must be > DEFAULT_TIMEOUT for a timeout to occur.
    .withBody(PRODUCT_OFFERS)));

3. Using completeOnTimeout()

3.使用 completeOnTimeout()

The completeOnTimeout() method resolves the CompletableFuture with a default value if the task doesn’t finish within the specified time.

如果任务未在指定时间内完成,completeOnTimeout() 方法将使用默认值解析 CompletableFuture

With this method, we can set the default value <T> to return when a timeout occurs. This method returns the CompletableFuture on which this method is invoked.

通过此方法,我们可以设置超时时返回的默认值<T>。此方法返回调用此方法的 CompletableFuture

In this example, let’s default to DEFAULT_PRODUCT:

在此示例中,我们默认使用 DEFAULT_PRODUCT

CompletableFuture<Integer> productDataFuture = fetchProductData();
productDataFuture.completeOnTimeout(DEFAULT_PRODUCT, DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS);
assertEquals(DEFAULT_PRODUCT, productDataFuture.get());

If we aim for results that remain meaningful even in the event of a failure or timeout during the request, then this approach is appropriate.

如果我们的目标是即使在请求失败或超时的情况下,结果仍然有意义,那么这种方法就是合适的。

In an e-commerce scenario, for instance, when presenting product promotions, if retrieving special promo product data fails or exceeds the timeout, the system displays the default product instead.

例如,在电子商务场景中,当显示产品促销信息时,如果检索特殊促销产品数据失败或超时,系统就会显示默认产品。

4. Using orTimeout()

4.使用 orTimeout()

We can use orTimeout() to augment a CompletableFuture with timeout handling behavior if the future is not completed within a specific time.

我们可以使用 orTimeout() 来增强 CompletableFuture 的超时处理行为,如果将来时未在特定时间内完成。

This method returns the same CompletableFuture on which this method is applied and will throw a TimeoutException in case of a timeout.

此方法返回与应用此方法的 CompletableFuture 相同的 CompletableFuture,如果超时, 将抛出 TimeoutException 异常

Then, to test this method, we should use assertThrows() to prove an exception was raised:

然后,为了测试这个方法,我们应该使用 assertThrows() 来证明出现了异常:

CompletableFuture<Integer> productDataFuture = fetchProductData();
productDataFuture.orTimeout(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS);
assertThrows(ExecutionException.class, productDataFuture::get);

If our priority is responsiveness or time-consuming tasks and we want to provide quick action when a timeout occurs, then this is a suitable approach.

如果我们优先考虑的是响应速度或耗时的任务,并且希望在超时发生时提供快速操作,那么这种方法是合适的。

However, proper handling of these exceptions is required for good performance because this method explicitly throws exceptions.

不过,由于该方法会明确抛出异常,因此需要妥善处理这些异常,以获得良好的性能。

Additionally, the approach is applicable in various scenarios, such as managing network connections, handling IO operations, processing real-time data, and managing queues.

此外,该方法还适用于各种场景,如管理网络连接、处理 IO 操作、处理实时数据和管理队列。

5. Using completeExceptionally()

5.使用 completeExceptionally()

The CompletableFuture class’s completeExceptionally() method lets us complete the future exceptionally with a specific exception. Subsequent calls to result retrieval methods like get() and join() will throw the specified exception.

CompletableFuture 类的 completeExceptionally() 方法可让我们通过特定异常完成 future。对结果检索方法(如 get()join() )的后续调用将抛出指定的异常。

This method returns true if the method call resulted in the transition of the CompletableFuture to a completed state. Otherwise, it returns false.

如果方法调用导致 CompletableFuture 过渡到完成状态,则此方法返回 true。否则,它将返回 false

Here, we’ll use ScheduledExecutorService, which is an interface in Java used to schedule and manage task executions at specific times or with delays. It provides flexibility in scheduling recurring tasks, handling timeouts, and managing errors in a concurrent environment:

在此,我们将使用 ScheduledExecutorService这是 Java 中的一个接口,用于在特定时间或延迟时间调度和管理任务执行。它提供了在并发环境中调度重复任务、处理超时和管理错误的灵活性:

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
//...
CompletableFuture<Integer> productDataFuture = fetchProductData();
executorService.schedule(() -> productDataFuture.completeExceptionally(
  new TimeoutException("Timeout occurred")), DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS);
assertThrows(ExecutionException.class, productDataFuture::get);

If we need to handle TimeoutException along with other exceptions or we want to make it custom or specific, maybe this is a suitable way. We commonly use this approach to handle failed data validation, fatal errors, or when a task does not have a default value.

如果我们需要将 TimeoutException 与其他 异常一起处理,或者我们想将其自定义或特定,也许这是一种合适的方法。我们通常使用这种方法来处理数据验证失败、致命错误或任务没有默认值的情况。

6. Comparison: completeOnTimeout() vs orTimeout() vs completeExceptionally()

6.比较:completeOnTimeout() vs orTimeout() vs completeExceptionally()

With all these methods, we can manage and control the behavior of CompletableFuture in different scenarios, especially when working with asynchronous operations that require timing and handling timeouts or errors.

通过所有这些方法,我们可以管理和控制 CompletableFuture 在不同场景中的行为,尤其是在处理需要计时和处理超时或错误的 异步操作时。

Let’s compare the advantages and disadvantages of the completeOnTimeout(), orTimeout(), and completeExceptionally():

让我们比较一下 completeOnTimeout()、orTimeout() completeExceptionally() 的优缺点:

Method Advantages Disadvantages
completeOnTimeout()

Allowed to replace the default result if a long-running task takes too long

如果长时间运行的任务耗时过长,允许替换默认结果

Useful for avoiding timeouts without throwing exceptions  

用于避免超时而不抛出 异常</em

Doesn’t explicitly mark that a timeout occurred
orTimeout()

Generates a TimeoutException explicitly when a timeout occurs

当超时发生时,显式生成 TimeoutException 异常

Can handle timeouts in a specific manner

能以特定方式处理超时

Doesn’t provide an option to replace the default result
completeExceptionally()

Allowed to explicitly mark the result with a custom exception

允许使用自定义 异常明确标记结果

Useful for indicating failure in asynchronous operations

用于在 异步操作中指示失败

More general purpose than managing timeouts

7. Conclusion

7.结论

In this article, we’ve looked at three different ways of responding to a timeout in an asynchronous process inside CompletableFuture.

在本文中,我们探讨了在 CompletableFuture 内异步进程中响应超时的三种不同方法。

When selecting our approach, we should consider our needs for managing long-running tasks. We should decide between default values, indicating asynchronous operation timeouts with specific exceptions.

在选择方法时,我们应考虑管理长期运行任务的需要。我们应该在默认值、指示异步操作超时和特定异常之间做出选择。

As always, the full source code is available over on GitHub.

与往常一样,完整的源代码可在 GitHub 上获取