From 1fa513194b07c949f21ab73af40f3ff5464d93f5 Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Wed, 15 Aug 2018 18:45:53 -0400 Subject: [PATCH] Adds API and implementation of ReactiveLoadBalancer API lives in spring-cloud-commons module and implementation in spring-cloud-loadbalancer. --- pom.xml | 1 + spring-cloud-commons-dependencies/pom.xml | 5 + .../cloud/client/DefaultServiceInstance.java | 2 +- .../reactive/CompletionContext.java | 64 ++++++++ .../loadbalancer/reactive/DefaultRequest.java | 24 +++ .../reactive/DefaultResponse.java | 46 ++++++ .../loadbalancer/reactive/EmptyResponse.java | 40 +++++ .../reactive/ReactiveLoadBalancer.java | 38 +++++ .../client/loadbalancer/reactive/Request.java | 5 + .../loadbalancer/reactive/Response.java | 16 ++ .../named/ClientFactoryObjectProvider.java | 116 ++++++++++++++ .../context/named/NamedContextFactory.java | 32 ++++ spring-cloud-loadbalancer/pom.xml | 88 +++++++++++ .../annotation/LoadBalancerClient.java | 67 ++++++++ .../LoadBalancerClientConfiguration.java | 50 ++++++ ...dBalancerClientConfigurationRegistrar.java | 88 +++++++++++ .../LoadBalancerClientSpecification.java | 84 ++++++++++ .../annotation/LoadBalancerClients.java | 52 +++++++ .../config/LoadBalancerAutoConfiguration.java | 49 ++++++ .../core/CachingServiceInstanceSupplier.java | 73 +++++++++ ...iscoveryClientServiceInstanceSupplier.java | 51 +++++++ .../core/ReactorLoadBalancer.java | 36 +++++ .../core/RoundRobinLoadBalancer.java | 75 +++++++++ .../core/ServiceInstanceSupplier.java | 31 ++++ .../support/LoadBalancerClientFactory.java | 46 ++++++ .../support/ServiceInstanceSuppliers.java | 43 ++++++ .../support/SimpleObjectProvider.java | 49 ++++++ .../main/resources/META-INF/spring.factories | 4 + .../loadbalancer/core/LoadBalancerTest.java | 144 ++++++++++++++++++ .../src/test/resources/application.yml | 36 +++++ 30 files changed, 1454 insertions(+), 1 deletion(-) create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/CompletionContext.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/DefaultRequest.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/DefaultResponse.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/EmptyResponse.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancer.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/Request.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/Response.java create mode 100644 spring-cloud-context/src/main/java/org/springframework/cloud/context/named/ClientFactoryObjectProvider.java create mode 100644 spring-cloud-loadbalancer/pom.xml create mode 100644 spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClient.java create mode 100644 spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java create mode 100644 spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfigurationRegistrar.java create mode 100644 spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientSpecification.java create mode 100644 spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClients.java create mode 100644 spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerAutoConfiguration.java create mode 100644 spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/CachingServiceInstanceSupplier.java create mode 100644 spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/DiscoveryClientServiceInstanceSupplier.java create mode 100644 spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ReactorLoadBalancer.java create mode 100644 spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RoundRobinLoadBalancer.java create mode 100644 spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceSupplier.java create mode 100644 spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/LoadBalancerClientFactory.java create mode 100644 spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/ServiceInstanceSuppliers.java create mode 100644 spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/SimpleObjectProvider.java create mode 100644 spring-cloud-loadbalancer/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/LoadBalancerTest.java create mode 100644 spring-cloud-loadbalancer/src/test/resources/application.yml diff --git a/pom.xml b/pom.xml index 9dfbf2c7..ab3b0b2a 100644 --- a/pom.xml +++ b/pom.xml @@ -126,6 +126,7 @@ spring-cloud-context spring-cloud-context-integration-tests spring-cloud-commons + spring-cloud-loadbalancer spring-cloud-starter docs diff --git a/spring-cloud-commons-dependencies/pom.xml b/spring-cloud-commons-dependencies/pom.xml index c7e8642c..74e1dd83 100644 --- a/spring-cloud-commons-dependencies/pom.xml +++ b/spring-cloud-commons-dependencies/pom.xml @@ -43,6 +43,11 @@ spring-cloud-context ${project.version} + + org.springframework.cloud + spring-cloud-loadbalancer + ${project.version} + org.springframework.cloud spring-cloud-starter diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/DefaultServiceInstance.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/DefaultServiceInstance.java index d9498482..69a7a4b9 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/DefaultServiceInstance.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/DefaultServiceInstance.java @@ -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()); + this(serviceId, host, port, secure, new LinkedHashMap<>()); } @Override diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/CompletionContext.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/CompletionContext.java new file mode 100644 index 00000000..23fa0899 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/CompletionContext.java @@ -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(); + } + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/DefaultRequest.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/DefaultRequest.java new file mode 100644 index 00000000..2ac7dc93 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/DefaultRequest.java @@ -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 { + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/DefaultResponse.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/DefaultResponse.java new file mode 100644 index 00000000..340a8d9e --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/DefaultResponse.java @@ -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 { + + 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 + } +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/EmptyResponse.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/EmptyResponse.java new file mode 100644 index 00000000..ce5ea1ba --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/EmptyResponse.java @@ -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 { + + @Override + public boolean hasServer() { + return false; + } + + @Override + public ServiceInstance getServer() { + return null; + } + + @Override + public void onComplete(CompletionContext completionContext) { + //TODO: implement + } +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancer.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancer.java new file mode 100644 index 00000000..c3051856 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancer.java @@ -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 { + + Request REQUEST = new DefaultRequest(); + + /** + * Choose the next server based on the load balancing algorithm + * @param request + * @return + */ + Publisher> choose(Request request); + + default Publisher> choose() { //conflicting name + return choose(REQUEST); + } +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/Request.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/Request.java new file mode 100644 index 00000000..309254af --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/Request.java @@ -0,0 +1,5 @@ +package org.springframework.cloud.client.loadbalancer.reactive; + +public interface Request { + //TODO: define contents +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/Response.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/Response.java new file mode 100644 index 00000000..47c14ccc --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/Response.java @@ -0,0 +1,16 @@ +package org.springframework.cloud.client.loadbalancer.reactive; + +/** + * Response created for each request. + */ +public interface Response { + boolean hasServer(); + + T getServer(); + + /** + * Notification that the request completed + * @param completionContext + */ + void onComplete(CompletionContext completionContext); +} diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/named/ClientFactoryObjectProvider.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/named/ClientFactoryObjectProvider.java new file mode 100644 index 00000000..1de53ae0 --- /dev/null +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/named/ClientFactoryObjectProvider.java @@ -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 + */ +class ClientFactoryObjectProvider implements ObjectProvider { + + private final NamedContextFactory clientFactory; + private final String name; + private final Class type; + private ObjectProvider provider; + + public ClientFactoryObjectProvider(NamedContextFactory clientFactory, String name, Class 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 defaultSupplier) throws BeansException { + return delegate().getIfAvailable(defaultSupplier); + } + + @Override + public void ifAvailable(Consumer dependencyConsumer) throws BeansException { + delegate().ifAvailable(dependencyConsumer); + } + + @Override + @Nullable + public T getIfUnique() throws BeansException { + return delegate().getIfUnique(); + } + + @Override + public T getIfUnique(Supplier defaultSupplier) throws BeansException { + return delegate().getIfUnique(defaultSupplier); + } + + @Override + public void ifUnique(Consumer dependencyConsumer) throws BeansException { + delegate().ifUnique(dependencyConsumer); + } + + @Override + public Iterator iterator() { + return delegate().iterator(); + } + + @Override + public Stream stream() { + return delegate().stream(); + } + + @Override + public T getObject() throws BeansException { + return delegate().getObject(); + } + + @Override + public void forEach(Consumer action) { + delegate().forEach(action); + } + + @Override + public Spliterator spliterator() { + return delegate().spliterator(); + } + + @SuppressWarnings("unchecked") + private ObjectProvider delegate() { + if (this.provider == null) { + provider = this.clientFactory.getProvider(name, type); + } + return provider; + } +} diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/named/NamedContextFactory.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/named/NamedContextFactory.java index c6460b46..6287bc04 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/context/named/NamedContextFactory.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/named/NamedContextFactory.java @@ -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 implements DisposableBean, ApplicationContextAware { @@ -131,6 +134,35 @@ public abstract class NamedContextFactory ObjectProvider getLazyProvider(String name, Class type) { + return new ClientFactoryObjectProvider<>(this, name, type); + } + + public ObjectProvider getProvider(String name, Class type) { + AnnotationConfigApplicationContext context = getContext(name); + return context.getBeanProvider(type); + } + + public T getInstance(String name, Class clazz, Class... generics) { + ResolvableType type = ResolvableType.forClassWithGenerics(clazz, generics); + return getInstance(name, type); + } + + @SuppressWarnings("unchecked") + public 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 Map getInstances(String name, Class type) { AnnotationConfigApplicationContext context = getContext(name); if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, diff --git a/spring-cloud-loadbalancer/pom.xml b/spring-cloud-loadbalancer/pom.xml new file mode 100644 index 00000000..02b2b61e --- /dev/null +++ b/spring-cloud-loadbalancer/pom.xml @@ -0,0 +1,88 @@ + + + 4.0.0 + + + org.springframework.cloud + spring-cloud-commons-parent + 2.1.0.BUILD-SNAPSHOT + .. + + spring-cloud-loadbalancer + jar + Spring Cloud Load Balancer + Spring Cloud Balancer + + + org.springframework.cloud + spring-cloud-commons + + + org.springframework.cloud + spring-cloud-context + + + org.springframework.boot + spring-boot-starter-actuator + true + + + + org.apache.logging.log4j + log4j-to-slf4j + + + + + io.projectreactor + reactor-core + + + io.projectreactor.addons + reactor-extra + + 3.2.0.M2 + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-webflux + test + + + org.springframework.boot + spring-boot-starter-cache + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.cloud + spring-cloud-test-support + test + + + io.projectreactor + reactor-test + test + + + com.github.ben-manes.caffeine + caffeine + test + + + diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClient.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClient.java new file mode 100644 index 00000000..ff84c880 --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClient.java @@ -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 + * @Configuration 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 @Configuration for the load balancer client. Can contain override + * @Bean definition for the pieces that make up the client. + * + * @see LoadBalancerClientConfiguration for the defaults + */ + Class[] configuration() default {}; + +} diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java new file mode 100644 index 00000000..95eb8f35 --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java @@ -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) { + //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; + } + +} diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfigurationRegistrar.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfigurationRegistrar.java new file mode 100644 index 00000000..67c0fa07 --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfigurationRegistrar.java @@ -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 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 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 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()); + } + +} diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientSpecification.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientSpecification.java new file mode 100644 index 00000000..b990e1a3 --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientSpecification.java @@ -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); + } +} diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClients.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClients.java new file mode 100644 index 00000000..f7f94254 --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClients.java @@ -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 @LoadBalancerClient + * 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 {}; + +} diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerAutoConfiguration.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerAutoConfiguration.java new file mode 100644 index 00000000..f078adf3 --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerAutoConfiguration.java @@ -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> configurations; + + public LoadBalancerAutoConfiguration(ObjectProvider> configurations) { + this.configurations = configurations; + } + + @Bean + public LoadBalancerClientFactory loadBalancerClientFactory() { + LoadBalancerClientFactory clientFactory = new LoadBalancerClientFactory(); + clientFactory.setConfigurations(configurations.getIfAvailable(Collections::emptyList)); + return clientFactory; + } +} diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/CachingServiceInstanceSupplier.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/CachingServiceInstanceSupplier.java new file mode 100644 index 00000000..8f8ad69c --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/CachingServiceInstanceSupplier.java @@ -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 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 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 get() { + return this.serviceInstances; + } + + @Override + public String getServiceId() { + return this.delegate.getServiceId(); + } +} diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/DiscoveryClientServiceInstanceSupplier.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/DiscoveryClientServiceInstanceSupplier.java new file mode 100644 index 00000000..7b75a5b9 --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/DiscoveryClientServiceInstanceSupplier.java @@ -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 get() { + List instances = delegate.getInstances(serviceId); + return Flux.fromIterable(instances); + } + + public String getServiceId() { + return this.serviceId; + } +} diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ReactorLoadBalancer.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ReactorLoadBalancer.java new file mode 100644 index 00000000..3b3f0b6f --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ReactorLoadBalancer.java @@ -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 extends ReactiveLoadBalancer { + /** + * Choose the next server based on the load balancing algorithm + * @param request + * @return + */ + Mono> choose(Request request); + + default Mono> choose() { + return choose(REQUEST); + } +} diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RoundRobinLoadBalancer.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RoundRobinLoadBalancer.java new file mode 100644 index 00000000..bd17c40e --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RoundRobinLoadBalancer.java @@ -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 { + + private static final Log log = LogFactory.getLog(RoundRobinLoadBalancer.class); + + private final AtomicInteger position; + private final ObjectProvider serviceInstanceSupplier; + private final String serviceId; + + public RoundRobinLoadBalancer(String serviceId, ObjectProvider serviceInstanceSupplier) { + this(serviceId, serviceInstanceSupplier, new Random().nextInt(1000)); + } + + public RoundRobinLoadBalancer(String serviceId, ObjectProvider 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> 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); + }); + } +} diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceSupplier.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceSupplier.java new file mode 100644 index 00000000..96ba3ede --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceSupplier.java @@ -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> { + + String getServiceId(); +} diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/LoadBalancerClientFactory.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/LoadBalancerClientFactory.java new file mode 100644 index 00000000..a7623d08 --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/LoadBalancerClientFactory.java @@ -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 { + + 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); + } + +} + diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/ServiceInstanceSuppliers.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/ServiceInstanceSuppliers.java new file mode 100644 index 00000000..2c23bc8f --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/ServiceInstanceSuppliers.java @@ -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 get() { + return Flux.just(instances); + } + + @Override + public String getServiceId() { + return serviceId; + } + }; + } + + public static ObjectProvider toProvider(String serviceId, ServiceInstance... instances) { + return new SimpleObjectProvider<>(from(serviceId, instances)); + } +} diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/SimpleObjectProvider.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/SimpleObjectProvider.java new file mode 100644 index 00000000..0b2d0366 --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/SimpleObjectProvider.java @@ -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 implements ObjectProvider { + + 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; + } +} diff --git a/spring-cloud-loadbalancer/src/main/resources/META-INF/spring.factories b/spring-cloud-loadbalancer/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..b12b173d --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +# AutoConfiguration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration + diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/LoadBalancerTest.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/LoadBalancerTest.java new file mode 100644 index 00000000..ac8e8a96 --- /dev/null +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/LoadBalancerTest.java @@ -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 reactiveLoadBalancer = this.clientFactory + .getInstance("myservice", ReactiveLoadBalancer.class, ServiceInstance.class); + + assertThat(reactiveLoadBalancer).isInstanceOf(RoundRobinLoadBalancer.class); + assertThat(reactiveLoadBalancer).isInstanceOf(ReactorLoadBalancer.class); + ReactorLoadBalancer loadBalancer = (ReactorLoadBalancer) reactiveLoadBalancer; + + //order dependent on seedPosition -1 of RoundRobinLoadBalancer + List hosts = Arrays.asList("a.host", "c.host", "b.host-secure", "a.host"); + + assertLoadBalancer(loadBalancer, hosts); + } + + private void assertLoadBalancer(ReactorLoadBalancer loadBalancer, List hosts) { + for (String host : hosts) { + Mono> 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 loadBalancer = this.clientFactory.getInstance("unknownservice", type); + + assertThat(loadBalancer).isInstanceOf(RoundRobinLoadBalancer.class); + + Mono> 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); + } + } +} diff --git a/spring-cloud-loadbalancer/src/test/resources/application.yml b/spring-cloud-loadbalancer/src/test/resources/application.yml new file mode 100644 index 00000000..41b22c67 --- /dev/null +++ b/spring-cloud-loadbalancer/src/test/resources/application.yml @@ -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 \ No newline at end of file