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:
committed by
GitHub
parent
c8a57be0f1
commit
46bf5a01ac
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -176,7 +176,7 @@ public class LoadBalancerTests {
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@SpringBootConfiguration
|
||||
@SpringBootConfiguration(proxyBeanMethods = false)
|
||||
@LoadBalancerClients({
|
||||
@LoadBalancerClient(name = "myservice",
|
||||
configuration = MyServiceConfig.class),
|
||||
|
||||
Reference in New Issue
Block a user