如何管理 CompletableFuture 的逾時
1. 概述
當我們建構依賴其他服務的服務時,我們經常需要處理依賴服務回應太慢的情況。
如果我們使用CompletableFuture
來非同步管理對依賴項的調用,它的逾時功能使我們能夠設定結果的最大等待時間。如果預期結果沒有在指定時間內到達,我們可以採取措施,例如提供預設值,以防止我們的應用程式陷入冗長的過程。
在本文中,我們將討論在CompletableFuture
中管理逾時的三種不同方法。
2. 管理超時
想像一個電子商務應用程式需要呼叫外部服務來獲取特殊產品優惠。我們可以使用具有超時設定的CompletableFuture
來保持回應能力。如果服務未能足夠快地回應,這可能會引發錯誤或提供預設值。
例如,在本例中,假設我們要向傳回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 "";
}
});
}
要使用 WireMock 測試逾時,我們可以按照 WireMock 使用指南輕鬆配置模擬伺服器逾時。假設典型網路連線上合理的網頁載入時間為 1000 毫秒,因此我們將DEFAULT_TIMEOUT
設定為該值:
private static final int DEFAULT_TIMEOUT = 1000; // 1 seconds
然後,我們將建立一個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. 使用completeOnTimeout()
如果任務未在指定時間內完成,則completeOnTimeout()
方法將使用預設值解析CompletableFuture
。
透過這個方法,我們可以設定超時時回傳的預設值<T>
。此方法傳回呼叫此方法的CompletableFuture
。
在此範例中,我們預設為DEFAULT_PRODUCT
:
CompletableFuture<Integer> productDataFuture = fetchProductData();
productDataFuture.completeOnTimeout(DEFAULT_PRODUCT, DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS);
assertEquals(DEFAULT_PRODUCT, productDataFuture.get());
如果我們的目標是即使在請求期間失敗或逾時的情況下結果仍然有意義,那麼這種方法是合適的。
例如在電商情境中,展示商品促銷時,如果取得特價促銷商品資料失敗或超過逾時時間,系統會顯示預設商品。
4.使用orTimeout()
如果 future 未在特定時間內完成,我們可以使用orTimeout()
來增強CompletableFuture
的逾時處理行為。
此方法傳回應用此方法的相同CompletableFuture
,並在逾時的情況下拋出TimeoutException
。
然後,為了測試這個方法,我們應該使用assertThrows()
來證明引發了異常:
CompletableFuture<Integer> productDataFuture = fetchProductData();
productDataFuture.orTimeout(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS);
assertThrows(ExecutionException.class, productDataFuture::get);
如果我們的優先順序是回應能力或耗時的任務,並且我們希望在發生逾時時提供快速操作,那麼這是一個合適的方法。
但是,為了獲得良好的效能,需要正確處理這些異常,因為此方法會明確引發異常。
此外,此方法適用於各種場景,例如管理網路連線、處理 IO 操作、處理即時資料和管理佇列。
5. 使用completeExceptionally()
CompletableFuture
類別的completeExceptionally()
方法讓我們可以透過特定的異常異常地完成Future。對結果檢索方法(如get()
和join()
的後續呼叫將引發指定的異常。
如果方法呼叫導致CompletableFuture
轉換為完成狀態,則此方法傳回true
。否則,它會傳回false
。
在這裡,我們將使用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);
如果我們需要處理TimeoutException
以及其他exceptions
,或者我們想要自訂或特定它,也許這是一個合適的方法。我們通常使用這種方法來處理失敗的資料驗證、致命錯誤或任務沒有預設值的情況。
6. 比較: completeOnTimeout()
vs orTimeout()
completeExceptionally()
所有這些方法,我們都可以管理和控制CompletableFuture
在不同場景中的行為,特別是在處理需要計時和處理逾時或錯誤的非asynchronous
操作時。
讓我們來比較一下completeOnTimeout(), orTimeout(),
和completeExceptionally()
的優缺點:
方法 | 優點 | 缺點 |
completeOnTimeout() |
| 沒有明確標記發生超時 |
orTimeout() |
| 不提供替換預設結果的選項 |
completeExceptionally() |
| 比管理超時更通用 |
七、結論
在本文中,我們研究了在CompletableFuture
內的非同步進程中回應逾時的三種不同方法。
在選擇我們的方法時,我們應該考慮管理長時間運行的任務的需求。我們應該在預設值之間做出決定,指示具有特定異常的非同步操作逾時。
與往常一樣,完整的源代碼可以在 GitHub 上取得。