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