Lb complete lifecycle (#783)

* Add LoadBalancerLifecycle. Trigger lifecycle callbacks. Set hints from properties.

* Trigger LB lifecycle callbacks from BlockingLoadBalancerClient and RetryLoadBalancerInterceptor.

* Register LifecycleProcessors with @LoadBalancerClients

* Register LifecycleProcessors with @LoadBalancerClients configuration.

* Handle null lifecycle beans map returned from factory. Adjust tests to code changes.

* Handle null lifecycle beans map returned from factory in RetryLoadBalancerInterceptor. Ensure ReactiveLoadBalancer.Factory bean is present while instantiating RetryLoadBalancerInterceptor. Add more tests.

* Remove generics from supports(...) method signature.

* Allow setting hint per service via properties.

* Add some toString() methods. Add more info on deprecated callbacks in javadocs and comments.

* Add javadocs.

* Format javadocs. Add docs.

* Update hint docs.

* Fix docs.

* Fix docs.

* Extract filtering supported lifecycle processors to a separate class; Execute onComplete() calls for DISCARD status in RetryLoadBalancerInterceptor. Remove duplicated `onComplete` calls for FAILED and SUCCESS status in RetryLoadBalancerInterceptor. Add test for no duplicated lifecycle calls in RetryLoadBalancerInterceptorTest.

* Small refactoring: remove deprecated methods use, add final keywords, remove unnecessary keywords.

* Add javadoc.
This commit is contained in:
Olga Maciaszek-Sharma
2020-07-21 11:38:52 -05:00
committed by GitHub
parent c8a57be0f1
commit 46bf5a01ac
32 changed files with 1219 additions and 236 deletions

View File

@@ -18,38 +18,67 @@ package org.springframework.cloud.loadbalancer.blocking.client;
import java.io.IOException;
import java.net.URI;
import java.util.Set;
import reactor.core.publisher.Mono;
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.EmptyResponse;
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.LoadBalancerRequest;
import org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.util.ReflectionUtils;
import static org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer.REQUEST;
/**
* The default {@link LoadBalancerClient} implementation.
*
* @author Olga Maciaszek-Sharma
* @since 2.2.0
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public class BlockingLoadBalancerClient implements LoadBalancerClient {
private final LoadBalancerClientFactory loadBalancerClientFactory;
public BlockingLoadBalancerClient(
LoadBalancerClientFactory loadBalancerClientFactory) {
private final LoadBalancerProperties properties;
public BlockingLoadBalancerClient(LoadBalancerClientFactory loadBalancerClientFactory,
LoadBalancerProperties properties) {
this.loadBalancerClientFactory = loadBalancerClientFactory;
this.properties = properties;
}
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request)
throws IOException {
ServiceInstance serviceInstance = choose(serviceId);
String hint = getHint(serviceId);
DefaultRequest<DefaultRequestContext> lbRequest = new DefaultRequest<>(
new DefaultRequestContext(request, hint));
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
.getSupportedLifecycleProcessors(
loadBalancerClientFactory.getInstances(serviceId,
LoadBalancerLifecycle.class),
DefaultRequestContext.class, Object.class, ServiceInstance.class);
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
ServiceInstance serviceInstance = choose(serviceId, lbRequest);
if (serviceInstance == null) {
supportedLifecycleProcessors
.forEach(lifecycle -> lifecycle.onComplete(new CompletionContext<>(
CompletionContext.Status.DISCARD, new EmptyResponse())));
throw new IllegalStateException("No instances available for " + serviceId);
}
return execute(serviceId, serviceInstance, request);
@@ -58,13 +87,29 @@ public class BlockingLoadBalancerClient implements LoadBalancerClient {
@Override
public <T> T execute(String serviceId, ServiceInstance serviceInstance,
LoadBalancerRequest<T> request) throws IOException {
DefaultResponse defaultResponse = new DefaultResponse(serviceInstance);
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
.getSupportedLifecycleProcessors(
loadBalancerClientFactory.getInstances(serviceId,
LoadBalancerLifecycle.class),
DefaultRequestContext.class, Object.class, ServiceInstance.class);
try {
return request.apply(serviceInstance);
T response = request.apply(serviceInstance);
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
.onComplete(new CompletionContext<>(CompletionContext.Status.SUCCESS,
defaultResponse, response)));
return response;
}
catch (IOException iOException) {
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
.onComplete(new CompletionContext<>(CompletionContext.Status.FAILED,
iOException, defaultResponse)));
throw iOException;
}
catch (Exception exception) {
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
.onComplete(new CompletionContext<>(CompletionContext.Status.FAILED,
exception, defaultResponse)));
ReflectionUtils.rethrowRuntimeException(exception);
}
return null;
@@ -77,17 +122,28 @@ public class BlockingLoadBalancerClient implements LoadBalancerClient {
@Override
public ServiceInstance choose(String serviceId) {
return choose(serviceId, REQUEST);
}
@Override
public <T> ServiceInstance choose(String serviceId, Request<T> request) {
ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory
.getInstance(serviceId);
if (loadBalancer == null) {
return null;
}
Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose())
.block();
Response<ServiceInstance> loadBalancerResponse = Mono
.from(loadBalancer.choose(request)).block();
if (loadBalancerResponse == null) {
return null;
}
return loadBalancerResponse.getServer();
}
private String getHint(String serviceId) {
String defaultHint = properties.getHint().getOrDefault("default", "default");
String hintPropertyValue = properties.getHint().get(serviceId);
return hintPropertyValue != null ? hintPropertyValue : defaultHint;
}
}

View File

@@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.client.loadbalancer.AsyncLoadBalancerAutoConfiguration;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
@@ -49,8 +50,9 @@ public class BlockingLoadBalancerClientAutoConfiguration {
@ConditionalOnBean(LoadBalancerClientFactory.class)
@ConditionalOnMissingBean
public LoadBalancerClient blockingLoadBalancerClient(
LoadBalancerClientFactory loadBalancerClientFactory) {
return new BlockingLoadBalancerClient(loadBalancerClientFactory);
LoadBalancerClientFactory loadBalancerClientFactory,
LoadBalancerProperties properties) {
return new BlockingLoadBalancerClient(loadBalancerClientFactory, properties);
}
}

View File

@@ -18,35 +18,42 @@ package org.springframework.cloud.loadbalancer.blocking.client;
import java.io.IOException;
import java.net.URI;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
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.CompletionContext;
import org.springframework.cloud.client.loadbalancer.DefaultRequestContext;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
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.assertThatCode;
import static org.assertj.core.api.Assertions.fail;
/**
@@ -55,7 +62,6 @@ import static org.assertj.core.api.Assertions.fail;
* @author Olga Maciaszek-Sharma
*/
@SpringBootTest
@ExtendWith(SpringExtension.class)
class BlockingLoadBalancerClientTests {
@Autowired
@@ -64,6 +70,12 @@ class BlockingLoadBalancerClientTests {
@Autowired
private SimpleDiscoveryProperties properties;
@Autowired
ReactiveLoadBalancer.Factory<ServiceInstance> factory;
@Autowired
LoadBalancerProperties loadBalancerProperties;
@BeforeEach
void setUp() {
properties.getInstances().put("myservice",
@@ -141,19 +153,65 @@ class BlockingLoadBalancerClientTests {
}
}
@Test
void exceptionNotThrownWhenFactoryReturnsNullLifecycleProcessorsMap() {
assertThatCode(
() -> loadBalancerClient.execute("serviceWithNoLifecycleProcessors",
(LoadBalancerRequest<Object>) instance -> {
assertThat(instance.getHost()).isEqualTo("test.example");
return "result";
})).doesNotThrowAnyException();
}
@Test
void loadBalancerLifecycleCallbacksExecuted() throws IOException {
String callbackTestHint = "callbackTestHint";
loadBalancerProperties.getHint().put("myservice", "callbackTestHint");
final String result = "callbackTestResult";
Object actualResult = loadBalancerClient.execute("myservice",
(LoadBalancerRequest<Object>) instance -> {
assertThat(instance.getHost()).isEqualTo("test.example");
return result;
});
Collection<Request<Object>> lifecycleLogRequests = ((TestLoadBalancerLifecycle) factory
.getInstances("myservice", LoadBalancerLifecycle.class)
.get("loadBalancerLifecycle")).getStartLog().values();
Collection<CompletionContext<Object, ServiceInstance>> anotherLifecycleLogRequests = ((AnotherLoadBalancerLifecycle) factory
.getInstances("myservice", LoadBalancerLifecycle.class)
.get("anotherLoadBalancerLifecycle")).getCompleteLog().values();
assertThat(actualResult).isEqualTo(result);
assertThat(lifecycleLogRequests).extracting(
request -> ((DefaultRequestContext) request.getContext()).getHint())
.contains(callbackTestHint);
assertThat(anotherLifecycleLogRequests)
.extracting(CompletionContext::getClientResponse).contains(result);
}
@Configuration(proxyBeanMethods = false)
@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) })
@LoadBalancerClient(name = "myservice",
configuration = MyServiceConfig.class),
@LoadBalancerClient(name = "unknownservice",
configuration = UnknownServiceConfig.class),
@LoadBalancerClient(name = "serviceWithNoLifecycleProcessors",
configuration = NoLifecycleProcessorsConfig.class) })
protected static class Config {
}
protected static class NoLifecycleProcessorsConfig {
@Bean
ReactorLoadBalancer<ServiceInstance> reactiveLoadBalancer(
DiscoveryClient discoveryClient) {
return new DiscoveryClientBasedReactiveLoadBalancer("myservice",
discoveryClient);
}
}
protected static class MyServiceConfig {
@Bean
@@ -163,6 +221,16 @@ class BlockingLoadBalancerClientTests {
discoveryClient);
}
@Bean
LoadBalancerLifecycle<Object, Object, ServiceInstance> loadBalancerLifecycle() {
return new TestLoadBalancerLifecycle();
}
@Bean
LoadBalancerLifecycle<Object, Object, ServiceInstance> anotherLoadBalancerLifecycle() {
return new AnotherLoadBalancerLifecycle();
}
}
protected static class UnknownServiceConfig {
@@ -176,8 +244,51 @@ class BlockingLoadBalancerClientTests {
}
protected static class TestLoadBalancerLifecycle
implements LoadBalancerLifecycle<Object, Object, ServiceInstance> {
final ConcurrentHashMap<String, Request<Object>> startLog = new ConcurrentHashMap<>();
final ConcurrentHashMap<String, CompletionContext<Object, ServiceInstance>> completeLog = new ConcurrentHashMap<>();
@Override
public void onStart(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, 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();
}
}
}
@SuppressWarnings("rawtypes")
class DiscoveryClientBasedReactiveLoadBalancer
implements ReactorServiceInstanceLoadBalancer {

View File

@@ -110,8 +110,9 @@ class CachingServiceInstanceListSupplierTests {
@Bean
BlockingLoadBalancerClient blockingLoadBalancerClient(
LoadBalancerClientFactory loadBalancerClientFactory) {
return new BlockingLoadBalancerClient(loadBalancerClientFactory);
LoadBalancerClientFactory loadBalancerClientFactory,
LoadBalancerProperties properties) {
return new BlockingLoadBalancerClient(loadBalancerClientFactory, properties);
}
@Bean

View File

@@ -176,7 +176,7 @@ public class LoadBalancerTests {
}
@EnableAutoConfiguration
@SpringBootConfiguration
@SpringBootConfiguration(proxyBeanMethods = false)
@LoadBalancerClients({
@LoadBalancerClient(name = "myservice",
configuration = MyServiceConfig.class),