Adds API and implementation of ReactiveLoadBalancer

API lives in spring-cloud-commons module and implementation in
spring-cloud-loadbalancer.
This commit is contained in:
Spencer Gibb
2018-08-15 18:45:53 -04:00
parent 53f1f0bc3c
commit 1fa513194b
30 changed files with 1454 additions and 1 deletions

View File

@@ -126,6 +126,7 @@
<module>spring-cloud-context</module>
<module>spring-cloud-context-integration-tests</module>
<module>spring-cloud-commons</module>
<module>spring-cloud-loadbalancer</module>
<module>spring-cloud-starter</module>
<module>docs</module>
</modules>

View File

@@ -43,6 +43,11 @@
<artifactId>spring-cloud-context</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>

View File

@@ -50,7 +50,7 @@ public class DefaultServiceInstance implements ServiceInstance {
public DefaultServiceInstance(String serviceId, String host, int port,
boolean secure) {
this(serviceId, host, port, secure, new LinkedHashMap<String, String>());
this(serviceId, host, port, secure, new LinkedHashMap<>());
}
@Override

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2013-2017 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
*
* http://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.client.loadbalancer.reactive;
import org.springframework.core.style.ToStringCreator;
/**
* @author Spencer Gibb
*/
//TODO: add metrics
public class CompletionContext {
public enum Status {
/** Request was handled successfully */
SUCCESSS,
/** Request reached the server but failed due to timeout or internal error */
FAILED,
/** Request did not go off box and should not be counted for statistics */
DISCARD,
}
private final Status status;
private final Throwable throwable;
public CompletionContext(Status status) {
this(status, null);
}
public CompletionContext(Status status, Throwable throwable) {
this.status = status;
this.throwable = throwable;
}
public Status getStatus() {
return status;
}
public Throwable getThrowable() {
return throwable;
}
@Override
public String toString() {
ToStringCreator to = new ToStringCreator(this);
to.append("status", status);
to.append("throwable", throwable);
return to.toString();
}
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2013-2017 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
*
* http://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.client.loadbalancer.reactive;
/**
* @author Spencer Gibb
*/
public class DefaultRequest implements Request {
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2013-2017 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
*
* http://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.client.loadbalancer.reactive;
import org.springframework.cloud.client.ServiceInstance;
/**
* @author Spencer Gibb
*/
public class DefaultResponse implements Response<ServiceInstance> {
private final ServiceInstance serviceInstance;
public DefaultResponse(ServiceInstance serviceInstance) {
this.serviceInstance = serviceInstance;
}
@Override
public boolean hasServer() {
return this.serviceInstance != null;
}
@Override
public ServiceInstance getServer() {
return this.serviceInstance;
}
@Override
public void onComplete(CompletionContext completionContext) {
//TODO: implement
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2013-2017 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
*
* http://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.client.loadbalancer.reactive;
import org.springframework.cloud.client.ServiceInstance;
/**
* @author Spencer Gibb
*/
public class EmptyResponse implements Response<ServiceInstance> {
@Override
public boolean hasServer() {
return false;
}
@Override
public ServiceInstance getServer() {
return null;
}
@Override
public void onComplete(CompletionContext completionContext) {
//TODO: implement
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2013-2017 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
*
* http://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.client.loadbalancer.reactive;
import org.reactivestreams.Publisher;
/**
* @author Spencer Gibb
*/
public interface ReactiveLoadBalancer<T> {
Request REQUEST = new DefaultRequest();
/**
* Choose the next server based on the load balancing algorithm
* @param request
* @return
*/
Publisher<Response<T>> choose(Request request);
default Publisher<Response<T>> choose() { //conflicting name
return choose(REQUEST);
}
}

View File

@@ -0,0 +1,5 @@
package org.springframework.cloud.client.loadbalancer.reactive;
public interface Request {
//TODO: define contents
}

View File

@@ -0,0 +1,16 @@
package org.springframework.cloud.client.loadbalancer.reactive;
/**
* Response created for each request.
*/
public interface Response<T> {
boolean hasServer();
T getServer();
/**
* Notification that the request completed
* @param completionContext
*/
void onComplete(CompletionContext completionContext);
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2013-2018 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
*
* http://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.context.named;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.lang.Nullable;
import java.util.Iterator;
import java.util.Spliterator;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Stream;
/**
* Special ObjectProvider that allows the actual ObjectProvider to be resolved
* later because of the creation of the named child context.
* @param <T>
*/
class ClientFactoryObjectProvider<T> implements ObjectProvider<T> {
private final NamedContextFactory clientFactory;
private final String name;
private final Class<T> type;
private ObjectProvider<T> provider;
public ClientFactoryObjectProvider(NamedContextFactory clientFactory, String name, Class<T> type) {
this.clientFactory = clientFactory;
this.name = name;
this.type = type;
}
@Override
public T getObject(Object... args) throws BeansException {
return delegate().getObject(args);
}
@Override
@Nullable
public T getIfAvailable() throws BeansException {
return delegate().getIfAvailable();
}
@Override
public T getIfAvailable(Supplier<T> defaultSupplier) throws BeansException {
return delegate().getIfAvailable(defaultSupplier);
}
@Override
public void ifAvailable(Consumer<T> dependencyConsumer) throws BeansException {
delegate().ifAvailable(dependencyConsumer);
}
@Override
@Nullable
public T getIfUnique() throws BeansException {
return delegate().getIfUnique();
}
@Override
public T getIfUnique(Supplier<T> defaultSupplier) throws BeansException {
return delegate().getIfUnique(defaultSupplier);
}
@Override
public void ifUnique(Consumer<T> dependencyConsumer) throws BeansException {
delegate().ifUnique(dependencyConsumer);
}
@Override
public Iterator<T> iterator() {
return delegate().iterator();
}
@Override
public Stream<T> stream() {
return delegate().stream();
}
@Override
public T getObject() throws BeansException {
return delegate().getObject();
}
@Override
public void forEach(Consumer<? super T> action) {
delegate().forEach(action);
}
@Override
public Spliterator<T> spliterator() {
return delegate().spliterator();
}
@SuppressWarnings("unchecked")
private ObjectProvider<T> delegate() {
if (this.provider == null) {
provider = this.clientFactory.getProvider(name, type);
}
return provider;
}
}

View File

@@ -11,10 +11,12 @@ import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.core.env.MapPropertySource;
/**
@@ -26,6 +28,7 @@ import org.springframework.core.env.MapPropertySource;
* @author Spencer Gibb
* @author Dave Syer
*/
//TODO: add javadoc
public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
implements DisposableBean, ApplicationContextAware {
@@ -131,6 +134,35 @@ public abstract class NamedContextFactory<C extends NamedContextFactory.Specific
return null;
}
public <T> ObjectProvider<T> getLazyProvider(String name, Class<T> type) {
return new ClientFactoryObjectProvider<>(this, name, type);
}
public <T> ObjectProvider<T> getProvider(String name, Class<T> type) {
AnnotationConfigApplicationContext context = getContext(name);
return context.getBeanProvider(type);
}
public <T> T getInstance(String name, Class<?> clazz, Class<?>... generics) {
ResolvableType type = ResolvableType.forClassWithGenerics(clazz, generics);
return getInstance(name, type);
}
@SuppressWarnings("unchecked")
public <T> T getInstance(String name, ResolvableType type) {
AnnotationConfigApplicationContext context = getContext(name);
String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
type);
if (beanNames.length > 0) {
for (String beanName : beanNames) {
if (context.isTypeMatch(beanName, type)) {
return (T) context.getBean(beanName);
}
}
}
return null;
}
public <T> Map<String, T> getInstances(String name, Class<T> type) {
AnnotationConfigApplicationContext context = getContext(name);
if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons-parent</artifactId>
<version>2.1.0.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-loadbalancer</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Load Balancer</name>
<description>Spring Cloud Balancer</description>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<optional>true</optional>
<!-- TODO: remove this when it's fixed in ModifiedClassPathRunner -->
<exclusions>
<exclusion>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor.addons</groupId>
<artifactId>reactor-extra</artifactId>
<!-- TODO: remove after next reactor milestone -->
<version>3.2.0.M2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-test-support</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2013-2018 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
*
* http://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.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AliasFor;
/**
* Declarative configuration for a load balancer client. Add this annotation to any
* <code>@Configuration</code> and then inject a {@link LoadBalancerClientFactory} to access the
* client that is created.
*
* @author Dave Syer
*/
@Configuration
@Import(LoadBalancerClientConfigurationRegistrar.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LoadBalancerClient {
/**
* Synonym for name (the name of the client)
*
* @see #name()
*/
@AliasFor("name")
String value() default "";
/**
* The name of the load balancer client, uniquely identifying a set of client resources,
* including a load balancer.
*/
@AliasFor("value")
String name() default "";
/**
* A custom <code>@Configuration</code> for the load balancer client. Can contain override
* <code>@Bean</code> definition for the pieces that make up the client.
*
* @see LoadBalancerClientConfiguration for the defaults
*/
Class<?>[] configuration() default {};
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2013-2018 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
*
* http://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.annotation;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceSupplier;
import org.springframework.cloud.loadbalancer.core.DiscoveryClientServiceInstanceSupplier;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
/**
* @author Spencer Gibb
*/
@Configuration
@EnableConfigurationProperties
public class LoadBalancerClientConfiguration {
@Bean
@ConditionalOnMissingBean
public ServiceInstanceSupplier discoveryClientServiceInstanceSupplier(
DiscoveryClient discoveryClient, Environment env, ObjectProvider<CacheManager> cacheManager) {
//TODO: bean post processor to enable caching?
DiscoveryClientServiceInstanceSupplier delegate = new DiscoveryClientServiceInstanceSupplier(discoveryClient, env);
if (cacheManager.getIfAvailable() != null) {
return new CachingServiceInstanceSupplier(delegate, cacheManager.getIfAvailable());
}
return delegate;
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2013-2018 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
*
* http://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.annotation;
import java.util.Map;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.StringUtils;
/**
* @author Dave Syer
*/
public class LoadBalancerClientConfigurationRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
Map<String, Object> attrs = metadata.getAnnotationAttributes(
LoadBalancerClients.class.getName(), true);
if (attrs != null && attrs.containsKey("value")) {
AnnotationAttributes[] clients = (AnnotationAttributes[]) attrs.get("value");
for (AnnotationAttributes client : clients) {
registerClientConfiguration(registry, getClientName(client),
client.get("configuration"));
}
}
if (attrs != null && attrs.containsKey("defaultConfiguration")) {
String name;
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
} else {
name = "default." + metadata.getClassName();
}
registerClientConfiguration(registry, name,
attrs.get("defaultConfiguration"));
}
Map<String, Object> client = metadata.getAnnotationAttributes(
LoadBalancerClient.class.getName(), true);
String name = getClientName(client);
if (name != null) {
registerClientConfiguration(registry, name, client.get("configuration"));
}
}
private static String getClientName(Map<String, Object> client) {
if (client == null) {
return null;
}
String value = (String) client.get("value");
if (!StringUtils.hasText(value)) {
value = (String) client.get("name");
}
if (StringUtils.hasText(value)) {
return value;
}
throw new IllegalStateException(
"Either 'name' or 'value' must be provided in @LoadBalancerClient");
}
private static void registerClientConfiguration(BeanDefinitionRegistry registry,
Object name, Object configuration) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(LoadBalancerClientSpecification.class);
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
registry.registerBeanDefinition(name + ".LoadBalancerClientSpecification",
builder.getBeanDefinition());
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2013-2018 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
*
* http://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.annotation;
import java.util.Arrays;
import java.util.Objects;
import org.springframework.cloud.context.named.NamedContextFactory;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
/**
* @author Dave Syer
*/
public class LoadBalancerClientSpecification implements NamedContextFactory.Specification {
private String name;
private Class<?>[] configuration;
public LoadBalancerClientSpecification() {
}
public LoadBalancerClientSpecification(String name, Class<?>[] configuration) {
Assert.hasText(name, "name must not be empty");
this.name = name;
Assert.notNull(configuration, "configuration must not be null");
this.configuration = configuration;
}
public String getName() {
return name;
}
public void setName(String name) {
Assert.hasText(name, "name must not be empty");
this.name = name;
}
public Class<?>[] getConfiguration() {
return configuration;
}
public void setConfiguration(Class<?>[] configuration) {
Assert.notNull(configuration, "configuration must not be null");
this.configuration = configuration;
}
@Override
public String toString() {
ToStringCreator to = new ToStringCreator(this);
to.append("name", name);
to.append("configuration", configuration);
return to.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LoadBalancerClientSpecification that = (LoadBalancerClientSpecification) o;
return Objects.equals(name, that.name) &&
Arrays.equals(configuration, that.configuration);
}
@Override
public int hashCode() {
return Objects.hash(name, configuration);
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2013-2018 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
*
* http://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.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
/**
* Convenience annotation that allows user to combine multiple <code>@LoadBalancerClient</code>
* annotations on a single class (including in Java 7).
*
* @author Dave Syer
*/
@Configuration
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
@Documented
@Import(LoadBalancerClientConfigurationRegistrar.class)
public @interface LoadBalancerClients {
LoadBalancerClient[] value() default {};
/**
* {@link LoadBalancerClientConfigurationRegistrar} creates a {@link LoadBalancerClientSpecification}
* with this as an argument. These in turn are added as default contexts in {@link LoadBalancerClientFactory}.
* Configuration defined in these classes are used as defaults if values aren't defined via
* {@link LoadBalancerClient#configuration()}
*/
Class<?>[] defaultConfiguration() default {};
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2013-2018 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
*
* http://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 java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientSpecification;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Spencer Gibb
*/
@Configuration
@LoadBalancerClients
// @EnableCaching //TODO: how to enforce, or check conditions?
// @AutoConfigureBefore(CacheAutoConfiguration.class)
public class LoadBalancerAutoConfiguration {
private final ObjectProvider<List<LoadBalancerClientSpecification>> configurations;
public LoadBalancerAutoConfiguration(ObjectProvider<List<LoadBalancerClientSpecification>> configurations) {
this.configurations = configurations;
}
@Bean
public LoadBalancerClientFactory loadBalancerClientFactory() {
LoadBalancerClientFactory clientFactory = new LoadBalancerClientFactory();
clientFactory.setConfigurations(configurations.getIfAvailable(Collections::emptyList));
return clientFactory;
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2013-2018 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
*
* http://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.core;
import java.util.List;
import reactor.cache.CacheFlux;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cloud.client.ServiceInstance;
/**
* @author Spencer Gibb
*/
public class CachingServiceInstanceSupplier implements ServiceInstanceSupplier {
public static final String SERVICE_INSTANCE_CACHE_NAME = CachingServiceInstanceSupplier.class.getSimpleName()+"Cache";
private final ServiceInstanceSupplier delegate;
private final Flux<ServiceInstance> serviceInstances;
@SuppressWarnings("unchecked")
public CachingServiceInstanceSupplier(ServiceInstanceSupplier delegate, CacheManager cacheManager) {
this.delegate = delegate;
this.serviceInstances = CacheFlux.lookup(key -> {
Cache cache = cacheManager.getCache(SERVICE_INSTANCE_CACHE_NAME); //TODO: configurable cache name
List<ServiceInstance> list = cache.get(key, List.class);
if (list == null || list.isEmpty()) {
return Mono.empty();
}
return Flux.fromIterable(list)
.materialize()
.collectList();
}, delegate.getServiceId())
.onCacheMissResume(this.delegate::get)
.andWriteWith((key, signals) -> Flux.fromIterable(signals)
.dematerialize()
.cast(ServiceInstance.class)
.collectList()
.doOnNext(instances -> {
Cache cache = cacheManager.getCache(SERVICE_INSTANCE_CACHE_NAME);
cache.put(key, instances);
})
.then());
}
@Override
public Flux<ServiceInstance> get() {
return this.serviceInstances;
}
@Override
public String getServiceId() {
return this.delegate.getServiceId();
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2013-2018 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
*
* http://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.core;
import java.util.List;
import reactor.core.publisher.Flux;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.core.env.Environment;
import static org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory.PROPERTY_NAME;
/**
* @author Spencer Gibb
*/
public class DiscoveryClientServiceInstanceSupplier implements ServiceInstanceSupplier {
private final DiscoveryClient delegate;
private final String serviceId;
public DiscoveryClientServiceInstanceSupplier(DiscoveryClient delegate, Environment environment) {
this.delegate = delegate;
serviceId = environment.getProperty(PROPERTY_NAME);
}
@Override
public Flux<ServiceInstance> get() {
List<ServiceInstance> instances = delegate.getInstances(serviceId);
return Flux.fromIterable(instances);
}
public String getServiceId() {
return this.serviceId;
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2013-2018 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
*
* http://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.core;
import reactor.core.publisher.Mono;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.client.loadbalancer.reactive.Request;
import org.springframework.cloud.client.loadbalancer.reactive.Response;
public interface ReactorLoadBalancer<T> extends ReactiveLoadBalancer<T> {
/**
* Choose the next server based on the load balancing algorithm
* @param request
* @return
*/
Mono<Response<T>> choose(Request request);
default Mono<Response<T>> choose() {
return choose(REQUEST);
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2013-2018 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
*
* http://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.core;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
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;
/**
* @author Spencer Gibb
*/
public class RoundRobinLoadBalancer implements ReactorLoadBalancer<ServiceInstance> {
private static final Log log = LogFactory.getLog(RoundRobinLoadBalancer.class);
private final AtomicInteger position;
private final ObjectProvider<ServiceInstanceSupplier> serviceInstanceSupplier;
private final String serviceId;
public RoundRobinLoadBalancer(String serviceId, ObjectProvider<ServiceInstanceSupplier> serviceInstanceSupplier) {
this(serviceId, serviceInstanceSupplier, new Random().nextInt(1000));
}
public RoundRobinLoadBalancer(String serviceId, ObjectProvider<ServiceInstanceSupplier> serviceInstanceSupplier,
int seedPosition) {
this.serviceId = serviceId;
this.serviceInstanceSupplier = serviceInstanceSupplier;
this.position = new AtomicInteger(seedPosition);
}
@Override
/**
* see original https://github.com/Netflix/ocelli/blob/master/ocelli-core/src/main/java/netflix/ocelli/loadbalancer/RoundRobinLoadBalancer.java
*/
public Mono<Response<ServiceInstance>> choose(Request request) {
// TODO: move supplier to Request?
ServiceInstanceSupplier supplier = serviceInstanceSupplier.getIfAvailable();
return supplier.get().collectList().map(instances -> {
if (instances.isEmpty()) {
log.warn("No servers available for service: " + this.serviceId);
return new EmptyResponse();
}
// TODO: enforce order?
int pos = Math.abs(position.incrementAndGet());
ServiceInstance instance = instances.get(pos % instances.size());
return new DefaultResponse(instance);
});
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2013-2018 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
*
* http://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.core;
import java.util.function.Supplier;
import reactor.core.publisher.Flux;
import org.springframework.cloud.client.ServiceInstance;
/**
* @author Spencer Gibb
*/
public interface ServiceInstanceSupplier extends Supplier<Flux<ServiceInstance>> {
String getServiceId();
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2013-2018 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
*
* http://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.support;
import org.springframework.cloud.context.named.NamedContextFactory;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientConfiguration;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientSpecification;
import org.springframework.core.env.Environment;
/**
* A factory that creates client, load balancer and client configuration instances. It
* creates a Spring ApplicationContext per client name, and extracts the beans that it
* needs from there.
*
* @author Spencer Gibb
* @author Dave Syer
*/
public class LoadBalancerClientFactory extends NamedContextFactory<LoadBalancerClientSpecification> {
public static final String NAMESPACE = "loadbalancer";
public static final String PROPERTY_NAME = NAMESPACE + ".client.name";
public LoadBalancerClientFactory() {
super(LoadBalancerClientConfiguration.class, NAMESPACE, PROPERTY_NAME);
}
public String getName(Environment environment) {
return environment.getProperty(PROPERTY_NAME);
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2013-2018 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
*
* http://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.support;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceSupplier;
import reactor.core.publisher.Flux;
public class ServiceInstanceSuppliers {
public static ServiceInstanceSupplier from(String serviceId, ServiceInstance... instances) {
return new ServiceInstanceSupplier() {
@Override
public Flux<ServiceInstance> get() {
return Flux.just(instances);
}
@Override
public String getServiceId() {
return serviceId;
}
};
}
public static ObjectProvider<ServiceInstanceSupplier> toProvider(String serviceId, ServiceInstance... instances) {
return new SimpleObjectProvider<>(from(serviceId, instances));
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2013-2018 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
*
* http://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.support;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
public class SimpleObjectProvider<T> implements ObjectProvider<T> {
private final T object;
public SimpleObjectProvider(T object) {
this.object = object;
}
@Override
public T getObject(Object... args) throws BeansException {
return this.object;
}
@Override
public T getIfAvailable() throws BeansException {
return this.object;
}
@Override
public T getIfUnique() throws BeansException {
return this.object;
}
@Override
public T getObject() throws BeansException {
return this.object;
}
}

View File

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

View File

@@ -0,0 +1,144 @@
/*
* Copyright 2013-2018 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
*
* http://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.core;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
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.cache.annotation.EnableCaching;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.CompletionContext;
import org.springframework.cloud.client.loadbalancer.reactive.CompletionContext.Status;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.client.loadbalancer.reactive.Response;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.cloud.loadbalancer.support.ServiceInstanceSuppliers;
import org.springframework.context.annotation.Bean;
import org.springframework.core.ResolvableType;
import org.springframework.core.env.Environment;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Spencer Gibb
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class LoadBalancerTest {
@Autowired
private LoadBalancerClientFactory clientFactory;
@Test
public void roundRobbinLoadbalancerWorks() {
ReactiveLoadBalancer<ServiceInstance> reactiveLoadBalancer = this.clientFactory
.getInstance("myservice", ReactiveLoadBalancer.class, ServiceInstance.class);
assertThat(reactiveLoadBalancer).isInstanceOf(RoundRobinLoadBalancer.class);
assertThat(reactiveLoadBalancer).isInstanceOf(ReactorLoadBalancer.class);
ReactorLoadBalancer<ServiceInstance> loadBalancer = (ReactorLoadBalancer<ServiceInstance>) reactiveLoadBalancer;
//order dependent on seedPosition -1 of RoundRobinLoadBalancer
List<String> hosts = Arrays.asList("a.host", "c.host", "b.host-secure", "a.host");
assertLoadBalancer(loadBalancer, hosts);
}
private void assertLoadBalancer(ReactorLoadBalancer<ServiceInstance> loadBalancer, List<String> hosts) {
for (String host : hosts) {
Mono<Response<ServiceInstance>> source = loadBalancer.choose();
StepVerifier.create(source).consumeNextWith(response -> {
assertThat(response).isNotNull();
assertThat(response.hasServer()).isTrue();
ServiceInstance instance = response.getServer();
assertThat(instance).isNotNull();
assertThat(instance.getHost())
.as("instance host is incorrent %s", host)
.isEqualTo(host);
if (host.contains("secure")) {
assertThat(instance.isSecure()).isTrue();
} else {
assertThat(instance.isSecure()).isFalse();
}
response.onComplete(new CompletionContext(Status.SUCCESSS));
}).verifyComplete();
}
}
@Test
public void emptyHosts() {
ResolvableType type = ResolvableType.forClassWithGenerics(ReactorLoadBalancer.class, ServiceInstance.class);
ReactorLoadBalancer<ServiceInstance> loadBalancer = this.clientFactory.getInstance("unknownservice", type);
assertThat(loadBalancer).isInstanceOf(RoundRobinLoadBalancer.class);
Mono<Response<ServiceInstance>> source = loadBalancer.choose();
StepVerifier.create(source).consumeNextWith(response -> {
assertThat(response).isNotNull();
assertThat(response.hasServer()).isFalse();
}).verifyComplete();
}
@Test
public void staticConfigurationWorks() {
String serviceId = "test1";
RoundRobinLoadBalancer loadBalancer = new RoundRobinLoadBalancer(serviceId,
ServiceInstanceSuppliers.toProvider(serviceId, instance(serviceId, "1.host", false),
instance(serviceId, "2.host-secure", true)),
-1);
assertLoadBalancer(loadBalancer, Arrays.asList("1.host", "2.host-secure"));
}
private DefaultServiceInstance instance(String serviceId, String host, boolean secure) {
return new DefaultServiceInstance(serviceId, host, 80, secure);
}
@EnableAutoConfiguration
@SpringBootConfiguration
@LoadBalancerClients({
@LoadBalancerClient(name = "myservice", configuration = MyServiceConfig.class),
@LoadBalancerClient(name = "unknownservice", configuration = MyServiceConfig.class),
})
@EnableCaching
protected static class Config { }
protected static class MyServiceConfig {
@Bean
public RoundRobinLoadBalancer roundRobinContextLoadBalancer(LoadBalancerClientFactory clientFactory, Environment env) {
String serviceId = clientFactory.getName(env);
return new RoundRobinLoadBalancer(serviceId,
clientFactory.getLazyProvider(serviceId, ServiceInstanceSupplier.class),
-1);
}
}
}

View File

@@ -0,0 +1,36 @@
spring:
cloud:
discovery:
client:
simple:
instances:
myservice:
-
service-id: myservice
uri: http://a.host
-
service-id: myservice
uri: http://c.host
-
service-id: myservice
uri: https://b.host-secure
anotherservice:
-
service-id: myservice
uri: http://d.host
-
service-id: myservice
uri: http://f.host
-
service-id: myservice
uri: https://e.host
thirdservice:
-
service-id: myservice
uri: http://g.host
-
service-id: myservice
uri: http://h.host
-
service-id: myservice
uri: https://i.host