Skip to content

Commit da23fe4

Browse files
Handle exceptions and timeouts new (#767)
* Handle timeouts and exceptions while retrieving instances. * Update docs.
1 parent 87e5d7a commit da23fe4

File tree

5 files changed

+147
-16
lines changed

5 files changed

+147
-16
lines changed

docs/src/main/asciidoc/_configprops.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@
2929
|spring.cloud.inetutils.use-only-site-local-interfaces | false | Whether to use only interfaces with site local addresses. See {@link InetAddress#isSiteLocalAddress()} for more details.
3030
|spring.cloud.loadbalancer.cache.caffeine.spec | | The spec to use to create caches. See CaffeineSpec for more details on the spec format.
3131
|spring.cloud.loadbalancer.cache.capacity | 256 | Initial cache capacity expressed as int.
32+
|spring.cloud.loadbalancer.cache.enabled | true | Enables Spring Cloud LoadBalancer caching mechanism.
3233
|spring.cloud.loadbalancer.cache.ttl | 35s | Time To Live - time counted from writing of the record, after which cache entries are expired, expressed as a {@link Duration}. The property {@link String} has to be in keeping with the appropriate syntax as specified in Spring Boot <code>StringToDurationConverter</code>. @see <a href= "https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/StringToDurationConverter.java">StringToDurationConverter.java</a>
3334
|spring.cloud.loadbalancer.health-check.initial-delay | 0 | Initial delay value for the HealthCheck scheduler.
3435
|spring.cloud.loadbalancer.health-check.interval | 25s | Interval for rerunning the HealthCheck scheduler.
3536
|spring.cloud.loadbalancer.health-check.path | |
3637
|spring.cloud.loadbalancer.retry.enabled | true |
3738
|spring.cloud.loadbalancer.ribbon.enabled | true | Causes `RibbonLoadBalancerClient` to be used by default.
39+
|spring.cloud.loadbalancer.service-discovery.timeout | | String representation of Duration of the timeout for calls to service discovery.
3840
|spring.cloud.loadbalancer.zone | | Spring Cloud LoadBalancer zone.
3941
|spring.cloud.refresh.enabled | true | Enables autoconfiguration for the refresh scope and associated features.
4042
|spring.cloud.refresh.extra-refreshable | true | Additional class names for beans to post process into refresh scope.

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -938,16 +938,21 @@ TIP: This mechanism is particularly helpful while using the `SimpleDiscoveryClie
938938
clients backed by an actual Service Registry, it's not necessary to use, as we already get
939939
healthy instances after querying the external ServiceDiscovery.
940940

941+
TIP:: This supplier is also recommended for setups with a small number of instances per service
942+
in order to avoid retrying calls on a failing instance.
943+
941944
`HealthCheckServiceInstanceListSupplier` uses properties prefixed with
942945
`spring.cloud.loadbalancer.healthcheck`. You can set the `initialDelay` and `interval`
943946
for the scheduler. You can set the default path for the healthcheck URL by setting
944947
the value of the `spring.cloud.loadbalancer.healthcheck.path.default`. You can also set a specific value
945948
for any given service by setting the value of the `spring.cloud.loadbalancer.healthcheck.path.[SERVICE_ID]`, substituting the `[SERVICE_ID]` with the correct ID of your service. If the path is not set, `/actuator/health` is used by default.
946949

950+
TIP:: If you rely on the default path (`/actuator/health`), make sure you add `spring-boot-starter-actuator` to your collaborator's dependencies, unless you are planning to add such an endpoint on your own.
951+
947952
In order to use the health-check scheduler approach, you will have to instantiate a `HealthCheckServiceInstanceListSupplier` bean in a <<custom-loadbalancer-configuration,custom configuration>>.
948953

949954
We use delegates to work with `ServiceInstanceListSupplier` beans.
950-
We suggest passing a `DiscoveryClientServiceInstanceListSupplier` delegate in the constructor of `HealthCheckServiceInstanceListSupplier` and, in turn, wrapping the latter with a `CachingServiceInstanceListSupplier` to leverage <<loadbalancer-caching, LoadBalancer caching mechanism>>.
955+
We suggest passing a `DiscoveryClientServiceInstanceListSupplier` delegate in the constructor of `HealthCheckServiceInstanceListSupplier`.
951956

952957
You could use this sample configuration to set it up:
953958

@@ -969,9 +974,6 @@ public class CustomLoadBalancerConfiguration {
969974

970975
NOTE:: `HealthCheckServiceInstanceListSupplier` has its own caching mechanism based on Reactor Flux `replay()`, therefore, if it's being used, you may want to skip wrapping that supplier with `CachingServiceInstanceListSupplier`.
971976

972-
TIP:: In order to make working on your own LoadBalancer configuration easier, we have added a `builder()` method to the `ServiceInstanceListSupplier` class.
973-
974-
TIP:: You can also use our alternative predefined configurations in place of the default ones by setting the value of `spring.cloud.loadbalancer.configurations` property to `zone-preference` to use `ZonePreferenceServiceInstanceListSupplier` with caching or to `health-check` to use `HealthCheckServiceInstanceListSupplier` with caching.
975977

976978
[[spring-cloud-loadbalancer-starter]]
977979
=== Spring Cloud LoadBalancer Starter
@@ -1005,6 +1007,10 @@ public class MyConfiguration {
10051007
}
10061008
}
10071009
----
1010+
1011+
TIP:: In order to make working on your own LoadBalancer configuration easier, we have added a `builder()` method to the `ServiceInstanceListSupplier` class.
1012+
1013+
TIP:: You can also use our alternative predefined configurations in place of the default ones by setting the value of `spring.cloud.loadbalancer.configurations` property to `zone-preference` to use `ZonePreferenceServiceInstanceListSupplier` with caching or to `health-check` to use `HealthCheckServiceInstanceListSupplier` with caching.
10081014
====
10091015

10101016
You can use this feature to instantiate different implementations of `ServiceInstanceListSupplier` or `ReactorLoadBalancer`, either written by you, or provided by us as alternatives (for example `ZonePreferenceServiceInstanceListSupplier`) to override the default setup.

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

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@
1616

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

19+
import java.time.Duration;
20+
import java.util.ArrayList;
1921
import java.util.List;
2022

23+
import org.apache.commons.logging.Log;
24+
import org.apache.commons.logging.LogFactory;
2125
import reactor.core.publisher.Flux;
2226
import reactor.core.scheduler.Schedulers;
2327

28+
import org.springframework.boot.convert.DurationStyle;
2429
import org.springframework.cloud.client.ServiceInstance;
2530
import org.springframework.cloud.client.discovery.DiscoveryClient;
2631
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
@@ -39,23 +44,47 @@
3944
public class DiscoveryClientServiceInstanceListSupplier
4045
implements ServiceInstanceListSupplier {
4146

47+
/**
48+
* Property that establishes the timeout for calls to service discovery.
49+
*/
50+
public static final String SERVICE_DISCOVERY_TIMEOUT = "spring.cloud.loadbalancer.service-discovery.timeout";
51+
52+
private static final Log LOG = LogFactory
53+
.getLog(DiscoveryClientServiceInstanceListSupplier.class);
54+
55+
private Duration timeout = Duration.ofSeconds(30);
56+
4257
private final String serviceId;
4358

4459
private final Flux<List<ServiceInstance>> serviceInstances;
4560

4661
public DiscoveryClientServiceInstanceListSupplier(DiscoveryClient delegate,
4762
Environment environment) {
4863
this.serviceId = environment.getProperty(PROPERTY_NAME);
64+
resolveTimeout(environment);
4965
this.serviceInstances = Flux
5066
.defer(() -> Flux.just(delegate.getInstances(serviceId)))
51-
.subscribeOn(Schedulers.boundedElastic());
67+
.timeout(timeout, Flux.defer(() -> {
68+
logTimeout();
69+
return Flux.just(new ArrayList<>());
70+
})).onErrorResume(error -> {
71+
logException(error);
72+
return Flux.just(new ArrayList<>());
73+
}).subscribeOn(Schedulers.boundedElastic());
5274
}
5375

5476
public DiscoveryClientServiceInstanceListSupplier(ReactiveDiscoveryClient delegate,
5577
Environment environment) {
5678
this.serviceId = environment.getProperty(PROPERTY_NAME);
57-
this.serviceInstances = Flux
58-
.defer(() -> delegate.getInstances(serviceId).collectList().flux());
79+
resolveTimeout(environment);
80+
this.serviceInstances = Flux.defer(() -> delegate.getInstances(serviceId)
81+
.collectList().flux().timeout(timeout, Flux.defer(() -> {
82+
logTimeout();
83+
return Flux.just(new ArrayList<>());
84+
})).onErrorResume(error -> {
85+
logException(error);
86+
return Flux.just(new ArrayList<>());
87+
}));
5988
}
6089

6190
@Override
@@ -68,4 +97,28 @@ public Flux<List<ServiceInstance>> get() {
6897
return serviceInstances;
6998
}
7099

100+
private void resolveTimeout(Environment environment) {
101+
String providedTimeout = environment.getProperty(SERVICE_DISCOVERY_TIMEOUT);
102+
if (providedTimeout != null) {
103+
timeout = DurationStyle.detectAndParse(providedTimeout);
104+
}
105+
}
106+
107+
private void logTimeout() {
108+
if (LOG.isDebugEnabled()) {
109+
LOG.debug(String.format(
110+
"Timeout occurred while retrieving instances for service %s."
111+
+ "The instances could not be retrieved during %s",
112+
serviceId, timeout));
113+
}
114+
}
115+
116+
private void logException(Throwable error) {
117+
if (LOG.isDebugEnabled()) {
118+
LOG.debug(String.format(
119+
"Exception occurred while retrieving instances for service %s",
120+
serviceId), error);
121+
}
122+
}
123+
71124
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@
1010
"name": "spring.cloud.loadbalancer.zone",
1111
"type": "java.lang.String",
1212
"description": "Spring Cloud LoadBalancer zone."
13+
},
14+
{
15+
"name": "spring.cloud.loadbalancer.service-discovery.timeout",
16+
"description": "String representation of Duration of the timeout for calls to service discovery.",
17+
"type": "java.lang.String"
18+
},
19+
{
20+
"defaultValue": true,
21+
"name": "spring.cloud.loadbalancer.cache.enabled",
22+
"description": "Enables Spring Cloud LoadBalancer caching mechanism.",
23+
"type": "java.lang.Boolean"
1324
}
1425
]
1526
}

spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/DiscoveryClientServiceInstanceListSupplierTests.java

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616

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

19+
import java.time.Duration;
20+
1921
import org.assertj.core.util.Lists;
2022
import org.junit.jupiter.api.BeforeEach;
2123
import org.junit.jupiter.api.Test;
24+
import org.mockito.internal.stubbing.answers.AnswersWithDelay;
25+
import org.mockito.internal.stubbing.answers.Returns;
2226
import reactor.core.publisher.Flux;
2327
import reactor.test.StepVerifier;
2428

@@ -29,6 +33,7 @@
2933

3034
import static org.mockito.Mockito.mock;
3135
import static org.mockito.Mockito.when;
36+
import static org.springframework.cloud.loadbalancer.core.DiscoveryClientServiceInstanceListSupplier.SERVICE_DISCOVERY_TIMEOUT;
3237

3338
/**
3439
* Tests for {@link DiscoveryClientServiceInstanceListSupplier}.
@@ -39,6 +44,8 @@ class DiscoveryClientServiceInstanceListSupplierTests {
3944

4045
private static final String SERVICE_ID = "test";
4146

47+
private static final Duration VERIFICATION_TIMEOUT = Duration.ofSeconds(10);
48+
4249
private final MockEnvironment environment = new MockEnvironment();
4350

4451
private final ReactiveDiscoveryClient reactiveDiscoveryClient = mock(
@@ -66,9 +73,10 @@ void shouldReturnRetrievedInstances() {
6673
supplier = new DiscoveryClientServiceInstanceListSupplier(
6774
reactiveDiscoveryClient, environment);
6875
return supplier.get();
69-
}).expectSubscription().expectNext(
70-
Lists.list(instance("1host", false), instance("2host-secure", true)))
71-
.thenCancel().verify();
76+
}).expectSubscription()
77+
.expectNext(Lists.list(instance("1host", false),
78+
instance("2host-secure", true)))
79+
.thenCancel().verify(VERIFICATION_TIMEOUT);
7280
}
7381

7482
@Test
@@ -81,7 +89,7 @@ void shouldUpdateReturnRetrievedInstances() {
8189
StepVerifier.withVirtualTime(() -> supplier.get()).expectSubscription()
8290
.expectNext(Lists.list(instance("1host", false),
8391
instance("2host-secure", true)))
84-
.thenCancel().verify();
92+
.thenCancel().verify(VERIFICATION_TIMEOUT);
8593

8694
when(reactiveDiscoveryClient.getInstances(SERVICE_ID))
8795
.thenReturn(Flux.just(instance("1host", false),
@@ -90,7 +98,31 @@ void shouldUpdateReturnRetrievedInstances() {
9098
StepVerifier.withVirtualTime(() -> supplier.get()).expectSubscription()
9199
.expectNext(Lists.list(instance("1host", false),
92100
instance("2host-secure", true), instance("3host", false)))
93-
.thenCancel().verify();
101+
.thenCancel().verify(VERIFICATION_TIMEOUT);
102+
}
103+
104+
@Test
105+
void shouldReturnEmptyInstancesListOnException() {
106+
when(reactiveDiscoveryClient.getInstances(SERVICE_ID))
107+
.thenReturn(Flux.error(new RuntimeException("Exception")));
108+
109+
StepVerifier.withVirtualTime(() -> {
110+
supplier = new DiscoveryClientServiceInstanceListSupplier(
111+
reactiveDiscoveryClient, environment);
112+
return supplier.get();
113+
}).expectSubscription().expectNext(Lists.emptyList()).thenCancel()
114+
.verify(VERIFICATION_TIMEOUT);
115+
}
116+
117+
@Test
118+
void shouldReturnEmptyInstancesListOnTimeout() {
119+
environment.setProperty(SERVICE_DISCOVERY_TIMEOUT, "100ms");
120+
when(reactiveDiscoveryClient.getInstances(SERVICE_ID)).thenReturn(Flux.never());
121+
StepVerifier
122+
.create(new DiscoveryClientServiceInstanceListSupplier(
123+
reactiveDiscoveryClient, environment).get())
124+
.expectSubscription().expectNext(Lists.emptyList()).thenCancel()
125+
.verify(VERIFICATION_TIMEOUT);
94126
}
95127

96128
@Test
@@ -102,9 +134,10 @@ void shouldReturnRetrievedInstancesBlockingClient() {
102134
supplier = new DiscoveryClientServiceInstanceListSupplier(discoveryClient,
103135
environment);
104136
return supplier.get();
105-
}).expectSubscription().expectNext(
106-
Lists.list(instance("1host", false), instance("2host-secure", true)))
107-
.thenCancel().verify();
137+
}).expectSubscription()
138+
.expectNext(Lists.list(instance("1host", false),
139+
instance("2host-secure", true)))
140+
.thenCancel().verify(VERIFICATION_TIMEOUT);
108141
}
109142

110143
@Test
@@ -123,7 +156,33 @@ void shouldUpdateReturnRetrievedInstancesBlockingClient() {
123156
}).expectSubscription()
124157
.expectNext(Lists.list(instance("1host", false),
125158
instance("2host-secure", true), instance("3host", false)))
126-
.thenCancel().verify();
159+
.thenCancel().verify(VERIFICATION_TIMEOUT);
160+
}
161+
162+
@Test
163+
void shouldReturnEmptyInstancesListOnExceptionBlockingClient() {
164+
when(discoveryClient.getInstances(SERVICE_ID))
165+
.thenThrow(new RuntimeException("Exception"));
166+
167+
StepVerifier.withVirtualTime(() -> {
168+
supplier = new DiscoveryClientServiceInstanceListSupplier(discoveryClient,
169+
environment);
170+
return supplier.get();
171+
}).expectSubscription().expectNext(Lists.emptyList()).thenCancel()
172+
.verify(VERIFICATION_TIMEOUT);
173+
}
174+
175+
@Test
176+
void shouldReturnEmptyInstancesListOnTimeoutBlockingClient() {
177+
environment.setProperty(SERVICE_DISCOVERY_TIMEOUT, "100ms");
178+
when(discoveryClient.getInstances(SERVICE_ID)).thenAnswer(
179+
new AnswersWithDelay(200, new Returns(Lists.list(instance("1host", false),
180+
instance("2host-secure", true), instance("3host", false)))));
181+
StepVerifier
182+
.create(new DiscoveryClientServiceInstanceListSupplier(discoveryClient,
183+
environment).get())
184+
.expectSubscription().expectNext(Lists.emptyList()).thenCancel()
185+
.verify(VERIFICATION_TIMEOUT);
127186
}
128187

129188
}

0 commit comments

Comments
 (0)