Vert.X CompositeFuture 用法

Robothy發表於2022-05-01

CompositeFuture 是一種特殊的 Future,它可以包裝一個 Future 列表,從而讓一組非同步操作並行執行;然後協調這一組操作的結果,作為 CompositeFuture 的結果。本文將介紹 CompositeFuture 的用法以及幾種協調方式,掌握這些內容有助於解決多個 Future 的整合問題。

1. 概念

開發人員從兩個方面關注一個 Future:狀態和結果。CompositeFuture 是 Future,同樣要從這兩個方面關注它。

CompositeFuture 的結果(訊息)型別是 CompositeFuture 本身,也就是說 CompositeFuture#result() 返回值型別固定是 CompositeFuture。它可以用來表示一組結果,可以通過 list() causes() 一次性讀取一組結果或異常,也可以通過resultAt(index)cause(index)讀取單個結果或異常。

CompositeFuture cf = CompositeFuture.all(future1, future2);
cf.onSuccess(res -> {
    CompositeFuture result = cf.result(); // 它的結果也是 CompositeFuture 型別
    List<Double> results = result.list(); // 一次性讀取一組結果
    List<Throwable> cause = result.causes(); // 一次性讀取一組異常
    Double r1 = result.resultAt(0); // 讀取第 0 個 Future 的結果
    Throwbale t1 = result.cause(0); // 讀取第 0 個 Future 的異常
});

CompositeFuture 的狀態隨著列表中 Future 狀態的變化而變化。例如:當列表中所有 Future 都成功完成時,CompositeFuture 才是成功完成;或者,當列表中只要有一個 Future 成功完成時,CompositeFuture 就是成功完成。

CompositeFuture 作為 Future 協調器,提供了 all, any, join 三種協調方式。所謂協調,就是把一組 Future 包裝成一個新的 Future,即 CompositeFuture,它的狀態隨著所包裝列表中 Future 狀態的變化而變化。

下面通過幾種場景示例詳細介紹這幾種協調方式。

2. all

假如有一個金融系統,它有一個功能是統計某個客戶的銀行存款總額,需要向各個不同的銀行系統傳送餘額查詢請求,然後把這些請求的返回結果相加作為最終結果。一種高效的方式是並行傳送請求,然後在所有請求都有結果時將結果彙總。使用 CompositeFuture 的 all 操作可以很方便地達到這個目的。

Future<Double> f1 = queryBalance("bank_a", "Robothy");
Future<Double> f2 = queryBalance("bank_b", "Robothy");
CompositeFuture future = CompositeFuture.all(f1, f2);
future.map(results -> { // results 是一個 CompositeFuture
    Double balance1 = results.resultAt(0);
    Double balance2 = results.resultAt(1);
    return balance1 + balance2;
}).onSuccess(totalBalance -> System.out.println("Total balance is " + totalBalance))
  .onFailure(Throwable::printStackTrace);

其中 queryBalance 的方法簽名:

Future<Double> queryBalance(String bank, String username);

queryBalance 返回的是 Future,CompositeFuture#all() 把兩個 Future 包裝了起來,返回一個 CompositeFuture,當兩個 Future 都成功完成時,這個 CompositeFuture 才算成功完成;隨後轉換操作 map 所設定的同步函式得以執行,通過 resultAt 方法讀取結果,並將結果相加,作為總餘額返回。此外,兩個 queryBalance 並行執行。

上面這個例子只從 2 個銀行獲取餘額,實際上,每個客戶開會行的數量是不定的,一個客戶通常對應著一個銀行列表。對於這種情況,可以將多個 Future 放到一個 List 當中,再通過 CompositeFuture#all(List<Future> futures)方法對一個 Future 列表進行包裝。

List<String> banks = Arrays.asList("bank1", "bank2", "bank3", ...);
List<Future> futureList = banks.stream().map(bank -> queryBalance(bank, "Robothy"))
    .collect(Collectors.toList());
CompositeFuture.all(futureList)
    .map(results -> {
        Double totalBalance = 0.0;
        List<Double> balanceList = results.list();
        for (Double balance : balanceList) {
            totalBalance += balance;
        }
        return totalBalance;
    })
    .onSuccess(totalBalance -> System.out.println("Total balance is " + totalBalance))
  	.onFailure(Throwable::printStackTrace);

上面程式碼中,客戶 "Robothy" 對應著一個銀行列表,通過流操作把銀行列表轉化為 List<Future> futureList。隨後通過 CompositeFuture#all 對這個 Future 列表進行包裝,當列表中的 Future 全部成功完成時,all 返回的 CompositeFuture 才算成功完成。在讀取結果的時候,這裡使用了 list()方法一次性讀取所有 Future 的結果。

CompositeFuture 不僅能夠協調結果型別相同的多個 Future,還可以協調結果型別不同的 Future。例如:下面例子以並行的方式獲取使用者資訊和賬戶資訊。

Future<User> f1 = getUserByName("Robothy");
Future<Account> f2 = getAccountByName("Robothy");
CompositeFuture.all(f1, f2)
    .onSuccess(results -> {
        User user = resuts.resultAt(0);
        Account account = results.resultAt(1);
    })
    .onFailure(Throwable::printStackTrace);

通過 CompositeFuture#all 包裝得到 Future 列表都已成功完成時,all() 返回的 CompositeFuture 才算完成;當其中一個 Future 失敗時,CompositeFuture 立即失敗。

3. join

join 是另一種協調方式,當它包裝的所有 Future 都已成功完成時,join() 返回的 CompositeFuture 才算成功完成。當其中一個 Future 失敗,CompositeFuture 在等待所有 Future 都(成功或失敗)完成之後失敗。

下面這個例子的目的是找到網路延遲最小的伺服器。

List<String> hosts = Arrays.asList("10.0.1.123", "10.0.1.124", "10.0.1.125", ...);
// 非同步函式 ping 將 host 對映為 Future<Long>
List<Future> futures = hosts.stream().map(host -> ping(host)).collect(Collectors.toList());
CompositeFuture cf = CompositeFuture.join(futures);
cf.onComplete(res -> { // 這裡是 onComplete,無論 cf 成功還是失敗,都將執行。
    CompositeFuture delays = res.result();
    Long minDelay = Long.MAX_VALUE;
    int minDelayIdx = -1;
    for (int i=0; i<delays.size(); i++) {
        Long delay = delays.resultAt(i);
        if (null != delay) { // 第 i 個 Future 成功完成(如果失敗, delay 為 null)
            if (delay < minDelay) {
                minDelay = delay;
                minDelayIdx = i;
            }
        }
    }
    
    if (minDelayIdx != -1) {
        System.out.println("Min delay: " + minDelay + ", server: " + hosts.get(minDelayIdx));
    } else {
        System.out.println("All servers are unreachable.");
    }
})
.onFailure(Throwable::printStackTrace);

join 和 all 的區別在於:當包裝的 Future 列表中有 1 個失敗時,all() 得到的 CompositeFuture 立即失敗,而join()所包裝的 CompositeFuture 會等待列表中的所有 Future 都完成時才失敗。

此外,CompositeFuture 包裝了多個 Future,意味著可能會有多個失敗的 Future,而 Future#onFailure 只能夠處理一個異常物件。這個異常物件是失敗 Future 中,索引號最小的 Throwable 物件,並非最先失敗的 Future 對應的 Throwable。例如下面這個例子總是輸出 "f1 error"。

CompositeFuture.join(
        Future.future(promise -> promise.fail(new RuntimeException("f1 error"))),
        Future.future(promise -> promise.fail(new RuntimeException("f2 error")))
    )
     .onFailure(cause -> System.err.println(cause.getMessage())); // 總是輸出 f1 error
  }

要處理每個異常物件,需要在 onComplete 設定的處理器中進行操作。

CompositeFuture.join(f1, f2, ...)
    .onComplete(res -> {
        CompositeFuture results = res.result();
        for (int i=0; i<results.size(); i++) {
            Throwbale cause = results.cause(i);
            if (null != cause) { // 第 i 個 Future 失敗了
                cause.printStackTrace();
            }
        }
    });

4. any

all 和 join 都需要在列表中所有的 Future 都成功的情況下,CompositeFuture 才算成功,而 any 只要有一個 Future 成功了,CompositeFuture 就會立即成功完成;當所有 Future 都失敗了,CompositeFuture 才是失敗。

下面這個例子表示客戶端向一個分散式系統的多個副本節點傳送相同的訊息,只要有一個節點返回成功,則表示訊息傳送成功。

String msg = "Hello";
List<String> hosts = Arrays.asList("10.0.0.1", "10.0.0.2", ...);
List<Future> futures = hosts.stream().map(host -> send(host, msg)).collect(Collectors.toList());
CompositeFuture.any(futures)
    .onSuccess(results -> {
        for (int i=0; i<results.size(); i++) {
            if (null != results.resultAt(i)) {
                System.out.println(hosts.get(i) + " received message.");
            }
        }
    })
    .onFailure(Throwable::printStackTrace);

5. 小結

CompositeFuture 作為 Future 的子介面,和普通 Future 一樣可以處理訊息和轉化為另一個 Future 的能力。特殊的是,它的訊息型別也是 CompositeFuture,它的狀態隨著所包裝的若干 Future 狀態的變化而變化。

CompositFuture 包裝 Future 的方式或者說協調方式有三種:all, join, any。需要根據不同的應用場景來選擇不同的包裝方式。

  • all: 所有的 Future 都成功完成時,才算成功完成;只要有一個 Future 失敗,則立即失敗;
  • join: 所有的 Future 都成功完成時,才算成功完成;有 Future 失敗時,等待所有的 Future 都完成才完成;
  • any: 只要有一個 Future 成功完成,則立即成功完成;所有 Future 都失敗時,才算失敗

相關文章