Fix gh 491 gh 553 non reactive loadbalancer client (#590)

* Provide non-reactive LB client implemenation to use with RestTemplate.
Fixes gh-491.
Fixes gh-553.

* Add javadoc.
This commit is contained in:
Olga Maciaszek-Sharma
2019-08-12 12:21:38 +02:00
committed by GitHub
parent 3f17c0d902
commit c60df0f18e
8 changed files with 413 additions and 7 deletions

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2012-2019 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.loadbalancer.blocking.client;
import java.io.IOException;
import java.net.URI;
import reactor.core.publisher.Mono;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest;
import org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.client.loadbalancer.reactive.Response;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.util.ReflectionUtils;
/**
* The default {@link LoadBalancerClient} implementation.
*
* @author Olga Maciaszek-Sharma
* @since 2.2.0
*/
public class BlockingLoadBalancerClient implements LoadBalancerClient {
private final LoadBalancerClientFactory loadBalancerClientFactory;
public BlockingLoadBalancerClient(
LoadBalancerClientFactory loadBalancerClientFactory) {
this.loadBalancerClientFactory = loadBalancerClientFactory;
}
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request)
throws IOException {
ServiceInstance serviceInstance = choose(serviceId);
if (serviceInstance == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
return execute(serviceId, serviceInstance, request);
}
@Override
public <T> T execute(String serviceId, ServiceInstance serviceInstance,
LoadBalancerRequest<T> request) throws IOException {
try {
return request.apply(serviceInstance);
}
catch (IOException iOException) {
throw iOException;
}
catch (Exception exception) {
ReflectionUtils.rethrowRuntimeException(exception);
}
return null;
}
@Override
public URI reconstructURI(ServiceInstance serviceInstance, URI original) {
return LoadBalancerUriTools.reconstructURI(serviceInstance, original);
}
@Override
public ServiceInstance choose(String serviceId) {
ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory
.getInstance(serviceId);
if (loadBalancer == null) {
return null;
}
Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose())
.block();
if (loadBalancerResponse == null) {
return null;
}
return loadBalancerResponse.getServer();
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2012-2019 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.loadbalancer.config;
import javax.annotation.PostConstruct;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
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.ConditionalOnMissingClass;
import org.springframework.cloud.client.loadbalancer.AsyncLoadBalancerAutoConfiguration;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* An autoconfiguration for {@link BlockingLoadBalancerClient}.
*
* @author Olga Maciaszek-Sharma
* @since 2.2.0
*/
@Configuration
@LoadBalancerClients
@AutoConfigureAfter(LoadBalancerAutoConfiguration.class)
@AutoConfigureBefore({
org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration.class,
AsyncLoadBalancerAutoConfiguration.class })
public class BlockingLoadBalancerClientAutoConfiguration {
@Bean
@ConditionalOnClass(
name = "org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient")
public RibbonWarnLogger ribbonWarnLogger() {
return new RibbonWarnLogger();
}
@Bean
@ConditionalOnBean(LoadBalancerClientFactory.class)
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnMissingBean
@ConditionalOnMissingClass("org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient")
public LoadBalancerClient loadBalancerClient(
LoadBalancerClientFactory loadBalancerClientFactory) {
return new BlockingLoadBalancerClient(loadBalancerClientFactory);
}
}
class RibbonWarnLogger {
private static final Log LOG = LogFactory.getLog(RibbonWarnLogger.class);
@PostConstruct
void logWarning() {
if (LOG.isWarnEnabled()) {
LOG.warn(
"You already have RibbonLoadBalancerClient on your classpath. It will be used by default. To use "
+ BlockingLoadBalancerClient.class.getSimpleName()
+ " remove spring-cloud-starter-netflix-ribbon from your project.");
}
}
}

View File

@@ -1,4 +1,4 @@
# AutoConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration
org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration,\
org.springframework.cloud.loadbalancer.config.BlockingLoadBalancerClientAutoConfiguration

View File

@@ -0,0 +1,211 @@
/*
* Copyright 2012-2019 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.loadbalancer.blocking.client;
import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.core.publisher.Mono;
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.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryProperties;
import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest;
import org.springframework.cloud.client.loadbalancer.reactive.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.reactive.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.reactive.Request;
import org.springframework.cloud.client.loadbalancer.reactive.Response;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
/**
* Tests for {@link BlockingLoadBalancerClient}.
*
* @author Olga Maciaszek-Sharma
*/
@SpringBootTest
@ExtendWith(SpringExtension.class)
class BlockingLoadBalancerClientTests {
@Autowired
private BlockingLoadBalancerClient loadBalancerClient;
@Autowired
private SimpleDiscoveryProperties properties;
@BeforeEach
void setUp() {
properties.getInstances().put("myservice",
Collections.singletonList(
new SimpleDiscoveryProperties.SimpleServiceInstance(
URI.create("https://test.example:9999"))));
}
@Test
void correctServiceInstanceChosen() {
ServiceInstance serviceInstance = loadBalancerClient.choose("myservice");
assertThat(serviceInstance.getHost()).isEqualTo("test.example");
}
@Test
void nullReturnedIfInstanceMissing() {
ServiceInstance serviceInstance = loadBalancerClient.choose("unknownservice");
assertThat(serviceInstance).isNull();
}
@Test
void requestExecutedAgainstCorrectInstance() throws IOException {
final String result = "result";
Object actualResult = loadBalancerClient.execute("myservice",
(LoadBalancerRequest<Object>) instance -> {
assertThat(instance.getHost()).isEqualTo("test.example");
return result;
});
assertThat(actualResult).isEqualTo(result);
}
@Test
void exceptionThrownIfInstanceNotAvailableForRequestExecution() throws IOException {
try {
final String result = "result";
Object actualResult = loadBalancerClient.execute("unknownservice",
(LoadBalancerRequest<Object>) instance -> result);
assertThat(actualResult).isEqualTo(result);
fail("Should have thrown exception.");
}
catch (Exception exception) {
assertThat(exception).isNotNull();
assertThat(exception).isInstanceOf(IllegalStateException.class);
assertThat(exception).hasMessage("No instances available for unknownservice");
}
}
@Test
void exceptionRethrownAsRuntime() {
try {
loadBalancerClient.execute("myservice", instance -> {
assertThat(instance.getHost()).isEqualTo("test.example");
throw new Exception("Should throw exception.");
});
fail("Should have thrown exception.");
}
catch (Exception exception) {
assertThat(exception).isNotNull();
assertThat(exception).isInstanceOf(RuntimeException.class);
}
}
@Test
void IOExceptionRethrown() {
try {
loadBalancerClient.execute("myservice", instance -> {
assertThat(instance.getHost()).isEqualTo("test.example");
throw new IOException("Should throw IO exception.");
});
fail("Should have thrown exception.");
}
catch (Exception exception) {
assertThat(exception).isNotNull();
assertThat(exception).isInstanceOf(IOException.class);
}
}
@Configuration
@EnableAutoConfiguration
@SpringBootConfiguration
@LoadBalancerClients({
@org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient(
name = "myservice", configuration = MyServiceConfig.class),
@org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient(
name = "unknownservice",
configuration = UnknownServiceConfig.class) })
protected static class Config {
}
protected static class MyServiceConfig {
@Bean
ReactorLoadBalancer<ServiceInstance> reactiveLoadBalancer(
DiscoveryClient discoveryClient) {
return new DiscoveryClientBasedReactiveLoadBalancer("myservice",
discoveryClient);
}
}
protected static class UnknownServiceConfig {
@Bean
ReactorLoadBalancer<ServiceInstance> reactiveLoadBalancer(
DiscoveryClient discoveryClient) {
return new DiscoveryClientBasedReactiveLoadBalancer("unknownservice",
discoveryClient);
}
}
}
class DiscoveryClientBasedReactiveLoadBalancer
implements ReactorServiceInstanceLoadBalancer {
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 Mono<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 Mono<Response<ServiceInstance>> choose(Request request) {
return choose();
}
}