Add Reactive retries for SC LoadBalancer (#847)
* Implement retry logic. * Fix retrying on next instance when RetryExhausted in same instance. * Fix retrying on next instance when RetryExhausted in same instance. * Fix retrying on next instance when RetryExhausted in same instance. * Move duplicated methods to utility class. Fix checkstyle. * Fix test. * Add more tests. * Fix test. * Add autoConfiguration. * Refactor and add javadocs. * Add javadocs. * Use RetryAwareServiceInstanceListSupplier with reactive retries. * Update properties. * Fix the docs. * Rename utility class. * Verify interactions in order.
This commit is contained in:
committed by
GitHub
parent
1195581e02
commit
7f23cb852f
@@ -32,6 +32,10 @@
|
||||
|spring.cloud.loadbalancer.health-check.path | |
|
||||
|spring.cloud.loadbalancer.hint | | Allows setting the value of <code>hint</code> that is passed on to the LoadBalancer request and can subsequently be used in {@link ReactiveLoadBalancer} implementations.
|
||||
|spring.cloud.loadbalancer.retry.avoid-previous-instance | true | Enables wrapping ServiceInstanceListSupplier beans with `RetryAwareServiceInstanceListSupplier` if Spring-Retry is in the classpath.
|
||||
|spring.cloud.loadbalancer.retry.backoff.enabled | false | Indicates whether Reactor Retry backoffs should be applied.
|
||||
|spring.cloud.loadbalancer.retry.backoff.jitter | 0.5 | Used to set {@link RetryBackoffSpec#jitter}.
|
||||
|spring.cloud.loadbalancer.retry.backoff.max-backoff | | Used to set {@link RetryBackoffSpec#maxBackoff}.
|
||||
|spring.cloud.loadbalancer.retry.backoff.min-backoff | 5ms | Used to set {@link RetryBackoffSpec#minBackoff}.
|
||||
|spring.cloud.loadbalancer.retry.enabled | true |
|
||||
|spring.cloud.loadbalancer.retry.max-retries-on-next-service-instance | 1 | Number of retries to be executed on the next <code>ServiceInstance</code>. A <code>ServiceInstance</code> is chosen before each retry call.
|
||||
|spring.cloud.loadbalancer.retry.max-retries-on-same-service-instance | 0 | Number of retries to be executed on the same <code>ServiceInstance</code>.
|
||||
|
||||
@@ -437,11 +437,13 @@ Then, `ReactiveLoadBalancer` is used underneath.
|
||||
|
||||
A load-balanced `RestTemplate` can be configured to retry failed requests.
|
||||
By default, this logic is disabled.
|
||||
You can enable it by adding link:https://github.com/spring-projects/spring-retry[Spring Retry] to your application's classpath.
|
||||
For the non-reactive version (with `RestTemplate`), you can enable it by adding link:https://github.com/spring-projects/spring-retry[Spring Retry] to your application's classpath. For the reactive version (with `WebTestClient), you need to set `spring.cloud.loadbalancer.retry.enabled=false`.
|
||||
|
||||
If you would like to disable the retry logic with Spring Retry on the classpath, you can set `spring.cloud.loadbalancer.retry.enabled=false`.
|
||||
If you would like to disable the retry logic with Spring Retry or Reactive Retry on the classpath, you can set `spring.cloud.loadbalancer.retry.enabled=false`.
|
||||
|
||||
If you would like to implement a `BackOffPolicy` in your retries, you need to create a bean of type `LoadBalancedRetryFactory` and override the `createBackOffPolicy()` method.
|
||||
For the non-reactive implementation, if you would like to implement a `BackOffPolicy` in your retries, you need to create a bean of type `LoadBalancedRetryFactory` and override the `createBackOffPolicy()` method.
|
||||
|
||||
For the reactive implementation, you just need to enable it by setting `spring.cloud.loadbalancer.retry.backoff.enabled` to `false`.
|
||||
|
||||
You can set:
|
||||
|
||||
@@ -449,6 +451,13 @@ You can set:
|
||||
- `spring.cloud.loadbalancer.retry.maxRetriesOnNextServiceInstance` - indicates how many times a request should be retried a newly selected `ServiceInstance`
|
||||
- `spring.cloud.loadbalancer.retry.retryableStatusCodes` - the status codes on which to always retry a failed request.
|
||||
|
||||
For the reactive implementation, you can additionally set:
|
||||
- `spring.cloud.loadbalancer.retry.backoff.minBackoff` - Sets the minimum backoff duration (by default, 5 milliseconds)
|
||||
- `spring.cloud.loadbalancer.retry.backoff.maxBackoff` - Sets the maximum backoff duration (by default, max long value of milliseconds)
|
||||
- `spring.cloud.loadbalancer.retry.backoff.jitter` - Sets the jitter used for calculationg the actual backoff duration for each call (by default, 0.5).
|
||||
|
||||
For the reactive implementation, you can also implement your own `LoadBalancerRetryPolicy` to have more detailed control over the load-balanced call retries.
|
||||
|
||||
NOTE: For load-balanced retries, by default, we wrap the `ServiceInstanceListSupplier` bean with `RetryAwareServiceInstanceListSupplier` to select a different instance from the one previously chosen, if available. You can disable this behavior by setting the value of `spring.cloud.loadbalancer.retry.avoidPreviousInstance` to `false`.
|
||||
|
||||
====
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.cloud.client.loadbalancer;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
|
||||
/**
|
||||
@@ -36,4 +37,8 @@ public class ClientRequestContext extends DefaultRequestContext {
|
||||
return (ClientRequest) super.getClientRequest();
|
||||
}
|
||||
|
||||
public HttpMethod method() {
|
||||
return ((ClientRequest) super.getClientRequest()).method();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://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 org.springframework.cloud.client.loadbalancer.reactive;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
|
||||
|
||||
/**
|
||||
* A utility class for load-balanced {@link ExchangeFilterFunction} instances.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public final class ExchangeFilterFunctionUtils {
|
||||
|
||||
private ExchangeFilterFunctionUtils() {
|
||||
throw new IllegalStateException("Can't instantiate a utility class.");
|
||||
}
|
||||
|
||||
static String getHint(String serviceId, Map<String, String> hints) {
|
||||
String defaultHint = hints.getOrDefault("default", "default");
|
||||
String hintPropertyValue = hints.get(serviceId);
|
||||
return hintPropertyValue != null ? hintPropertyValue : defaultHint;
|
||||
}
|
||||
|
||||
static ClientRequest buildClientRequest(ClientRequest request, URI uri) {
|
||||
return ClientRequest.create(request.method(), uri).headers(headers -> headers.addAll(request.headers()))
|
||||
.cookies(cookies -> cookies.addAll(request.cookies()))
|
||||
.attributes(attributes -> attributes.putAll(request.attributes())).body(request.body()).build();
|
||||
}
|
||||
|
||||
static String serviceInstanceUnavailableMessage(String serviceId) {
|
||||
return "LoadBalancer does not contain an instance for the service " + serviceId;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://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 org.springframework.cloud.client.loadbalancer.reactive;
|
||||
|
||||
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
|
||||
|
||||
/**
|
||||
* A marker interface for load-balanced {@link ExchangeFilterFunction} instances.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public interface LoadBalancedExchangeFilterFunction extends ExchangeFilterFunction {
|
||||
|
||||
}
|
||||
@@ -58,8 +58,8 @@ public class LoadBalancerBeanPostProcessorAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
DeferringLoadBalancerExchangeFilterFunction<ReactorLoadBalancerExchangeFilterFunction> reactorDeferringLoadBalancerExchangeFilterFunction(
|
||||
ObjectProvider<ReactorLoadBalancerExchangeFilterFunction> exchangeFilterFunctionProvider) {
|
||||
DeferringLoadBalancerExchangeFilterFunction<LoadBalancedExchangeFilterFunction> reactorDeferringLoadBalancerExchangeFilterFunction(
|
||||
ObjectProvider<LoadBalancedExchangeFilterFunction> exchangeFilterFunctionProvider) {
|
||||
return new DeferringLoadBalancerExchangeFilterFunction<>(exchangeFilterFunctionProvider);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import reactor.util.retry.RetryBackoffSpec;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.util.LinkedCaseInsensitiveMap;
|
||||
@@ -47,7 +49,7 @@ public class LoadBalancerProperties {
|
||||
private Map<String, String> hint = new LinkedCaseInsensitiveMap<>();
|
||||
|
||||
/**
|
||||
* Properties for Spring-Retry support in Spring Cloud LoadBalancer.
|
||||
* Properties for Spring-Retry and Reactor Retry support in Spring Cloud LoadBalancer.
|
||||
*/
|
||||
private Retry retry = new Retry();
|
||||
|
||||
@@ -141,6 +143,11 @@ public class LoadBalancerProperties {
|
||||
*/
|
||||
private Set<Integer> retryableStatusCodes = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Properties for Reactor Retry backoffs in Spring Cloud LoadBalancer.
|
||||
*/
|
||||
private Backoff backoff = new Backoff();
|
||||
|
||||
/**
|
||||
* Returns true if the load balancer should retry failed requests.
|
||||
* @return True if the load balancer should retry failed requests; false
|
||||
@@ -190,6 +197,70 @@ public class LoadBalancerProperties {
|
||||
this.retryableStatusCodes = retryableStatusCodes;
|
||||
}
|
||||
|
||||
public Backoff getBackoff() {
|
||||
return backoff;
|
||||
}
|
||||
|
||||
public void setBackoff(Backoff backoff) {
|
||||
this.backoff = backoff;
|
||||
}
|
||||
|
||||
public static class Backoff {
|
||||
|
||||
/**
|
||||
* Indicates whether Reactor Retry backoffs should be applied.
|
||||
*/
|
||||
private boolean enabled = false;
|
||||
|
||||
/**
|
||||
* Used to set {@link RetryBackoffSpec#minBackoff}.
|
||||
*/
|
||||
private Duration minBackoff = Duration.ofMillis(5);
|
||||
|
||||
/**
|
||||
* Used to set {@link RetryBackoffSpec#maxBackoff}.
|
||||
*/
|
||||
private Duration maxBackoff = Duration.ofMillis(Long.MAX_VALUE);
|
||||
|
||||
/**
|
||||
* Used to set {@link RetryBackoffSpec#jitter}.
|
||||
*/
|
||||
private double jitter = 0.5d;
|
||||
|
||||
public Duration getMinBackoff() {
|
||||
return minBackoff;
|
||||
}
|
||||
|
||||
public void setMinBackoff(Duration minBackoff) {
|
||||
this.minBackoff = minBackoff;
|
||||
}
|
||||
|
||||
public Duration getMaxBackoff() {
|
||||
return maxBackoff;
|
||||
}
|
||||
|
||||
public void setMaxBackoff(Duration maxBackoff) {
|
||||
this.maxBackoff = maxBackoff;
|
||||
}
|
||||
|
||||
public double getJitter() {
|
||||
return jitter;
|
||||
}
|
||||
|
||||
public void setJitter(double jitter) {
|
||||
this.jitter = jitter;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://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 org.springframework.cloud.client.loadbalancer.reactive;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
|
||||
/**
|
||||
* Stores the data for a load-balanced call that is being retried.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 3.0.0
|
||||
*/
|
||||
class LoadBalancerRetryContext {
|
||||
|
||||
private final ClientRequest request;
|
||||
|
||||
private ClientResponse clientResponse;
|
||||
|
||||
private Integer retriesSameServiceInstance = 0;
|
||||
|
||||
private Integer retriesNextServiceInstance = 0;
|
||||
|
||||
LoadBalancerRetryContext(ClientRequest request) {
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
ClientRequest getRequest() {
|
||||
return request;
|
||||
}
|
||||
|
||||
ClientResponse getClientResponse() {
|
||||
return clientResponse;
|
||||
}
|
||||
|
||||
void setClientResponse(ClientResponse clientResponse) {
|
||||
this.clientResponse = clientResponse;
|
||||
}
|
||||
|
||||
Integer getRetriesSameServiceInstance() {
|
||||
return retriesSameServiceInstance;
|
||||
}
|
||||
|
||||
void incrementRetriesSameServiceInstance() {
|
||||
retriesSameServiceInstance++;
|
||||
}
|
||||
|
||||
void resetRetriesSameServiceInstance() {
|
||||
retriesSameServiceInstance = 0;
|
||||
}
|
||||
|
||||
Integer getRetriesNextServiceInstance() {
|
||||
return retriesNextServiceInstance;
|
||||
}
|
||||
|
||||
void incrementRetriesNextServiceInstance() {
|
||||
retriesNextServiceInstance++;
|
||||
}
|
||||
|
||||
Integer getResponseStatusCode() {
|
||||
return clientResponse.statusCode().value();
|
||||
}
|
||||
|
||||
HttpMethod getRequestMethod() {
|
||||
return request.method();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://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 org.springframework.cloud.client.loadbalancer.reactive;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
|
||||
/**
|
||||
* Pluggable policy used to establish whether a given load-balanced call should be
|
||||
* retried.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public interface LoadBalancerRetryPolicy {
|
||||
|
||||
/**
|
||||
* Return <code>true</code> to retry on the same service instance.
|
||||
* @param context the context for the retry operation
|
||||
* @return true to retry on the same service instance
|
||||
*/
|
||||
boolean canRetrySameServiceInstance(LoadBalancerRetryContext context);
|
||||
|
||||
/**
|
||||
* Return <code>true</code> to retry on the next service instance.
|
||||
* @param context the context for the retry operation
|
||||
* @return true to retry on the same service instance
|
||||
*/
|
||||
boolean canRetryNextServiceInstance(LoadBalancerRetryContext context);
|
||||
|
||||
/**
|
||||
* Return <code>true</code> to retry on the provided HTTP status code.
|
||||
* @param statusCode the HTTP status code
|
||||
* @return true to retry on the provided HTTP status code
|
||||
*/
|
||||
boolean retryableStatusCode(int statusCode);
|
||||
|
||||
/**
|
||||
* Return <code>true</code> to retry on the provided HTTP method.
|
||||
* @param method the HTTP request method
|
||||
* @return true to retry on the provided HTTP method
|
||||
*/
|
||||
boolean canRetryOnMethod(HttpMethod method);
|
||||
|
||||
}
|
||||
@@ -19,6 +19,8 @@ package org.springframework.cloud.client.loadbalancer.reactive;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -38,10 +40,28 @@ import org.springframework.web.reactive.function.client.WebClient;
|
||||
public class ReactorLoadBalancerClientAutoConfiguration {
|
||||
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "false",
|
||||
matchIfMissing = true)
|
||||
@Bean
|
||||
public ReactorLoadBalancerExchangeFilterFunction loadBalancerExchangeFilterFunction(
|
||||
ReactiveLoadBalancer.Factory loadBalancerFactory, LoadBalancerProperties properties) {
|
||||
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory, LoadBalancerProperties properties) {
|
||||
return new ReactorLoadBalancerExchangeFilterFunction(loadBalancerFactory, properties);
|
||||
}
|
||||
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true")
|
||||
@Bean
|
||||
public RetryableLoadBalancerExchangeFilterFunction retryableLoadBalancerExchangeFilterFunction(
|
||||
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory, LoadBalancerProperties properties,
|
||||
LoadBalancerRetryPolicy retryPolicy) {
|
||||
return new RetryableLoadBalancerExchangeFilterFunction(retryPolicy, loadBalancerFactory, properties);
|
||||
}
|
||||
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true")
|
||||
@Bean
|
||||
public LoadBalancerRetryPolicy loadBalancerRetryPolicy(LoadBalancerProperties properties) {
|
||||
return new RetryableExchangeFilterFunctionLoadBalancerRetryPolicy(properties);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import org.springframework.cloud.client.loadbalancer.DefaultRequest;
|
||||
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycleValidator;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools;
|
||||
import org.springframework.cloud.client.loadbalancer.Request;
|
||||
import org.springframework.cloud.client.loadbalancer.Response;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -39,6 +38,11 @@ import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFunction;
|
||||
|
||||
import static org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools.reconstructURI;
|
||||
import static org.springframework.cloud.client.loadbalancer.reactive.ExchangeFilterFunctionUtils.buildClientRequest;
|
||||
import static org.springframework.cloud.client.loadbalancer.reactive.ExchangeFilterFunctionUtils.getHint;
|
||||
import static org.springframework.cloud.client.loadbalancer.reactive.ExchangeFilterFunctionUtils.serviceInstanceUnavailableMessage;
|
||||
|
||||
/**
|
||||
* An {@link ExchangeFilterFunction} that uses {@link ReactiveLoadBalancer} to execute
|
||||
* requests against a correct {@link ServiceInstance}.
|
||||
@@ -47,7 +51,7 @@ import org.springframework.web.reactive.function.client.ExchangeFunction;
|
||||
* @since 2.2.0
|
||||
*/
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public class ReactorLoadBalancerExchangeFilterFunction implements ExchangeFilterFunction {
|
||||
public class ReactorLoadBalancerExchangeFilterFunction implements LoadBalancedExchangeFilterFunction {
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(ReactorLoadBalancerExchangeFilterFunction.class);
|
||||
|
||||
@@ -76,7 +80,7 @@ public class ReactorLoadBalancerExchangeFilterFunction implements ExchangeFilter
|
||||
.getSupportedLifecycleProcessors(
|
||||
loadBalancerFactory.getInstances(serviceId, LoadBalancerLifecycle.class),
|
||||
ClientRequestContext.class, ClientResponse.class, ServiceInstance.class);
|
||||
String hint = getHint(serviceId);
|
||||
String hint = getHint(serviceId, properties.getHint());
|
||||
DefaultRequest<ClientRequestContext> lbRequest = new DefaultRequest<>(
|
||||
new ClientRequestContext(clientRequest, hint));
|
||||
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
|
||||
@@ -94,7 +98,7 @@ public class ReactorLoadBalancerExchangeFilterFunction implements ExchangeFilter
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug(String.format("Load balancer has retrieved the instance for service %s: %s", serviceId,
|
||||
LOG.debug(String.format("LoadBalancer has retrieved the instance for service %s: %s", serviceId,
|
||||
instance.getUri()));
|
||||
}
|
||||
ClientRequest newRequest = buildClientRequest(clientRequest, reconstructURI(instance, originalUrl));
|
||||
@@ -108,10 +112,6 @@ public class ReactorLoadBalancerExchangeFilterFunction implements ExchangeFilter
|
||||
});
|
||||
}
|
||||
|
||||
protected URI reconstructURI(ServiceInstance instance, URI original) {
|
||||
return LoadBalancerUriTools.reconstructURI(instance, original);
|
||||
}
|
||||
|
||||
protected Mono<Response<ServiceInstance>> choose(String serviceId, Request<ClientRequestContext> request) {
|
||||
ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerFactory.getInstance(serviceId);
|
||||
if (loadBalancer == null) {
|
||||
@@ -120,20 +120,4 @@ public class ReactorLoadBalancerExchangeFilterFunction implements ExchangeFilter
|
||||
return Mono.from(loadBalancer.choose(request));
|
||||
}
|
||||
|
||||
private String serviceInstanceUnavailableMessage(String serviceId) {
|
||||
return "Load balancer does not contain an instance for the service " + serviceId;
|
||||
}
|
||||
|
||||
private ClientRequest buildClientRequest(ClientRequest request, URI uri) {
|
||||
return ClientRequest.create(request.method(), uri).headers(headers -> headers.addAll(request.headers()))
|
||||
.cookies(cookies -> cookies.addAll(request.cookies()))
|
||||
.attributes(attributes -> attributes.putAll(request.attributes())).body(request.body()).build();
|
||||
}
|
||||
|
||||
private String getHint(String serviceId) {
|
||||
String defaultHint = properties.getHint().getOrDefault("default", "default");
|
||||
String hintPropertyValue = properties.getHint().get(serviceId);
|
||||
return hintPropertyValue != null ? hintPropertyValue : defaultHint;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://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 org.springframework.cloud.client.loadbalancer.reactive;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
|
||||
/**
|
||||
* The default implementation of {@link LoadBalancerRetryPolicy}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class RetryableExchangeFilterFunctionLoadBalancerRetryPolicy implements LoadBalancerRetryPolicy {
|
||||
|
||||
private final LoadBalancerProperties properties;
|
||||
|
||||
public RetryableExchangeFilterFunctionLoadBalancerRetryPolicy(LoadBalancerProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRetrySameServiceInstance(LoadBalancerRetryContext context) {
|
||||
return context.getRetriesSameServiceInstance() < properties.getRetry().getMaxRetriesOnSameServiceInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRetryNextServiceInstance(LoadBalancerRetryContext context) {
|
||||
return context.getRetriesNextServiceInstance() < properties.getRetry().getMaxRetriesOnNextServiceInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean retryableStatusCode(int statusCode) {
|
||||
return properties.getRetry().getRetryableStatusCodes().contains(statusCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRetryOnMethod(HttpMethod method) {
|
||||
return HttpMethod.GET.equals(method) || properties.getRetry().isRetryOnAllOperations();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://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 org.springframework.cloud.client.loadbalancer.reactive;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.Exceptions;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
import reactor.util.retry.RetrySpec;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.ClientRequestContext;
|
||||
import org.springframework.cloud.client.loadbalancer.CompletionContext;
|
||||
import org.springframework.cloud.client.loadbalancer.DefaultRequest;
|
||||
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycleValidator;
|
||||
import org.springframework.cloud.client.loadbalancer.Request;
|
||||
import org.springframework.cloud.client.loadbalancer.Response;
|
||||
import org.springframework.cloud.client.loadbalancer.RetryableRequestContext;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFunction;
|
||||
|
||||
import static org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools.reconstructURI;
|
||||
import static org.springframework.cloud.client.loadbalancer.reactive.ExchangeFilterFunctionUtils.buildClientRequest;
|
||||
import static org.springframework.cloud.client.loadbalancer.reactive.ExchangeFilterFunctionUtils.getHint;
|
||||
import static org.springframework.cloud.client.loadbalancer.reactive.ExchangeFilterFunctionUtils.serviceInstanceUnavailableMessage;
|
||||
|
||||
/**
|
||||
* An {@link ExchangeFilterFunction} that uses {@link ReactiveLoadBalancer} to execute
|
||||
* requests against a correct {@link ServiceInstance} and Reactor Retries to retry the
|
||||
* call both against the same and the next service instance, based on the provided
|
||||
* {@link LoadBalancerRetryPolicy}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class RetryableLoadBalancerExchangeFilterFunction implements LoadBalancedExchangeFilterFunction {
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(RetryableLoadBalancerExchangeFilterFunction.class);
|
||||
|
||||
private static final List<Class<? extends Throwable>> exceptions = Arrays.asList(IOException.class,
|
||||
TimeoutException.class,
|
||||
org.springframework.cloud.client.loadbalancer.reactive.RetryableStatusCodeException.class);
|
||||
|
||||
private final LoadBalancerRetryPolicy retryPolicy;
|
||||
|
||||
private final LoadBalancerProperties properties;
|
||||
|
||||
private final ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory;
|
||||
|
||||
public RetryableLoadBalancerExchangeFilterFunction(LoadBalancerRetryPolicy retryPolicy,
|
||||
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory, LoadBalancerProperties properties) {
|
||||
this.retryPolicy = retryPolicy;
|
||||
this.loadBalancerFactory = loadBalancerFactory;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
@Override
|
||||
public Mono<ClientResponse> filter(ClientRequest clientRequest, ExchangeFunction next) {
|
||||
LoadBalancerRetryContext loadBalancerRetryContext = new LoadBalancerRetryContext(clientRequest);
|
||||
Retry exchangeRetry = buildRetrySpec(properties.getRetry().getMaxRetriesOnSameServiceInstance(), true);
|
||||
Retry filterRetry = buildRetrySpec(properties.getRetry().getMaxRetriesOnNextServiceInstance(), false);
|
||||
|
||||
URI originalUrl = clientRequest.url();
|
||||
String serviceId = originalUrl.getHost();
|
||||
if (serviceId == null) {
|
||||
String message = String.format("Request URI does not contain a valid hostname: %s", originalUrl.toString());
|
||||
if (LOG.isWarnEnabled()) {
|
||||
LOG.warn(message);
|
||||
}
|
||||
return Mono.just(ClientResponse.create(HttpStatus.BAD_REQUEST).body(message).build());
|
||||
}
|
||||
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
|
||||
.getSupportedLifecycleProcessors(
|
||||
loadBalancerFactory.getInstances(serviceId, LoadBalancerLifecycle.class),
|
||||
ClientRequestContext.class, ClientResponse.class, ServiceInstance.class);
|
||||
String hint = getHint(serviceId, properties.getHint());
|
||||
DefaultRequest<RetryableRequestContext> lbRequest = new DefaultRequest<>(
|
||||
new RetryableRequestContext(null, clientRequest, hint));
|
||||
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
|
||||
return Mono.defer(() -> choose(serviceId, lbRequest).flatMap(lbResponse -> {
|
||||
ServiceInstance instance = lbResponse.getServer();
|
||||
lbRequest.setContext(new RetryableRequestContext(instance, clientRequest, hint));
|
||||
if (instance == null) {
|
||||
String message = serviceInstanceUnavailableMessage(serviceId);
|
||||
if (LOG.isWarnEnabled()) {
|
||||
LOG.warn(message);
|
||||
}
|
||||
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
|
||||
.onComplete(new CompletionContext<>(CompletionContext.Status.DISCARD, lbResponse)));
|
||||
return Mono.just(ClientResponse.create(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.body(serviceInstanceUnavailableMessage(serviceId)).build());
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug(String.format("LoadBalancer has retrieved the instance for service %s: %s", serviceId,
|
||||
instance.getUri()));
|
||||
}
|
||||
ClientRequest newRequest = buildClientRequest(clientRequest, reconstructURI(instance, originalUrl));
|
||||
return next.exchange(newRequest)
|
||||
.doOnError(throwable -> supportedLifecycleProcessors.forEach(
|
||||
lifecycle -> lifecycle.onComplete(new CompletionContext<ClientResponse, ServiceInstance>(
|
||||
CompletionContext.Status.FAILED, throwable, lbResponse))))
|
||||
.doOnSuccess(clientResponse -> supportedLifecycleProcessors.forEach(
|
||||
lifecycle -> lifecycle.onComplete(new CompletionContext<ClientResponse, ServiceInstance>(
|
||||
CompletionContext.Status.SUCCESS, lbResponse, clientResponse))))
|
||||
.map(clientResponse -> {
|
||||
loadBalancerRetryContext.setClientResponse(clientResponse);
|
||||
if (shouldRetrySameServiceInstance(loadBalancerRetryContext)) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug(String.format("Retrying on status code: %d",
|
||||
clientResponse.statusCode().value()));
|
||||
}
|
||||
throw new RetryableStatusCodeException();
|
||||
}
|
||||
return clientResponse;
|
||||
|
||||
});
|
||||
}).map(clientResponse -> {
|
||||
loadBalancerRetryContext.setClientResponse(clientResponse);
|
||||
if (shouldRetryNextServiceInstance(loadBalancerRetryContext)) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug(String.format("Retrying on status code: %d", clientResponse.statusCode().value()));
|
||||
}
|
||||
throw new RetryableStatusCodeException();
|
||||
}
|
||||
return clientResponse;
|
||||
|
||||
}).retryWhen(exchangeRetry)).retryWhen(filterRetry);
|
||||
}
|
||||
|
||||
private Retry buildRetrySpec(int max, boolean transientErrors) {
|
||||
LoadBalancerProperties.Retry.Backoff backoffProperties = properties.getRetry().getBackoff();
|
||||
if (backoffProperties.isEnabled()) {
|
||||
return RetrySpec.backoff(max, backoffProperties.getMinBackoff()).filter(this::isRetryException)
|
||||
.maxBackoff(backoffProperties.getMaxBackoff()).jitter(backoffProperties.getJitter())
|
||||
.transientErrors(transientErrors);
|
||||
}
|
||||
return RetrySpec.max(max).filter(this::isRetryException).transientErrors(transientErrors);
|
||||
}
|
||||
|
||||
private boolean shouldRetrySameServiceInstance(LoadBalancerRetryContext loadBalancerRetryContext) {
|
||||
boolean shouldRetry = retryPolicy.retryableStatusCode(loadBalancerRetryContext.getResponseStatusCode())
|
||||
&& retryPolicy.canRetryOnMethod(loadBalancerRetryContext.getRequestMethod())
|
||||
&& retryPolicy.canRetrySameServiceInstance(loadBalancerRetryContext);
|
||||
if (shouldRetry) {
|
||||
loadBalancerRetryContext.incrementRetriesSameServiceInstance();
|
||||
}
|
||||
return shouldRetry;
|
||||
}
|
||||
|
||||
private boolean shouldRetryNextServiceInstance(LoadBalancerRetryContext loadBalancerRetryContext) {
|
||||
boolean shouldRetry = retryPolicy.retryableStatusCode(loadBalancerRetryContext.getResponseStatusCode())
|
||||
&& retryPolicy.canRetryOnMethod(loadBalancerRetryContext.getRequestMethod())
|
||||
&& retryPolicy.canRetryNextServiceInstance(loadBalancerRetryContext);
|
||||
if (shouldRetry) {
|
||||
loadBalancerRetryContext.incrementRetriesNextServiceInstance();
|
||||
loadBalancerRetryContext.resetRetriesSameServiceInstance();
|
||||
}
|
||||
return shouldRetry;
|
||||
}
|
||||
|
||||
private boolean isRetryException(Throwable throwable) {
|
||||
return exceptions.stream()
|
||||
.anyMatch(exception -> exception.isInstance(throwable)
|
||||
|| throwable != null && exception.isInstance(throwable.getCause())
|
||||
|| Exceptions.isRetryExhausted(throwable));
|
||||
}
|
||||
|
||||
protected Mono<Response<ServiceInstance>> choose(String serviceId, Request<RetryableRequestContext> request) {
|
||||
ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerFactory.getInstance(serviceId);
|
||||
if (loadBalancer == null) {
|
||||
return Mono.just(new EmptyResponse());
|
||||
}
|
||||
return Mono.from(loadBalancer.choose(request));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://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 org.springframework.cloud.client.loadbalancer.reactive;
|
||||
|
||||
/**
|
||||
* An {@link IllegalStateException} used to trigger retries based on the returned HTTP
|
||||
* status code.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 3.0.0
|
||||
*/
|
||||
class RetryableStatusCodeException extends IllegalStateException {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://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 org.springframework.cloud.client.loadbalancer.reactive;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
|
||||
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
|
||||
import org.springframework.cloud.client.loadbalancer.Request;
|
||||
import org.springframework.cloud.client.loadbalancer.Response;
|
||||
import org.springframework.cloud.client.loadbalancer.RetryableRequestContext;
|
||||
|
||||
/**
|
||||
* A {@link ReactiveLoadBalancer} implementation used for tests.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 3.0.0
|
||||
*/
|
||||
class DiscoveryClientBasedReactiveLoadBalancer implements ReactiveLoadBalancer<ServiceInstance> {
|
||||
|
||||
private final Random random = new Random();
|
||||
|
||||
private final String serviceId;
|
||||
|
||||
private final DiscoveryClient discoveryClient;
|
||||
|
||||
DiscoveryClientBasedReactiveLoadBalancer(String serviceId, DiscoveryClient discoveryClient) {
|
||||
this.serviceId = serviceId;
|
||||
this.discoveryClient = discoveryClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<Response<ServiceInstance>> choose() {
|
||||
List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
|
||||
if (instances.size() == 0) {
|
||||
return Mono.just(new EmptyResponse());
|
||||
}
|
||||
int instanceIdx = this.random.nextInt(instances.size());
|
||||
return Mono.just(new DefaultResponse(instances.get(instanceIdx)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<Response<ServiceInstance>> choose(Request request) {
|
||||
|
||||
List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
|
||||
if (request.getContext() instanceof RetryableRequestContext) {
|
||||
RetryableRequestContext context = (RetryableRequestContext) request.getContext();
|
||||
if (context.getPreviousServiceInstance() != null) {
|
||||
List<ServiceInstance> instancesCopy = discoveryClient.getInstances(serviceId);
|
||||
instancesCopy.remove(context.getPreviousServiceInstance());
|
||||
if (!instancesCopy.isEmpty()) {
|
||||
instances = instancesCopy;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (instances.size() == 0) {
|
||||
return Mono.just(new EmptyResponse());
|
||||
}
|
||||
int instanceIdx = this.random.nextInt(instances.size());
|
||||
return Mono.just(new DefaultResponse(instances.get(instanceIdx)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -59,6 +59,27 @@ public class ReactorLoadBalancerClientAutoConfigurationTests {
|
||||
assertLoadBalanced(testService.webClient, ReactorLoadBalancerExchangeFilterFunction.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadBalancerFilterAddedToWebClientBuilderWithRetryEnabled() {
|
||||
System.setProperty("spring.cloud.loadbalancer.retry.enabled", "true");
|
||||
ConfigurableApplicationContext context = init(OneWebClientBuilder.class);
|
||||
final Map<String, WebClient.Builder> webClientBuilders = context.getBeansOfType(WebClient.Builder.class);
|
||||
|
||||
then(webClientBuilders).isNotNull().hasSize(1);
|
||||
WebClient.Builder webClientBuilder = webClientBuilders.values().iterator().next();
|
||||
then(webClientBuilder).isNotNull();
|
||||
|
||||
assertLoadBalanced(webClientBuilder, RetryableLoadBalancerExchangeFilterFunction.class);
|
||||
|
||||
final Map<String, OneWebClientBuilder.TestService> testServiceMap = context
|
||||
.getBeansOfType(OneWebClientBuilder.TestService.class);
|
||||
then(testServiceMap).isNotNull().hasSize(1);
|
||||
OneWebClientBuilder.TestService testService = testServiceMap.values().stream().findFirst().get();
|
||||
assertLoadBalanced(testService.webClient, RetryableLoadBalancerExchangeFilterFunction.class);
|
||||
|
||||
System.clearProperty("spring.cloud.loadbalancer.retry.enabled");
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadBalancerFilterAddedOnlyToLoadBalancedWebClientBuilder() {
|
||||
ConfigurableApplicationContext context = init(TwoWebClientBuilders.class);
|
||||
@@ -75,6 +96,24 @@ public class ReactorLoadBalancerClientAutoConfigurationTests {
|
||||
then(getFilters(two.nonLoadBalanced)).isNullOrEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadBalancerFilterAddedOnlyToLoadBalancedWebClientBuilderWithRetryEnabled() {
|
||||
System.setProperty("spring.cloud.loadbalancer.retry.enabled", "true");
|
||||
ConfigurableApplicationContext context = init(TwoWebClientBuilders.class);
|
||||
final Map<String, WebClient.Builder> webClientBuilders = context.getBeansOfType(WebClient.Builder.class);
|
||||
|
||||
then(webClientBuilders).hasSize(2);
|
||||
|
||||
TwoWebClientBuilders.Two two = context.getBean(TwoWebClientBuilders.Two.class);
|
||||
|
||||
then(two.loadBalanced).isNotNull();
|
||||
assertLoadBalanced(two.loadBalanced, RetryableLoadBalancerExchangeFilterFunction.class);
|
||||
|
||||
then(two.nonLoadBalanced).isNotNull();
|
||||
then(getFilters(two.nonLoadBalanced)).isNullOrEmpty();
|
||||
System.clearProperty("spring.cloud.loadbalancer.retry.enabled");
|
||||
}
|
||||
|
||||
@Test
|
||||
void noCustomWebClientBuilders() {
|
||||
ConfigurableApplicationContext context = init(NoWebClientBuilder.class);
|
||||
|
||||
@@ -20,17 +20,13 @@ import java.net.URI;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
@@ -44,11 +40,8 @@ import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.CompletionContext;
|
||||
import org.springframework.cloud.client.loadbalancer.DefaultRequestContext;
|
||||
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
|
||||
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
|
||||
import org.springframework.cloud.client.loadbalancer.Request;
|
||||
import org.springframework.cloud.client.loadbalancer.Response;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -247,33 +240,3 @@ class ReactorLoadBalancerExchangeFilterFunctionTests {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class DiscoveryClientBasedReactiveLoadBalancer implements ReactiveLoadBalancer<ServiceInstance> {
|
||||
|
||||
private final Random random = new Random();
|
||||
|
||||
private final String serviceId;
|
||||
|
||||
private final DiscoveryClient discoveryClient;
|
||||
|
||||
DiscoveryClientBasedReactiveLoadBalancer(String serviceId, DiscoveryClient discoveryClient) {
|
||||
this.serviceId = serviceId;
|
||||
this.discoveryClient = discoveryClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<Response<ServiceInstance>> choose() {
|
||||
List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
|
||||
if (instances.size() == 0) {
|
||||
return Mono.just(new EmptyResponse());
|
||||
}
|
||||
int instanceIdx = this.random.nextInt(instances.size());
|
||||
return Mono.just(new DefaultResponse(instances.get(instanceIdx)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<Response<ServiceInstance>> choose(Request request) {
|
||||
return choose();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://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 org.springframework.cloud.client.loadbalancer.reactive;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.CompletionContext;
|
||||
import org.springframework.cloud.client.loadbalancer.DefaultRequestContext;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
|
||||
import org.springframework.cloud.client.loadbalancer.Request;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.BDDAssertions.then;
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link RetryableLoadBalancerExchangeFilterFunction}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = RANDOM_PORT)
|
||||
class RetryableLoadBalancerExchangeFilterFunctionIntegrationTests {
|
||||
|
||||
@Autowired
|
||||
private RetryableLoadBalancerExchangeFilterFunction loadBalancerFunction;
|
||||
|
||||
@Autowired
|
||||
private SimpleDiscoveryProperties properties;
|
||||
|
||||
@Autowired
|
||||
private LoadBalancerProperties loadBalancerProperties;
|
||||
|
||||
@Autowired
|
||||
private ReactiveLoadBalancer.Factory<ServiceInstance> factory;
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
DefaultServiceInstance instance = new DefaultServiceInstance();
|
||||
instance.setServiceId("testservice");
|
||||
instance.setUri(URI.create("http://localhost:" + port));
|
||||
DefaultServiceInstance instanceWithNoLifecycleProcessors = new DefaultServiceInstance();
|
||||
instanceWithNoLifecycleProcessors.setServiceId("serviceWithNoLifecycleProcessors");
|
||||
instanceWithNoLifecycleProcessors.setUri(URI.create("http://localhost:" + port));
|
||||
properties.getInstances().put("testservice", Collections.singletonList(instance));
|
||||
properties.getInstances().put("serviceWithNoLifecycleProcessors",
|
||||
Collections.singletonList(instanceWithNoLifecycleProcessors));
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadBalancerLifecycleCallbacksExecuted() {
|
||||
final String callbackTestHint = "callbackTestHint";
|
||||
loadBalancerProperties.getHint().put("testservice", "callbackTestHint");
|
||||
final String result = "callbackTestResult";
|
||||
|
||||
ClientResponse clientResponse = WebClient.builder().baseUrl("http://testservice")
|
||||
.filter(this.loadBalancerFunction).build().get().uri("/callback").exchange().block();
|
||||
|
||||
Collection<Request<Object>> lifecycleLogRequests = ((TestLoadBalancerLifecycle) factory
|
||||
.getInstances("testservice", LoadBalancerLifecycle.class).get("loadBalancerLifecycle")).getStartLog()
|
||||
.values();
|
||||
Collection<CompletionContext<Object, ServiceInstance>> anotherLifecycleLogRequests = ((AnotherLoadBalancerLifecycle) factory
|
||||
.getInstances("testservice", LoadBalancerLifecycle.class).get("anotherLoadBalancerLifecycle"))
|
||||
.getCompleteLog().values();
|
||||
then(clientResponse.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(lifecycleLogRequests).extracting(request -> ((DefaultRequestContext) request.getContext()).getHint())
|
||||
.contains(callbackTestHint);
|
||||
assertThat(anotherLifecycleLogRequests)
|
||||
.extracting(completionContext -> ((ClientResponse) completionContext.getClientResponse())
|
||||
.bodyToMono(String.class).block())
|
||||
.contains(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void correctResponseReturnedForExistingHostAndInstancePresent() {
|
||||
ClientResponse clientResponse = WebClient.builder().baseUrl("http://testservice")
|
||||
.filter(this.loadBalancerFunction).build().get().uri("/hello").exchange().block();
|
||||
|
||||
then(clientResponse.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
then(clientResponse.bodyToMono(String.class).block()).isEqualTo("Hello World");
|
||||
}
|
||||
|
||||
@Test
|
||||
void correctResponseReturnedAfterRetryingOnSameServiceInstance() {
|
||||
loadBalancerProperties.getRetry().setMaxRetriesOnSameServiceInstance(1);
|
||||
loadBalancerProperties.getRetry().getRetryableStatusCodes().add(500);
|
||||
|
||||
ClientResponse clientResponse = WebClient.builder().baseUrl("http://testservice")
|
||||
.filter(this.loadBalancerFunction).build().get().uri("/exception").exchange().block();
|
||||
|
||||
then(clientResponse.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
then(clientResponse.bodyToMono(String.class).block()).isEqualTo("Hello World!");
|
||||
}
|
||||
|
||||
@Test
|
||||
void correctResponseReturnedAfterRetryingOnNextServiceInstanceWithBackoff() {
|
||||
loadBalancerProperties.getRetry().getBackoff().setEnabled(true);
|
||||
loadBalancerProperties.getRetry().setMaxRetriesOnSameServiceInstance(1);
|
||||
DefaultServiceInstance goodRetryTestInstance = new DefaultServiceInstance();
|
||||
goodRetryTestInstance.setServiceId("retrytest");
|
||||
goodRetryTestInstance.setUri(URI.create("http://localhost:" + port));
|
||||
DefaultServiceInstance badRetryTestInstance = new DefaultServiceInstance();
|
||||
badRetryTestInstance.setServiceId("retrytest");
|
||||
badRetryTestInstance.setUri(URI.create("http://localhost:" + 8080));
|
||||
properties.getInstances().put("retrytest", Arrays.asList(badRetryTestInstance, goodRetryTestInstance));
|
||||
loadBalancerProperties.getRetry().getRetryableStatusCodes().add(500);
|
||||
|
||||
ClientResponse clientResponse = WebClient.builder().baseUrl("http://retrytest")
|
||||
.filter(this.loadBalancerFunction).build().get().uri("/hello").exchange().block();
|
||||
|
||||
then(clientResponse.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
then(clientResponse.bodyToMono(String.class).block()).isEqualTo("Hello World");
|
||||
|
||||
ClientResponse secondClientResponse = WebClient.builder().baseUrl("http://retrytest")
|
||||
.filter(this.loadBalancerFunction).build().get().uri("/hello").exchange().block();
|
||||
|
||||
then(secondClientResponse.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
then(secondClientResponse.bodyToMono(String.class).block()).isEqualTo("Hello World");
|
||||
}
|
||||
|
||||
@Test
|
||||
void serviceUnavailableReturnedWhenNoInstancePresent() {
|
||||
ClientResponse clientResponse = WebClient.builder().baseUrl("http://xxx").filter(this.loadBalancerFunction)
|
||||
.build().get().exchange().block();
|
||||
|
||||
then(clientResponse.statusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
// FIXME 3.0.0
|
||||
void badRequestReturnedForIncorrectHost() {
|
||||
ClientResponse clientResponse = WebClient.builder().baseUrl("http:///xxx").filter(this.loadBalancerFunction)
|
||||
.build().get().exchange().block();
|
||||
|
||||
then(clientResponse.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
void exceptionNotThrownWhenFactoryReturnsNullLifecycleProcessorsMap() {
|
||||
assertThatCode(() -> WebClient.builder().baseUrl("http://serviceWithNoLifecycleProcessors")
|
||||
.filter(this.loadBalancerFunction).build().get().uri("/hello").exchange().block())
|
||||
.doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
@EnableDiscoveryClient
|
||||
@EnableAutoConfiguration
|
||||
@SpringBootConfiguration(proxyBeanMethods = false)
|
||||
@RestController
|
||||
static class Config {
|
||||
|
||||
AtomicInteger exceptionCallsCount = new AtomicInteger();
|
||||
|
||||
@GetMapping("/hello")
|
||||
public String hello() {
|
||||
return "Hello World";
|
||||
}
|
||||
|
||||
@GetMapping("/callback")
|
||||
String callbackTestResult() {
|
||||
return "callbackTestResult";
|
||||
}
|
||||
|
||||
@GetMapping("/exception")
|
||||
String exception() {
|
||||
int callCount = exceptionCallsCount.incrementAndGet();
|
||||
if (callCount % 2 != 0) {
|
||||
throw new IllegalStateException("Test!");
|
||||
}
|
||||
return "Hello World!";
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveLoadBalancer.Factory<ServiceInstance> reactiveLoadBalancerFactory(DiscoveryClient discoveryClient) {
|
||||
return new ReactiveLoadBalancer.Factory<ServiceInstance>() {
|
||||
|
||||
private final TestLoadBalancerLifecycle testLoadBalancerLifecycle = new TestLoadBalancerLifecycle();
|
||||
|
||||
private final TestLoadBalancerLifecycle anotherLoadBalancerLifecycle = new AnotherLoadBalancerLifecycle();
|
||||
|
||||
@Override
|
||||
public ReactiveLoadBalancer<ServiceInstance> getInstance(String serviceId) {
|
||||
return new org.springframework.cloud.client.loadbalancer.reactive.DiscoveryClientBasedReactiveLoadBalancer(
|
||||
serviceId, discoveryClient);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <X> Map<String, X> getInstances(String name, Class<X> type) {
|
||||
if (name.equals("serviceWithNoLifecycleProcessors")) {
|
||||
return null;
|
||||
}
|
||||
Map lifecycleProcessors = new HashMap<>();
|
||||
lifecycleProcessors.put("loadBalancerLifecycle", testLoadBalancerLifecycle);
|
||||
lifecycleProcessors.put("anotherLoadBalancerLifecycle", anotherLoadBalancerLifecycle);
|
||||
return lifecycleProcessors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <X> X getInstance(String name, Class<?> clazz, Class<?>... generics) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
LoadBalancerProperties loadBalancerProperties() {
|
||||
return new LoadBalancerProperties();
|
||||
}
|
||||
|
||||
@Bean
|
||||
RetryableLoadBalancerExchangeFilterFunction exchangeFilterFunction(LoadBalancerProperties properties,
|
||||
ReactiveLoadBalancer.Factory<ServiceInstance> factory) {
|
||||
return new RetryableLoadBalancerExchangeFilterFunction(
|
||||
new RetryableExchangeFilterFunctionLoadBalancerRetryPolicy(properties), factory, properties);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected static class TestLoadBalancerLifecycle implements LoadBalancerLifecycle<Object, Object, ServiceInstance> {
|
||||
|
||||
Map<String, Request<Object>> startLog = new ConcurrentHashMap<>();
|
||||
|
||||
Map<String, CompletionContext<Object, ServiceInstance>> completeLog = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void onStart(Request<Object> request) {
|
||||
startLog.put(getName() + UUID.randomUUID(), request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(CompletionContext<Object, ServiceInstance> completionContext) {
|
||||
completeLog.clear();
|
||||
completeLog.put(getName() + UUID.randomUUID(), completionContext);
|
||||
}
|
||||
|
||||
Map<String, Request<Object>> getStartLog() {
|
||||
return startLog;
|
||||
}
|
||||
|
||||
Map<String, CompletionContext<Object, ServiceInstance>> getCompleteLog() {
|
||||
return completeLog;
|
||||
}
|
||||
|
||||
protected String getName() {
|
||||
return this.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected static class AnotherLoadBalancerLifecycle extends TestLoadBalancerLifecycle {
|
||||
|
||||
@Override
|
||||
protected String getName() {
|
||||
return this.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://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 org.springframework.cloud.client.loadbalancer.reactive;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFunction;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link RetryableLoadBalancerExchangeFilterFunction}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
class RetryableLoadBalancerExchangeFilterFunctionTests {
|
||||
|
||||
private final LoadBalancerProperties properties = new LoadBalancerProperties();
|
||||
|
||||
private final LoadBalancerRetryPolicy policy = new RetryableExchangeFilterFunctionLoadBalancerRetryPolicy(
|
||||
properties);
|
||||
|
||||
private final ReactiveLoadBalancer.Factory<ServiceInstance> factory = mock(ReactiveLoadBalancer.Factory.class);
|
||||
|
||||
private final RetryableLoadBalancerExchangeFilterFunction filterFunction = new RetryableLoadBalancerExchangeFilterFunction(
|
||||
policy, factory, properties);
|
||||
|
||||
private final ClientRequest clientRequest = mock(ClientRequest.class);
|
||||
|
||||
private final ExchangeFunction next = mock(ExchangeFunction.class);
|
||||
|
||||
private final ClientResponse clientResponse = mock(ClientResponse.class);
|
||||
|
||||
private final InOrder inOrder = inOrder(next, factory);
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
properties.getRetry().setMaxRetriesOnSameServiceInstance(1);
|
||||
properties.getRetry().getRetryableStatusCodes().add(404);
|
||||
when(clientRequest.url()).thenReturn(URI.create("http://test"));
|
||||
when(factory.getInstance("test")).thenReturn(new TestReactiveLoadBalancer());
|
||||
when(clientRequest.headers()).thenReturn(new HttpHeaders());
|
||||
when(clientRequest.cookies()).thenReturn(new HttpHeaders());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRetryOnSameAndNextServiceInstanceOnException() {
|
||||
when(clientRequest.method()).thenReturn(HttpMethod.GET);
|
||||
when(next.exchange(any())).thenThrow(new IllegalStateException(new IOException()));
|
||||
|
||||
filterFunction.filter(clientRequest, next).subscribe();
|
||||
|
||||
inOrder.verify(factory, times(1)).getInstance(any());
|
||||
inOrder.verify(next, times(2)).exchange(any());
|
||||
inOrder.verify(factory, times(1)).getInstance(any());
|
||||
inOrder.verify(next, times(2)).exchange(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRetryOnSameAndNextServiceInstanceOnRetryableStatusCode() {
|
||||
when(clientRequest.method()).thenReturn(HttpMethod.GET);
|
||||
when(clientResponse.statusCode()).thenReturn(HttpStatus.NOT_FOUND);
|
||||
when(next.exchange(any())).thenReturn(Mono.just(clientResponse));
|
||||
|
||||
filterFunction.filter(clientRequest, next).subscribe();
|
||||
|
||||
inOrder.verify(factory, times(1)).getInstance(any());
|
||||
inOrder.verify(next, times(2)).exchange(any());
|
||||
inOrder.verify(factory, times(1)).getInstance(any());
|
||||
inOrder.verify(next, times(2)).exchange(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotRetryWhenNoRetryableExceptionOrStatusCode() {
|
||||
when(clientRequest.method()).thenReturn(HttpMethod.GET);
|
||||
when(clientResponse.statusCode()).thenReturn(HttpStatus.OK);
|
||||
when(next.exchange(any())).thenReturn(Mono.just(clientResponse));
|
||||
|
||||
filterFunction.filter(clientRequest, next).subscribe();
|
||||
|
||||
verify(next, times(1)).exchange(any());
|
||||
verify(factory, times(1)).getInstance(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotRetryOnMethodOtherThanGet() {
|
||||
when(clientRequest.method()).thenReturn(HttpMethod.POST);
|
||||
when(clientResponse.statusCode()).thenReturn(HttpStatus.NOT_FOUND);
|
||||
when(next.exchange(any())).thenReturn(Mono.just(clientResponse));
|
||||
|
||||
filterFunction.filter(clientRequest, next).subscribe();
|
||||
|
||||
verify(next, times(1)).exchange(any());
|
||||
verify(factory, times(1)).getInstance(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRetryOnMethodOtherThanGetWhenEnabled() {
|
||||
LoadBalancerProperties properties = new LoadBalancerProperties();
|
||||
properties.getRetry().setRetryOnAllOperations(true);
|
||||
properties.getRetry().setMaxRetriesOnSameServiceInstance(1);
|
||||
properties.getRetry().getRetryableStatusCodes().add(404);
|
||||
LoadBalancerRetryPolicy policy = new RetryableExchangeFilterFunctionLoadBalancerRetryPolicy(properties);
|
||||
RetryableLoadBalancerExchangeFilterFunction filterFunction = new RetryableLoadBalancerExchangeFilterFunction(
|
||||
policy, factory, properties);
|
||||
when(clientRequest.method()).thenReturn(HttpMethod.POST);
|
||||
when(clientResponse.statusCode()).thenReturn(HttpStatus.NOT_FOUND);
|
||||
when(next.exchange(any())).thenReturn(Mono.just(clientResponse));
|
||||
|
||||
filterFunction.filter(clientRequest, next).subscribe();
|
||||
|
||||
inOrder.verify(factory, times(1)).getInstance(any());
|
||||
inOrder.verify(next, times(2)).exchange(any());
|
||||
inOrder.verify(factory, times(1)).getInstance(any());
|
||||
inOrder.verify(next, times(2)).exchange(any());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.cloud.loadbalancer.annotation;
|
||||
|
||||
import reactor.util.retry.RetrySpec;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
@@ -139,7 +141,7 @@ public class LoadBalancerClientConfiguration {
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnBlockingDiscoveryEnabled
|
||||
@ConditionalOnClass(RetryTemplate.class)
|
||||
@Conditional(OnAvoidPreviousInstanceAndRetryEnabledCondition.class)
|
||||
@Conditional(BlockingOnAvoidPreviousInstanceAndRetryEnabledCondition.class)
|
||||
@AutoConfigureAfter(BlockingSupportConfiguration.class)
|
||||
@ConditionalOnBean(ServiceInstanceListSupplier.class)
|
||||
public static class BlockingRetryConfiguration {
|
||||
@@ -154,9 +156,27 @@ public class LoadBalancerClientConfiguration {
|
||||
|
||||
}
|
||||
|
||||
static final class OnAvoidPreviousInstanceAndRetryEnabledCondition extends AllNestedConditions {
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnBlockingDiscoveryEnabled
|
||||
@Conditional(ReactiveOnAvoidPreviousInstanceAndRetryEnabledCondition.class)
|
||||
@AutoConfigureAfter(ReactiveSupportConfiguration.class)
|
||||
@ConditionalOnBean(ServiceInstanceListSupplier.class)
|
||||
@ConditionalOnClass(RetrySpec.class)
|
||||
public static class ReactiveRetryConfiguration {
|
||||
|
||||
private OnAvoidPreviousInstanceAndRetryEnabledCondition() {
|
||||
@Bean
|
||||
@ConditionalOnBean(DiscoveryClient.class)
|
||||
@Primary
|
||||
public ServiceInstanceListSupplier retryAwareDiscoveryClientServiceInstanceListSupplier(
|
||||
ServiceInstanceListSupplier delegate) {
|
||||
return new RetryAwareServiceInstanceListSupplier(delegate);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static final class BlockingOnAvoidPreviousInstanceAndRetryEnabledCondition extends AllNestedConditions {
|
||||
|
||||
private BlockingOnAvoidPreviousInstanceAndRetryEnabledCondition() {
|
||||
super(ConfigurationPhase.REGISTER_BEAN);
|
||||
}
|
||||
|
||||
@@ -174,4 +194,23 @@ public class LoadBalancerClientConfiguration {
|
||||
|
||||
}
|
||||
|
||||
static final class ReactiveOnAvoidPreviousInstanceAndRetryEnabledCondition extends AllNestedConditions {
|
||||
|
||||
private ReactiveOnAvoidPreviousInstanceAndRetryEnabledCondition() {
|
||||
super(ConfigurationPhase.REGISTER_BEAN);
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true")
|
||||
static class LoadBalancerRetryEnabled {
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.avoid-previous-instance", havingValue = "true",
|
||||
matchIfMissing = true)
|
||||
static class AvoidPreviousInstanceEnabled {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user