io.temporal.internal.retryer.GrpcAsyncRetryer Maven / Gradle / Ivy
/*
* Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved.
*
* Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Modifications copyright (C) 2017 Uber Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this material except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.temporal.internal.retryer;
import io.grpc.Context;
import io.grpc.Deadline;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.temporal.api.workflowservice.v1.GetSystemInfoResponse;
import io.temporal.internal.BackoffThrottler;
import io.temporal.serviceclient.RpcRetryOptions;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class GrpcAsyncRetryer {
private static final Logger log = LoggerFactory.getLogger(GrpcRetryer.class);
private final ScheduledExecutorService executor;
private final GrpcRetryer.GrpcRetryerOptions options;
private final GetSystemInfoResponse.Capabilities serverCapabilities;
private final Supplier> function;
private final BackoffThrottler throttler;
private final Deadline retriesExpirationDeadline;
private StatusRuntimeException lastMeaningfulException = null;
public GrpcAsyncRetryer(
ScheduledExecutorService asyncThrottlerExecutor,
Supplier> function,
GrpcRetryer.GrpcRetryerOptions options,
GetSystemInfoResponse.Capabilities serverCapabilities) {
options.validate();
this.executor = asyncThrottlerExecutor;
this.options = options;
this.serverCapabilities = serverCapabilities;
this.function = function;
RpcRetryOptions rpcOptions = options.getOptions();
this.retriesExpirationDeadline =
GrpcRetryerUtils.mergeDurationWithAnAbsoluteDeadline(
rpcOptions.getExpiration(), options.getDeadline());
this.throttler =
new BackoffThrottler(
rpcOptions.getInitialInterval(),
rpcOptions.getCongestionInitialInterval(),
rpcOptions.getMaximumInterval(),
rpcOptions.getBackoffCoefficient(),
rpcOptions.getMaximumJitterCoefficient());
}
public CompletableFuture retry() {
CompletableFuture resultCF = new CompletableFuture<>();
retry(resultCF);
return resultCF;
}
private void retry(CompletableFuture resultCF) {
CompletableFuture throttleFuture = new CompletableFuture<>();
@SuppressWarnings({"FutureReturnValueIgnored", "unused"})
ScheduledFuture> ignored =
executor.schedule(
// preserving gRPC context between threads
Context.current().wrap(() -> throttleFuture.complete(null)),
throttler.getSleepTime(),
TimeUnit.MILLISECONDS);
throttleFuture.thenAccept(
(ignore) -> {
if (lastMeaningfulException != null) {
log.debug("Retrying after failure", lastMeaningfulException);
}
// try-catch is because get() call might throw.
try {
CompletableFuture result = function.get();
if (result == null) result = CompletableFuture.completedFuture(null);
result.whenComplete(
(r, e) -> {
if (e == null) {
throttler.success();
resultCF.complete(r);
} else {
throttler.failure(
(e instanceof StatusRuntimeException)
? ((StatusRuntimeException) e).getStatus().getCode()
: Status.Code.UNKNOWN);
failOrRetry(e, resultCF);
}
});
} catch (Throwable e) {
throttler.failure(
(e instanceof StatusRuntimeException)
? ((StatusRuntimeException) e).getStatus().getCode()
: Status.Code.UNKNOWN);
// function isn't supposed to throw exceptions, it should always return a
// CompletableFuture even if it's a failed one.
// But if this happens - process the same way as it would be an exception from
// completable future
// Do not retry if it's not StatusRuntimeException
failOrRetry(e, resultCF);
}
});
}
private void failOrRetry(Throwable currentException, CompletableFuture resultCF) {
// If exception is thrown from CompletionStage/CompletableFuture methods like compose or handle
// - it gets wrapped into CompletionException, so here we need to unwrap it. We can get not
// wrapped raw exception here too if CompletableFuture was explicitly filled with this exception
// using CompletableFuture.completeExceptionally
currentException = unwrapCompletionException(currentException);
// Do not retry if it's not StatusRuntimeException
if (!(currentException instanceof StatusRuntimeException)) {
resultCF.completeExceptionally(currentException);
return;
}
StatusRuntimeException statusRuntimeException = (StatusRuntimeException) currentException;
RuntimeException finalException =
GrpcRetryerUtils.createFinalExceptionIfNotRetryable(
statusRuntimeException, options.getOptions(), serverCapabilities);
if (finalException != null) {
log.debug("Final exception, throwing", finalException);
resultCF.completeExceptionally(finalException);
return;
}
this.lastMeaningfulException =
GrpcRetryerUtils.lastMeaningfulException(statusRuntimeException, lastMeaningfulException);
if (GrpcRetryerUtils.ranOutOfRetries(
options.getOptions(),
this.throttler.getAttemptCount(),
this.retriesExpirationDeadline,
Context.current().getDeadline())) {
log.debug("Out of retries, throwing", lastMeaningfulException);
resultCF.completeExceptionally(lastMeaningfulException);
} else {
retry(resultCF);
}
}
private static Throwable unwrapCompletionException(Throwable e) {
return e instanceof CompletionException ? e.getCause() : e;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy