Merge pull request #22302 from meistermeier

* pr/22302:
  Polish "Update Neo4j health check to use the Neo4j Driver"
  Update Neo4j health check to use the Neo4j Driver

Closes gh-22302
This commit is contained in:
Stephane Nicoll 2020-07-28 16:42:01 +02:00
commit 0482cc8f78
15 changed files with 564 additions and 124 deletions

View File

@ -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")

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -16,42 +16,36 @@
package org.springframework.boot.actuate.autoconfigure.neo4j;
import java.util.Map;
import org.neo4j.driver.Driver;
import org.neo4j.ogm.session.SessionFactory;
import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthContributorConfiguration;
import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator;
import org.springframework.boot.actuate.health.HealthContributor;
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.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.context.annotation.Bean;
import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link Neo4jHealthIndicator}.
* {@link EnableAutoConfiguration Auto-configuration} for
* {@link Neo4jReactiveHealthIndicator} and {@link Neo4jHealthIndicator}.
*
* @author Eric Spiegelberg
* @author Stephane Nicoll
* @author Michael J. Simons
* @since 2.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(SessionFactory.class)
@ConditionalOnBean(SessionFactory.class)
@ConditionalOnClass(Driver.class)
@ConditionalOnBean(Driver.class)
@ConditionalOnEnabledHealthIndicator("neo4j")
@AutoConfigureAfter(Neo4jDataAutoConfiguration.class)
public class Neo4jHealthContributorAutoConfiguration
extends CompositeHealthContributorConfiguration<Neo4jHealthIndicator, SessionFactory> {
@Bean
@ConditionalOnMissingBean(name = { "neo4jHealthIndicator", "neo4jHealthContributor" })
public HealthContributor neo4jHealthContributor(Map<String, SessionFactory> sessionFactories) {
return createContributor(sessionFactories);
}
@AutoConfigureAfter(Neo4jAutoConfiguration.class)
@Import({ Neo4jReactiveConfiguration.class, Neo4jConfiguration.class })
public class Neo4jHealthContributorAutoConfiguration {
}

View File

@ -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<Neo4jHealthIndicator, Driver> {
@Bean
@ConditionalOnMissingBean(name = { "neo4jHealthIndicator", "neo4jHealthContributor" })
HealthContributor neo4jHealthContributor(Map<String, Driver> drivers) {
return createContributor(drivers);
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Flux.class)
static class Neo4jReactiveConfiguration
extends CompositeReactiveHealthContributorConfiguration<Neo4jReactiveHealthIndicator, Driver> {
@Bean
@ConditionalOnMissingBean(name = { "neo4jHealthIndicator", "neo4jHealthContributor" })
ReactiveHealthContributor neo4jHealthContributor(Map<String, Driver> drivers) {
return createContributor(drivers);
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.

View File

@ -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 {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -13,17 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.neo4j;
import org.junit.jupiter.api.Test;
import org.neo4j.ogm.session.Session;
import org.neo4j.ogm.session.SessionFactory;
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.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.neo4j.Neo4jHealthIndicator;
import org.springframework.boot.actuate.neo4j.Neo4jReactiveHealthIndicator;
import org.springframework.boot.autoconfigure.AutoConfigurations;
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;
@ -37,39 +40,57 @@ import static org.mockito.Mockito.mock;
*
* @author Phillip Webb
* @author Stephane Nicoll
* @author Michael J. Simons
*/
class Neo4jHealthContributorAutoConfigurationTests {
private ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withUserConfiguration(Neo4jConfiguration.class).withConfiguration(AutoConfigurations
.of(Neo4jHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class));
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
Neo4jHealthContributorAutoConfiguration.class));
@Test
void runShouldCreateIndicator() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(Neo4jHealthIndicator.class));
void runShouldCreateHealthIndicator() {
this.contextRunner.withUserConfiguration(Neo4jConfiguration.class).run((context) -> assertThat(context)
.hasSingleBean(Neo4jReactiveHealthIndicator.class).doesNotHaveBean(Neo4jHealthIndicator.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.withPropertyValues("management.health.neo4j.enabled:false")
.run((context) -> assertThat(context).doesNotHaveBean(Neo4jHealthIndicator.class));
this.contextRunner.withUserConfiguration(Neo4jConfiguration.class)
.withPropertyValues("management.health.neo4j.enabled=false")
.run((context) -> assertThat(context).doesNotHaveBean(Neo4jHealthIndicator.class)
.doesNotHaveBean(Neo4jReactiveHealthIndicator.class));
}
@Test
void defaultIndicatorCanBeReplaced() {
this.contextRunner.withUserConfiguration(CustomIndicatorConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(Neo4jHealthIndicator.class);
Health health = context.getBean(Neo4jHealthIndicator.class).health();
assertThat(health.getDetails()).containsOnly(entry("test", true));
});
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 Neo4jConfiguration {
@Bean
SessionFactory sessionFactory() {
return mock(SessionFactory.class);
Driver driver() {
return mock(Driver.class);
}
}
@ -78,14 +99,12 @@ class Neo4jHealthContributorAutoConfigurationTests {
static class CustomIndicatorConfiguration {
@Bean
Neo4jHealthIndicator neo4jHealthIndicator(SessionFactory sessionFactory) {
return new Neo4jHealthIndicator(sessionFactory) {
HealthIndicator neo4jHealthIndicator() {
return new AbstractHealthIndicator() {
@Override
protected void extractResult(Session session, Health.Builder builder) {
protected void doHealthCheck(Health.Builder builder) {
builder.up().withDetail("test", true);
}
};
}

View File

@ -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")

View File

@ -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());
}
}
}

View File

@ -16,61 +16,85 @@
package org.springframework.boot.actuate.neo4j;
import java.util.Collections;
import org.neo4j.ogm.model.Result;
import org.neo4j.ogm.session.Session;
import org.neo4j.ogm.session.SessionFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.neo4j.driver.AccessMode;
import org.neo4j.driver.Driver;
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.ResultSummary;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Health.Builder;
import org.springframework.boot.actuate.health.HealthIndicator;
/**
* {@link HealthIndicator} that tests the status of a Neo4j by executing a Cypher
* statement.
* statement and extracting server and database information.
*
* @author Eric Spiegelberg
* @author Stephane Nicoll
* @author Michael J. Simons
* @since 2.0.0
*/
public class Neo4jHealthIndicator extends AbstractHealthIndicator {
private static final Log logger = LogFactory.getLog(Neo4jHealthIndicator.class);
/**
* The Cypher statement used to verify Neo4j is up.
*/
static final String CYPHER = "CALL dbms.components() YIELD versions, edition"
+ " UNWIND versions as version return version, edition";
private final SessionFactory sessionFactory;
static final String CYPHER = "CALL dbms.components() YIELD name, edition WHERE name = 'Neo4j Kernel' RETURN edition";
/**
* Create a new {@link Neo4jHealthIndicator} using the specified
* {@link SessionFactory}.
* @param sessionFactory the SessionFactory
* Message logged before retrying a health check.
*/
public Neo4jHealthIndicator(SessionFactory sessionFactory) {
super("Neo4J health check failed");
this.sessionFactory = sessionFactory;
static final String MESSAGE_SESSION_EXPIRED = "Neo4j session has expired, retrying one single time to retrieve server health.";
/**
* The default session config to use while connecting.
*/
static final SessionConfig DEFAULT_SESSION_CONFIG = SessionConfig.builder().withDefaultAccessMode(AccessMode.WRITE)
.build();
private final Driver driver;
private final Neo4jHealthDetailsHandler healthDetailsHandler;
public Neo4jHealthIndicator(Driver driver) {
super("Neo4j health check failed");
this.driver = driver;
this.healthDetailsHandler = new Neo4jHealthDetailsHandler();
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
Session session = this.sessionFactory.openSession();
extractResult(session, builder);
protected void doHealthCheck(Health.Builder builder) {
try {
try {
runHealthCheckQuery(builder);
}
catch (SessionExpiredException ex) {
// Retry one time when the session has been expired
logger.warn(MESSAGE_SESSION_EXPIRED);
runHealthCheckQuery(builder);
}
}
catch (Exception ex) {
builder.down().withException(ex);
}
}
/**
* Provide health details using the specified {@link Session} and {@link Builder
* Builder}.
* @param session the session to use to execute a cypher statement
* @param builder the builder to add details to
* @throws Exception if getting health details failed
*/
protected void extractResult(Session session, Health.Builder builder) throws Exception {
Result result = session.query(CYPHER, Collections.emptyMap());
builder.up().withDetails(result.queryResults().iterator().next());
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();
this.healthDetailsHandler.addHealthDetails(builder, edition, resultSummary);
}
}
}

View File

@ -0,0 +1,76 @@
/*
* 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.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
/**
* {@link ReactiveHealthIndicator} that tests the status of a Neo4j by executing a Cypher
* 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);
private final Driver driver;
private final Neo4jHealthDetailsHandler healthDetailsHandler;
public Neo4jReactiveHealthIndicator(Driver driver) {
this.driver = driver;
this.healthDetailsHandler = new Neo4jHealthDetailsHandler();
}
@Override
protected Mono<Health> doHealthCheck(Health.Builder builder) {
return runHealthCheckQuery()
.doOnError(SessionExpiredException.class,
(e) -> logger.warn(Neo4jHealthIndicator.MESSAGE_SESSION_EXPIRED))
.retryWhen(Retry.max(1).filter(SessionExpiredException.class::isInstance)).map((result) -> {
this.healthDetailsHandler.addHealthDetails(builder, result.getT1(), result.getT2());
return builder.build();
});
}
Mono<Tuple2<String, ResultSummary>> 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()));
}, RxSession::close);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.

View File

@ -16,25 +16,29 @@
package org.springframework.boot.actuate.neo4j;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.neo4j.ogm.exception.CypherException;
import org.neo4j.ogm.model.Result;
import org.neo4j.ogm.session.Session;
import org.neo4j.ogm.session.SessionFactory;
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;
/**
* Tests for {@link Neo4jHealthIndicator}.
@ -45,46 +49,85 @@ import static org.mockito.Mockito.mock;
*/
class Neo4jHealthIndicatorTests {
private Session session;
private Neo4jHealthIndicator neo4jHealthIndicator;
@BeforeEach
void before() {
this.session = mock(Session.class);
SessionFactory sessionFactory = mock(SessionFactory.class);
given(sessionFactory.openSession()).willReturn(this.session);
this.neo4jHealthIndicator = new Neo4jHealthIndicator(sessionFactory);
}
@Test
void neo4jUp() {
Result result = mock(Result.class);
given(this.session.query(Neo4jHealthIndicator.CYPHER, Collections.emptyMap())).willReturn(result);
Map<String, Object> expectedCypherDetails = new HashMap<>();
String edition = "community";
String version = "4.0.0";
expectedCypherDetails.put("edition", edition);
expectedCypherDetails.put("version", version);
List<Map<String, Object>> queryResults = new ArrayList<>();
queryResults.add(expectedCypherDetails);
given(result.queryResults()).willReturn(queryResults);
Health health = this.neo4jHealthIndicator.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);
Map<String, Object> details = health.getDetails();
String editionFromDetails = details.get("edition").toString();
String versionFromDetails = details.get("version").toString();
assertThat(editionFromDetails).isEqualTo(edition);
assertThat(versionFromDetails).isEqualTo(version);
assertThat(health.getDetails()).containsEntry("server", "4711@My Home");
assertThat(health.getDetails()).containsEntry("database", "test");
assertThat(health.getDetails()).containsEntry("edition", "ultimate collectors edition");
}
@Test
void neo4jDown() {
CypherException cypherException = new CypherException("Neo.ClientError.Statement.SyntaxError",
"Error executing Cypher");
given(this.session.query(Neo4jHealthIndicator.CYPHER, Collections.emptyMap())).willThrow(cypherException);
Health health = this.neo4jHealthIndicator.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@My Home");
assertThat(health.getDetails()).doesNotContainKey("database");
assertThat(health.getDetails()).containsEntry("edition", "some edition");
}
@Test
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@My Home");
assertThat(health.getDetails()).doesNotContainKey("database");
assertThat(health.getDetails()).containsEntry("edition", "some edition");
}
@Test
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 statementResult;
});
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@My Home");
verify(session, times(2)).close();
}
@Test
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;
}
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;
}
}

View File

@ -0,0 +1,116 @@
/*
* 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.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;
/**
* Tests for {@link Neo4jReactiveHealthIndicator}.
*
* @author Michael J. Simons
* @author Stephane Nicoll
*/
class Neo4jReactiveHealthIndicatorTest {
@Test
void neo4jIsUp() {
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) -> {
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails()).containsEntry("server", "4711@My Home");
assertThat(health.getDetails()).containsEntry("edition", "ultimate collectors edition");
}).verifyComplete();
}
@Test
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;
});
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) -> {
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails()).containsEntry("server", "4711@My Home");
assertThat(health.getDetails()).containsEntry("edition", "some edition");
}).verifyComplete();
verify(session, times(2)).close();
}
@Test
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) -> {
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;
}
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;
}
}

View File

@ -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;
}
}

View File

@ -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.
|===