web-dev-qa-db-ja.com

CompletableFutureでロジックを再試行する

作業中の非同期フレームワークでタスクを送信する必要がありますが、例外をキャッチし、「中止」する前に同じタスクを複数回再試行する必要があります。

私が使用しているコードは次のとおりです。

int retries = 0;
public CompletableFuture<Result> executeActionAsync() {

    // Execute the action async and get the future
    CompletableFuture<Result> f = executeMycustomActionHere();

    // If the future completes with exception:
    f.exceptionally(ex -> {
        retries++; // Increment the retry count
        if (retries < MAX_RETRIES)
            return executeActionAsync();  // <--- Submit one more time

        // Abort with a null value
        return null;
    });

    // Return the future    
    return f;
}

ラムダの戻り値の型が間違っているため、これは現在コンパイルされていません。Resultが必要ですが、executeActionAsyncCompletableFuture<Result>を返します。

この完全非同期再試行ロジックを実装するにはどうすればよいですか?

16
xmas79

私は成功したと思います。これが、私が作成したサンプルクラスとテストコードです。


RetriableTask.Java

public class RetriableTask
{
    protected static final int MAX_RETRIES = 10;
    protected int retries = 0;
    protected int n = 0;
    protected CompletableFuture<Integer> future = new CompletableFuture<Integer>();

    public RetriableTask(int number) {
        n = number;
    }

    public CompletableFuture<Integer> executeAsync() {
        // Create a failure within variable timeout
        Duration timeoutInMilliseconds = Duration.ofMillis(1*(int)Math.pow(2, retries));
        CompletableFuture<Integer> timeoutFuture = Utils.failAfter(timeoutInMilliseconds);

        // Create a dummy future and complete only if (n > 5 && retries > 5) so we can test for both completion and timeouts. 
        // In real application this should be a real future
        final CompletableFuture<Integer> taskFuture = new CompletableFuture<>();
        if (n > 5 && retries > 5)
            taskFuture.complete(retries * n);

        // Attach the failure future to the task future, and perform a check on completion
        taskFuture.applyToEither(timeoutFuture, Function.identity())
            .whenCompleteAsync((result, exception) -> {
                if (exception == null) {
                    future.complete(result);
                } else {
                    retries++;
                    if (retries >= MAX_RETRIES) {
                        future.completeExceptionally(exception);
                    } else {
                        executeAsync();
                    }
                }
            });

        // Return the future    
        return future;
    }
}

使用法

int size = 10;
System.out.println("generating...");
List<RetriableTask> tasks = new ArrayList<>();
for (int i = 0; i < size; i++) {
    tasks.add(new RetriableTask(i));
}

System.out.println("issuing...");
List<CompletableFuture<Integer>> futures = new ArrayList<>();
for (int i = 0; i < size; i++) {
    futures.add(tasks.get(i).executeAsync());
}

System.out.println("Waiting...");
for (int i = 0; i < size; i++) {
    try {
        CompletableFuture<Integer> future = futures.get(i);
        int result = future.get();
        System.out.println(i + " result is " + result);
    } catch (Exception ex) {
        System.out.println(i + " I got exception!");
    }
}
System.out.println("Done waiting...");

出力

generating...
issuing...
Waiting...
0 I got exception!
1 I got exception!
2 I got exception!
3 I got exception!
4 I got exception!
5 I got exception!
6 result is 36
7 result is 42
8 result is 48
9 result is 54
Done waiting...

主なアイデアといくつかのグルーコード(failAfter関数)は、 here に由来します。

他の提案や改善は大歓迎です。

6
xmas79

後続の再試行の連鎖は簡単です。

_public CompletableFuture<Result> executeActionAsync() {
    CompletableFuture<Result> f=executeMycustomActionHere();
    for(int i=0; i<MAX_RETRIES; i++) {
        f=f.exceptionally(t -> executeMycustomActionHere().join());
    }
    return f;
}
_

以下の欠点について読む
これらの後続のステージは例外的でない場合には何もしないので、これは意図した回数だけ再試行を連鎖します。

1つの欠点は、最初の試行がすぐに失敗し、最初のfハンドラーがチェーンされたときにexceptionallyがすでに例外的に完了した場合、呼び出し元のスレッドによってアクションが呼び出され、非同期の性質が削除されることですリクエストの完全に。通常、join()はスレッドをブロックする場合があります(デフォルトのエグゼキューターは新しい補正スレッドを開始しますが、それでもお勧めしません)。残念ながら、exceptionallyAsyncメソッドもexceptionallyComposeメソッドもありません。

join()を呼び出さないソリューションは

_public CompletableFuture<Result> executeActionAsync() {
    CompletableFuture<Result> f=executeMycustomActionHere();
    for(int i=0; i<MAX_RETRIES; i++) {
        f=f.thenApply(CompletableFuture::completedFuture)
           .exceptionally(t -> executeMycustomActionHere())
           .thenCompose(Function.identity());
    }
    return f;
}
_

「compose」と「exceptionally」ハンドラーの組み合わせがどのように関与するかを示します。

さらに、すべての再試行が失敗した場合、最後の例外のみが報告されます。より良い解決策は、最初の例外を報告し、その後の再試行の例外は抑制された例外として追加する必要があります。 Gili's answer で示唆されているように、このようなソリューションは再帰呼び出しを連鎖することで構築できますが、例外処理にこのアイデアを使用するには、「構成」と「構成」を組み合わせる手順を使用する必要があります上記のように例外的に」:

_public CompletableFuture<Result> executeActionAsync() {
    return executeMycustomActionHere()
        .thenApply(CompletableFuture::completedFuture)
        .exceptionally(t -> retry(t, 0))
        .thenCompose(Function.identity());
}
private CompletableFuture<Result> retry(Throwable first, int retry) {
    if(retry >= MAX_RETRIES) return CompletableFuture.failedFuture(first);
    return executeMycustomActionHere()
        .thenApply(CompletableFuture::completedFuture)
        .exceptionally(t -> { first.addSuppressed(t); return retry(first, retry+1); })
        .thenCompose(Function.identity());
}
_

_CompletableFuture.failedFuture_はJava 9メソッドですが、必要に応じてコードにJava 8互換のバックポートを追加するのは簡単です。

_public static <T> CompletableFuture<T> failedFuture(Throwable t) {
    final CompletableFuture<T> cf = new CompletableFuture<>();
    cf.completeExceptionally(t);
    return cf;
}
_
13
Holger

guava-retrying ライブラリを使用して、最近同様の問題を解決しました。

Callable<Result> callable = new Callable<Result>() {
    public Result call() throws Exception {
        return executeMycustomActionHere();
    }
};

Retryer<Boolean> retryer = RetryerBuilder.<Result>newBuilder()
        .retryIfResult(Predicates.<Result>isNull())
        .retryIfExceptionOfType(IOException.class)
        .retryIfRuntimeException()
        .withStopStrategy(StopStrategies.stopAfterAttempt(MAX_RETRIES))
        .build();

CompletableFuture.supplyAsync( () -> {
    try {
        retryer.call(callable);
    } catch (RetryException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
       e.printStackTrace();
    }
});
5
Alex Fargus

以下は、任意のCompletionStageサブクラスで機能し、他の先物によって更新されるまで待機するだけのダミーCompletableFutureを返さないアプローチです。

/**
 * Sends a request that may run as many times as necessary.
 *
 * @param request  a supplier initiates an HTTP request
 * @param executor the Executor used to run the request
 * @return the server response
 */
public CompletionStage<Response> asyncRequest(Supplier<CompletionStage<Response>> request, Executor executor)
{
    return retry(request, executor, 0);
}

/**
 * Sends a request that may run as many times as necessary.
 *
 * @param request  a supplier initiates an HTTP request
 * @param executor the Executor used to run the request
 * @param tries    the number of times the operation has been retried
 * @return the server response
 */
private CompletionStage<Response> retry(Supplier<CompletionStage<Response>> request, Executor executor, int tries)
{
    if (tries >= MAX_RETRIES)
        throw new CompletionException(new IOException("Request failed after " + MAX_RETRIES + " tries"));
    return request.get().thenComposeAsync(response ->
    {
        if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL)
            return retry(request, executor, tries + 1);
        return CompletableFuture.completedFuture(response);
    }, executor);
}
3
Gili

utilクラス:

public class RetryUtil {

    public static <R> CompletableFuture<R> retry(Supplier<CompletableFuture<R>> supplier, int maxRetries) {
        CompletableFuture<R> f = supplier.get();
        for(int i=0; i<maxRetries; i++) {
            f=f.thenApply(CompletableFuture::completedFuture)
                .exceptionally(t -> {
                    System.out.println("retry for: "+t.getMessage());
                    return supplier.get();
                })
                .thenCompose(Function.identity());
        }
        return f;
    }
}

使用法:

public CompletableFuture<String> lucky(){
    return CompletableFuture.supplyAsync(()->{
        double luckNum = Math.random();
        double luckEnough = 0.6;
        if(luckNum < luckEnough){
            throw new RuntimeException("not luck enough: " + luckNum);
        }
        return "I'm lucky: "+luckNum;
    });
}
@Test
public void testRetry(){
    CompletableFuture<String> retry = RetryUtil.retry(this::lucky, 10);
    System.out.println("async check");
    String join = retry.join();
    System.out.println("lucky? "+join);
}

出力

async check
retry for: Java.lang.RuntimeException: not luck enough: 0.412296354211683
retry for: Java.lang.RuntimeException: not luck enough: 0.4099777199676573
lucky? I'm lucky: 0.8059089479049389
1
殷振南

独自の再試行ロジックを実装する代わりに、 failsafe のような実績のあるライブラリを使用することをお勧めします。これは、将来の組み込みサポートを備えています(そして guava-retrying よりも人気があります)。たとえば、次のようになります。

_private static RetryPolicy retryPolicy = new RetryPolicy()
    .withMaxRetries(MAX_RETRIES);

public CompletableFuture<Result> executeActionAsync() {
    return Failsafe.with(retryPolicy)
        .with(executor)
        .withFallback(null)
        .future(this::executeMycustomActionHere);
}
_

おそらく.withFallback(null)を避け、返されたfutureの.get()メソッドに結果の例外をスローさせて、メソッドの呼び出し元が具体的に処理できるようにする必要がありますが、それは設計上の決定です作る。

考慮すべき他の事柄には、すぐに再試行するか、試行と試行の間の一定の時間を待つべきか、あらゆる種類の再帰的バックオフ(ダウンしている可能性のあるWebサービスを呼び出すときに有用)、および特定の例外がないかどうかが含まれます再試行する価値はありません(メソッドへのパラメーターが無効な場合など)。

1
theazureshadow