web-dev-qa-db-fra.com

Nouvelle tentative de logique avec CompletableFuture

J'ai besoin de soumettre une tâche dans un cadre asynchrone sur lequel je travaille, mais je dois intercepter les exceptions et réessayer la même tâche plusieurs fois avant de "suspendre".

Le code avec lequel je travaille est:

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;
}

Ceci ne compile pas actuellement car le type de retour du lambda est incorrect: il attend un Result, mais le executeActionAsync retourne un CompletableFuture<Result>.

Comment puis-je implémenter cette logique de nouvelle tentative asynchrone?

16
xmas79

Je pense que j'ai réussi. Voici un exemple de classe que j'ai créé et le code de test:


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;
    }
}

Usage

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...");

Production

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...

L'idée principale et du code de collage (fonction failAfter) viennent de ici .

Toutes autres suggestions ou améliorations sont les bienvenues.

6
xmas79

L'enchaînement des tentatives suivantes peut être simple:

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

Découvrez les inconvénients ci-dessous
Cela enchaîne simplement autant de tentatives que prévu, car ces étapes ultérieures ne feront rien dans le cas non exceptionnel.

Un inconvénient est que si la première tentative échoue immédiatement, de sorte que f est déjà terminée exceptionnellement lorsque le premier gestionnaire exceptionally est chaîné, l'action sera invoquée par le thread appelant, supprimant la nature asynchrone entièrement de la demande. Et généralement, join() peut bloquer un thread (l'exécuteur par défaut lancera alors un nouveau thread de compensation, mais il est déconseillé). Malheureusement, il n'y a ni méthode exceptionallyAsync ni exceptionallyCompose.

Une solution n'invoquant pas join() serait

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;
}

démontrant à quel point il est nécessaire de combiner "composer" et un gestionnaire "exceptionnellement".

En outre, seule la dernière exception sera signalée, si toutes les tentatives échouent. Une meilleure solution devrait signaler la première exception, avec les exceptions suivantes des tentatives ajoutées en tant qu'exceptions supprimées. Une telle solution peut être construite en enchaînant un appel récursif, comme l'indique réponse de Gili , cependant, afin d'utiliser cette idée pour la gestion des exceptions, nous devons utiliser les étapes pour combiner "composer" et " exceptionnellement "ci-dessus:

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 Est une méthode Java 9, mais il serait trivial d'ajouter un backport compatible Java 8 à votre code si nécessaire:

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

J'ai récemment résolu un problème similaire en utilisant la bibliothèque 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

Voici une approche qui fonctionnera pour n'importe quelle sous-classe CompletionStage et ne retourne pas un CompletableFuture factice qui ne fait rien de plus qu'attendre d'être mis à jour par d'autres futurs.

/**
 * 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

classe 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;
    }
}

usage:

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);
}

production

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
殷振南

Au lieu d'implémenter votre propre logique de nouvelle tentative, je recommande d'utiliser une bibliothèque éprouvée comme Failuresafe , qui a un support intégré pour les futurs (et semble plus populaire que guava-retrying ). Pour votre exemple, cela ressemblerait à quelque chose comme:

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

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

Vous devriez probablement éviter .withFallback(null) et simplement laisser la méthode .get() du futur renvoyé lever l'exception résultante pour que l'appelant de votre méthode puisse la gérer spécifiquement, mais c'est une décision de conception que vous aurez faire.

Vous devez également penser à savoir si vous devez réessayer immédiatement ou attendre un certain temps entre les tentatives, toute sorte d'interruption récursive (utile lorsque vous appelez un service Web qui peut être en panne) et s'il existe des exceptions spécifiques qui ne sont pas '' il vaut mieux réessayer (par exemple si les paramètres de la méthode ne sont pas valides).

1
theazureshadow