From d6be04468c783f36cc778e5da1b7e72d6746155e Mon Sep 17 00:00:00 2001 From: Tim Ysewyn Date: Wed, 11 Sep 2019 17:39:10 +0200 Subject: [PATCH] Added support for reactive service discovery client --- .../main/asciidoc/spring-cloud-commons.adoc | 8 +- spring-cloud-commons/pom.xml | 5 + .../CommonsClientAutoConfiguration.java | 2 + ...ConditionalOnBlockingDiscoveryEnabled.java | 43 ++++ ...ConditionalOnReactiveDiscoveryEnabled.java | 47 +++++ ...eactiveCommonsClientAutoConfiguration.java | 74 +++++++ .../discovery/ReactiveDiscoveryClient.java | 64 ++++++ .../ReactiveCompositeDiscoveryClient.java | 74 +++++++ ...ositeDiscoveryClientAutoConfiguration.java | 49 +++++ ...eactiveDiscoveryClientHealthIndicator.java | 114 +++++++++++ ...veDiscoveryCompositeHealthContributor.java | 80 ++++++++ .../ReactiveDiscoveryHealthIndicator.java | 43 ++++ ...impleDiscoveryClientAutoConfiguration.java | 9 +- .../SimpleReactiveDiscoveryClient.java | 59 ++++++ ...ctiveDiscoveryClientAutoConfiguration.java | 109 ++++++++++ .../SimpleReactiveDiscoveryProperties.java | 188 ++++++++++++++++++ .../main/resources/META-INF/spring.factories | 3 + .../CommonsClientAutoConfigurationTests.java | 129 +++++++----- ...veCommonsClientAutoConfigurationTests.java | 166 ++++++++++++++++ ...DiscoveryClientAutoConfigurationTests.java | 89 +++++++++ ...ReactiveCompositeDiscoveryClientTests.java | 124 ++++++++++++ ...veDiscoveryClientHealthIndicatorTests.java | 115 +++++++++++ ...coveryCompositeHealthContributorTests.java | 82 ++++++++ ...DiscoveryClientAutoConfigurationTests.java | 92 +++++++++ .../SimpleReactiveDiscoveryClientTests.java | 82 ++++++++ 25 files changed, 1792 insertions(+), 58 deletions(-) create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/ConditionalOnBlockingDiscoveryEnabled.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/ConditionalOnReactiveDiscoveryEnabled.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/ReactiveCommonsClientAutoConfiguration.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/ReactiveDiscoveryClient.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClient.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientAutoConfiguration.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryClientHealthIndicator.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryCompositeHealthContributor.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryHealthIndicator.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClient.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientAutoConfiguration.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryProperties.java create mode 100644 spring-cloud-commons/src/test/java/org/springframework/cloud/client/ReactiveCommonsClientAutoConfigurationTests.java create mode 100644 spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientAutoConfigurationTests.java create mode 100644 spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientTests.java create mode 100644 spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryClientHealthIndicatorTests.java create mode 100644 spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryCompositeHealthContributorTests.java create mode 100644 spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientAutoConfigurationTests.java create mode 100644 spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientTests.java diff --git a/docs/src/main/asciidoc/spring-cloud-commons.adoc b/docs/src/main/asciidoc/spring-cloud-commons.adoc index 568246b9..96ba462c 100644 --- a/docs/src/main/asciidoc/spring-cloud-commons.adoc +++ b/docs/src/main/asciidoc/spring-cloud-commons.adoc @@ -241,10 +241,14 @@ Patterns such as service discovery, load balancing, and circuit breakers lend th === @EnableDiscoveryClient Spring Cloud Commons provides the `@EnableDiscoveryClient` annotation. -This looks for implementations of the `DiscoveryClient` interface with `META-INF/spring.factories`. -Implementations of the Discovery Client add a configuration class to `spring.factories` under the `org.springframework.cloud.client.discovery.EnableDiscoveryClient` key. +This looks for implementations of the `DiscoveryClient` and `ReactiveDiscoveryClient` interfaces with `META-INF/spring.factories`. +Implementations of the discovery client add a configuration class to `spring.factories` under the `org.springframework.cloud.client.discovery.EnableDiscoveryClient` key. Examples of `DiscoveryClient` implementations include https://cloud.spring.io/spring-cloud-netflix/[Spring Cloud Netflix Eureka], https://cloud.spring.io/spring-cloud-consul/[Spring Cloud Consul Discovery], and https://cloud.spring.io/spring-cloud-zookeeper/[Spring Cloud Zookeeper Discovery]. +Spring Cloud will provide both the blocking and reactive service discovery clients by default. +You can disable the blocking and/or reactive clients easily by setting `spring.cloud.discovery.blocking.enabled=false` or `spring.cloud.discovery.reactive.enabled=false`. +To completely disable service discovery you just need to set `spring.cloud.discovery.enabled=false`. + By default, implementations of `DiscoveryClient` auto-register the local Spring Boot server with the remote discovery server. This behavior can be disabled by setting `autoRegister=false` in `@EnableDiscoveryClient`. diff --git a/spring-cloud-commons/pom.xml b/spring-cloud-commons/pom.xml index 7a864ee1..875ca30f 100644 --- a/spring-cloud-commons/pom.xml +++ b/spring-cloud-commons/pom.xml @@ -163,5 +163,10 @@ spring-cloud-test-support test + + io.projectreactor + reactor-test + test + diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/CommonsClientAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/CommonsClientAutoConfiguration.java index 0f890f16..d3de269f 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/CommonsClientAutoConfiguration.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/CommonsClientAutoConfiguration.java @@ -47,6 +47,7 @@ import org.springframework.context.annotation.Configuration; * * @author Spencer Gibb * @author Olga Maciaszek-Sharma + * @author Tim Ysewyn */ @Configuration @AutoConfigureOrder(0) @@ -57,6 +58,7 @@ public class CommonsClientAutoConfiguration { @ConditionalOnClass(HealthIndicator.class) @ConditionalOnBean(DiscoveryClient.class) @ConditionalOnDiscoveryEnabled + @ConditionalOnBlockingDiscoveryEnabled protected static class DiscoveryLoadBalancerConfiguration { @Bean diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/ConditionalOnBlockingDiscoveryEnabled.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/ConditionalOnBlockingDiscoveryEnabled.java new file mode 100644 index 00000000..e026fa5d --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/ConditionalOnBlockingDiscoveryEnabled.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.client; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +/** + * Provides a more succinct conditional + * spring.cloud.discovery.blocking.enabled. + * + * @author Tim Ysewyn + * @since 2.2.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ConditionalOnProperty(value = "spring.cloud.discovery.blocking.enabled", + matchIfMissing = true) +public @interface ConditionalOnBlockingDiscoveryEnabled { + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/ConditionalOnReactiveDiscoveryEnabled.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/ConditionalOnReactiveDiscoveryEnabled.java new file mode 100644 index 00000000..9a7bacee --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/ConditionalOnReactiveDiscoveryEnabled.java @@ -0,0 +1,47 @@ +/* + * Copyright 2019-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.client; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * Provides a more succinct conditional + * spring.cloud.discovery.reactive.enabled. Also takes into account whether + * or not `WebClient` is on the classpath. + * + * @author Tim Ysewyn + * @since 2.2.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ConditionalOnClass(WebClient.class) +@ConditionalOnProperty(value = "spring.cloud.discovery.reactive.enabled", + matchIfMissing = true) +public @interface ConditionalOnReactiveDiscoveryEnabled { + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/ReactiveCommonsClientAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/ReactiveCommonsClientAutoConfiguration.java new file mode 100644 index 00000000..44ca9839 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/ReactiveCommonsClientAutoConfiguration.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.client; + +import java.util.Collection; + +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.client.actuator.HasFeatures; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicatorProperties; +import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryCompositeHealthContributor; +import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryHealthIndicator; +import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for reactive Spring Cloud Commons + * Client. + * + * @author Tim Ysewyn + * @since 2.2.0 + */ +@Configuration +@AutoConfigureOrder(0) +public class ReactiveCommonsClientAutoConfiguration { + + @Configuration + @EnableConfigurationProperties(DiscoveryClientHealthIndicatorProperties.class) + @ConditionalOnClass(HealthIndicator.class) + @ConditionalOnBean(ReactiveDiscoveryClient.class) + @ConditionalOnDiscoveryEnabled + @ConditionalOnReactiveDiscoveryEnabled + protected static class ReactiveDiscoveryLoadBalancerConfiguration { + + @Bean + @ConditionalOnProperty( + value = "spring.cloud.discovery.client.composite-indicator.enabled", + matchIfMissing = true) + @ConditionalOnBean({ ReactiveDiscoveryHealthIndicator.class }) + public ReactiveDiscoveryCompositeHealthContributor reactiveDiscoveryClients( + Collection indicators) { + return new ReactiveDiscoveryCompositeHealthContributor(indicators); + } + + @Bean + public HasFeatures reactiveCommonsFeatures() { + return HasFeatures.abstractFeatures(ReactiveDiscoveryClient.class, + ReactiveLoadBalancer.class); + } + + } + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/ReactiveDiscoveryClient.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/ReactiveDiscoveryClient.java new file mode 100644 index 00000000..dbbd1ccb --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/ReactiveDiscoveryClient.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.client.discovery; + +import reactor.core.publisher.Flux; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.core.Ordered; + +/** + * Represents read operations commonly available to discovery services such as Netflix + * Eureka or consul.io. + * + * @author Tim Ysewyn + */ +public interface ReactiveDiscoveryClient extends Ordered { + + /** + * Default order of the discovery client. + */ + int DEFAULT_ORDER = 0; + + /** + * A human-readable description of the implementation, used in HealthIndicator. + * @return The description. + */ + String description(); + + /** + * Gets all ServiceInstances associated with a particular serviceId. + * @param serviceId The serviceId to query. + * @return A List of ServiceInstance. + */ + Flux getInstances(String serviceId); + + /** + * @return All known service IDs. + */ + Flux getServices(); + + /** + * Default implementation for getting order of discovery clients. + * @return order + */ + @Override + default int getOrder() { + return DEFAULT_ORDER; + } + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClient.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClient.java new file mode 100644 index 00000000..01889c09 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClient.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.client.discovery.composite.reactive; + +import java.util.ArrayList; +import java.util.List; + +import reactor.core.publisher.Flux; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; + +/** + * A {@link ReactiveDiscoveryClient} that is composed of other discovery clients and + * delegates calls to each of them in order. + * + * @author Tim Ysewyn + */ +public class ReactiveCompositeDiscoveryClient implements ReactiveDiscoveryClient { + + private final List discoveryClients; + + public ReactiveCompositeDiscoveryClient( + List discoveryClients) { + AnnotationAwareOrderComparator.sort(discoveryClients); + this.discoveryClients = discoveryClients; + } + + @Override + public String description() { + return "Composite Reactive Discovery Client"; + } + + @Override + public Flux getInstances(String serviceId) { + if (discoveryClients == null || discoveryClients.isEmpty()) { + return Flux.empty(); + } + List> serviceInstances = new ArrayList<>(); + for (ReactiveDiscoveryClient discoveryClient : discoveryClients) { + serviceInstances.add(discoveryClient.getInstances(serviceId)); + } + return Flux.first(serviceInstances); + } + + @Override + public Flux getServices() { + if (discoveryClients == null || discoveryClients.isEmpty()) { + return Flux.empty(); + } + return Flux.fromIterable(discoveryClients) + .flatMap(ReactiveDiscoveryClient::getServices); + } + + List getDiscoveryClients() { + return discoveryClients; + } + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientAutoConfiguration.java new file mode 100644 index 00000000..b6a0c0ac --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientAutoConfiguration.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.client.discovery.composite.reactive; + +import java.util.List; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; +import org.springframework.cloud.client.ConditionalOnReactiveDiscoveryEnabled; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryClientAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +/** + * Auto-configuration for reactive composite discovery client. + * + * @author Tim Ysewyn + * @since 2.2.0 + */ +@Configuration +@ConditionalOnDiscoveryEnabled +@ConditionalOnReactiveDiscoveryEnabled +@AutoConfigureBefore(SimpleReactiveDiscoveryClientAutoConfiguration.class) +public class ReactiveCompositeDiscoveryClientAutoConfiguration { + + @Bean + @Primary + public ReactiveCompositeDiscoveryClient reactiveCompositeDiscoveryClient( + List discoveryClients) { + return new ReactiveCompositeDiscoveryClient(discoveryClients); + } + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryClientHealthIndicator.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryClientHealthIndicator.java new file mode 100644 index 00000000..b289187a --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryClientHealthIndicator.java @@ -0,0 +1,114 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.client.discovery.health.reactive; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent; +import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicatorProperties; +import org.springframework.context.ApplicationListener; +import org.springframework.core.Ordered; + +import static java.util.Collections.emptyList; + +/** + * A health indicator which indicates whether or not the discovery client has been + * initialized. + * + * @author Tim Ysewyn + */ +public class ReactiveDiscoveryClientHealthIndicator + implements ReactiveDiscoveryHealthIndicator, Ordered, + ApplicationListener> { + + private final ReactiveDiscoveryClient discoveryClient; + + private final DiscoveryClientHealthIndicatorProperties properties; + + private final Log log = LogFactory + .getLog(ReactiveDiscoveryClientHealthIndicator.class); + + private AtomicBoolean discoveryInitialized = new AtomicBoolean(false); + + private int order = Ordered.HIGHEST_PRECEDENCE; + + public ReactiveDiscoveryClientHealthIndicator(ReactiveDiscoveryClient discoveryClient, + DiscoveryClientHealthIndicatorProperties properties) { + this.discoveryClient = discoveryClient; + this.properties = properties; + } + + @Override + public void onApplicationEvent(InstanceRegisteredEvent event) { + if (this.discoveryInitialized.compareAndSet(false, true)) { + this.log.debug("Discovery Client has been initialized"); + } + } + + @Override + public Mono health() { + if (this.discoveryInitialized.get()) { + return doHealthCheck(); + } + else { + return Mono.just(Health.status(new Status(Status.UNKNOWN.getCode(), + "Discovery Client not initialized")).build()); + } + } + + private Mono doHealthCheck() { + // @formatter:off + return Mono.justOrEmpty(this.discoveryClient) + .flatMapMany(ReactiveDiscoveryClient::getServices) + .collectList() + .defaultIfEmpty(emptyList()) + .map(services -> { + ReactiveDiscoveryClient client = this.discoveryClient; + String description = (this.properties.isIncludeDescription()) + ? client.description() : ""; + return Health.status(new Status("UP", description)) + .withDetail("services", services).build(); + }) + .onErrorResume(exception -> { + this.log.error("Error", exception); + return Mono.just(Health.down().withException(exception).build()); + }); + // @formatter:on + } + + @Override + public String getName() { + return discoveryClient.description(); + } + + @Override + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryCompositeHealthContributor.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryCompositeHealthContributor.java new file mode 100644 index 00000000..202dcbd9 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryCompositeHealthContributor.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.client.discovery.health.reactive; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.springframework.boot.actuate.health.CompositeReactiveHealthContributor; +import org.springframework.boot.actuate.health.NamedContributor; +import org.springframework.boot.actuate.health.ReactiveHealthContributor; +import org.springframework.boot.actuate.health.ReactiveHealthIndicator; +import org.springframework.util.Assert; + +/** + * A composite health contributor specific to a reactive discovery client implementation. + * + * @author Tim Ysewyn + */ +public class ReactiveDiscoveryCompositeHealthContributor + implements CompositeReactiveHealthContributor { + + private Map indicators; + + public ReactiveDiscoveryCompositeHealthContributor( + Collection indicators) { + Assert.notNull(indicators, "'indicators' must not be null"); + this.indicators = indicators.stream().collect(Collectors + .toMap(ReactiveDiscoveryHealthIndicator::getName, Function.identity())); + } + + @Override + public ReactiveHealthContributor getContributor(String name) { + return asHealthIndicator(indicators.get(name)); + } + + @Override + public Iterator> iterator() { + return indicators.values().stream().map(this::asNamedContributor).iterator(); + } + + private NamedContributor asNamedContributor( + ReactiveDiscoveryHealthIndicator indicator) { + return new NamedContributor() { + + @Override + public String getName() { + return indicator.getName(); + } + + @Override + public ReactiveHealthContributor getContributor() { + return asHealthIndicator(indicator); + } + + }; + } + + private ReactiveHealthIndicator asHealthIndicator( + ReactiveDiscoveryHealthIndicator indicator) { + return (indicator != null) ? indicator::health : null; + } + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryHealthIndicator.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryHealthIndicator.java new file mode 100644 index 00000000..6b5f535d --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryHealthIndicator.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.client.discovery.health.reactive; + +import reactor.core.publisher.Mono; + +import org.springframework.boot.actuate.health.Health; + +/** + * A health indicator interface specific to a reactive discovery client implementation. + * + * @author Tim Ysewyn + */ +public interface ReactiveDiscoveryHealthIndicator { + + /** + * Provide the name of health indicator. + * @return a {@link String} that provides the name of health indicator, usually the + * name of the implementation. + */ + String getName(); + + /** + * Provide the indicator of health. + * @return a {@link Mono} that provides the {@link Health} + */ + Mono health(); + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/SimpleDiscoveryClientAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/SimpleDiscoveryClientAutoConfiguration.java index 0062e2e6..1fb375e8 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/SimpleDiscoveryClientAutoConfiguration.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/SimpleDiscoveryClientAutoConfiguration.java @@ -23,10 +23,10 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.web.context.WebServerInitializedEvent; +import org.springframework.cloud.client.CommonsClientAutoConfiguration; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.discovery.noop.NoopDiscoveryClientAutoConfiguration; import org.springframework.cloud.commons.util.InetUtils; -import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -38,18 +38,15 @@ import org.springframework.core.annotation.Order; * * @author Biju Kunjummen */ - @Configuration -@AutoConfigureBefore(NoopDiscoveryClientAutoConfiguration.class) +@AutoConfigureBefore({ NoopDiscoveryClientAutoConfiguration.class, + CommonsClientAutoConfiguration.class }) public class SimpleDiscoveryClientAutoConfiguration implements ApplicationListener { @Autowired(required = false) private ServerProperties server; - @Autowired - private ApplicationContext context; - @Value("${spring.application.name:application}") private String serviceId; diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClient.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClient.java new file mode 100644 index 00000000..b4c28af4 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClient.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.client.discovery.simple.reactive; + +import reactor.core.publisher.Flux; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; + +/** + * A {@link ReactiveDiscoveryClient} that will use the properties file as a source of + * service instances. + * + * @author Tim Ysewyn + */ +public class SimpleReactiveDiscoveryClient implements ReactiveDiscoveryClient { + + private SimpleReactiveDiscoveryProperties simpleDiscoveryProperties; + + public SimpleReactiveDiscoveryClient( + SimpleReactiveDiscoveryProperties simpleDiscoveryProperties) { + this.simpleDiscoveryProperties = simpleDiscoveryProperties; + } + + @Override + public String description() { + return "Simple Reactive Discovery Client"; + } + + @Override + public Flux getInstances(String serviceId) { + return this.simpleDiscoveryProperties.getInstances(serviceId); + } + + @Override + public Flux getServices() { + return Flux.fromIterable(this.simpleDiscoveryProperties.getInstances().keySet()); + } + + @Override + public int getOrder() { + return this.simpleDiscoveryProperties.getOrder(); + } + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientAutoConfiguration.java new file mode 100644 index 00000000..f5681a15 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientAutoConfiguration.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.client.discovery.simple.reactive; + +import java.net.URI; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.context.WebServerInitializedEvent; +import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; +import org.springframework.cloud.client.ConditionalOnReactiveDiscoveryEnabled; +import org.springframework.cloud.client.ReactiveCommonsClientAutoConfiguration; +import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicatorProperties; +import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryClientHealthIndicator; +import org.springframework.cloud.commons.util.InetUtils; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; + +/** + * Spring Boot auto-configuration for simple properties-based reactive discovery client. + * + * @author Tim Ysewyn + * @since 2.2.0 + */ +@Configuration +@ConditionalOnDiscoveryEnabled +@ConditionalOnReactiveDiscoveryEnabled +@EnableConfigurationProperties(DiscoveryClientHealthIndicatorProperties.class) +@AutoConfigureBefore(ReactiveCommonsClientAutoConfiguration.class) +public class SimpleReactiveDiscoveryClientAutoConfiguration + implements ApplicationListener { + + @Autowired(required = false) + private ServerProperties server; + + @Value("${spring.application.name:application}") + private String serviceId; + + @Autowired + private InetUtils inet; + + private int port = 0; + + private SimpleReactiveDiscoveryProperties simple = new SimpleReactiveDiscoveryProperties(); + + @Bean + public SimpleReactiveDiscoveryProperties simpleReactiveDiscoveryProperties() { + simple.getLocal().setServiceId(serviceId); + simple.getLocal().setUri(URI.create("http://" + + inet.findFirstNonLoopbackHostInfo().getHostname() + ":" + findPort())); + return simple; + } + + @Bean + @Order + public SimpleReactiveDiscoveryClient simpleReactiveDiscoveryClient() { + return new SimpleReactiveDiscoveryClient(simpleReactiveDiscoveryProperties()); + } + + @Bean + @ConditionalOnProperty( + value = "spring.cloud.discovery.client.health-indicator.enabled", + matchIfMissing = true) + public ReactiveDiscoveryClientHealthIndicator simpleReactiveDiscoveryClientHealthIndicator( + DiscoveryClientHealthIndicatorProperties properties) { + return new ReactiveDiscoveryClientHealthIndicator(simpleReactiveDiscoveryClient(), + properties); + } + + private int findPort() { + if (port > 0) { + return port; + } + if (server != null && server.getPort() != null && server.getPort() > 0) { + return server.getPort(); + } + return 8080; + } + + @Override + public void onApplicationEvent(WebServerInitializedEvent webServerInitializedEvent) { + port = webServerInitializedEvent.getWebServer().getPort(); + if (port > 0) { + simple.getLocal().setUri(URI.create("http://" + + inet.findFirstNonLoopbackHostInfo().getHostname() + ":" + port)); + } + } + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryProperties.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryProperties.java new file mode 100644 index 00000000..11f28b4e --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryProperties.java @@ -0,0 +1,188 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.client.discovery.simple.reactive; + +import java.net.URI; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.annotation.PostConstruct; + +import reactor.core.publisher.Flux; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; + +import static java.util.Collections.emptyList; + +/** + * Properties to hold the details of a {@link ReactiveDiscoveryClient} service instance + * for a given service. It also holds the user-configurable order that will be used to + * establish the precedence of this client in the list of clients used by + * {@link org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClient}. + * + * @author Tim Ysewyn + * @since 2.2.0 + */ +@ConfigurationProperties(prefix = "spring.cloud.discovery.client.simple") +public class SimpleReactiveDiscoveryProperties { + + private Map> instances = new HashMap<>(); + + /** + * The properties of the local instance (if it exists). Users should set these + * properties explicitly if they are exporting data (e.g. metrics) that need to be + * identified by the service instance. + */ + private SimpleServiceInstance local = new SimpleServiceInstance(); + + private int order = DiscoveryClient.DEFAULT_ORDER; + + public Flux getInstances(String service) { + return Flux.fromIterable(instances.getOrDefault(service, emptyList())); + } + + Map> getInstances() { + return instances; + } + + public void setInstances(Map> instances) { + this.instances = instances; + } + + public SimpleServiceInstance getLocal() { + return this.local; + } + + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + + @PostConstruct + public void init() { + for (String key : this.instances.keySet()) { + for (SimpleServiceInstance instance : this.instances.get(key)) { + instance.setServiceId(key); + } + } + } + + /** + * Basic implementation of {@link ServiceInstance}. + */ + public static class SimpleServiceInstance implements ServiceInstance { + + /** + * The URI of the service instance. Will be parsed to extract the scheme, host, + * and port. + */ + private URI uri; + + private String host; + + private int port; + + private boolean secure; + + /** + * Metadata for the service instance. Can be used by discovery clients to modify + * their behaviour per instance, e.g. when load balancing. + */ + private Map metadata = new LinkedHashMap<>(); + + /** + * The unique identifier or name for the service instance. + */ + private String instanceId; + + /** + * The identifier or name for the service. Multiple instances might share the same + * service ID. + */ + private String serviceId; + + public SimpleServiceInstance() { + } + + public SimpleServiceInstance(URI uri) { + setUri(uri); + } + + @Override + public String getInstanceId() { + return this.instanceId; + } + + public void setInstanceId(String id) { + this.instanceId = id; + } + + @Override + public String getServiceId() { + return this.serviceId; + } + + public void setServiceId(String id) { + this.serviceId = id; + } + + @Override + public String getHost() { + return this.host; + } + + @Override + public int getPort() { + return this.port; + } + + @Override + public boolean isSecure() { + return this.secure; + } + + @Override + public URI getUri() { + return this.uri; + } + + public void setUri(URI uri) { + this.uri = uri; + this.host = this.uri.getHost(); + this.port = this.uri.getPort(); + String scheme = this.uri.getScheme(); + if ("https".equals(scheme)) { + this.secure = true; + } + } + + @Override + public Map getMetadata() { + return this.metadata; + } + + } + +} diff --git a/spring-cloud-commons/src/main/resources/META-INF/spring.factories b/spring-cloud-commons/src/main/resources/META-INF/spring.factories index 16d4bb38..bd9662cb 100644 --- a/spring-cloud-commons/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-commons/src/main/resources/META-INF/spring.factories @@ -1,9 +1,12 @@ # AutoConfiguration org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.client.CommonsClientAutoConfiguration,\ +org.springframework.cloud.client.ReactiveCommonsClientAutoConfiguration,\ org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClientAutoConfiguration,\ +org.springframework.cloud.client.discovery.composite.reactive.ReactiveCompositeDiscoveryClientAutoConfiguration,\ org.springframework.cloud.client.discovery.noop.NoopDiscoveryClientAutoConfiguration,\ org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration,\ +org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryClientAutoConfiguration,\ org.springframework.cloud.client.hypermedia.CloudHypermediaAutoConfiguration,\ org.springframework.cloud.client.loadbalancer.AsyncLoadBalancerAutoConfiguration,\ org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration,\ diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/CommonsClientAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/CommonsClientAutoConfigurationTests.java index ef0c5bd9..0da31750 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/CommonsClientAutoConfigurationTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/CommonsClientAutoConfigurationTests.java @@ -19,18 +19,19 @@ package org.springframework.cloud.client; import org.junit.Test; import org.springframework.beans.BeansException; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.client.actuator.FeaturesEndpoint; import org.springframework.cloud.client.actuator.HasFeatures; import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicator; import org.springframework.cloud.client.discovery.health.DiscoveryCompositeHealthIndicator; -import org.springframework.cloud.client.discovery.noop.NoopDiscoveryClientAutoConfiguration; +import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration; +import org.springframework.cloud.commons.util.UtilAutoConfiguration; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; @@ -39,66 +40,95 @@ import static org.assertj.core.api.BDDAssertions.then; /** * @author Spencer Gibb * @author Olga Maciaszek-Sharma + * @author Tim Ysewyn */ public class CommonsClientAutoConfigurationTests { + ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(HealthEndpointAutoConfiguration.class, + CommonsClientAutoConfiguration.class, + SimpleDiscoveryClientAutoConfiguration.class, + UtilAutoConfiguration.class)); + @Test public void beansCreatedNormally() { - try (ConfigurableApplicationContext ctxt = init()) { + applicationContextRunner.run(ctxt -> { then(ctxt.getBean(DiscoveryClientHealthIndicator.class)).isNotNull(); then(ctxt.getBean(DiscoveryCompositeHealthIndicator.class)).isNotNull(); then(ctxt.getBean(FeaturesEndpoint.class)).isNotNull(); then(ctxt.getBeansOfType(HasFeatures.class).values()).isNotEmpty(); - } + }); } @Test public void disableAll() { - try (ConfigurableApplicationContext ctxt = init( - "spring.cloud.discovery.enabled=false")) { - assertBeanNonExistant(ctxt, DiscoveryClientHealthIndicator.class); - assertBeanNonExistant(ctxt, DiscoveryCompositeHealthIndicator.class); - then(ctxt.getBean(FeaturesEndpoint.class)).isNotNull(); // features - // actuator - // is - // independent - // of - // discovery - assertBeanNonExistant(ctxt, HasFeatures.class); - } + applicationContextRunner + .withPropertyValues("spring.cloud.discovery.enabled=false").run(ctxt -> { + assertBeanNonExistant(ctxt, DiscoveryClientHealthIndicator.class); + assertBeanNonExistant(ctxt, DiscoveryCompositeHealthIndicator.class); + then(ctxt.getBean(FeaturesEndpoint.class)).isNotNull(); + // features actuator is independent of discovery + assertBeanNonExistant(ctxt, HasFeatures.class); + }); + } + + @Test + public void disableBlocking() { + applicationContextRunner + .withPropertyValues("spring.cloud.discovery.blocking.enabled=false") + .run(ctxt -> { + assertBeanNonExistant(ctxt, DiscoveryClientHealthIndicator.class); + assertBeanNonExistant(ctxt, DiscoveryCompositeHealthIndicator.class); + then(ctxt.getBean(FeaturesEndpoint.class)).isNotNull(); + // features actuator is independent of discovery + assertBeanNonExistant(ctxt, HasFeatures.class); + }); } @Test public void disableAllIndividually() { - try (ConfigurableApplicationContext ctxt = init( + applicationContextRunner.withPropertyValues( "spring.cloud.discovery.client.health-indicator.enabled=false", "spring.cloud.discovery.client.composite-indicator.enabled=false", - "spring.cloud.features.enabled=false")) { - assertBeanNonExistant(ctxt, DiscoveryClientHealthIndicator.class); - assertBeanNonExistant(ctxt, DiscoveryCompositeHealthIndicator.class); - assertBeanNonExistant(ctxt, FeaturesEndpoint.class); - } + "spring.cloud.features.enabled=false").run(ctxt -> { + assertBeanNonExistant(ctxt, DiscoveryClientHealthIndicator.class); + assertBeanNonExistant(ctxt, DiscoveryCompositeHealthIndicator.class); + assertBeanNonExistant(ctxt, FeaturesEndpoint.class); + }); } @Test public void disableHealthIndicator() { - try (ConfigurableApplicationContext ctxt = init( - "spring.cloud.discovery.client.health-indicator.enabled=false")) { - assertBeanNonExistant(ctxt, DiscoveryClientHealthIndicator.class); - assertBeanNonExistant(ctxt, DiscoveryCompositeHealthIndicator.class); - } + applicationContextRunner + .withPropertyValues( + "spring.cloud.discovery.client.health-indicator.enabled=false") + .run(ctxt -> { + assertBeanNonExistant(ctxt, DiscoveryClientHealthIndicator.class); + assertBeanNonExistant(ctxt, DiscoveryCompositeHealthIndicator.class); + }); } @Test public void conditionalOnDiscoveryEnabledWorks() { - try (ConfigurableApplicationContext context = init( - "spring.cloud.discovery.enabled=false")) { - assertBeanNonExistant(context, TestBean.class); - } - try (ConfigurableApplicationContext context = init( - "spring.cloud.discovery.enabled=true")) { - assertThat(context.getBean(TestBean.class)).isNotNull(); - } + applicationContextRunner.withUserConfiguration(DiscoveryEnabledConfig.class) + .withPropertyValues("spring.cloud.discovery.enabled=false") + .run(context -> assertBeanNonExistant(context, TestBean.class)); + applicationContextRunner.withUserConfiguration(DiscoveryEnabledConfig.class) + .withPropertyValues("spring.cloud.discovery.enabled=true") + .run(context -> assertThat(context.getBean(TestBean.class)).isNotNull()); + } + + @Test + public void conditionalOnBlockingDiscoveryEnabledWorks() { + applicationContextRunner + .withUserConfiguration(BlockingDiscoveryEnabledConfig.class) + .withPropertyValues("spring.cloud.discovery.blocking.enabled=false") + .run(context -> assertBeanNonExistant(context, TestBean.class)); + applicationContextRunner + .withUserConfiguration(BlockingDiscoveryEnabledConfig.class) + .withPropertyValues("spring.cloud.discovery.blocking.enabled=true") + .run(context -> assertThat(context.getBean(TestBean.class)).isNotNull()); } private void assertBeanNonExistant(ConfigurableApplicationContext ctxt, @@ -112,18 +142,6 @@ public class CommonsClientAutoConfigurationTests { } } - protected ConfigurableApplicationContext init(String... pairs) { - return new SpringApplicationBuilder().web(WebApplicationType.NONE) - .sources(Config.class).properties(pairs).run(); - } - - @Configuration - @EnableAutoConfiguration - @Import({ NoopDiscoveryClientAutoConfiguration.class, DiscoveryEnabledConfig.class }) - protected static class Config { - - } - @Configuration @ConditionalOnDiscoveryEnabled protected static class DiscoveryEnabledConfig { @@ -135,6 +153,17 @@ public class CommonsClientAutoConfigurationTests { } + @TestConfiguration + @ConditionalOnBlockingDiscoveryEnabled + protected static class BlockingDiscoveryEnabledConfig { + + @Bean + TestBean testBean() { + return new TestBean(); + } + + } + private static class TestBean { } diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/ReactiveCommonsClientAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/ReactiveCommonsClientAutoConfigurationTests.java new file mode 100644 index 00000000..3ededeff --- /dev/null +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/ReactiveCommonsClientAutoConfigurationTests.java @@ -0,0 +1,166 @@ +/* + * Copyright 2019-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.client; + +import org.junit.Test; + +import org.springframework.beans.BeansException; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.client.actuator.FeaturesEndpoint; +import org.springframework.cloud.client.actuator.HasFeatures; +import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryClientHealthIndicator; +import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryCompositeHealthContributor; +import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryClientAutoConfiguration; +import org.springframework.cloud.commons.util.UtilAutoConfiguration; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.BDDAssertions.then; + +/** + * @author Tim Ysewyn + */ +public class ReactiveCommonsClientAutoConfigurationTests { + + ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(CommonsClientAutoConfiguration.class, + SimpleReactiveDiscoveryClientAutoConfiguration.class, + UtilAutoConfiguration.class, + ReactiveCommonsClientAutoConfiguration.class)); + + @Test + public void beansCreatedNormally() { + applicationContextRunner.run(context -> { + then(context.getBean(ReactiveDiscoveryClientHealthIndicator.class)) + .isNotNull(); + then(context.getBean(ReactiveDiscoveryCompositeHealthContributor.class)) + .isNotNull(); + then(context.getBean(FeaturesEndpoint.class)).isNotNull(); + then(context.getBeansOfType(HasFeatures.class).values()).isNotEmpty(); + }); + } + + @Test + public void disableAll() { + applicationContextRunner + .withPropertyValues("spring.cloud.discovery.enabled=false") + .run(context -> { + assertBeanNonExistant(context, + ReactiveDiscoveryClientHealthIndicator.class); + assertBeanNonExistant(context, + ReactiveDiscoveryCompositeHealthContributor.class); + // features actuator is independent of discovery + then(context.getBean(FeaturesEndpoint.class)).isNotNull(); + assertBeanNonExistant(context, HasFeatures.class); + }); + } + + @Test + public void disableReactive() { + applicationContextRunner + .withPropertyValues("spring.cloud.discovery.reactive.enabled=false") + .run(context -> { + assertBeanNonExistant(context, + ReactiveDiscoveryClientHealthIndicator.class); + assertBeanNonExistant(context, + ReactiveDiscoveryCompositeHealthContributor.class); + // features actuator is independent of discovery + then(context.getBean(FeaturesEndpoint.class)).isNotNull(); + assertBeanNonExistant(context, HasFeatures.class); + }); + } + + @Test + public void disableAllIndividually() { + applicationContextRunner.withPropertyValues( + "spring.cloud.discovery.client.health-indicator.enabled=false", + "spring.cloud.discovery.client.composite-indicator.enabled=false", + "spring.cloud.features.enabled=false").run(context -> { + assertBeanNonExistant(context, + ReactiveDiscoveryClientHealthIndicator.class); + assertBeanNonExistant(context, + ReactiveDiscoveryCompositeHealthContributor.class); + assertBeanNonExistant(context, FeaturesEndpoint.class); + }); + } + + @Test + public void disableHealthIndicator() { + applicationContextRunner + .withPropertyValues( + "spring.cloud.discovery.client.health-indicator.enabled=false") + .run(context -> { + assertBeanNonExistant(context, + ReactiveDiscoveryClientHealthIndicator.class); + assertBeanNonExistant(context, + ReactiveDiscoveryCompositeHealthContributor.class); + }); + } + + @Test + public void conditionalOnReactiveDiscoveryEnabledWorks() { + ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(ReactiveDiscoveryEnabledConfig.class); + contextRunner.withPropertyValues("spring.cloud.discovery.reactive.enabled=false") + .run(context -> assertBeanNonExistant(context, TestBean.class)); + contextRunner.withPropertyValues("spring.cloud.discovery.reactive.enabled=true") + .run(context -> assertThat(context.getBean(TestBean.class)).isNotNull()); + } + + private void assertBeanNonExistant(ConfigurableApplicationContext ctxt, + Class beanClass) { + try { + ctxt.getBean(beanClass); + fail("Bean of type " + beanClass + " should not have been created"); + } + catch (BeansException e) { + // should fail with exception + } + } + + @Configuration + @ConditionalOnDiscoveryEnabled + protected static class DiscoveryEnabledConfig { + + @Bean + TestBean testBean() { + return new TestBean(); + } + + } + + @TestConfiguration + @ConditionalOnReactiveDiscoveryEnabled + protected static class ReactiveDiscoveryEnabledConfig { + + @Bean + TestBean testBean() { + return new TestBean(); + } + + } + + private static class TestBean { + + } + +} diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientAutoConfigurationTests.java new file mode 100644 index 00000000..a78ec7bf --- /dev/null +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientAutoConfigurationTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.client.discovery.composite.reactive; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.context.annotation.Bean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Tim Ysewyn + */ +class ReactiveCompositeDiscoveryClientAutoConfigurationTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations + .of(ReactiveCompositeDiscoveryClientAutoConfiguration.class)); + + @Test + public void shouldCreateCompositeReactiveDiscoveryClientWithoutDelegates() { + this.contextRunner.run((context) -> { + ReactiveDiscoveryClient client = context + .getBean(ReactiveDiscoveryClient.class); + assertThat(client).isNotNull(); + assertThat(client).isInstanceOf(ReactiveCompositeDiscoveryClient.class); + assertThat(((ReactiveCompositeDiscoveryClient) client).getDiscoveryClients()) + .isEmpty(); + }); + } + + @Test + public void shouldCreateCompositeReactiveDiscoveryClientWithDelegate() { + this.contextRunner.withUserConfiguration(Configuration.class).run((context) -> { + ReactiveDiscoveryClient client = context + .getBean(ReactiveDiscoveryClient.class); + assertThat(client).isNotNull(); + assertThat(client).isInstanceOf(ReactiveCompositeDiscoveryClient.class); + assertThat(((ReactiveCompositeDiscoveryClient) client).getDiscoveryClients()) + .hasSize(1); + }); + } + + @TestConfiguration + static class Configuration { + + @Bean + ReactiveDiscoveryClient discoveryClient() { + return new ReactiveDiscoveryClient() { + @Override + public String description() { + return "Reactive Test Discovery Client"; + } + + @Override + public Flux getInstances(String serviceId) { + return Flux.empty(); + } + + @Override + public Flux getServices() { + return Flux.empty(); + } + }; + } + + } + +} diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientTests.java new file mode 100644 index 00000000..b64d805a --- /dev/null +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.client.discovery.composite.reactive; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; +import reactor.test.publisher.TestPublisher; + +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +/** + * @author Tim Ysewyn + */ +@ExtendWith(MockitoExtension.class) +class ReactiveCompositeDiscoveryClientTests { + + @Mock + private ReactiveDiscoveryClient discoveryClient1; + + @Mock + private ReactiveDiscoveryClient discoveryClient2; + + @Test + public void shouldReturnEmptyFluxOfServices() { + ReactiveCompositeDiscoveryClient client = new ReactiveCompositeDiscoveryClient( + emptyList()); + + Flux services = client.getServices(); + + StepVerifier.create(services).expectComplete().verify(); + } + + @Test + public void shouldReturnFluxOfServices() { + TestPublisher discoveryClient1Publisher = TestPublisher.createCold(); + discoveryClient1Publisher.emit("serviceAFromClient1"); + discoveryClient1Publisher.emit("serviceBFromClient1"); + discoveryClient1Publisher.complete(); + + TestPublisher discoveryClient2Publisher = TestPublisher.createCold(); + discoveryClient2Publisher.emit("serviceCFromClient2"); + discoveryClient2Publisher.complete(); + + when(discoveryClient1.getServices()).thenReturn(discoveryClient1Publisher.flux()); + when(discoveryClient2.getServices()).thenReturn(discoveryClient2Publisher.flux()); + + ReactiveCompositeDiscoveryClient client = new ReactiveCompositeDiscoveryClient( + asList(discoveryClient1, discoveryClient2)); + + assertThat(client.description()).isEqualTo("Composite Reactive Discovery Client"); + + Flux services = client.getServices(); + + StepVerifier.create(services).expectNext("serviceAFromClient1") + .expectNext("serviceBFromClient1").expectNext("serviceCFromClient2") + .expectComplete().verify(); + } + + @Test + public void shouldReturnEmptyFluxOfServiceInstances() { + ReactiveCompositeDiscoveryClient client = new ReactiveCompositeDiscoveryClient( + emptyList()); + + Flux instances = client.getInstances("service"); + + StepVerifier.create(instances).expectComplete().verify(); + } + + @Test + public void shouldReturnFluxOfServiceInstances() { + DefaultServiceInstance serviceInstance1 = new DefaultServiceInstance("instance", + "service", "localhost", 8080, false); + DefaultServiceInstance serviceInstance2 = new DefaultServiceInstance("instance2", + "service", "localhost", 8080, false); + TestPublisher discoveryClient1Publisher = TestPublisher + .createCold(); + discoveryClient1Publisher.emit(serviceInstance1); + discoveryClient1Publisher.emit(serviceInstance2); + discoveryClient1Publisher.complete(); + + TestPublisher discoveryClient2Publisher = TestPublisher + .createCold(); + discoveryClient2Publisher.complete(); + + when(discoveryClient1.getInstances("service")) + .thenReturn(discoveryClient1Publisher.flux()); + when(discoveryClient2.getInstances("service")) + .thenReturn(discoveryClient2Publisher.flux()); + + ReactiveCompositeDiscoveryClient client = new ReactiveCompositeDiscoveryClient( + asList(discoveryClient1, discoveryClient2)); + + Flux instances = client.getInstances("service"); + + StepVerifier.create(instances).expectNext(serviceInstance1) + .expectNext(serviceInstance2).expectComplete().verify(); + } + +} diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryClientHealthIndicatorTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryClientHealthIndicatorTests.java new file mode 100644 index 00000000..bf7091e5 --- /dev/null +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryClientHealthIndicatorTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.client.discovery.health.reactive; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent; +import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicatorProperties; +import org.springframework.core.Ordered; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +/** + * @author Tim Ysewyn + */ +@ExtendWith(MockitoExtension.class) +class ReactiveDiscoveryClientHealthIndicatorTests { + + @Mock + private ReactiveDiscoveryClient discoveryClient; + + @Mock + private DiscoveryClientHealthIndicatorProperties properties; + + @InjectMocks + private ReactiveDiscoveryClientHealthIndicator indicator; + + @Test + public void shouldReturnCorrectOrder() { + assertThat(indicator.getOrder()).isEqualTo(Ordered.HIGHEST_PRECEDENCE); + indicator.setOrder(0); + assertThat(indicator.getOrder()).isEqualTo(0); + } + + @Test + public void shouldReturnUnknownStatusWhenNotInitialized() { + Health expectedHealth = Health.status( + new Status(Status.UNKNOWN.getCode(), "Discovery Client not initialized")) + .build(); + Mono health = indicator.health(); + StepVerifier.create(health).expectNext(expectedHealth).expectComplete().verify(); + } + + @Test + public void shouldReturnUpStatusWithoutServices() { + when(discoveryClient.description()).thenReturn("Mocked Service Discovery Client"); + when(discoveryClient.getServices()).thenReturn(Flux.empty()); + Health expectedHealth = Health.status(new Status(Status.UP.getCode(), "")) + .withDetail("services", emptyList()).build(); + + indicator.onApplicationEvent(new InstanceRegisteredEvent<>(this, null)); + Mono health = indicator.health(); + + assertThat(indicator.getName()).isEqualTo("Mocked Service Discovery Client"); + StepVerifier.create(health).expectNext(expectedHealth).expectComplete().verify(); + } + + @Test + public void shouldReturnUpStatusWithServices() { + when(discoveryClient.getServices()).thenReturn(Flux.just("service")); + when(properties.isIncludeDescription()).thenReturn(true); + when(discoveryClient.description()).thenReturn("Mocked Service Discovery Client"); + Health expectedHealth = Health + .status(new Status(Status.UP.getCode(), + "Mocked Service Discovery Client")) + .withDetail("services", singletonList("service")).build(); + + indicator.onApplicationEvent(new InstanceRegisteredEvent<>(this, null)); + Mono health = indicator.health(); + + assertThat(indicator.getName()).isEqualTo("Mocked Service Discovery Client"); + StepVerifier.create(health).expectNext(expectedHealth).expectComplete().verify(); + } + + @Test + public void shouldReturnDownStatusWhenServicesCouldNotBeRetrieved() { + RuntimeException ex = new RuntimeException("something went wrong"); + Health expectedHealth = Health.down(ex).build(); + when(discoveryClient.getServices()).thenReturn(Flux.error(ex)); + + indicator.onApplicationEvent(new InstanceRegisteredEvent<>(this, null)); + Mono health = indicator.health(); + + StepVerifier.create(health).expectNext(expectedHealth).expectComplete() + .verifyThenAssertThat(); + } + +} diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryCompositeHealthContributorTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryCompositeHealthContributorTests.java new file mode 100644 index 00000000..6e321106 --- /dev/null +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryCompositeHealthContributorTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.client.discovery.health.reactive; + +import java.util.Iterator; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.NamedContributor; +import org.springframework.boot.actuate.health.ReactiveHealthContributor; +import org.springframework.boot.actuate.health.ReactiveHealthIndicator; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Tim Ysewyn + */ +class ReactiveDiscoveryCompositeHealthContributorTests { + + @Test + void shouldReturnEmptyIterator() { + ReactiveDiscoveryCompositeHealthContributor healthContributor = new ReactiveDiscoveryCompositeHealthContributor( + emptyList()); + assertThat(healthContributor.iterator().hasNext()).isFalse(); + } + + @Test + void shouldReturnNullForUnknownContributor() { + ReactiveDiscoveryCompositeHealthContributor healthContributor = new ReactiveDiscoveryCompositeHealthContributor( + emptyList()); + assertThat(healthContributor.getContributor("unknown")).isNull(); + } + + @Test + void shouldReturnKnownContributor() { + ReactiveDiscoveryHealthIndicator indicator = mock( + ReactiveDiscoveryHealthIndicator.class); + Health health = Health.up().build(); + when(indicator.getName()).thenReturn("known"); + when(indicator.health()).thenReturn(Mono.just(health)); + + ReactiveDiscoveryCompositeHealthContributor healthContributor = new ReactiveDiscoveryCompositeHealthContributor( + singletonList(indicator)); + + assertThat(healthContributor.getContributor("known")).isNotNull(); + Iterator> iterator = healthContributor + .iterator(); + assertThat(iterator.hasNext()).isTrue(); + NamedContributor contributor = iterator.next(); + assertThat(contributor).isNotNull(); + assertThat(contributor.getName()).isEqualTo("known"); + assertThat(contributor.getContributor()).isNotNull(); + assertThat(contributor.getContributor()) + .isInstanceOf(ReactiveHealthIndicator.class); + ReactiveHealthIndicator healthIndicator = (ReactiveHealthIndicator) contributor + .getContributor(); + StepVerifier.create(healthIndicator.getHealth(true)).expectNext(health) + .expectComplete().verify(); + } + +} diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientAutoConfigurationTests.java new file mode 100644 index 00000000..47b56b5d --- /dev/null +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientAutoConfigurationTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.client.discovery.simple.reactive; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.cloud.commons.util.InetUtils; +import org.springframework.cloud.commons.util.UtilAutoConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Tim Ysewyn + */ +class SimpleReactiveDiscoveryClientAutoConfigurationTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of( + SimpleReactiveDiscoveryClientAutoConfiguration.class, + UtilAutoConfiguration.class)); + + @Test + public void shouldUseDefaults() { + this.contextRunner.run((context) -> { + ReactiveDiscoveryClient client = context + .getBean(ReactiveDiscoveryClient.class); + assertThat(client).isNotNull(); + assertThat(client.getOrder()) + .isEqualTo(ReactiveDiscoveryClient.DEFAULT_ORDER); + InetUtils inet = context.getBean(InetUtils.class); + assertThat(inet).isNotNull(); + SimpleReactiveDiscoveryProperties properties = context + .getBean(SimpleReactiveDiscoveryProperties.class); + assertThat(properties).isNotNull(); + assertThat(properties.getLocal().getServiceId()).isEqualTo("application"); + assertThat(properties.getLocal().getHost()) + .isEqualTo(inet.findFirstNonLoopbackHostInfo().getHostname()); + assertThat(properties.getLocal().getPort()).isEqualTo(8080); + }); + } + + @Test + public void shouldUseCustomConfiguration() { + this.contextRunner.withUserConfiguration(Configuration.class) + .withPropertyValues("spring.application.name=my-service", + "spring.cloud.discovery.client.simple.order=1", + "server.port=8443") + .run((context) -> { + ReactiveDiscoveryClient client = context + .getBean(ReactiveDiscoveryClient.class); + assertThat(client).isNotNull(); + assertThat(client.getOrder()).isEqualTo(1); + InetUtils inet = context.getBean(InetUtils.class); + assertThat(inet).isNotNull(); + SimpleReactiveDiscoveryProperties properties = context + .getBean(SimpleReactiveDiscoveryProperties.class); + assertThat(properties).isNotNull(); + assertThat(properties.getLocal().getServiceId()) + .isEqualTo("my-service"); + assertThat(properties.getLocal().getHost()) + .isEqualTo(inet.findFirstNonLoopbackHostInfo().getHostname()); + assertThat(properties.getLocal().getPort()).isEqualTo(8443); + }); + } + + @TestConfiguration + @EnableConfigurationProperties(ServerProperties.class) + static class Configuration { + + } + +} diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientTests.java new file mode 100644 index 00000000..1c9c5cb5 --- /dev/null +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.client.discovery.simple.reactive; + +import java.net.URI; +import java.util.Arrays; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryProperties.SimpleServiceInstance; + +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Tim Ysewyn + */ +public class SimpleReactiveDiscoveryClientTests { + + private final SimpleServiceInstance service1Inst1 = new SimpleServiceInstance( + URI.create("http://host1:8080")); + + private final SimpleServiceInstance service1Inst2 = new SimpleServiceInstance( + URI.create("https://host2:8443")); + + private SimpleReactiveDiscoveryClient client; + + @BeforeEach + public void setUp() { + SimpleReactiveDiscoveryProperties simpleReactiveDiscoveryProperties = new SimpleReactiveDiscoveryProperties(); + simpleReactiveDiscoveryProperties.setInstances( + singletonMap("service", Arrays.asList(service1Inst1, service1Inst2))); + simpleReactiveDiscoveryProperties.init(); + this.client = new SimpleReactiveDiscoveryClient( + simpleReactiveDiscoveryProperties); + } + + @Test + public void verifyDefaults() { + assertThat(client.description()).isEqualTo("Simple Reactive Discovery Client"); + assertThat(client.getOrder()).isEqualTo(ReactiveDiscoveryClient.DEFAULT_ORDER); + } + + @Test + public void shouldReturnFluxOfServices() { + Flux services = this.client.getServices(); + StepVerifier.create(services).expectNext("service").expectComplete().verify(); + } + + @Test + public void shouldReturnEmptyFluxForNonExistingService() { + Flux instances = this.client.getInstances("undefined"); + StepVerifier.create(instances).expectComplete(); + } + + @Test + public void shouldReturnFluxOfServiceInstances() { + Flux services = this.client.getInstances("service"); + StepVerifier.create(services).expectNext(service1Inst1).expectNext(service1Inst2) + .expectComplete().verify(); + } + +}