From c6fde1e4d5c44fe65514c952a9f48210eb57688e Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 28 Jul 2020 15:53:32 +0200 Subject: [PATCH] Polish "Update Neo4j health check to use the Neo4j Driver" See gh-22302 --- .../build.gradle | 2 +- .../Neo4jDriverMetricsAutoConfiguration.java | 78 --------- ...o4jHealthContributorAutoConfiguration.java | 63 ++----- .../Neo4jHealthContributorConfigurations.java | 67 +++++++ .../SpringApplicationHierarchyTests.java | 5 +- ...o4jDriverMetricsAutoConfigurationTest.java | 141 --------------- .../autoconfigure/neo4j/Neo4jDriverMocks.java | 62 ------- ...althContributorAutoConfigurationTests.java | 126 ++++--------- .../spring-boot-actuator/build.gradle | 2 +- .../actuate/neo4j/Neo4jDriverMetrics.java | 124 ------------- .../neo4j/Neo4jHealthDetailsHandler.java | 49 ++++++ .../actuate/neo4j/Neo4jHealthIndicator.java | 52 ++---- .../neo4j/Neo4jReactiveHealthIndicator.java | 19 +- .../neo4j/ResultSummaryWithEdition.java | 37 ---- .../neo4j/Neo4jDriverMetricsTests.java | 66 ------- .../boot/actuate/neo4j/Neo4jDriverMocks.java | 62 ------- .../neo4j/Neo4jHealthIndicatorTestBase.java | 64 ------- .../neo4j/Neo4jHealthIndicatorTests.java | 165 +++++++----------- .../Neo4jReactiveHealthIndicatorTest.java | 105 ++++++----- .../boot/actuate/neo4j/ResultSummaryMock.java | 48 +++++ .../asciidoc/production-ready-features.adoc | 3 + 21 files changed, 357 insertions(+), 983 deletions(-) delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jDriverMetricsAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorConfigurations.java delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jDriverMetricsAutoConfigurationTest.java delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jDriverMocks.java delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jDriverMetrics.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthDetailsHandler.java delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/ResultSummaryWithEdition.java delete mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jDriverMetricsTests.java delete mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jDriverMocks.java delete mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicatorTestBase.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/ResultSummaryMock.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index bf42411621..3898b33774 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -86,6 +86,7 @@ dependencies { optional("org.liquibase:liquibase-core") optional("org.mongodb:mongodb-driver-reactivestreams") optional("org.mongodb:mongodb-driver-sync") + optional("org.neo4j.driver:neo4j-java-driver") optional("org.springframework:spring-jdbc") optional("org.springframework:spring-jms") optional("org.springframework:spring-messaging") @@ -96,7 +97,6 @@ dependencies { optional("org.springframework.data:spring-data-couchbase") optional("org.springframework.data:spring-data-ldap") optional("org.springframework.data:spring-data-mongodb") - optional("org.springframework.data:spring-data-neo4j") optional("org.springframework.data:spring-data-redis") optional("org.springframework.data:spring-data-elasticsearch") optional("org.springframework.data:spring-data-solr") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jDriverMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jDriverMetricsAutoConfiguration.java deleted file mode 100644 index 17f5f2c838..0000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jDriverMetricsAutoConfiguration.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2012-2020 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.boot.actuate.autoconfigure.neo4j; - -import io.micrometer.core.instrument.MeterRegistry; - -import java.util.Collections; -import java.util.Map; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.neo4j.driver.Driver; -import org.springframework.boot.actuate.neo4j.Neo4jDriverMetrics; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -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.neo4j.Neo4jDriverAutoConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for metrics on all available - * {@link Driver drivers}. - *

- * The reason we are doing this dance with the manual binding is the fact that this - * autoconfiguration should work with more than one instance of the driver. If a user has - * multiple instances configured, than each instance should be bound via the binder to - * registry. Without that requirement, we could just add a {@link Bean @Bean} of type - * {@link Neo4jDriverMetrics} to the context and be done. - * - * @author Michael J. Simons - * @since 2.4.0 - */ -@Configuration(proxyBeanMethods = false) -@AutoConfigureAfter({ MetricsAutoConfiguration.class, Neo4jDriverAutoConfiguration.class, - SimpleMetricsExportAutoConfiguration.class }) -@ConditionalOnClass({ Driver.class, MeterRegistry.class }) -@ConditionalOnBean({ Driver.class, MeterRegistry.class }) -public class Neo4jDriverMetricsAutoConfiguration { - - private static final Log logger = LogFactory.getLog(Neo4jDriverMetricsAutoConfiguration.class); - - @Autowired - public void bindDataSourcesToRegistry(Map drivers, MeterRegistry registry) { - - drivers.forEach((name, driver) -> { - if (!Neo4jDriverMetrics.metricsAreEnabled(driver)) { - return; - } - driver.verifyConnectivityAsync() - .thenRunAsync(() -> new Neo4jDriverMetrics(name, driver, Collections.emptyList()).bindTo(registry)) - .exceptionally(e -> { - logger.warn("Could not verify connection for " + driver + " and thus not bind to metrics: " - + e.getMessage()); - return null; - }); - }); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfiguration.java index 48a8ee9382..7730ddf629 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfiguration.java @@ -16,36 +16,24 @@ package org.springframework.boot.actuate.autoconfigure.neo4j; -import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthContributorConfiguration; -import org.springframework.boot.actuate.autoconfigure.health.CompositeReactiveHealthContributorConfiguration; +import org.neo4j.driver.Driver; + import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; -import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration; -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.HealthContributor; -import org.springframework.boot.actuate.health.ReactiveHealthContributor; +import org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jHealthContributorConfigurations.Neo4jConfiguration; +import org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jHealthContributorConfigurations.Neo4jReactiveConfiguration; import org.springframework.boot.actuate.neo4j.Neo4jHealthIndicator; import org.springframework.boot.actuate.neo4j.Neo4jReactiveHealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.AutoConfigureBefore; 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.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration; -import org.springframework.boot.autoconfigure.neo4j.Neo4jDriverAutoConfiguration; -import org.springframework.context.annotation.Bean; +import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration; import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; -import reactor.core.publisher.Flux; - -import java.util.Map; - -import org.neo4j.driver.Driver; +import org.springframework.context.annotation.Import; /** - * {@link EnableAutoConfiguration Auto-configuration} for {@link Neo4jHealthIndicator}. - * The auto-configuration here is responsible for both imperative and reactive health - * checks. The reactive health check has precedence over the imperative one. + * {@link EnableAutoConfiguration Auto-configuration} for + * {@link Neo4jReactiveHealthIndicator} and {@link Neo4jHealthIndicator}. * * @author Eric Spiegelberg * @author Stephane Nicoll @@ -53,40 +41,11 @@ import org.neo4j.driver.Driver; * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ Driver.class, Health.class }) +@ConditionalOnClass(Driver.class) @ConditionalOnBean(Driver.class) @ConditionalOnEnabledHealthIndicator("neo4j") -@AutoConfigureBefore(HealthContributorAutoConfiguration.class) -@AutoConfigureAfter({ Neo4jDriverAutoConfiguration.class, Neo4jDataAutoConfiguration.class }) +@AutoConfigureAfter(Neo4jAutoConfiguration.class) +@Import({ Neo4jReactiveConfiguration.class, Neo4jConfiguration.class }) public class Neo4jHealthContributorAutoConfiguration { - @Configuration(proxyBeanMethods = false) - @Order(-20) - static class Neo4jHealthIndicatorConfiguration - extends CompositeHealthContributorConfiguration { - - @Bean - // If Neo4jReactiveHealthIndicatorConfiguration kicked in, don't add the - // imperative version as well - @ConditionalOnMissingBean(name = "neo4jHealthContributor") - public HealthContributor neo4jHealthContributor(Map drivers) { - return createContributor(drivers); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass({ Flux.class }) - @Order(-30) - static class Neo4jReactiveHealthIndicatorConfiguration - extends CompositeReactiveHealthContributorConfiguration { - - @Bean - @ConditionalOnMissingBean(name = "neo4jHealthContributor") - public ReactiveHealthContributor neo4jHealthContributor(Map drivers) { - return createComposite(drivers); - } - - } - } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorConfigurations.java new file mode 100644 index 0000000000..e53e14d27c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorConfigurations.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2020 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.boot.actuate.autoconfigure.neo4j; + +import java.util.Map; + +import org.neo4j.driver.Driver; +import reactor.core.publisher.Flux; + +import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthContributorConfiguration; +import org.springframework.boot.actuate.autoconfigure.health.CompositeReactiveHealthContributorConfiguration; +import org.springframework.boot.actuate.health.HealthContributor; +import org.springframework.boot.actuate.health.ReactiveHealthContributor; +import org.springframework.boot.actuate.neo4j.Neo4jHealthIndicator; +import org.springframework.boot.actuate.neo4j.Neo4jReactiveHealthIndicator; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Health contributor options for Neo4j. + * + * @author Michael J. Simons + * @author Stephane Nicoll + */ +class Neo4jHealthContributorConfigurations { + + @Configuration(proxyBeanMethods = false) + static class Neo4jConfiguration extends CompositeHealthContributorConfiguration { + + @Bean + @ConditionalOnMissingBean(name = { "neo4jHealthIndicator", "neo4jHealthContributor" }) + HealthContributor neo4jHealthContributor(Map drivers) { + return createContributor(drivers); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(Flux.class) + static class Neo4jReactiveConfiguration + extends CompositeReactiveHealthContributorConfiguration { + + @Bean + @ConditionalOnMissingBean(name = { "neo4jHealthIndicator", "neo4jHealthContributor" }) + ReactiveHealthContributor neo4jHealthContributor(Map drivers) { + return createContributor(drivers); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/SpringApplicationHierarchyTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/SpringApplicationHierarchyTests.java index c7fdf0c932..5de0389b31 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/SpringApplicationHierarchyTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/SpringApplicationHierarchyTests.java @@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoCo import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; +import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.test.util.ApplicationContextTestUtils; import org.springframework.context.ConfigurableApplicationContext; @@ -69,7 +70,7 @@ class SpringApplicationHierarchyTests { @EnableAutoConfiguration(exclude = { ElasticsearchDataAutoConfiguration.class, ElasticsearchRepositoriesAutoConfiguration.class, CassandraAutoConfiguration.class, CassandraDataAutoConfiguration.class, MongoDataAutoConfiguration.class, - MongoReactiveDataAutoConfiguration.class, Neo4jDataAutoConfiguration.class, + MongoReactiveDataAutoConfiguration.class, Neo4jAutoConfiguration.class, Neo4jDataAutoConfiguration.class, Neo4jRepositoriesAutoConfiguration.class, RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class, FlywayAutoConfiguration.class, MetricsAutoConfiguration.class }) static class Parent { @@ -80,7 +81,7 @@ class SpringApplicationHierarchyTests { @EnableAutoConfiguration(exclude = { ElasticsearchDataAutoConfiguration.class, ElasticsearchRepositoriesAutoConfiguration.class, CassandraAutoConfiguration.class, CassandraDataAutoConfiguration.class, MongoDataAutoConfiguration.class, - MongoReactiveDataAutoConfiguration.class, Neo4jDataAutoConfiguration.class, + MongoReactiveDataAutoConfiguration.class, Neo4jAutoConfiguration.class, Neo4jDataAutoConfiguration.class, Neo4jRepositoriesAutoConfiguration.class, RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class, FlywayAutoConfiguration.class, MetricsAutoConfiguration.class }) static class Child { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jDriverMetricsAutoConfigurationTest.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jDriverMetricsAutoConfigurationTest.java deleted file mode 100644 index 02eff7faeb..0000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jDriverMetricsAutoConfigurationTest.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2012-2020 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.boot.actuate.autoconfigure.neo4j; - -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; -import org.springframework.boot.actuate.neo4j.Neo4jDriverMetrics; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.assertj.AssertableApplicationContext; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.boot.test.context.runner.ContextConsumer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import org.neo4j.driver.Driver; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jDriverMocks.mockDriverWithMetrics; -import static org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jDriverMocks.mockDriverWithoutMetrics; - -/** - * @author Michael J. Simons - */ -class Neo4jDriverMetricsAutoConfigurationTest { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( - AutoConfigurations.of(MetricsAutoConfiguration.class, Neo4jDriverMetricsAutoConfiguration.class)); - - private final ContextConsumer assertNoInteractionsWithRegistry = ctx -> { - - if (ctx.getBeansOfType(MeterRegistry.class).isEmpty()) { - return; - } - - MeterRegistry mockedRegistry = ctx.getBean(MeterRegistry.class); - verify(mockedRegistry).config(); - verifyNoMoreInteractions(mockedRegistry); - }; - - @Nested - class NoMatches { - - @Test - void shouldRequireAllNeededClasses() { - contextRunner.withUserConfiguration(WithMeterRegistry.class) - .withClassLoader(new FilteredClassLoader(Driver.class)).run(assertNoInteractionsWithRegistry); - - contextRunner.withUserConfiguration(WithDriverWithMetrics.class) - .withClassLoader(new FilteredClassLoader(MeterRegistry.class)) - .run(assertNoInteractionsWithRegistry); - } - - @Test - void shouldRequireAllNeededBeans() { - contextRunner.withUserConfiguration(WithDriverWithMetrics.class).run(assertNoInteractionsWithRegistry); - - contextRunner.withUserConfiguration(WithMeterRegistry.class).run(assertNoInteractionsWithRegistry); - } - - @Test - void shouldRequireDriverWithMetrics() { - contextRunner.withUserConfiguration(WithDriverWithoutMetrics.class, WithMeterRegistry.class) - .run(assertNoInteractionsWithRegistry); - - } - - } - - @Nested - class Matches { - - @Test - void shouldRequireDriverWithMetrics() { - contextRunner.withUserConfiguration(WithDriverWithMetrics.class, WithMeterRegistry.class).run(ctx -> { - - // Wait a bit to let the completable future of the test that mocks - // connectiviy complete. - Thread.sleep(500L); - - MeterRegistry meterRegistry = ctx.getBean(MeterRegistry.class); - assertThat(meterRegistry.getMeters()).extracting(m -> m.getId().getName()) - .filteredOn(s -> s.startsWith(Neo4jDriverMetrics.PREFIX)).isNotEmpty(); - }); - } - - } - - @Configuration(proxyBeanMethods = false) - static class WithDriverWithMetrics { - - @Bean - Driver driver() { - return mockDriverWithMetrics(); - } - - } - - @Configuration(proxyBeanMethods = false) - static class WithDriverWithoutMetrics { - - @Bean - Driver driver() { - return mockDriverWithoutMetrics(); - } - - } - - @Configuration(proxyBeanMethods = false) - static class WithMeterRegistry { - - @Bean - MeterRegistry meterRegistry() { - - MeterRegistry meterRegistry = spy(SimpleMeterRegistry.class); - return meterRegistry; - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jDriverMocks.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jDriverMocks.java deleted file mode 100644 index e8331e2e0b..0000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jDriverMocks.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2012-2020 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.boot.actuate.autoconfigure.neo4j; - -import java.util.Collections; -import java.util.concurrent.CompletableFuture; - -import org.neo4j.driver.ConnectionPoolMetrics; -import org.neo4j.driver.Driver; -import org.neo4j.driver.Metrics; -import org.neo4j.driver.exceptions.ClientException; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * Some predefined mocks, only to be used internally for tests. - * - * @author Michael J. Simons - */ -final class Neo4jDriverMocks { - - public static Driver mockDriverWithMetrics() { - ConnectionPoolMetrics p1 = mock(ConnectionPoolMetrics.class); - when(p1.id()).thenReturn("p1"); - - Metrics metrics = mock(Metrics.class); - when(metrics.connectionPoolMetrics()).thenReturn(Collections.singletonList(p1)); - - Driver driver = mock(Driver.class); - when(driver.metrics()).thenReturn(metrics); - - when(driver.verifyConnectivityAsync()).thenReturn(CompletableFuture.completedFuture(null)); - - return driver; - } - - public static Driver mockDriverWithoutMetrics() { - - Driver driver = mock(Driver.class); - when(driver.metrics()).thenThrow(ClientException.class); - return driver; - } - - private Neo4jDriverMocks() { - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfigurationTests.java index b2f6976bc3..8e18f2e380 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfigurationTests.java @@ -1,14 +1,11 @@ /* - * Copyright (c) 2019-2020 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. + * Copyright 2012-2020 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 + * 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, @@ -18,14 +15,14 @@ */ package org.springframework.boot.actuate.autoconfigure.neo4j; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import reactor.core.publisher.Flux; + import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration; import org.springframework.boot.actuate.health.AbstractHealthIndicator; -import org.springframework.boot.actuate.health.CompositeReactiveHealthContributor; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.boot.actuate.health.PingHealthIndicator; import org.springframework.boot.actuate.neo4j.Neo4jHealthIndicator; import org.springframework.boot.actuate.neo4j.Neo4jReactiveHealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -33,14 +30,10 @@ import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import reactor.core.publisher.Flux; - -import org.neo4j.driver.Driver; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import static org.mockito.Mockito.mock; -import static org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jDriverMocks.mockDriverWithoutMetrics; /** * Tests for {@link Neo4jHealthContributorAutoConfiguration}. @@ -55,106 +48,61 @@ class Neo4jHealthContributorAutoConfigurationTests { .withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class, Neo4jHealthContributorAutoConfiguration.class)); - @Nested - class NoMatches { - - @Test - void shouldRespectManagementDecisions() { - contextRunner.withUserConfiguration(WithDriver.class) - .withPropertyValues("management.health.neo4j.enabled=false") - .run(ctx -> assertThat(ctx).doesNotHaveBean(Neo4jHealthIndicator.class) - .doesNotHaveBean(Neo4jReactiveHealthIndicator.class) - .hasSingleBean(PingHealthIndicator.class)); - } - - @Test - void shouldRequireHealthClass() { - contextRunner.withUserConfiguration(WithDriver.class).withClassLoader(new FilteredClassLoader(Health.class)) - .run(ctx -> assertThat(ctx).doesNotHaveBean(Neo4jHealthIndicator.class) - .doesNotHaveBean(Neo4jReactiveHealthIndicator.class)); - } - - @Test - void shouldRequireDriverClass() { - contextRunner.withUserConfiguration(WithDriver.class).withClassLoader(new FilteredClassLoader(Driver.class)) - .run(ctx -> assertThat(ctx).doesNotHaveBean(Neo4jHealthIndicator.class) - .doesNotHaveBean(Neo4jReactiveHealthIndicator.class)); - } - - @Test - void shouldRequireDriverBean() { - contextRunner.run(ctx -> assertThat(ctx).doesNotHaveBean(Neo4jHealthIndicator.class) - .doesNotHaveBean(Neo4jReactiveHealthIndicator.class)); - } - - @Test - void shouldRequireHealthIndicatorClasses() { - contextRunner.withUserConfiguration(WithDriver.class) - .withClassLoader(new FilteredClassLoader(Health.class, Flux.class)) - .run(ctx -> assertThat(ctx).doesNotHaveBean(Neo4jHealthIndicator.class) - .doesNotHaveBean(Neo4jReactiveHealthIndicator.class)); - - contextRunner.withUserConfiguration(WithDriver.class).withClassLoader(new FilteredClassLoader(Flux.class)) - .run(ctx -> assertThat(ctx).hasSingleBean(Neo4jHealthIndicator.class) - .doesNotHaveBean(Neo4jReactiveHealthIndicator.class)); - } - - @Test - void defaultIndicatorCanBeReplaced() { - contextRunner.withUserConfiguration(WithDriver.class, WithCustomIndicator.class).run((context) -> { - assertThat(context).hasBean("neo4jHealthIndicator"); - Health health = context.getBean("neo4jHealthIndicator", HealthIndicator.class).health(); - assertThat(health.getDetails()).containsOnly(entry("test", true)); - }); - } - + @Test + void runShouldCreateHealthIndicator() { + this.contextRunner.withUserConfiguration(Neo4jConfiguration.class).run((context) -> assertThat(context) + .hasSingleBean(Neo4jReactiveHealthIndicator.class).doesNotHaveBean(Neo4jHealthIndicator.class)); } - @Nested - class Matches { - - @Test - void shouldCreateHealthIndicator() { - contextRunner.withUserConfiguration(WithDriver.class).withClassLoader(new FilteredClassLoader(Flux.class)) - .run(ctx -> assertThat(ctx).hasSingleBean(Neo4jHealthIndicator.class) - .doesNotHaveBean(Neo4jReactiveHealthIndicator.class)); - } + @Test + void runWithoutReactorShouldCreateHealthIndicator() { + this.contextRunner.withUserConfiguration(Neo4jConfiguration.class) + .withClassLoader(new FilteredClassLoader(Flux.class)).run((context) -> assertThat(context) + .hasSingleBean(Neo4jHealthIndicator.class).doesNotHaveBean(Neo4jReactiveHealthIndicator.class)); + } + @Test + void runWhenDisabledShouldNotCreateIndicator() { + this.contextRunner.withUserConfiguration(Neo4jConfiguration.class) + .withPropertyValues("management.health.neo4j.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(Neo4jHealthIndicator.class) + .doesNotHaveBean(Neo4jReactiveHealthIndicator.class)); } @Test - void reactiveHealthCheckShouldHavePrecedence() { - contextRunner.withUserConfiguration(WithDriver.class).run(ctx -> { - assertThat(ctx).doesNotHaveBean(Neo4jHealthIndicator.class).hasBean("neo4jHealthContributor"); - - assertThat(ctx).getBean("neo4jHealthContributor").isInstanceOf(CompositeReactiveHealthContributor.class) - .satisfies(o -> { - CompositeReactiveHealthContributor chc = (CompositeReactiveHealthContributor) o; - assertThat(chc.getContributor("driver")).isInstanceOf(Neo4jReactiveHealthIndicator.class); - }); - } + void defaultIndicatorCanBeReplaced() { + this.contextRunner.withUserConfiguration(Neo4jConfiguration.class, CustomIndicatorConfiguration.class) + .run((context) -> { + assertThat(context).hasBean("neo4jHealthIndicator"); + Health health = context.getBean("neo4jHealthIndicator", HealthIndicator.class).health(); + assertThat(health.getDetails()).containsOnly(entry("test", true)); + }); + } - ); + @Test + void shouldRequireDriverBean() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(Neo4jHealthIndicator.class) + .doesNotHaveBean(Neo4jReactiveHealthIndicator.class)); } @Configuration(proxyBeanMethods = false) - static class WithDriver { + static class Neo4jConfiguration { @Bean Driver driver() { - return mockDriverWithoutMetrics(); + return mock(Driver.class); } } @Configuration(proxyBeanMethods = false) - static class WithCustomIndicator { + static class CustomIndicatorConfiguration { @Bean HealthIndicator neo4jHealthIndicator() { return new AbstractHealthIndicator() { - protected void doHealthCheck(Health.Builder builder) throws Exception { + protected void doHealthCheck(Health.Builder builder) { builder.up().withDetail("test", true); } }; diff --git a/spring-boot-project/spring-boot-actuator/build.gradle b/spring-boot-project/spring-boot-actuator/build.gradle index 9f188d3827..8545696e91 100644 --- a/spring-boot-project/spring-boot-actuator/build.gradle +++ b/spring-boot-project/spring-boot-actuator/build.gradle @@ -46,6 +46,7 @@ dependencies { optional("org.liquibase:liquibase-core") optional("org.mongodb:mongodb-driver-reactivestreams") optional("org.mongodb:mongodb-driver-sync") + optional("org.neo4j.driver:neo4j-java-driver") optional("org.springframework:spring-jdbc") optional("org.springframework:spring-messaging") optional("org.springframework:spring-webflux") @@ -57,7 +58,6 @@ dependencies { optional("org.springframework.data:spring-data-elasticsearch") optional("org.springframework.data:spring-data-ldap") optional("org.springframework.data:spring-data-mongodb") - optional("org.springframework.data:spring-data-neo4j") optional("org.springframework.data:spring-data-redis") optional("org.springframework.data:spring-data-rest-webmvc") optional("org.springframework.data:spring-data-solr") diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jDriverMetrics.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jDriverMetrics.java deleted file mode 100644 index 65ebb6395f..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jDriverMetrics.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2012-2020 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.boot.actuate.neo4j; - -import java.util.function.Consumer; - -import io.micrometer.core.instrument.FunctionCounter; -import io.micrometer.core.instrument.Gauge; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Tag; -import io.micrometer.core.instrument.Tags; -import io.micrometer.core.instrument.binder.MeterBinder; -import org.neo4j.driver.ConnectionPoolMetrics; -import org.neo4j.driver.Driver; -import org.neo4j.driver.Metrics; -import org.neo4j.driver.exceptions.ClientException; - -import org.springframework.util.Assert; - -/** - * This is a {@link MeterBinder} that binds all available Neo4j driver metrics to - * Micrometer. - * - * @author Michael J. Simons - * @since 2.4.0 - */ -public final class Neo4jDriverMetrics implements MeterBinder { - - /** - * Prefixed used for driver metrics. - */ - public static final String PREFIX = "neo4j.driver.connections"; - - private static final String BASE_UNIT_CONNECTIONS = "connections"; - - private final Driver driver; - - private final Iterable tags; - - public Neo4jDriverMetrics(String name, Driver driver, Iterable tags) { - - Assert.notNull(name, "Bean name must not be null"); - Assert.notNull(driver, "Driver must not be null"); - Assert.notNull(tags, "Tags must not be null (but may be empty)"); - this.driver = driver; - this.tags = Tags.concat(tags, "name", name); - } - - @Override - public void bindTo(MeterRegistry meterRegistry) { - - Metrics metrics = this.driver.metrics(); - metrics.connectionPoolMetrics().forEach(this.getPoolMetricsBinder(meterRegistry)); - } - - Consumer getPoolMetricsBinder(MeterRegistry meterRegistry) { - return (poolMetrics) -> { - Iterable poolTags = Tags.concat(this.tags, "poolId", poolMetrics.id()); - - FunctionCounter.builder(PREFIX + ".acquired", poolMetrics, ConnectionPoolMetrics::acquired).tags(poolTags) - .baseUnit(BASE_UNIT_CONNECTIONS).description("The amount of connections that have been acquired.") - .register(meterRegistry); - - FunctionCounter.builder(PREFIX + ".closed", poolMetrics, ConnectionPoolMetrics::closed).tags(poolTags) - .baseUnit(BASE_UNIT_CONNECTIONS).description("The amount of connections have been closed.") - .register(meterRegistry); - - FunctionCounter.builder(PREFIX + ".created", poolMetrics, ConnectionPoolMetrics::created).tags(poolTags) - .baseUnit(BASE_UNIT_CONNECTIONS).description("The amount of connections have ever been created.") - .register(meterRegistry); - - FunctionCounter.builder(PREFIX + ".failedToCreate", poolMetrics, ConnectionPoolMetrics::failedToCreate) - .tags(poolTags).baseUnit(BASE_UNIT_CONNECTIONS) - .description("The amount of connections have been failed to create.").register(meterRegistry); - - Gauge.builder(PREFIX + ".idle", poolMetrics, ConnectionPoolMetrics::idle).tags(poolTags) - .baseUnit(BASE_UNIT_CONNECTIONS).description("The amount of connections that are currently idle.") - .register(meterRegistry); - - Gauge.builder(PREFIX + ".inUse", poolMetrics, ConnectionPoolMetrics::inUse).tags(poolTags) - .baseUnit(BASE_UNIT_CONNECTIONS).description("The amount of connections that are currently in-use.") - .register(meterRegistry); - - FunctionCounter - .builder(PREFIX + ".timedOutToAcquire", poolMetrics, ConnectionPoolMetrics::timedOutToAcquire) - .tags(poolTags).baseUnit(BASE_UNIT_CONNECTIONS) - .description( - "The amount of failures to acquire a connection from a pool within maximum connection acquisition timeout.") - .register(meterRegistry); - }; - } - - /** - * Utility method to check whether driver metrics are enabled without throwing an - * exception. - * @param driver the bean to check whether it has metrics enabled or not - * @return true, if the given bean exposes metrics - */ - public static boolean metricsAreEnabled(Driver driver) { - - try { - driver.metrics(); - return true; - } - catch (ClientException ex) { - return false; - } - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthDetailsHandler.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthDetailsHandler.java new file mode 100644 index 0000000000..5bf908a3cb --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthDetailsHandler.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2020 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.boot.actuate.neo4j; + +import org.neo4j.driver.summary.DatabaseInfo; +import org.neo4j.driver.summary.ResultSummary; +import org.neo4j.driver.summary.ServerInfo; + +import org.springframework.boot.actuate.health.Health.Builder; +import org.springframework.util.StringUtils; + +/** + * Handle health check details for a Neo4j server. + * + * @author Stephane Nicoll + */ +class Neo4jHealthDetailsHandler { + + /** + * Add health details for the specified {@link ResultSummary} and {@code edition}. + * @param builder the {@link Builder} to use + * @param edition the edition of the server + * @param resultSummary server information + */ + void addHealthDetails(Builder builder, String edition, ResultSummary resultSummary) { + ServerInfo serverInfo = resultSummary.server(); + builder.up().withDetail("server", serverInfo.version() + "@" + serverInfo.address()).withDetail("edition", + edition); + DatabaseInfo databaseInfo = resultSummary.database(); + if (StringUtils.hasText(databaseInfo.name())) { + builder.withDetail("database", databaseInfo.name()); + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicator.java index 1239594154..782be4cb2b 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicator.java @@ -24,14 +24,11 @@ import org.neo4j.driver.Result; import org.neo4j.driver.Session; import org.neo4j.driver.SessionConfig; import org.neo4j.driver.exceptions.SessionExpiredException; -import org.neo4j.driver.summary.DatabaseInfo; import org.neo4j.driver.summary.ResultSummary; -import org.neo4j.driver.summary.ServerInfo; import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.util.StringUtils; /** * {@link HealthIndicator} that tests the status of a Neo4j by executing a Cypher @@ -51,11 +48,6 @@ public class Neo4jHealthIndicator extends AbstractHealthIndicator { */ static final String CYPHER = "CALL dbms.components() YIELD name, edition WHERE name = 'Neo4j Kernel' RETURN edition"; - /** - * Message indicating that the health check failed. - */ - static final String MESSAGE_HEALTH_CHECK_FAILED = "Neo4j health check failed"; - /** * Message logged before retrying a health check. */ @@ -67,65 +59,41 @@ public class Neo4jHealthIndicator extends AbstractHealthIndicator { static final SessionConfig DEFAULT_SESSION_CONFIG = SessionConfig.builder().withDefaultAccessMode(AccessMode.WRITE) .build(); - /** - * The driver for this health indicator instance. - */ private final Driver driver; + private final Neo4jHealthDetailsHandler healthDetailsHandler; + public Neo4jHealthIndicator(Driver driver) { - super(MESSAGE_HEALTH_CHECK_FAILED); + super("Neo4j health check failed"); this.driver = driver; + this.healthDetailsHandler = new Neo4jHealthDetailsHandler(); } @Override protected void doHealthCheck(Health.Builder builder) { - try { - ResultSummaryWithEdition resultSummaryWithEdition; - // Retry one time when the session has been expired try { - resultSummaryWithEdition = runHealthCheckQuery(); + runHealthCheckQuery(builder); } - catch (SessionExpiredException sessionExpiredException) { + catch (SessionExpiredException ex) { + // Retry one time when the session has been expired logger.warn(MESSAGE_SESSION_EXPIRED); - resultSummaryWithEdition = runHealthCheckQuery(); + runHealthCheckQuery(builder); } - buildStatusUp(resultSummaryWithEdition, builder); } catch (Exception ex) { builder.down().withException(ex); } } - /** - * Applies the given {@link ResultSummary} to the {@link Health.Builder builder} - * without actually calling {@code build}. - * @param resultSummaryWithEdition the result summary returned by the server - * @param builder the health builder to be modified - * @return the modified health builder - */ - static Health.Builder buildStatusUp(ResultSummaryWithEdition resultSummaryWithEdition, Health.Builder builder) { - ServerInfo serverInfo = resultSummaryWithEdition.resultSummary.server(); - DatabaseInfo databaseInfo = resultSummaryWithEdition.resultSummary.database(); - - builder.up().withDetail("server", serverInfo.version() + "@" + serverInfo.address()).withDetail("edition", - resultSummaryWithEdition.edition); - - if (StringUtils.hasText(databaseInfo.name())) { - builder.withDetail("database", databaseInfo.name()); - } - - return builder; - } - - ResultSummaryWithEdition runHealthCheckQuery() { + private void runHealthCheckQuery(Health.Builder builder) { // We use WRITE here to make sure UP is returned for a server that supports // all possible workloads try (Session session = this.driver.session(DEFAULT_SESSION_CONFIG)) { Result result = session.run(CYPHER); String edition = result.single().get("edition").asString(); ResultSummary resultSummary = result.consume(); - return new ResultSummaryWithEdition(resultSummary, edition); + this.healthDetailsHandler.addHealthDetails(builder, edition, resultSummary); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jReactiveHealthIndicator.java index 535fcc4043..77b8d51bcb 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jReactiveHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jReactiveHealthIndicator.java @@ -22,7 +22,9 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.exceptions.SessionExpiredException; import org.neo4j.driver.reactive.RxResult; import org.neo4j.driver.reactive.RxSession; +import org.neo4j.driver.summary.ResultSummary; import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; import reactor.util.retry.Retry; import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; @@ -34,19 +36,20 @@ import org.springframework.boot.actuate.health.ReactiveHealthIndicator; * statement and extracting server and database information. * * @author Michael J. Simons + * @author Stephane Nicoll * @since 2.4.0 */ public final class Neo4jReactiveHealthIndicator extends AbstractReactiveHealthIndicator { private static final Log logger = LogFactory.getLog(Neo4jReactiveHealthIndicator.class); - /** - * The driver for this health indicator instance. - */ private final Driver driver; + private final Neo4jHealthDetailsHandler healthDetailsHandler; + public Neo4jReactiveHealthIndicator(Driver driver) { this.driver = driver; + this.healthDetailsHandler = new Neo4jHealthDetailsHandler(); } @Override @@ -54,17 +57,19 @@ public final class Neo4jReactiveHealthIndicator extends AbstractReactiveHealthIn return runHealthCheckQuery() .doOnError(SessionExpiredException.class, (e) -> logger.warn(Neo4jHealthIndicator.MESSAGE_SESSION_EXPIRED)) - .retryWhen(Retry.max(1).filter(SessionExpiredException.class::isInstance)) - .map((r) -> Neo4jHealthIndicator.buildStatusUp(r, builder).build()); + .retryWhen(Retry.max(1).filter(SessionExpiredException.class::isInstance)).map((result) -> { + this.healthDetailsHandler.addHealthDetails(builder, result.getT1(), result.getT2()); + return builder.build(); + }); } - Mono runHealthCheckQuery() { + Mono> runHealthCheckQuery() { // We use WRITE here to make sure UP is returned for a server that supports // all possible workloads return Mono.using(() -> this.driver.rxSession(Neo4jHealthIndicator.DEFAULT_SESSION_CONFIG), (session) -> { RxResult result = session.run(Neo4jHealthIndicator.CYPHER); return Mono.from(result.records()).map((record) -> record.get("edition").asString()) - .zipWhen((edition) -> Mono.from(result.consume()), (e, r) -> new ResultSummaryWithEdition(r, e)); + .zipWhen((edition) -> Mono.from(result.consume())); }, RxSession::close); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/ResultSummaryWithEdition.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/ResultSummaryWithEdition.java deleted file mode 100644 index 9220a5d525..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/ResultSummaryWithEdition.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2012-2020 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.boot.actuate.neo4j; - -import org.neo4j.driver.summary.ResultSummary; - -/** - * A holder for a {@link ResultSummary result summary} and database edition. - * - * @author Michael J. Simons - */ -final class ResultSummaryWithEdition { - - final ResultSummary resultSummary; - - final String edition; - - ResultSummaryWithEdition(ResultSummary resultSummary, String edition) { - this.resultSummary = resultSummary; - this.edition = edition; - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jDriverMetricsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jDriverMetricsTests.java deleted file mode 100644 index 7b85151d65..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jDriverMetricsTests.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2012-2020 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.boot.actuate.neo4j; - -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import org.junit.jupiter.api.Test; - -import java.util.Collections; - -import org.neo4j.driver.Driver; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.boot.actuate.neo4j.Neo4jDriverMetrics.PREFIX; -import static org.springframework.boot.actuate.neo4j.Neo4jDriverMocks.mockDriverWithMetrics; -import static org.springframework.boot.actuate.neo4j.Neo4jDriverMocks.mockDriverWithoutMetrics; - -/** - * @author Michael J. Simons - */ -class Neo4jDriverMetricsTests { - - @Test - void shouldDetectEnabledMetrics() { - - Driver driver = mockDriverWithMetrics(); - assertThat(Neo4jDriverMetrics.metricsAreEnabled(driver)).isTrue(); - } - - @Test - void shouldDetectDisabledMetrics() { - - Driver driver = mockDriverWithoutMetrics(); - assertThat(Neo4jDriverMetrics.metricsAreEnabled(driver)).isFalse(); - } - - @Test - void shouldRegisterCorrectMeters() { - - SimpleMeterRegistry registry = new SimpleMeterRegistry(); - Neo4jDriverMetrics metrics = new Neo4jDriverMetrics("driver", mockDriverWithMetrics(), Collections.emptyList()); - metrics.bindTo(registry); - - assertThat(registry.get(PREFIX + ".acquired").functionCounter()).isNotNull(); - assertThat(registry.get(PREFIX + ".closed").functionCounter()).isNotNull(); - assertThat(registry.get(PREFIX + ".created").functionCounter()).isNotNull(); - assertThat(registry.get(PREFIX + ".failedToCreate").functionCounter()).isNotNull(); - assertThat(registry.get(PREFIX + ".idle").gauge()).isNotNull(); - assertThat(registry.get(PREFIX + ".inUse").gauge()).isNotNull(); - assertThat(registry.get(PREFIX + ".timedOutToAcquire").functionCounter()).isNotNull(); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jDriverMocks.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jDriverMocks.java deleted file mode 100644 index f6e44d5fdc..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jDriverMocks.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2012-2020 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.boot.actuate.neo4j; - -import java.util.Collections; -import java.util.concurrent.CompletableFuture; - -import org.neo4j.driver.ConnectionPoolMetrics; -import org.neo4j.driver.Driver; -import org.neo4j.driver.Metrics; -import org.neo4j.driver.exceptions.ClientException; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * Some predefined mocks, only to be used internally for tests. - * - * @author Michael J. Simons - */ -final class Neo4jDriverMocks { - - public static Driver mockDriverWithMetrics() { - ConnectionPoolMetrics p1 = mock(ConnectionPoolMetrics.class); - when(p1.id()).thenReturn("p1"); - - Metrics metrics = mock(Metrics.class); - when(metrics.connectionPoolMetrics()).thenReturn(Collections.singletonList(p1)); - - Driver driver = mock(Driver.class); - when(driver.metrics()).thenReturn(metrics); - - when(driver.verifyConnectivityAsync()).thenReturn(CompletableFuture.completedFuture(null)); - - return driver; - } - - public static Driver mockDriverWithoutMetrics() { - - Driver driver = mock(Driver.class); - when(driver.metrics()).thenThrow(ClientException.class); - return driver; - } - - private Neo4jDriverMocks() { - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicatorTestBase.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicatorTestBase.java deleted file mode 100644 index 036ce85254..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicatorTestBase.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) 2019-2020 "Neo4j," - * Neo4j Sweden AB [https://neo4j.com] - * - * This file is part of Neo4j. - * - * 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.boot.actuate.neo4j; - -import org.mockito.Mock; - -import org.neo4j.driver.Driver; -import org.neo4j.driver.Record; -import org.neo4j.driver.Values; -import org.neo4j.driver.summary.DatabaseInfo; -import org.neo4j.driver.summary.ResultSummary; -import org.neo4j.driver.summary.ServerInfo; - -import static org.mockito.Mockito.when; - -/** - * Contains some shared mocks for the health indicator tests. - * - * @author Michael J. Simons - */ -abstract class Neo4jHealthIndicatorTestBase { - - @Mock - protected Driver driver; - - @Mock - protected ResultSummary resultSummary; - - @Mock - protected ServerInfo serverInfo; - - @Mock - protected DatabaseInfo databaseInfo; - - @Mock - protected Record record; - - protected void prepareSharedMocks() { - - when(serverInfo.version()).thenReturn("4711"); - when(serverInfo.address()).thenReturn("Zu Hause"); - when(databaseInfo.name()).thenReturn("n/a"); - when(resultSummary.server()).thenReturn(serverInfo); - when(resultSummary.database()).thenReturn(databaseInfo); - when(record.get("edition")).thenReturn(Values.value("ultimate collectors edition")); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicatorTests.java index c81bd28923..a72f1fa8fd 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicatorTests.java @@ -16,29 +16,29 @@ package org.springframework.boot.actuate.neo4j; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.Status; - import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Record; import org.neo4j.driver.Result; import org.neo4j.driver.Session; import org.neo4j.driver.SessionConfig; import org.neo4j.driver.Values; import org.neo4j.driver.exceptions.ServiceUnavailableException; import org.neo4j.driver.exceptions.SessionExpiredException; +import org.neo4j.driver.summary.ResultSummary; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; /** * Tests for {@link Neo4jHealthIndicator}. @@ -47,126 +47,87 @@ import static org.mockito.Mockito.when; * @author Stephane Nicoll * @author Michael Simons */ -@ExtendWith(MockitoExtension.class) -class Neo4jHealthIndicatorTests extends Neo4jHealthIndicatorTestBase { - - @Mock - private Session session; - - @Mock - private Result statementResult; +class Neo4jHealthIndicatorTests { @Test - void shouldWorkWithoutDatabaseName() { - when(this.serverInfo.version()).thenReturn("4711"); - when(this.serverInfo.address()).thenReturn("Zu Hause"); - when(this.resultSummary.server()).thenReturn(this.serverInfo); - when(this.resultSummary.database()).thenReturn(this.databaseInfo); - - when(this.databaseInfo.name()).thenReturn(null); - - when(record.get("edition")).thenReturn(Values.value("some edition")); - when(this.statementResult.single()).thenReturn(this.record); - when(this.statementResult.consume()).thenReturn(this.resultSummary); - when(this.session.run(anyString())).thenReturn(this.statementResult); - - when(this.driver.session(any(SessionConfig.class))).thenReturn(this.session); - - Neo4jHealthIndicator healthIndicator = new Neo4jHealthIndicator(this.driver); - Health health = healthIndicator.health(); + void neo4jIsUp() { + ResultSummary resultSummary = ResultSummaryMock.createResultSummary("4711", "My Home", "test"); + Driver driver = mockDriver(resultSummary, "ultimate collectors edition"); + Health health = new Neo4jHealthIndicator(driver).health(); assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health.getDetails()).containsEntry("server", "4711@Zu Hause"); - assertThat(health.getDetails()).doesNotContainKey("database"); - assertThat(health.getDetails()).containsEntry("edition", "some edition"); + assertThat(health.getDetails()).containsEntry("server", "4711@My Home"); + assertThat(health.getDetails()).containsEntry("database", "test"); + assertThat(health.getDetails()).containsEntry("edition", "ultimate collectors edition"); } @Test - void shouldWorkWithEmptyDatabaseName() { - when(this.serverInfo.version()).thenReturn("4711"); - when(this.serverInfo.address()).thenReturn("Zu Hause"); - when(this.resultSummary.server()).thenReturn(this.serverInfo); - when(this.resultSummary.database()).thenReturn(this.databaseInfo); - - when(this.databaseInfo.name()).thenReturn(""); - - when(record.get("edition")).thenReturn(Values.value("some edition")); - when(this.statementResult.single()).thenReturn(this.record); - when(this.statementResult.consume()).thenReturn(this.resultSummary); - when(this.session.run(anyString())).thenReturn(this.statementResult); - - when(driver.session(any(SessionConfig.class))).thenReturn(this.session); - - Neo4jHealthIndicator healthIndicator = new Neo4jHealthIndicator(this.driver); - Health health = healthIndicator.health(); + void neo4jIsUpWithoutDatabaseName() { + ResultSummary resultSummary = ResultSummaryMock.createResultSummary("4711", "My Home", null); + Driver driver = mockDriver(resultSummary, "some edition"); + Health health = new Neo4jHealthIndicator(driver).health(); assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health.getDetails()).containsEntry("server", "4711@Zu Hause"); + assertThat(health.getDetails()).containsEntry("server", "4711@My Home"); assertThat(health.getDetails()).doesNotContainKey("database"); assertThat(health.getDetails()).containsEntry("edition", "some edition"); } @Test - void neo4jIsUp() { - - prepareSharedMocks(); - - when(this.statementResult.single()).thenReturn(this.record); - when(this.statementResult.consume()).thenReturn(this.resultSummary); - when(this.session.run(anyString())).thenReturn(this.statementResult); - - when(this.driver.session(any(SessionConfig.class))).thenReturn(this.session); - - Neo4jHealthIndicator healthIndicator = new Neo4jHealthIndicator(this.driver); - Health health = healthIndicator.health(); + void neo4jIsUpWithEmptyDatabaseName() { + ResultSummary resultSummary = ResultSummaryMock.createResultSummary("4711", "My Home", ""); + Driver driver = mockDriver(resultSummary, "some edition"); + Health health = new Neo4jHealthIndicator(driver).health(); assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health.getDetails()).containsEntry("server", "4711@Zu Hause"); - assertThat(health.getDetails()).containsEntry("database", "n/a"); - assertThat(health.getDetails()).containsEntry("edition", "ultimate collectors edition"); - - verify(session).close(); - verifyNoMoreInteractions(this.driver, this.session, this.statementResult, this.resultSummary, this.serverInfo, - this.databaseInfo); + assertThat(health.getDetails()).containsEntry("server", "4711@My Home"); + assertThat(health.getDetails()).doesNotContainKey("database"); + assertThat(health.getDetails()).containsEntry("edition", "some edition"); } @Test - void neo4jSessionIsExpiredOnce() { - - AtomicInteger cnt = new AtomicInteger(0); - - prepareSharedMocks(); - when(this.statementResult.single()).thenReturn(this.record); - when(this.statementResult.consume()).thenReturn(this.resultSummary); - when(this.session.run(anyString())).thenAnswer(invocation -> { - if (cnt.compareAndSet(0, 1)) { + void neo4jIsUpWithOneSessionExpiredException() { + ResultSummary resultSummary = ResultSummaryMock.createResultSummary("4711", "My Home", ""); + Session session = mock(Session.class); + Result statementResult = mockStatementResult(resultSummary, "some edition"); + AtomicInteger count = new AtomicInteger(0); + given(session.run(anyString())).will((invocation) -> { + if (count.compareAndSet(0, 1)) { throw new SessionExpiredException("Session expired"); } - return Neo4jHealthIndicatorTests.this.statementResult; + return statementResult; }); - when(driver.session(any(SessionConfig.class))).thenReturn(this.session); - - Neo4jHealthIndicator healthIndicator = new Neo4jHealthIndicator(this.driver); + Driver driver = mock(Driver.class); + given(driver.session(any(SessionConfig.class))).willReturn(session); + Neo4jHealthIndicator healthIndicator = new Neo4jHealthIndicator(driver); Health health = healthIndicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health.getDetails()).containsEntry("server", "4711@Zu Hause"); - - verify(this.session, times(2)).close(); - verifyNoMoreInteractions(this.driver, this.session, this.statementResult, this.resultSummary, this.serverInfo, - this.databaseInfo); + assertThat(health.getDetails()).containsEntry("server", "4711@My Home"); + verify(session, times(2)).close(); } @Test - void neo4jSessionIsDown() { - - when(driver.session(any(SessionConfig.class))).thenThrow(ServiceUnavailableException.class); - - Neo4jHealthIndicator healthIndicator = new Neo4jHealthIndicator(driver); - Health health = healthIndicator.health(); - + void neo4jIsDown() { + Driver driver = mock(Driver.class); + given(driver.session(any(SessionConfig.class))).willThrow(ServiceUnavailableException.class); + Health health = new Neo4jHealthIndicator(driver).health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); assertThat(health.getDetails()).containsKeys("error"); + } + + private Result mockStatementResult(ResultSummary resultSummary, String edition) { + Record record = mock(Record.class); + given(record.get("edition")).willReturn(Values.value(edition)); + Result statementResult = mock(Result.class); + given(statementResult.single()).willReturn(record); + given(statementResult.consume()).willReturn(resultSummary); + return statementResult; + } - verifyNoMoreInteractions(this.driver, this.session, this.statementResult, this.resultSummary, this.serverInfo, - this.databaseInfo); + private Driver mockDriver(ResultSummary resultSummary, String edition) { + Result statementResult = mockStatementResult(resultSummary, edition); + Session session = mock(Session.class); + given(session.run(anyString())).willReturn(statementResult); + Driver driver = mock(Driver.class); + given(driver.session(any(SessionConfig.class))).willReturn(session); + return driver; } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jReactiveHealthIndicatorTest.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jReactiveHealthIndicatorTest.java index 4963b35f95..9379d92d0f 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jReactiveHealthIndicatorTest.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jReactiveHealthIndicatorTest.java @@ -16,102 +16,101 @@ package org.springframework.boot.actuate.neo4j; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.boot.actuate.health.Status; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Record; import org.neo4j.driver.SessionConfig; +import org.neo4j.driver.Values; import org.neo4j.driver.exceptions.ServiceUnavailableException; import org.neo4j.driver.exceptions.SessionExpiredException; import org.neo4j.driver.reactive.RxResult; import org.neo4j.driver.reactive.RxSession; +import org.neo4j.driver.summary.ResultSummary; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.boot.actuate.health.Status; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; /** + * Tests for {@link Neo4jReactiveHealthIndicator}. + * * @author Michael J. Simons + * @author Stephane Nicoll */ -@ExtendWith(MockitoExtension.class) -class Neo4jReactiveHealthIndicatorTest extends Neo4jHealthIndicatorTestBase { - - @Mock - private RxSession session; - - @Mock - private RxResult statementResult; +class Neo4jReactiveHealthIndicatorTest { @Test void neo4jIsUp() { - - prepareSharedMocks(); - when(statementResult.records()).thenReturn(Mono.just(record)); - when(statementResult.consume()).thenReturn(Mono.just(resultSummary)); - when(session.run(anyString())).thenReturn(statementResult); - - when(driver.rxSession(any(SessionConfig.class))).thenReturn(session); - + ResultSummary resultSummary = ResultSummaryMock.createResultSummary("4711", "My Home", "test"); + Driver driver = mockDriver(resultSummary, "ultimate collectors edition"); Neo4jReactiveHealthIndicator healthIndicator = new Neo4jReactiveHealthIndicator(driver); - healthIndicator.health().as(StepVerifier::create).consumeNextWith(health -> { + healthIndicator.health().as(StepVerifier::create).consumeNextWith((health) -> { assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health.getDetails()).containsEntry("server", "4711@Zu Hause"); + assertThat(health.getDetails()).containsEntry("server", "4711@My Home"); assertThat(health.getDetails()).containsEntry("edition", "ultimate collectors edition"); }).verifyComplete(); - - verify(session).close(); - verifyNoMoreInteractions(driver, session, statementResult, resultSummary, serverInfo, databaseInfo); } @Test - void neo4jSessionIsExpiredOnce() { - - AtomicInteger cnt = new AtomicInteger(0); - - prepareSharedMocks(); - when(statementResult.records()).thenReturn(Mono.just(record)); - when(statementResult.consume()).thenReturn(Mono.just(resultSummary)); - when(session.run(anyString())).thenAnswer(invocation -> { - if (cnt.compareAndSet(0, 1)) { + void neo4jIsUpWithOneSessionExpiredException() { + ResultSummary resultSummary = ResultSummaryMock.createResultSummary("4711", "My Home", ""); + RxSession session = mock(RxSession.class); + RxResult statementResult = mockStatementResult(resultSummary, "some edition"); + AtomicInteger count = new AtomicInteger(0); + given(session.run(anyString())).will((invocation) -> { + if (count.compareAndSet(0, 1)) { throw new SessionExpiredException("Session expired"); } return statementResult; }); - when(driver.rxSession(any(SessionConfig.class))).thenReturn(session); - + Driver driver = mock(Driver.class); + given(driver.rxSession(any(SessionConfig.class))).willReturn(session); Neo4jReactiveHealthIndicator healthIndicator = new Neo4jReactiveHealthIndicator(driver); - healthIndicator.health().as(StepVerifier::create).consumeNextWith(health -> { + healthIndicator.health().as(StepVerifier::create).consumeNextWith((health) -> { assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health.getDetails()).containsEntry("server", "4711@Zu Hause"); - assertThat(health.getDetails()).containsEntry("edition", "ultimate collectors edition"); + assertThat(health.getDetails()).containsEntry("server", "4711@My Home"); + assertThat(health.getDetails()).containsEntry("edition", "some edition"); }).verifyComplete(); - verify(session, times(2)).close(); - verifyNoMoreInteractions(driver, session, statementResult, resultSummary, serverInfo, databaseInfo); } @Test - void neo4jSessionIsDown() { - - when(driver.rxSession(any(SessionConfig.class))).thenThrow(ServiceUnavailableException.class); - + void neo4jIsDown() { + Driver driver = mock(Driver.class); + given(driver.rxSession(any(SessionConfig.class))).willThrow(ServiceUnavailableException.class); Neo4jReactiveHealthIndicator healthIndicator = new Neo4jReactiveHealthIndicator(driver); - healthIndicator.health().as(StepVerifier::create).consumeNextWith(health -> { + healthIndicator.health().as(StepVerifier::create).consumeNextWith((health) -> { assertThat(health.getStatus()).isEqualTo(Status.DOWN); assertThat(health.getDetails()).containsKeys("error"); }).verifyComplete(); + } + + private RxResult mockStatementResult(ResultSummary resultSummary, String edition) { + Record record = mock(Record.class); + given(record.get("edition")).willReturn(Values.value(edition)); + RxResult statementResult = mock(RxResult.class); + given(statementResult.records()).willReturn(Mono.just(record)); + given(statementResult.consume()).willReturn(Mono.just(resultSummary)); + return statementResult; + } - verifyNoMoreInteractions(driver, session, statementResult, resultSummary, serverInfo, databaseInfo); + private Driver mockDriver(ResultSummary resultSummary, String edition) { + RxResult statementResult = mockStatementResult(resultSummary, edition); + RxSession session = mock(RxSession.class); + given(session.run(anyString())).willReturn(statementResult); + Driver driver = mock(Driver.class); + given(driver.rxSession(any(SessionConfig.class))).willReturn(session); + return driver; } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/ResultSummaryMock.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/ResultSummaryMock.java new file mode 100644 index 0000000000..4bbd715a0e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/ResultSummaryMock.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2020 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.boot.actuate.neo4j; + +import org.neo4j.driver.summary.DatabaseInfo; +import org.neo4j.driver.summary.ResultSummary; +import org.neo4j.driver.summary.ServerInfo; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Test utility to mock {@link ResultSummary}. + * + * @author Stephane Nicoll + */ +final class ResultSummaryMock { + + private ResultSummaryMock() { + } + + static ResultSummary createResultSummary(String serverVersion, String serverAddress, String databaseName) { + ServerInfo serverInfo = mock(ServerInfo.class); + given(serverInfo.version()).willReturn(serverVersion); + given(serverInfo.address()).willReturn(serverAddress); + DatabaseInfo databaseInfo = mock(DatabaseInfo.class); + given(databaseInfo.name()).willReturn(databaseName); + ResultSummary resultSummary = mock(ResultSummary.class); + given(resultSummary.server()).willReturn(serverInfo); + given(resultSummary.database()).willReturn(databaseInfo); + return resultSummary; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc index 5109c93106..b872595e9b 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc @@ -856,6 +856,9 @@ The following `ReactiveHealthIndicators` are auto-configured by Spring Boot when | {spring-boot-actuator-module-code}/mongo/MongoReactiveHealthIndicator.java[`MongoReactiveHealthIndicator`] | Checks that a Mongo database is up. +| {spring-boot-actuator-module-code}/neo4j/Neo4jReactiveHealthIndicator.java[`Neo4jReactiveHealthIndicator`] +| Checks that a Neo4j database is up. + | {spring-boot-actuator-module-code}/redis/RedisReactiveHealthIndicator.java[`RedisReactiveHealthIndicator`] | Checks that a Redis server is up. |===