Skip to content

Commit d612436

Browse files
committed
Handle avoiding previous instance with ServiceInstanceListSupplier in place of LoadBalancer.
1 parent fd013b4 commit d612436

13 files changed

+239
-205
lines changed

docs/src/main/asciidoc/_configprops.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
|spring.cloud.loadbalancer.health-check.interval | 25s | Interval for rerunning the HealthCheck scheduler.
3636
|spring.cloud.loadbalancer.health-check.path | |
3737
|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.
38-
|spring.cloud.loadbalancer.retry.avoidPreviousInstance.enabled | true | Instantiates AvoidPreviousInstanceRoundRobinLoadBalancer if Spring-Retry is in the classpath.
38+
|spring.cloud.loadbalancer.retry.avoidPreviousInstance | true | Enables wrapping ServiceInstanceListSupplier beans with `RetryAwareServiceInstanceListSupplier` if Spring-Retry is in the classpath.
3939
|spring.cloud.loadbalancer.retry.enabled | true |
4040
|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.
4141
|spring.cloud.loadbalancer.retry.max-retries-on-same-service-instance | 0 | Number of retries to be executed on the same <code>ServiceInstance</code>.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ You can set:
449449
- `spring.cloud.loadbalancer.retry.maxRetriesOnNextServiceInstance` - indicates how many times a request should be retried a newly selected `ServiceInstance`
450450
- `spring.cloud.loadbalancer.retry.retryableStatusCodes` - the status codes on which to always retry a failed request.
451451

452-
NOTE: For load-balanced retries, by default, we instantiate `AvoidPreviousInstanceRoundRobinLoadBalancer` to select a different instance from the one previously chosen, if available. You can switch back to using the basic `RoundRobinLoadBalancer` by setting the value of `spring.cloud.loadbalancer.retry.avoidPreviousInstance.enabled` to `false`.
452+
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 behaviour by setting the value of `spring.cloud.loadbalancer.retry.avoidPreviousInstance` to `false`.
453453

454454
====
455455
[source,java,indent=0]

spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.cloud.loadbalancer.annotation;
1818

19+
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
1920
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
2021
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
2122
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -27,8 +28,8 @@
2728
import org.springframework.cloud.client.ServiceInstance;
2829
import org.springframework.cloud.client.discovery.DiscoveryClient;
2930
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
30-
import org.springframework.cloud.loadbalancer.core.AvoidPreviousInstanceRoundRobinLoadBalancer;
3131
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
32+
import org.springframework.cloud.loadbalancer.core.RetryAwareServiceInstanceListSupplier;
3233
import org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer;
3334
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
3435
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
@@ -39,6 +40,7 @@
3940
import org.springframework.context.annotation.Primary;
4041
import org.springframework.core.annotation.Order;
4142
import org.springframework.core.env.Environment;
43+
import org.springframework.retry.support.RetryTemplate;
4244

4345
/**
4446
* @author Spencer Gibb
@@ -60,17 +62,6 @@ public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(E
6062
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
6163
}
6264

63-
@Bean
64-
@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
65-
@Conditional(OnAvoidPreviousInstanceAndRetryEnabledCondition.class)
66-
@Primary
67-
public ReactorLoadBalancer<ServiceInstance> avoidRetryingOnSameServiceInstanceLoadBalancer(Environment environment,
68-
LoadBalancerClientFactory loadBalancerClientFactory) {
69-
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
70-
return new AvoidPreviousInstanceRoundRobinLoadBalancer(
71-
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
72-
}
73-
7465
@Configuration(proxyBeanMethods = false)
7566
@ConditionalOnReactiveDiscoveryEnabled
7667
@Order(REACTIVE_SERVICE_INSTANCE_SUPPLIER_ORDER)
@@ -145,6 +136,24 @@ public ServiceInstanceListSupplier healthCheckDiscoveryClientServiceInstanceList
145136

146137
}
147138

139+
@Configuration(proxyBeanMethods = false)
140+
@ConditionalOnBlockingDiscoveryEnabled
141+
@ConditionalOnClass(RetryTemplate.class)
142+
@Conditional(OnAvoidPreviousInstanceAndRetryEnabledCondition.class)
143+
@AutoConfigureAfter(BlockingSupportConfiguration.class)
144+
@ConditionalOnBean(ServiceInstanceListSupplier.class)
145+
public static class BlockingRetryConfiguration {
146+
147+
@Bean
148+
@ConditionalOnBean(DiscoveryClient.class)
149+
@Primary
150+
public ServiceInstanceListSupplier retryAwareDiscoveryClientServiceInstanceListSupplier(
151+
ServiceInstanceListSupplier delegate) {
152+
return new RetryAwareServiceInstanceListSupplier(delegate);
153+
}
154+
155+
}
156+
148157
static final class OnAvoidPreviousInstanceAndRetryEnabledCondition extends AllNestedConditions {
149158

150159
private OnAvoidPreviousInstanceAndRetryEnabledCondition() {
@@ -157,8 +166,8 @@ static class LoadBalancerRetryEnabled {
157166

158167
}
159168

160-
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.avoidPreviousInstance.enabled",
161-
havingValue = "true", matchIfMissing = true)
169+
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.avoidPreviousInstance", havingValue = "true",
170+
matchIfMissing = true)
162171
static class AvoidPreviousInstanceEnabled {
163172

164173
}

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

Lines changed: 0 additions & 102 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2012-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.loadbalancer.core;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
22+
import org.apache.commons.logging.Log;
23+
import org.apache.commons.logging.LogFactory;
24+
import reactor.core.publisher.Flux;
25+
26+
import org.springframework.cloud.client.ServiceInstance;
27+
import org.springframework.cloud.client.loadbalancer.Request;
28+
import org.springframework.cloud.client.loadbalancer.RetryableRequestContext;
29+
30+
/**
31+
* A {@link ServiceInstanceListSupplier} implementation that avoids picking the same
32+
* service instance while retrying requests.
33+
*
34+
* @author Olga Maciaszek-Sharma
35+
* @since 3.0.0
36+
*/
37+
public class RetryAwareServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier {
38+
39+
private final Log LOG = LogFactory.getLog(RetryAwareServiceInstanceListSupplier.class);
40+
41+
public RetryAwareServiceInstanceListSupplier(ServiceInstanceListSupplier delegate) {
42+
super(delegate);
43+
}
44+
45+
@Override
46+
public String getServiceId() {
47+
return delegate.getServiceId();
48+
}
49+
50+
@Override
51+
public Flux<List<ServiceInstance>> get(Request request) {
52+
if (!(request.getContext() instanceof RetryableRequestContext)) {
53+
return get();
54+
}
55+
RetryableRequestContext context = (RetryableRequestContext) request.getContext();
56+
ServiceInstance previousServiceInstance = context.getPreviousServiceInstance();
57+
if (previousServiceInstance == null) {
58+
return get();
59+
}
60+
return get().map(instances -> filteredByPreviousInstance(instances, previousServiceInstance));
61+
}
62+
63+
private List<ServiceInstance> filteredByPreviousInstance(List<ServiceInstance> instances,
64+
ServiceInstance previousServiceInstance) {
65+
List<ServiceInstance> filteredInstances = new ArrayList<>(instances);
66+
if (previousServiceInstance != null) {
67+
filteredInstances.remove(previousServiceInstance);
68+
}
69+
if (filteredInstances.size() > 0) {
70+
return filteredInstances;
71+
}
72+
if (LOG.isWarnEnabled()) {
73+
LOG.warn(String.format(
74+
"No instances found after removing previously used service instance from the search (%s). Returning all found instances.",
75+
previousServiceInstance));
76+
}
77+
return instances;
78+
}
79+
80+
@Override
81+
public Flux<List<ServiceInstance>> get() {
82+
return delegate.get();
83+
}
84+
85+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public RoundRobinLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> servic
7878
public Mono<Response<ServiceInstance>> choose(Request request) {
7979
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
8080
.getIfAvailable(NoopServiceInstanceListSupplier::new);
81-
return supplier.get().next().map(this::getInstanceResponse);
81+
return supplier.get(request).next().map(this::getInstanceResponse);
8282
}
8383

8484
Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import org.springframework.cloud.client.DefaultServiceInstance;
2626
import org.springframework.cloud.client.ServiceInstance;
27+
import org.springframework.cloud.client.loadbalancer.Request;
2728
import org.springframework.core.env.Environment;
2829

2930
import static org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory.PROPERTY_NAME;
@@ -38,6 +39,10 @@ public interface ServiceInstanceListSupplier extends Supplier<Flux<List<ServiceI
3839

3940
String getServiceId();
4041

42+
default Flux<List<ServiceInstance>> get(Request request) {
43+
return get();
44+
}
45+
4146
static ServiceInstanceListSupplierBuilder builder() {
4247
return new ServiceInstanceListSupplierBuilder();
4348
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ public ServiceInstanceListSupplierBuilder withCaching() {
172172
return this;
173173
}
174174

175+
public ServiceInstanceListSupplierBuilder withRetryAwareness() {
176+
DelegateCreator creator = (context, delegate) -> new RetryAwareServiceInstanceListSupplier(delegate);
177+
creators.add(creator);
178+
return this;
179+
}
180+
175181
/**
176182
* Builds the {@link ServiceInstanceListSupplier} hierarchy.
177183
* @param context application context

spring-cloud-loadbalancer/src/main/resources/META-INF/additional-spring-configuration-metadata.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
},
2525
{
2626
"defaultValue": true,
27-
"name": "spring.cloud.loadbalancer.retry.avoidPreviousInstance.enabled",
28-
"description": "Instantiates AvoidPreviousInstanceRoundRobinLoadBalancer if Spring-Retry is in the classpath.",
27+
"name": "spring.cloud.loadbalancer.retry.avoidPreviousInstance",
28+
"description": "Enables wrapping ServiceInstanceListSupplier beans with `RetryAwareServiceInstanceListSupplier` if Spring-Retry is in the classpath.",
2929
"type": "java.lang.Boolean"
3030
}
3131
]

0 commit comments

Comments
 (0)