Retries on gateway non-GET requests
In the previous series, Spring Cloud upgrade path – Hoxton – 5. To implement microservice invocation retry, we set up retries for both OpenFeign and Spring Cloud Gateway.
For OpenFeign:
- Get request: any non-200 response code, any exception, will be retried.
- Non-get requests: any IOException (except SocketTimeOutException, which is caused by read time out) and redilience circuit breaker exceptions will be retried.
For Spring Cloud Gateway:
- Get request: any 4XX, 5XX response code, any exception, will be retried.
Now we need to implement any IOException (except SocketTimeOutException, which is caused by Read Time out) for non-GET requests to the Spring Cloud Gateway, There is also redilience circuit breaker exception retry, Get because the request was not actually sent.
The existing design
Currently, the RetryFilterFactory in Spring Cloud Gateway does not allow different retries for different exceptions for Get and non-GET:
org.springframework.cloud.gateway.filter.factory.RetryGatewayFilterFactory
public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<RetryGatewayFilterFactory.RetryConfig> { /** * Retry Iteration Key. ServerWebExchange The key of an Attribute * This Attribute is used to +1 each time the Attribute is called, */ public static final String RETRY_ITERATION_KEY ="retry_iteration";
public RetryGatewayFilterFactory() { super(RetryConfig.class); } @override public GatewayFilter apply(RetryConfig RetryConfig) {validate(); Repeat<ServerWebExchange> statusCodeRepeat = null; // If a retried HTTP response status code is configured, check whether the response code can be retriedif(! retryConfig.getStatuses().isEmpty() || ! retryConfig.getSeries().isEmpty()) { Predicate<RepeatContext<ServerWebExchange>> repeatPredicate = context -> { ServerWebExchange exchange = context.applicationContext(); // Check whether the number of retries has been exceededif (exceedsMaxIterations(exchange, retryConfig)) {
return false; } // Determine whether to retry HttpStatus statusCode = exchange.getresponse ().getStatusCode(); boolean retryableStatusCode = retryConfig.getStatuses() .contains(statusCode);if(! retryableStatusCode && statusCode ! = null) { // try the series retryableStatusCode = retryConfig.getSeries().stream() .anyMatch(series -> statusCode.series().equals(series)); } final boolean finalRetryableStatusCode = retryableStatusCode; HttpMethod HttpMethod = exchange.getrequest ().getmethod (); boolean retryableMethod = retryConfig.getMethods().contains(httpMethod); // Returns an HTTP response status code that indicates whether the method can be retried and whether it can be retriedreturnretryableMethod && finalRetryableStatusCode; }; // Each time you retry, the route is reset, Reanalysis of routing statusCodeRepeat = Repeat. OnlyIf (repeatPredicate). DoOnRepeat (context - > reset (context. The applicationContext ())); // Set Backoff BackoffConfig Backoff = retryconfig.getBackoff ();if(backoff ! = null) { statusCodeRepeat = statusCodeRepeat.backoff(getBackoff(backoff)); } } // TODO: support timeout, backoff, jitter, etc...inBuilder check whether the exception can be Retry<ServerWebExchange> exceptionRetry = null;if(! retryConfig.getExceptions().isEmpty()) { Predicate<RetryContext<ServerWebExchange>> retryContextPredicate = context -> { ServerWebExchange exchange = context.applicationContext();if (exceedsMaxIterations(exchange, retryConfig)) {
return false;
}
Throwable exception = context.exception();
for (Class<? extends Throwable> retryableClass : retryConfig
.getExceptions()) {
if(retryableClass.isInstance(exception) || (exception ! = null && retryableClass.isInstance(exception.getCause()))) { trace("exception or its cause is retryable %s, configured exceptions %s",
() -> getExceptionNameWithCause(exception),
retryConfig::getExceptions);
HttpMethod httpMethod = exchange.getRequest().getMethod();
boolean retryableMethod = retryConfig.getMethods()
.contains(httpMethod);
trace("retryableMethod: %b, httpMethod %s, configured methods %s",
() -> retryableMethod, () -> httpMethod,
retryConfig::getMethods);
return retryableMethod;
}
}
trace("exception or its cause is not retryable %s, configured exceptions %s",
() -> getExceptionNameWithCause(exception),
retryConfig::getExceptions);
return false;
};
exceptionRetry = Retry.onlyIf(retryContextPredicate)
.doOnRetry(context -> reset(context.applicationContext()))
.retryMax(retryConfig.getRetries());
BackoffConfig backoff = retryConfig.getBackoff();
if(backoff ! = null) { exceptionRetry = exceptionRetry.backoff(getBackoff(backoff)); } } GatewayFilter gatewayFilter = apply(retryConfig.getRouteId(), statusCodeRepeat, exceptionRetry);return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
return gatewayFilter.filter(exchange, chain);
}
@Override
public String toString() {
return filterToStringCreator(RetryGatewayFilterFactory.this)
.append("retries", retryConfig.getRetries())
.append("series", retryConfig.getSeries())
.append("statuses", retryConfig.getStatuses())
.append("methods", retryConfig.getMethods())
.append("exceptions", retryConfig.getExceptions()).toString(); }}; } private String getExceptionNameWithCause(Throwable exception) {if(exception ! = null) { StringBuilder builder = new StringBuilder(exception.getClass().getName()); Throwable cause = exception.getCause();if(cause ! = null) { builder.append("{cause=").append(cause.getClass().getName()).append("}");
}
return builder.toString();
}
else {
return "null";
}
}
private Backoff getBackoff(BackoffConfig backoff) {
returnBackoff.exponential(backoff.firstBackoff, backoff.maxBackoff, backoff.factor, backoff.basedOnPreviousValue); } public boolean exceedsMaxIterations(ServerWebExchange exchange, RetryConfig retryConfig) { Integer iteration = exchange.getAttribute(RETRY_ITERATION_KEY); // Whether the number of retries exceeds 7. Boolean = Iteration! = null && iteration >= retryConfig.getRetries();returnexceeds; } public void reset (ServerWebExchange exchange) {/ / this method is mainly Set < String > addedHeaders = exchange. GetAttributeOrDefault ( CLIENT_RESPONSE_HEADER_NAMES, Collections.emptySet()); addedHeaders .forEach(header -> exchange.getResponse().getHeaders().remove(header)); removeAlreadyRouted(exchange); } public GatewayFilter apply(String routeId, Repeat<ServerWebExchange> repeat, Retry<ServerWebExchange> retry) {if(routeId ! = null && getPublisher() ! = null) { // send an event toenable caching
getPublisher().publishEvent(new EnableBodyCachingEvent(this, routeId));
}
return (exchange, chain) -> {
trace("Entering retry-filter");
// chain.filter returns a Mono<Void>
Publisher<Void> publisher = chain.filter(exchange)
// .log("retry-filter", Level.INFO)
.doOnSuccessOrError((aVoid, throwable) -> {
int iteration = exchange
.getAttributeOrDefault(RETRY_ITERATION_KEY, -1);
int newIteration = iteration + 1;
trace("setting new iteration in attr %d", () -> newIteration);
exchange.getAttributes().put(RETRY_ITERATION_KEY, newIteration);
});
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);
};
}
@SuppressWarnings("unchecked") public static class RetryConfig implements HasRouteId {// routeId private String routeId; Private int retries = 3; private int retries = 3; HttpStatus 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, TimeoutException.class); // Retry interval policy private BackoffConfig backoff; public voidvalidate() {// The number of retries must be greater than 10 assert. isTrue(this.retries > 0,"retries must be greater than 0"); // The series that can be retried, the status code that can be retried, and the exception that can be retried cannot all be empty, otherwise there will be no retry scenarios. Assert.istrue (! this.series.isEmpty() || ! this.statuses.isEmpty() || ! this.exceptions.isEmpty(),"series, status and exceptions may not all be empty"); // Retry Http methods cannot be null assert.notempty (this.methods,"methods may not be empty");
if(this.backoff ! = null) { this.backoff.validate(); }} // omit constructor, getter, Public static class BackoffConfig {private Duration firstBackoff = duration.ofmillis (5); private Duration firstBackoff = duration.ofmillis (5); Private Duration maxBackoff; Private int factor = 2; Private Boolean basedOnPreviousValue = Specifies whether to retain the last request retry interval and retry from this intervaltrue; // omit constructor, getter, setter public voidvalidate() {// The first retry interval cannot be empty assert.notnull (this.firstbackoff,"firstBackoff must be present"); }}}Copy the code
In summary, the process is simplified as follows:
- Check whether the HTTP method is included in retryConfig. methods and whether the HTTP response code is in retryConfig. series or the set of statuses. Check the retry_Iteration Attribute for this request, and see if the number of retries has been exceeded. If not, retry. If so, stop retry.
- Check whether the HTTP method is included in retryConfig. methods and whether the exception is in the retryConfig. exceptions collection (or a subclass of one of them). Check the retry_Iteration Attribute for this request, and see if the number of retries has been exceeded. If not, retry. If so, stop retry.
If an HTTP method contains all methods when configured, there is no way to distinguish between GET and non-GET requests. If two filters are created, one intercepting GET and the other intercepting non-GET, the Attribute they share will be +2 each time and the number of retries will be inaccurate.
As a result, we ended up with an inelegant design where GET and non-get use different RetryConfig, and GET is still based on the application.properties configuration, forcing the following exceptions to retry for non-GET requests:
- Io.net ty. Channel. ConnectTimeoutException. Class: connection timed out
- Abnormal java.net.ConnectException.class:No route to host
- IO. Making. Resilience4j. Circuitbreaker. CallNotPermittedException: resilience4j circuit breaker associated abnormalities
RetryGatewayFilter
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String serviceName = request.getheaders ().getfirst (commonconstant.service_name); // obtain the micro serviceName. HttpMethod method = exchange.getRequest().getMethod(); / / generates GatewayFilter, save to gatewayFilterMap GatewayFilter GatewayFilter = gatewayFilterMap.com puteIfAbsent (serviceName +":"+ method, k -> { Map<String, RetryConfig> retryConfigMap = apiGatewayRetryConfig.getRetry(); / / by micro service name, access to retry the configuration RetryConfig RetryConfig = retryConfigMap. Either containsKey (serviceName)? retryConfigMap.get(serviceName) : apiGatewayRetryConfig.getDefault(); // If the number of retries is 0, the system does not retryif (retryConfig.getRetries() == 0) {
returnnull; } // For non-GET requests, enforce retries and only retry exception B belowif(! HttpMethod.GET.equals(method)) { RetryConfig newConfig = new RetryConfig(); BeanUtils.copyProperties(retryConfig, newConfig); // restrict all methods to retry. Newconfig.setmethods (httpmethod.values ()); newConfig.setSeries(); newConfig.setStatuses(); NewConfig. SetExceptions (/ / connection timeout io.net ty. Channel. ConnectTimeoutException. Class, / / No route to host java.net.ConnectException.class, / / in view of the abnormal CallNotPermittedException Resilience4j class); retryConfig = newConfig; }return this.apply(retryConfig);
});
returngatewayFilter ! = null ? gatewayFilter.filter(exchange, chain) : chain.filter(exchange); }Copy the code