Skip to content

Commit 6649c86

Browse files
Support loadbalancer lifecycle (#418)
* Add LoadBalancerLifecycle support. * Add info on SC LoadBalancer documentation.
2 parents 89f8942 + 3f0c285 commit 6649c86

File tree

10 files changed

+325
-33
lines changed

10 files changed

+325
-33
lines changed

docs/src/main/asciidoc/spring-cloud-openfeign.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ it will resolve the service in the Eureka service registry. If you
6868
don't want to use Eureka, you can simply configure a list of servers
6969
in your external configuration using https://cloud.spring.io/spring-cloud-static/spring-cloud-commons/current/reference/html/#simplediscoveryclient[`SimpleDiscoveryClient`].
7070

71+
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].
72+
7173
[[spring-cloud-feign-overriding-defaults]]
7274
=== Overriding Feign Defaults
7375

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/DefaultFeignLoadBalancerConfiguration.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
2727
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
2828
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
29+
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
2930
import org.springframework.context.annotation.Bean;
3031
import org.springframework.context.annotation.Conditional;
3132
import org.springframework.context.annotation.Configuration;
@@ -44,8 +45,10 @@ class DefaultFeignLoadBalancerConfiguration {
4445
@Bean
4546
@ConditionalOnMissingBean
4647
@Conditional(OnRetryNotEnabledCondition.class)
47-
public Client feignClient(LoadBalancerClient loadBalancerClient, LoadBalancerProperties properties) {
48-
return new FeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient, properties);
48+
public Client feignClient(LoadBalancerClient loadBalancerClient, LoadBalancerProperties properties,
49+
LoadBalancerClientFactory loadBalancerClientFactory) {
50+
return new FeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient, properties,
51+
loadBalancerClientFactory);
4952
}
5053

5154
@Bean
@@ -55,9 +58,10 @@ public Client feignClient(LoadBalancerClient loadBalancerClient, LoadBalancerPro
5558
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
5659
matchIfMissing = true)
5760
public Client feignRetryClient(LoadBalancerClient loadBalancerClient,
58-
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties) {
61+
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
62+
LoadBalancerClientFactory loadBalancerClientFactory) {
5963
return new RetryableFeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient,
60-
loadBalancedRetryFactory, properties);
64+
loadBalancedRetryFactory, properties, loadBalancerClientFactory);
6165
}
6266

6367
}

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClient.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.IOException;
2020
import java.net.URI;
2121
import java.nio.charset.StandardCharsets;
22+
import java.util.Set;
2223

2324
import feign.Client;
2425
import feign.Request;
@@ -27,20 +28,30 @@
2728
import org.apache.commons.logging.LogFactory;
2829

2930
import org.springframework.cloud.client.ServiceInstance;
31+
import org.springframework.cloud.client.loadbalancer.CompletionContext;
3032
import org.springframework.cloud.client.loadbalancer.DefaultRequest;
3133
import org.springframework.cloud.client.loadbalancer.DefaultRequestContext;
34+
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
35+
import org.springframework.cloud.client.loadbalancer.HttpRequestContext;
3236
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
37+
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
38+
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycleValidator;
3339
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
40+
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
3441
import org.springframework.http.HttpStatus;
42+
import org.springframework.http.client.ClientHttpResponse;
3543
import org.springframework.util.Assert;
3644

45+
import static org.springframework.cloud.openfeign.loadbalancer.LoadBalancerUtils.executeWithLoadBalancerLifecycleProcessing;
46+
3747
/**
3848
* A {@link Client} implementation that uses {@link LoadBalancerClient} to select a
3949
* {@link ServiceInstance} to use while resolving the request host.
4050
*
4151
* @author Olga Maciaszek-Sharma
4252
* @since 2.2.0
4353
*/
54+
@SuppressWarnings({ "unchecked", "rawtypes" })
4455
public class FeignBlockingLoadBalancerClient implements Client {
4556

4657
private static final Log LOG = LogFactory.getLog(FeignBlockingLoadBalancerClient.class);
@@ -51,11 +62,14 @@ public class FeignBlockingLoadBalancerClient implements Client {
5162

5263
private final LoadBalancerProperties properties;
5364

65+
private final LoadBalancerClientFactory loadBalancerClientFactory;
66+
5467
public FeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient,
55-
LoadBalancerProperties properties) {
68+
LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
5669
this.delegate = delegate;
5770
this.loadBalancerClient = loadBalancerClient;
5871
this.properties = properties;
72+
this.loadBalancerClientFactory = loadBalancerClientFactory;
5973
}
6074

6175
@Override
@@ -66,19 +80,30 @@ public Response execute(Request request, Request.Options options) throws IOExcep
6680
String hint = getHint(serviceId);
6781
DefaultRequest<DefaultRequestContext> lbRequest = new DefaultRequest<>(
6882
new DefaultRequestContext(request, hint));
83+
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
84+
.getSupportedLifecycleProcessors(
85+
loadBalancerClientFactory.getInstances(serviceId, LoadBalancerLifecycle.class),
86+
HttpRequestContext.class, ClientHttpResponse.class, ServiceInstance.class);
87+
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
6988
ServiceInstance instance = loadBalancerClient.choose(serviceId, lbRequest);
89+
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse = new DefaultResponse(
90+
instance);
7091
if (instance == null) {
7192
String message = "Load balancer does not contain an instance for the service " + serviceId;
7293
if (LOG.isWarnEnabled()) {
7394
LOG.warn(message);
7495
}
96+
supportedLifecycleProcessors.forEach(
97+
lifecycle -> lifecycle.onComplete(new CompletionContext<ClientHttpResponse, ServiceInstance>(
98+
CompletionContext.Status.DISCARD, lbResponse)));
7599
return Response.builder().request(request).status(HttpStatus.SERVICE_UNAVAILABLE.value())
76100
.body(message, StandardCharsets.UTF_8).build();
77101
}
78102
String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri).toString();
79103
Request newRequest = Request.create(request.httpMethod(), reconstructedUrl, request.headers(), request.body(),
80104
request.charset(), request.requestTemplate());
81-
return delegate.execute(newRequest, options);
105+
return executeWithLoadBalancerLifecycleProcessing(delegate, options, newRequest, lbResponse,
106+
supportedLifecycleProcessors);
82107
}
83108

84109
// Visible for Sleuth instrumentation

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignLoadBalancerAutoConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
2525
import org.springframework.boot.context.properties.EnableConfigurationProperties;
2626
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
27+
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
2728
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
2829
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
2930
import org.springframework.context.annotation.Configuration;
@@ -37,7 +38,7 @@
3738
* @since 2.2.0
3839
*/
3940
@ConditionalOnClass(Feign.class)
40-
@ConditionalOnBean(LoadBalancerClient.class)
41+
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
4142
@AutoConfigureBefore(FeignAutoConfiguration.class)
4243
@EnableConfigurationProperties(FeignHttpClientProperties.class)
4344
@Configuration(proxyBeanMethods = false)

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/HttpClientFeignLoadBalancerConfiguration.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
2929
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
3030
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
31+
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
3132
import org.springframework.cloud.openfeign.clientconfig.HttpClientFeignConfiguration;
3233
import org.springframework.context.annotation.Bean;
3334
import org.springframework.context.annotation.Conditional;
@@ -43,7 +44,7 @@
4344
*/
4445
@Configuration(proxyBeanMethods = false)
4546
@ConditionalOnClass(ApacheHttpClient.class)
46-
@ConditionalOnBean(LoadBalancerClient.class)
47+
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
4748
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
4849
@Import(HttpClientFeignConfiguration.class)
4950
@EnableConfigurationProperties(LoadBalancerProperties.class)
@@ -53,9 +54,9 @@ class HttpClientFeignLoadBalancerConfiguration {
5354
@ConditionalOnMissingBean
5455
@Conditional(OnRetryNotEnabledCondition.class)
5556
public Client feignClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient,
56-
LoadBalancerProperties properties) {
57+
LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
5758
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
58-
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties);
59+
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties, loadBalancerClientFactory);
5960
}
6061

6162
@Bean
@@ -65,10 +66,11 @@ public Client feignClient(LoadBalancerClient loadBalancerClient, HttpClient http
6566
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
6667
matchIfMissing = true)
6768
public Client feignRetryClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient,
68-
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties) {
69+
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
70+
LoadBalancerClientFactory loadBalancerClientFactory) {
6971
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
7072
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory,
71-
properties);
73+
properties, loadBalancerClientFactory);
7274
}
7375

7476
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2013-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.openfeign.loadbalancer;
18+
19+
import java.io.IOException;
20+
import java.util.Set;
21+
22+
import feign.Client;
23+
import feign.Request;
24+
import feign.Response;
25+
26+
import org.springframework.cloud.client.ServiceInstance;
27+
import org.springframework.cloud.client.loadbalancer.CompletionContext;
28+
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
29+
30+
/**
31+
* @author Olga Maciaszek-Sharma
32+
*
33+
* A utility class for handling {@link LoadBalancerLifecycle} calls.
34+
*/
35+
@SuppressWarnings({ "unchecked", "rawtypes" })
36+
final class LoadBalancerUtils {
37+
38+
private LoadBalancerUtils() {
39+
throw new IllegalStateException("Can't instantiate a utility class");
40+
}
41+
42+
static Response executeWithLoadBalancerLifecycleProcessing(Client feignClient, Request.Options options,
43+
Request feignRequest, org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse,
44+
Set<LoadBalancerLifecycle> supportedLifecycleProcessors, boolean loadBalanced) throws IOException {
45+
try {
46+
Response response = feignClient.execute(feignRequest, options);
47+
if (loadBalanced) {
48+
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
49+
.onComplete(new CompletionContext<>(CompletionContext.Status.SUCCESS, lbResponse, response)));
50+
}
51+
return response;
52+
}
53+
catch (Exception exception) {
54+
if (loadBalanced) {
55+
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
56+
.onComplete(new CompletionContext<>(CompletionContext.Status.FAILED, exception, lbResponse)));
57+
}
58+
throw exception;
59+
}
60+
}
61+
62+
static Response executeWithLoadBalancerLifecycleProcessing(Client feignClient, Request.Options options,
63+
Request feignRequest, org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse,
64+
Set<LoadBalancerLifecycle> supportedLifecycleProcessors) throws IOException {
65+
return executeWithLoadBalancerLifecycleProcessing(feignClient, options, feignRequest, lbResponse,
66+
supportedLifecycleProcessors, true);
67+
}
68+
69+
}

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/OkHttpFeignLoadBalancerConfiguration.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
2828
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
2929
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
30+
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
3031
import org.springframework.cloud.openfeign.clientconfig.OkHttpFeignConfiguration;
3132
import org.springframework.context.annotation.Bean;
3233
import org.springframework.context.annotation.Conditional;
@@ -43,7 +44,7 @@
4344
@Configuration(proxyBeanMethods = false)
4445
@ConditionalOnClass(OkHttpClient.class)
4546
@ConditionalOnProperty("feign.okhttp.enabled")
46-
@ConditionalOnBean(LoadBalancerClient.class)
47+
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
4748
@Import(OkHttpFeignConfiguration.class)
4849
@EnableConfigurationProperties(LoadBalancerProperties.class)
4950
class OkHttpFeignLoadBalancerConfiguration {
@@ -52,9 +53,9 @@ class OkHttpFeignLoadBalancerConfiguration {
5253
@ConditionalOnMissingBean
5354
@Conditional(OnRetryNotEnabledCondition.class)
5455
public Client feignClient(okhttp3.OkHttpClient okHttpClient, LoadBalancerClient loadBalancerClient,
55-
LoadBalancerProperties properties) {
56+
LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
5657
OkHttpClient delegate = new OkHttpClient(okHttpClient);
57-
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties);
58+
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties, loadBalancerClientFactory);
5859
}
5960

6061
@Bean
@@ -64,10 +65,11 @@ public Client feignClient(okhttp3.OkHttpClient okHttpClient, LoadBalancerClient
6465
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
6566
matchIfMissing = true)
6667
public Client feignRetryClient(LoadBalancerClient loadBalancerClient, okhttp3.OkHttpClient okHttpClient,
67-
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties) {
68+
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
69+
LoadBalancerClientFactory loadBalancerClientFactory) {
6870
OkHttpClient delegate = new OkHttpClient(okHttpClient);
6971
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory,
70-
properties);
72+
properties, loadBalancerClientFactory);
7173
}
7274

7375
}

0 commit comments

Comments
 (0)