Realization of Spring Cloud Gateway Retry Mechanism
- 2021-07-03 00:09:46
- OfStack
Preface
Try again, I believe everyone is no stranger. When we call the Http interface, it will always fail for some reason. At this time, we can re-request the interface by retrying.
There are many such cases in life, such as making a phone call, the other party is on the phone, the signal is not good, and so on. You will always be unable to get through. When you fail to get through for the first time, you will call for the second time, the third time... and the fourth time.
Retry should also pay attention to the application scenario. The interface for reading data is more suitable for retry scenario, and the interface for writing data should pay attention to the idempotence of the interface. Also, if the number of retries is too much, it will double the number of requests, which will cause greater pressure on the back end. Setting a reasonable retry mechanism is the most critical.
Today, let's take a brief look at the retry mechanism and use in Spring Cloud Gateway.
Use explanation
RetryGatewayFilter is one GatewayFilter Factory provided by Spring Cloud Gateway for request retry.
Configuration mode:
spring:
cloud:
gateway:
routes:
- id: fsh-house
uri: lb://fsh-house
predicates:
- Path=/house/**
filters:
- name: Retry
args:
retries: 3
series:
- SERVER_ERROR
statuses:
- OK
methods:
- GET
- POST
exceptions:
- java.io.IOException
Configuration explanation
Configuration class source code org. springframework. cloud. gateway. filter. factory. RetryGatewayFilterFactory. RetryConfig:
public static class RetryConfig {
private int retries = 3;
private List<Series> series = toList(Series.SERVER_ERROR);
private List<HttpStatus> statuses = new ArrayList<>();
private List<HttpMethod> methods = toList(HttpMethod.GET);
private List<Class<? extends Throwable>> exceptions = toList(IOException.class);
// .....
}
retries: Number of retries, default is 3
series: Status code configuration (segmentation), the retry logic will be carried out only when a certain section of status code is matched. The default value is SERVER_ERROR, and the value is 5, that is, 5XX (status code beginning with 5), with a total of 5 values:
public enum Series {
INFORMATIONAL(1),
SUCCESSFUL(2),
REDIRECTION(3),
CLIENT_ERROR(4),
SERVER_ERROR(5);
}
statuses: Configuration of status code. Different from series, here is the configuration of specific status code. Please refer to: org. springframework. http. HttpStatus
methods: Specifies which method requests require retry logic. The default value is the GET method with the following values:
public enum HttpMethod {
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
}
exceptions: Specifies which exceptions require retry logic, default is java. io. IOException
Code testing
Just write an interface, record the number of requests in the interface, and then throw an exception simulation 500, and access this interface through the gateway. If you configure the retry number to be 3, the interface will output 4 results, which is correct, proving that the retry is effective.
AtomicInteger ac = new AtomicInteger();
@GetMapping("/data")
public HouseInfo getData(@RequestParam("name") String name) {
if (StringUtils.isBlank(name)) {
throw new RuntimeException("error");
}
System.err.println(ac.addAndGet(1));
return new HouseInfo(1L, " Shanghai ", " Hongkou ", "XX Community ");
}
More Spring Cloud codes are available at: https://github.com/yinjihuan/spring-cloud
Appreciation of source code
@Override
public GatewayFilter apply(RetryConfig retryConfig) {
// Verify that the retry configuration is formatted correctly
retryConfig.validate();
Repeat<ServerWebExchange> statusCodeRepeat = null;
if (!retryConfig.getStatuses().isEmpty() || !retryConfig.getSeries().isEmpty()) {
Predicate<RepeatContext<ServerWebExchange>> repeatPredicate = context -> {
ServerWebExchange exchange = context.applicationContext();
// Determines whether the number of retries has reached the configured maximum
if (exceedsMaxIterations(exchange, retryConfig)) {
return false;
}
// Gets the status code of the response
HttpStatus statusCode = exchange.getResponse().getStatusCode();
// Get the request method type
HttpMethod httpMethod = exchange.getRequest().getMethod();
// Determining whether the response status code exists in the configuration
boolean retryableStatusCode = retryConfig.getStatuses().contains(statusCode);
if (!retryableStatusCode && statusCode != null) { // null status code might mean a network exception?
// try the series
retryableStatusCode = retryConfig.getSeries().stream()
.anyMatch(series -> statusCode.series().equals(series));
}
// Determine whether the method is included in the configuration
boolean retryableMethod = retryConfig.getMethods().contains(httpMethod);
// Decide whether to retry
return retryableMethod && retryableStatusCode;
};
statusCodeRepeat = Repeat.onlyIf(repeatPredicate)
.doOnRepeat(context -> reset(context.applicationContext()));
}
//TODO: support timeout, backoff, jitter, etc... in Builder
Retry<ServerWebExchange> exceptionRetry = null;
if (!retryConfig.getExceptions().isEmpty()) {
Predicate<RetryContext<ServerWebExchange>> retryContextPredicate = context -> {
if (exceedsMaxIterations(context.applicationContext(), retryConfig)) {
return false;
}
// Abnormal judgment
for (Class<? extends Throwable> clazz : retryConfig.getExceptions()) {
if (clazz.isInstance(context.exception())) {
return true;
}
}
return false;
};
// Use reactor extra Adj. retry Component
exceptionRetry = Retry.onlyIf(retryContextPredicate)
.doOnRetry(context -> reset(context.applicationContext()))
.retryMax(retryConfig.getRetries());
}
return apply(statusCodeRepeat, exceptionRetry);
}
public boolean exceedsMaxIterations(ServerWebExchange exchange, RetryConfig retryConfig) {
Integer iteration = exchange.getAttribute(RETRY_ITERATION_KEY);
//TODO: deal with null iteration
return iteration != null && iteration >= retryConfig.getRetries();
}
public void reset(ServerWebExchange exchange) {
//TODO: what else to do to reset SWE?
exchange.getAttributes().remove(ServerWebExchangeUtils.GATEWAY_ALREADY_ROUTED_ATTR);
}
public GatewayFilter apply(Repeat<ServerWebExchange> repeat, Retry<ServerWebExchange> retry) {
return (exchange, chain) -> {
if (log.isTraceEnabled()) {
log.trace("Entering retry-filter");
}
// chain.filter returns a Mono<Void>
Publisher<Void> publisher = chain.filter(exchange)
//.log("retry-filter", Level.INFO)
.doOnSuccessOrError((aVoid, throwable) -> {
// Gets the number of times that has been retried, the default value is -1
int iteration = exchange.getAttributeOrDefault(RETRY_ITERATION_KEY, -1);
// Increase the number of retries
exchange.getAttributes().put(RETRY_ITERATION_KEY, iteration + 1);
});
if (retry != null) {
// retryWhen returns a Mono<Void>
// retry needs to go before repeat
publisher = ((Mono<Void>)publisher).retryWhen(retry.withApplicationContext(exchange));
}
if (repeat != null) {
// repeatWhen returns a Flux<Void>
// so this needs to be last and the variable a Publisher<Void>
publisher = ((Mono<Void>)publisher).repeatWhen(repeat.withApplicationContext(exchange));
}
return Mono.fromDirect(publisher);
};
}