Логику С класс 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, а executeActionAsync возвращает CompletableFuture<Result>.

как я могу реализовать эту полностью асинхронную логику повтора?

5 ответов


цепочка последующих попыток может быть прямой:

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

читайте о недостатках ниже
Это просто цепляет столько попыток, сколько предполагалось, поскольку эти последующие этапы ничего не сделают в не исключительном случае.

один недостаток заключается в том, что если первая попытка не удается сразу же, так что f уже завершено исключительно, когда первый exceptionally обработчик прикован, действие будет вызвано вызывающий поток, полностью удаляющий асинхронный характер запроса. И вообще, join() может блокировать поток (исполнитель по умолчанию запустит новый поток компенсации, но все же это не рекомендуется). К сожалению, нет ни того, ни другого, an 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" обработчика.

далее, только последний исключение будет сообщено, если все попытки завершились неудачно. Лучшее решение должно сообщать о первом исключении, с последующими исключениями попыток, добавленных как подавленные исключения. Такое решение может быть построено путем цепочки рекурсивного вызова, на что намекает Гили!--28-->, однако, чтобы использовать эта идея для обработки исключений, мы должны использовать шаги по объединению "compose "и" exceptionally", показанные выше:

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 совместимый backport в ваш код, если это необходимо:

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

недавно я решил аналогичную проблему, используя 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();
    }
});

думаю, мне это удалось. Вот пример класса, который я создал и тестовый код:


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

основная идея и некоторый код клея () от здесь.

любые другие предложения или улучшения приветствуются.


вот подход, который будет работать для любого 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);
}

вместо реализации собственной логики повтора я рекомендую использовать проверенную библиотеку, такую как ФС, который имеет встроенную поддержку фьючерсов (и кажется более популярным, чем 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) и просто позвольте возвращенному будущему .get() метод вызывает результирующее исключение, поэтому вызывающий метод может обрабатывать его специально, но это дизайн тебе придется принять решение.

другие вещи, о которых нужно подумать, включают ли вы должны немедленно повторить попытку или подождать некоторый период времени между попытками, какой-либо рекурсивный отход (полезный, когда вы вызываете веб-службу, которая может быть отключена), и есть ли конкретные исключения, которые не стоит повторять (например, если параметры метода недействительны).