Support loadbalancer lifecycle (#418)

* Add LoadBalancerLifecycle support.

* Add info on SC LoadBalancer documentation.
This commit is contained in:
Olga Maciaszek-Sharma
2020-10-27 11:12:34 -05:00
committed by GitHub
10 changed files with 325 additions and 33 deletions

View File

@@ -68,6 +68,8 @@ it will resolve the service in the Eureka service registry. If you
don't want to use Eureka, you can simply configure a list of servers
in your external configuration using https://cloud.spring.io/spring-cloud-static/spring-cloud-commons/current/reference/html/#simplediscoveryclient[`SimpleDiscoveryClient`].
Spring Cloud OpenFeign supports all the features available for the blocking mode of Spring Cloud LoadBalancer. You can read more about them in the https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-loadbalancer[project documentation].
[[spring-cloud-feign-overriding-defaults]]
=== Overriding Feign Defaults

View File

@@ -26,6 +26,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@@ -44,8 +45,10 @@ class DefaultFeignLoadBalancerConfiguration {
@Bean
@ConditionalOnMissingBean
@Conditional(OnRetryNotEnabledCondition.class)
public Client feignClient(LoadBalancerClient loadBalancerClient, LoadBalancerProperties properties) {
return new FeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient, properties);
public Client feignClient(LoadBalancerClient loadBalancerClient, LoadBalancerProperties properties,
LoadBalancerClientFactory loadBalancerClientFactory) {
return new FeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient, properties,
loadBalancerClientFactory);
}
@Bean
@@ -55,9 +58,10 @@ class DefaultFeignLoadBalancerConfiguration {
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
matchIfMissing = true)
public Client feignRetryClient(LoadBalancerClient loadBalancerClient,
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties) {
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
LoadBalancerClientFactory loadBalancerClientFactory) {
return new RetryableFeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient,
loadBalancedRetryFactory, properties);
loadBalancedRetryFactory, properties, loadBalancerClientFactory);
}
}

View File

@@ -19,6 +19,7 @@ package org.springframework.cloud.openfeign.loadbalancer;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import feign.Client;
import feign.Request;
@@ -27,13 +28,22 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.CompletionContext;
import org.springframework.cloud.client.loadbalancer.DefaultRequest;
import org.springframework.cloud.client.loadbalancer.DefaultRequestContext;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.HttpRequestContext;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycleValidator;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.Assert;
import static org.springframework.cloud.openfeign.loadbalancer.LoadBalancerUtils.executeWithLoadBalancerLifecycleProcessing;
/**
* A {@link Client} implementation that uses {@link LoadBalancerClient} to select a
* {@link ServiceInstance} to use while resolving the request host.
@@ -41,6 +51,7 @@ import org.springframework.util.Assert;
* @author Olga Maciaszek-Sharma
* @since 2.2.0
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public class FeignBlockingLoadBalancerClient implements Client {
private static final Log LOG = LogFactory.getLog(FeignBlockingLoadBalancerClient.class);
@@ -51,11 +62,14 @@ public class FeignBlockingLoadBalancerClient implements Client {
private final LoadBalancerProperties properties;
private final LoadBalancerClientFactory loadBalancerClientFactory;
public FeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient,
LoadBalancerProperties properties) {
LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
this.delegate = delegate;
this.loadBalancerClient = loadBalancerClient;
this.properties = properties;
this.loadBalancerClientFactory = loadBalancerClientFactory;
}
@Override
@@ -66,19 +80,30 @@ public class FeignBlockingLoadBalancerClient implements Client {
String hint = getHint(serviceId);
DefaultRequest<DefaultRequestContext> lbRequest = new DefaultRequest<>(
new DefaultRequestContext(request, hint));
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
.getSupportedLifecycleProcessors(
loadBalancerClientFactory.getInstances(serviceId, LoadBalancerLifecycle.class),
HttpRequestContext.class, ClientHttpResponse.class, ServiceInstance.class);
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
ServiceInstance instance = loadBalancerClient.choose(serviceId, lbRequest);
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse = new DefaultResponse(
instance);
if (instance == null) {
String message = "Load balancer does not contain an instance for the service " + serviceId;
if (LOG.isWarnEnabled()) {
LOG.warn(message);
}
supportedLifecycleProcessors.forEach(
lifecycle -> lifecycle.onComplete(new CompletionContext<ClientHttpResponse, ServiceInstance>(
CompletionContext.Status.DISCARD, lbResponse)));
return Response.builder().request(request).status(HttpStatus.SERVICE_UNAVAILABLE.value())
.body(message, StandardCharsets.UTF_8).build();
}
String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri).toString();
Request newRequest = Request.create(request.httpMethod(), reconstructedUrl, request.headers(), request.body(),
request.charset(), request.requestTemplate());
return delegate.execute(newRequest, options);
return executeWithLoadBalancerLifecycleProcessing(delegate, options, newRequest, lbResponse,
supportedLifecycleProcessors);
}
// Visible for Sleuth instrumentation

View File

@@ -24,6 +24,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
import org.springframework.context.annotation.Configuration;
@@ -37,7 +38,7 @@ import org.springframework.context.annotation.Import;
* @since 2.2.0
*/
@ConditionalOnClass(Feign.class)
@ConditionalOnBean(LoadBalancerClient.class)
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
@AutoConfigureBefore(FeignAutoConfiguration.class)
@EnableConfigurationProperties(FeignHttpClientProperties.class)
@Configuration(proxyBeanMethods = false)

View File

@@ -28,6 +28,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.cloud.openfeign.clientconfig.HttpClientFeignConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
@@ -43,7 +44,7 @@ import org.springframework.context.annotation.Import;
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnBean(LoadBalancerClient.class)
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
@Import(HttpClientFeignConfiguration.class)
@EnableConfigurationProperties(LoadBalancerProperties.class)
@@ -53,9 +54,9 @@ class HttpClientFeignLoadBalancerConfiguration {
@ConditionalOnMissingBean
@Conditional(OnRetryNotEnabledCondition.class)
public Client feignClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient,
LoadBalancerProperties properties) {
LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties);
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties, loadBalancerClientFactory);
}
@Bean
@@ -65,10 +66,11 @@ class HttpClientFeignLoadBalancerConfiguration {
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
matchIfMissing = true)
public Client feignRetryClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient,
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties) {
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
LoadBalancerClientFactory loadBalancerClientFactory) {
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory,
properties);
properties, loadBalancerClientFactory);
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2013-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.openfeign.loadbalancer;
import java.io.IOException;
import java.util.Set;
import feign.Client;
import feign.Request;
import feign.Response;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.CompletionContext;
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
/**
* @author Olga Maciaszek-Sharma
*
* A utility class for handling {@link LoadBalancerLifecycle} calls.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
final class LoadBalancerUtils {
private LoadBalancerUtils() {
throw new IllegalStateException("Can't instantiate a utility class");
}
static Response executeWithLoadBalancerLifecycleProcessing(Client feignClient, Request.Options options,
Request feignRequest, org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse,
Set<LoadBalancerLifecycle> supportedLifecycleProcessors, boolean loadBalanced) throws IOException {
try {
Response response = feignClient.execute(feignRequest, options);
if (loadBalanced) {
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
.onComplete(new CompletionContext<>(CompletionContext.Status.SUCCESS, lbResponse, response)));
}
return response;
}
catch (Exception exception) {
if (loadBalanced) {
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
.onComplete(new CompletionContext<>(CompletionContext.Status.FAILED, exception, lbResponse)));
}
throw exception;
}
}
static Response executeWithLoadBalancerLifecycleProcessing(Client feignClient, Request.Options options,
Request feignRequest, org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse,
Set<LoadBalancerLifecycle> supportedLifecycleProcessors) throws IOException {
return executeWithLoadBalancerLifecycleProcessing(feignClient, options, feignRequest, lbResponse,
supportedLifecycleProcessors, true);
}
}

View File

@@ -27,6 +27,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.cloud.openfeign.clientconfig.OkHttpFeignConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
@@ -43,7 +44,7 @@ import org.springframework.context.annotation.Import;
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnProperty("feign.okhttp.enabled")
@ConditionalOnBean(LoadBalancerClient.class)
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
@Import(OkHttpFeignConfiguration.class)
@EnableConfigurationProperties(LoadBalancerProperties.class)
class OkHttpFeignLoadBalancerConfiguration {
@@ -52,9 +53,9 @@ class OkHttpFeignLoadBalancerConfiguration {
@ConditionalOnMissingBean
@Conditional(OnRetryNotEnabledCondition.class)
public Client feignClient(okhttp3.OkHttpClient okHttpClient, LoadBalancerClient loadBalancerClient,
LoadBalancerProperties properties) {
LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
OkHttpClient delegate = new OkHttpClient(okHttpClient);
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties);
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties, loadBalancerClientFactory);
}
@Bean
@@ -64,10 +65,11 @@ class OkHttpFeignLoadBalancerConfiguration {
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
matchIfMissing = true)
public Client feignRetryClient(LoadBalancerClient loadBalancerClient, okhttp3.OkHttpClient okHttpClient,
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties) {
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
LoadBalancerClientFactory loadBalancerClientFactory) {
OkHttpClient delegate = new OkHttpClient(okHttpClient);
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory,
properties);
properties, loadBalancerClientFactory);
}
}

View File

@@ -23,6 +23,7 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import feign.Client;
import feign.Request;
@@ -31,19 +32,26 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.CompletionContext;
import org.springframework.cloud.client.loadbalancer.DefaultRequest;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.HttpRequestContext;
import org.springframework.cloud.client.loadbalancer.InterceptorRetryPolicy;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRecoveryCallback;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycleValidator;
import org.springframework.cloud.client.loadbalancer.RetryableRequestContext;
import org.springframework.cloud.client.loadbalancer.RetryableStatusCodeException;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.retry.RetryListener;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.NoBackOffPolicy;
@@ -51,6 +59,8 @@ import org.springframework.retry.policy.NeverRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
import static org.springframework.cloud.openfeign.loadbalancer.LoadBalancerUtils.executeWithLoadBalancerLifecycleProcessing;
/**
* A {@link Client} implementation that provides Spring Retry support for requests
* load-balanced with Spring Cloud LoadBalancer.
@@ -58,6 +68,7 @@ import org.springframework.util.Assert;
* @author Olga Maciaszek-Sharma
* @since 2.2.6
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public class RetryableFeignBlockingLoadBalancerClient implements Client {
private static final Log LOG = LogFactory.getLog(FeignBlockingLoadBalancerClient.class);
@@ -70,12 +81,16 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client {
private final LoadBalancerProperties properties;
private final LoadBalancerClientFactory loadBalancerClientFactory;
public RetryableFeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient,
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties) {
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
LoadBalancerClientFactory loadBalancerClientFactory) {
this.delegate = delegate;
this.loadBalancerClient = loadBalancerClient;
this.loadBalancedRetryFactory = loadBalancedRetryFactory;
this.properties = properties;
this.loadBalancerClientFactory = loadBalancerClientFactory;
}
@Override
@@ -88,6 +103,11 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client {
RetryTemplate retryTemplate = buildRetryTemplate(serviceId, request, retryPolicy);
return retryTemplate.execute(context -> {
Request feignRequest = null;
ServiceInstance retrievedServiceInstance = null;
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
.getSupportedLifecycleProcessors(
loadBalancerClientFactory.getInstances(serviceId, LoadBalancerLifecycle.class),
HttpRequestContext.class, ClientHttpResponse.class, ServiceInstance.class);
// On retries the policy will choose the server and set it in the context
// and extract the server and update the request being made
if (context instanceof LoadBalancedRetryContext) {
@@ -102,30 +122,40 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client {
String hint = getHint(serviceId);
DefaultRequest<RetryableRequestContext> lbRequest = new DefaultRequest<>(
new RetryableRequestContext(previousServiceInstance, request, hint));
serviceInstance = loadBalancerClient.choose(serviceId, lbRequest);
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
retrievedServiceInstance = loadBalancerClient.choose(serviceId, lbRequest);
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Selected service instance: %s", serviceInstance));
LOG.debug(String.format("Selected service instance: %s", retrievedServiceInstance));
}
lbContext.setServiceInstance(serviceInstance);
lbContext.setServiceInstance(retrievedServiceInstance);
}
if (serviceInstance != null) {
if (retrievedServiceInstance == null) {
if (LOG.isWarnEnabled()) {
LOG.warn("Service instance was not resolved, executing the original request");
}
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse = new DefaultResponse(
retrievedServiceInstance);
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete(
new CompletionContext<ClientHttpResponse, ServiceInstance>(CompletionContext.Status.DISCARD,
lbResponse)));
feignRequest = request;
}
else {
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Using service instance from LoadBalancedRetryContext: %s",
serviceInstance));
retrievedServiceInstance));
}
String reconstructedUrl = loadBalancerClient.reconstructURI(serviceInstance, originalUri)
String reconstructedUrl = loadBalancerClient.reconstructURI(retrievedServiceInstance, originalUri)
.toString();
feignRequest = Request.create(request.httpMethod(), reconstructedUrl, request.headers(),
request.body(), request.charset(), request.requestTemplate());
}
}
if (feignRequest == null) {
if (LOG.isWarnEnabled()) {
LOG.warn("Service instance was not resolved, executing the original request");
}
feignRequest = request;
}
Response response = delegate.execute(feignRequest, options);
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse = new DefaultResponse(
retrievedServiceInstance);
Response response = executeWithLoadBalancerLifecycleProcessing(delegate, options, feignRequest, lbResponse,
supportedLifecycleProcessors, retrievedServiceInstance != null);
int responseStatus = response.status();
if (retryPolicy != null && retryPolicy.retryableStatusCode(responseStatus)) {
if (LOG.isDebugEnabled()) {

View File

@@ -23,6 +23,8 @@ 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 feign.Client;
import feign.Request;
@@ -34,8 +36,12 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
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.reactive.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -66,8 +72,12 @@ class FeignBlockingLoadBalancerClientTests {
private BlockingLoadBalancerClient loadBalancerClient = mock(BlockingLoadBalancerClient.class);
private final LoadBalancerClientFactory loadBalancerClientFactory = mock(LoadBalancerClientFactory.class);
private final LoadBalancerProperties loadBalancerProperties = new LoadBalancerProperties();
private FeignBlockingLoadBalancerClient feignBlockingLoadBalancerClient = new FeignBlockingLoadBalancerClient(
delegate, loadBalancerClient, new LoadBalancerProperties());
delegate, loadBalancerClient, loadBalancerProperties, loadBalancerClientFactory);
@Test
void shouldExtractServiceIdFromRequestUrl() throws IOException {
@@ -121,6 +131,35 @@ class FeignBlockingLoadBalancerClientTests {
assertThat(new String(actualRequest.body())).isEqualTo("hello");
}
@Test
void shouldExecuteLoadBalancerLifecycleCallbacks() throws IOException {
Request request = testRequest();
Request.Options options = new Request.Options();
String url = "http://127.0.0.1/path";
ServiceInstance serviceInstance = new DefaultServiceInstance("test-1", "test", "test-host", 8888, false);
when(loadBalancerClient.choose(eq("test"), any())).thenReturn(serviceInstance);
when(loadBalancerClient.reconstructURI(serviceInstance, URI.create("http://test/path")))
.thenReturn(URI.create(url));
String callbackTestHint = "callbackTestHint";
loadBalancerProperties.getHint().put("test", callbackTestHint);
Map<String, LoadBalancerLifecycle> loadBalancerLifecycleBeans = new HashMap<>();
loadBalancerLifecycleBeans.put("loadBalancerLifecycle", new TestLoadBalancerLifecycle());
loadBalancerLifecycleBeans.put("anotherLoadBalancerLifecycle", new AnotherLoadBalancerLifecycle());
when(loadBalancerClientFactory.getInstances("test", LoadBalancerLifecycle.class))
.thenReturn(loadBalancerLifecycleBeans);
Object actualResult = feignBlockingLoadBalancerClient.execute(request, options);
Collection<org.springframework.cloud.client.loadbalancer.Request<Object>> lifecycleLogRequests = ((TestLoadBalancerLifecycle) loadBalancerLifecycleBeans
.get("loadBalancerLifecycle")).getStartLog().values();
Collection<CompletionContext<Object, ServiceInstance>> anotherLifecycleLogRequests = ((AnotherLoadBalancerLifecycle) loadBalancerLifecycleBeans
.get("anotherLoadBalancerLifecycle")).getCompleteLog().values();
assertThat(lifecycleLogRequests)
.extracting(lbRequest -> ((DefaultRequestContext) lbRequest.getContext()).getHint())
.contains(callbackTestHint);
assertThat(anotherLifecycleLogRequests).extracting(CompletionContext::getClientResponse).contains(actualResult);
}
private Request testRequest() {
return testRequest("test");
}
@@ -137,4 +176,43 @@ class FeignBlockingLoadBalancerClientTests {
}
protected static class TestLoadBalancerLifecycle implements LoadBalancerLifecycle<Object, Object, ServiceInstance> {
final ConcurrentHashMap<String, org.springframework.cloud.client.loadbalancer.Request<Object>> startLog = new ConcurrentHashMap<>();
final ConcurrentHashMap<String, CompletionContext<Object, ServiceInstance>> completeLog = new ConcurrentHashMap<>();
@Override
public void onStart(org.springframework.cloud.client.loadbalancer.Request<Object> request) {
startLog.put(getName() + UUID.randomUUID(), request);
}
@Override
public void onComplete(CompletionContext<Object, ServiceInstance> completionContext) {
completeLog.put(getName() + UUID.randomUUID(), completionContext);
}
ConcurrentHashMap<String, org.springframework.cloud.client.loadbalancer.Request<Object>> getStartLog() {
return startLog;
}
ConcurrentHashMap<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();
}
}
}

View File

@@ -23,6 +23,8 @@ 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 feign.Client;
import feign.Request;
@@ -35,10 +37,14 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.CompletionContext;
import org.springframework.cloud.client.loadbalancer.DefaultRequestContext;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
import org.springframework.cloud.loadbalancer.blocking.retry.BlockingLoadBalancedRetryPolicy;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@@ -70,10 +76,12 @@ class RetryableFeignBlockingLoadBalancerClientTests {
private BlockingLoadBalancerClient loadBalancerClient = mock(BlockingLoadBalancerClient.class);
private final LoadBalancerClientFactory loadBalancerClientFactory = mock(LoadBalancerClientFactory.class);
private LoadBalancerProperties properties = new LoadBalancerProperties();
private RetryableFeignBlockingLoadBalancerClient feignBlockingLoadBalancerClient = new RetryableFeignBlockingLoadBalancerClient(
delegate, loadBalancerClient, retryFactory, properties);
delegate, loadBalancerClient, retryFactory, properties, loadBalancerClientFactory);
private ServiceInstance serviceInstance = new DefaultServiceInstance("test-a", "test", "testhost", 80, false);
@@ -165,6 +173,37 @@ class RetryableFeignBlockingLoadBalancerClientTests {
assertThat(new String(actualRequest.body())).isEqualTo("hello");
}
@Test
void shouldExecuteLoadBalancerLifecycleCallbacks() throws IOException {
Request request = testRequest();
Request.Options options = new Request.Options();
String url = "http://127.0.0.1/path";
ServiceInstance serviceInstance = new DefaultServiceInstance("test-1", "test", "test-host", 8888, false);
when(loadBalancerClient.choose(eq("test"), any())).thenReturn(serviceInstance);
when(loadBalancerClient.reconstructURI(serviceInstance, URI.create("http://test/path")))
.thenReturn(URI.create(url));
Response response = testResponse(200);
when(delegate.execute(any(), any())).thenReturn(response);
String callbackTestHint = "callbackTestHint";
properties.getHint().put("test", callbackTestHint);
Map<String, LoadBalancerLifecycle> loadBalancerLifecycleBeans = new HashMap<>();
loadBalancerLifecycleBeans.put("loadBalancerLifecycle", new TestLoadBalancerLifecycle());
loadBalancerLifecycleBeans.put("anotherLoadBalancerLifecycle", new AnotherLoadBalancerLifecycle());
when(loadBalancerClientFactory.getInstances("test", LoadBalancerLifecycle.class))
.thenReturn(loadBalancerLifecycleBeans);
Object actualResult = feignBlockingLoadBalancerClient.execute(request, options);
Collection<org.springframework.cloud.client.loadbalancer.Request<Object>> lifecycleLogRequests = ((TestLoadBalancerLifecycle) loadBalancerLifecycleBeans
.get("loadBalancerLifecycle")).getStartLog().values();
Collection<CompletionContext<Object, ServiceInstance>> anotherLifecycleLogRequests = ((AnotherLoadBalancerLifecycle) loadBalancerLifecycleBeans
.get("anotherLoadBalancerLifecycle")).getCompleteLog().values();
assertThat(lifecycleLogRequests)
.extracting(lbRequest -> ((DefaultRequestContext) lbRequest.getContext()).getHint())
.contains(callbackTestHint);
assertThat(anotherLifecycleLogRequests).extracting(CompletionContext::getClientResponse).contains(actualResult);
}
private Request testRequest() {
return testRequest("test");
}
@@ -181,4 +220,44 @@ class RetryableFeignBlockingLoadBalancerClientTests {
}
protected static class TestLoadBalancerLifecycle implements LoadBalancerLifecycle<Object, Object, ServiceInstance> {
final ConcurrentHashMap<String, org.springframework.cloud.client.loadbalancer.Request<Object>> startLog = new ConcurrentHashMap<>();
final ConcurrentHashMap<String, CompletionContext<Object, ServiceInstance>> completeLog = new ConcurrentHashMap<>();
@Override
public void onStart(org.springframework.cloud.client.loadbalancer.Request<Object> request) {
startLog.put(getName() + UUID.randomUUID(), request);
}
@Override
public void onComplete(CompletionContext<Object, ServiceInstance> completionContext) {
completeLog.put(getName() + UUID.randomUUID(), completionContext);
}
ConcurrentHashMap<String, org.springframework.cloud.client.loadbalancer.Request<Object>> getStartLog() {
return startLog;
}
ConcurrentHashMap<String, CompletionContext<Object, ServiceInstance>> getCompleteLog() {
return completeLog;
}
protected String getName() {
return this.getClass().getSimpleName();
}
}
protected static class AnotherLoadBalancerLifecycle
extends FeignBlockingLoadBalancerClientTests.TestLoadBalancerLifecycle {
@Override
protected String getName() {
return this.getClass().getSimpleName();
}
}
}