diff --git a/driver/pom.xml b/driver/pom.xml index 7632350b17..8c2bdd9360 100644 --- a/driver/pom.xml +++ b/driver/pom.xml @@ -78,6 +78,10 @@ io.projectreactor reactor-test + + com.oracle.substratevm + svm + @@ -229,6 +233,9 @@ org.neo4j.driver.internal.shaded.reactor + + + true true diff --git a/driver/src/main/java/org/neo4j/driver/Config.java b/driver/src/main/java/org/neo4j/driver/Config.java index a02a608899..d307ce7042 100644 --- a/driver/src/main/java/org/neo4j/driver/Config.java +++ b/driver/src/main/java/org/neo4j/driver/Config.java @@ -285,6 +285,9 @@ RetrySettings retrySettings() return retrySettings; } + /** + * @return if the metrics is enabled or not on this driver. + */ public boolean isMetricsEnabled() { return isMetricsEnabled; diff --git a/driver/src/main/java/org/neo4j/driver/ConnectionPoolMetrics.java b/driver/src/main/java/org/neo4j/driver/ConnectionPoolMetrics.java index 15e6f502c4..f3d1d42732 100644 --- a/driver/src/main/java/org/neo4j/driver/ConnectionPoolMetrics.java +++ b/driver/src/main/java/org/neo4j/driver/ConnectionPoolMetrics.java @@ -102,7 +102,7 @@ enum PoolStatus /** * The total acquisition time in milliseconds of all connection acquisition requests since the pool is created. * See {@link ConnectionPoolMetrics#acquired()} for the total amount of connection acquired since the driver is created. - * The average acquisition time can be calculated using the code bellow: + * The average acquisition time can be calculated using the code below: *

Example

*
      * {@code
@@ -126,7 +126,7 @@ enum PoolStatus
     /**
      * The total time in milliseconds spent to establishing new socket connections since the pool is created.
      * See {@link ConnectionPoolMetrics#created()} for the total amount of connections established since the pool is created.
-     * The average connection time can be calculated using the code bellow:
+     * The average connection time can be calculated using the code below:
      * 

Example

*
      * {@code
@@ -150,7 +150,7 @@ enum PoolStatus
     /**
      * The total time in milliseconds connections are borrowed out of the pool, such as the time spent in user's application code to run cypher queries.
      * See {@link ConnectionPoolMetrics#totalInUseCount()} for the total amount of connections that are borrowed out of the pool.
-     * The average in-use time can be calculated using the code bellow:
+     * The average in-use time can be calculated using the code below:
      * 

Example

*
      * {@code
diff --git a/driver/src/main/java/org/neo4j/driver/Driver.java b/driver/src/main/java/org/neo4j/driver/Driver.java
index e68d6a72b4..40dc10bff1 100644
--- a/driver/src/main/java/org/neo4j/driver/Driver.java
+++ b/driver/src/main/java/org/neo4j/driver/Driver.java
@@ -19,11 +19,10 @@
 package org.neo4j.driver;
 
 import java.util.concurrent.CompletionStage;
-import java.util.function.Consumer;
 
 import org.neo4j.driver.async.AsyncSession;
 import org.neo4j.driver.exceptions.ClientException;
-import org.neo4j.driver.internal.SessionParameters;
+import org.neo4j.driver.internal.SessionConfig;
 import org.neo4j.driver.reactive.RxSession;
 import org.neo4j.driver.types.TypeSystem;
 import org.neo4j.driver.util.Experimental;
@@ -74,21 +73,62 @@ public interface Driver extends AutoCloseable
     boolean isEncrypted();
 
     /**
-     * Create a new general purpose {@link Session} with default {@link SessionParameters session parameters}.
+     * Create a new general purpose {@link Session} with default {@link SessionConfig session configuration}.
      * 

- * Alias to {@link #session(Consumer)}}. + * Alias to {@link #session(SessionConfig)}}. * * @return a new {@link Session} object. */ Session session(); /** - * Create a new {@link Session} with a specified {@link SessionParametersTemplate}. - * @param templateConsumer specifies how the session parameter shall be built for this session. + * Create a new {@link Session} with a specified {@link SessionConfig session configuration}. + * Use {@link SessionConfig#forDatabase(String)} to obtain a general purpose session configuration for the specified database. + * @param sessionConfig specifies session configurations for this session. * @return a new {@link Session} object. - * @see SessionParameters + * @see SessionConfig */ - Session session( Consumer templateConsumer ); + Session session( SessionConfig sessionConfig ); + + /** + * Create a new general purpose {@link RxSession} with default {@link SessionConfig session configuration}. + * The {@link RxSession} provides a reactive way to run queries and process results. + *

+ * Alias to {@link #rxSession(SessionConfig)}}. + * + * @return @return a new {@link RxSession} object. + */ + RxSession rxSession(); + + /** + * Create a new {@link RxSession} with a specified {@link SessionConfig session configuration}. + * Use {@link SessionConfig#forDatabase(String)} to obtain a general purpose session configuration for the specified database. + * The {@link RxSession} provides a reactive way to run queries and process results. + * @param sessionConfig used to customize the session. + * @return @return a new {@link RxSession} object. + */ + RxSession rxSession( SessionConfig sessionConfig ); + + /** + * Create a new general purpose {@link AsyncSession} with default {@link SessionConfig session configuration}. + * The {@link AsyncSession} provides an asynchronous way to run queries and process results. + *

+ * Alias to {@link #asyncSession(SessionConfig)}}. + * + * @return @return a new {@link AsyncSession} object. + */ + AsyncSession asyncSession(); + + /** + * Create a new {@link AsyncSession} with a specified {@link SessionConfig session configuration}. + * Use {@link SessionConfig#forDatabase(String)} to obtain a general purpose session configuration for the specified database. + * The {@link AsyncSession} provides an asynchronous way to run queries and process results. + * + * @param sessionConfig used to customize the session. + * @return a new {@link AsyncSession} object. + */ + AsyncSession asyncSession( SessionConfig sessionConfig ); + /** * Close all the resources assigned to this driver, including open connections and IO threads. *

@@ -113,51 +153,38 @@ public interface Driver extends AutoCloseable * @return the driver metrics if enabled. * @throws ClientException if the driver metrics reporting is not enabled. */ + @Experimental Metrics metrics(); /** - * Create a new general purpose {@link RxSession} with default {@link SessionParameters session parameters}. - * The {@link RxSession} provides a reactive way to run queries and process results. - *

- * Alias to {@link #rxSession(Consumer)}}. + * This will return the type system supported by the driver. + * The types supported on a particular server a session is connected against might not contain all of the types defined here. * - * @return @return a new {@link RxSession} object. - */ - RxSession rxSession(); - - /** - * Create a new {@link RxSession} with a specified {@link SessionParametersTemplate}. - * The {@link RxSession} provides a reactive way to run queries and process results. - * @param templateConsumer used to customize the session parameters. - * @return @return a new {@link RxSession} object. + * @return type system used by this statement runner for classifying values */ - RxSession rxSession( Consumer templateConsumer ); + @Experimental + TypeSystem defaultTypeSystem(); /** - * Create a new general purpose {@link AsyncSession} with default {@link SessionParameters session parameters}. - * The {@link AsyncSession} provides an asynchronous way to run queries and process results. - *

- * Alias to {@link #asyncSession(Consumer)}}. + * This verifies if the driver can connect to a remote server or a cluster + * by establishing a network connection with the remote and possibly exchanging a few data before closing the connection. * - * @return @return a new {@link AsyncSession} object. + * It throws exception if fails to connect. Use the exception to further understand the cause of the connectivity problem. + * Note: Even if this method throws an exception, the driver still need to be closed via {@link #close()} to free up all resources. */ - AsyncSession asyncSession(); + void verifyConnectivity(); /** - * Create a new {@link AsyncSession} with a specified {@link SessionParametersTemplate}. - * The {@link AsyncSession} provides an asynchronous way to run queries and process results. + * This verifies if the driver can connect to a remote server or cluster + * by establishing a network connection with the remote and possibly exchanging a few data before closing the connection. * - * @param templateConsumer used to customize the session parameters. - * @return a new {@link AsyncSession} object. - */ - AsyncSession asyncSession( Consumer templateConsumer ); - - /** - * This will return the type system supported by the driver. - * The types supported on a particular server a session is connected against might not contain all of the types defined here. + * This operation is asynchronous and returns a {@link CompletionStage}. This stage is completed with + * {@code null} when the driver connects to the remote server or cluster successfully. + * It is completed exceptionally if the driver failed to connect the remote server or cluster. + * This exception can be used to further understand the cause of the connectivity problem. + * Note: Even if this method complete exceptionally, the driver still need to be closed via {@link #closeAsync()} to free up all resources. * - * @return type system used by this statement runner for classifying values + * @return a {@link CompletionStage completion stage} that represents the asynchronous verification. */ - @Experimental - TypeSystem defaultTypeSystem(); + CompletionStage verifyConnectivityAsync(); } diff --git a/driver/src/main/java/org/neo4j/driver/GraphDatabase.java b/driver/src/main/java/org/neo4j/driver/GraphDatabase.java index 07b6207c66..1895ca40e5 100644 --- a/driver/src/main/java/org/neo4j/driver/GraphDatabase.java +++ b/driver/src/main/java/org/neo4j/driver/GraphDatabase.java @@ -154,19 +154,40 @@ public static Driver routingDriver( Iterable routingUris, AuthToken authTok for ( URI uri : routingUris ) { + final Driver driver = driver( uri, authToken, config ); try { - return driver( uri, authToken, config ); + driver.verifyConnectivity(); + return driver; } catch ( ServiceUnavailableException e ) { log.warn( "Unable to create routing driver for URI: " + uri, e ); + closeDriver( driver, uri, log ); + } + catch ( Throwable e ) + { + // for any other errors, we first close the driver and then rethrow the original error out. + closeDriver( driver, uri, log ); + throw e; } } throw new ServiceUnavailableException( "Failed to discover an available server" ); } + private static void closeDriver( Driver driver, URI uri, Logger log ) + { + try + { + driver.close(); + } + catch ( Throwable closeError ) + { + log.warn( "Unable to close driver towards URI: " + uri, closeError ); + } + } + private static void assertRoutingUris( Iterable uris ) { for ( URI uri : uris ) diff --git a/driver/src/main/java/org/neo4j/driver/SessionParametersTemplate.java b/driver/src/main/java/org/neo4j/driver/SessionParametersTemplate.java deleted file mode 100644 index a0b979549e..0000000000 --- a/driver/src/main/java/org/neo4j/driver/SessionParametersTemplate.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2002-2019 "Neo4j," - * Neo4j Sweden AB [http://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 - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.neo4j.driver; - -import java.util.List; - -/** - * The template used to configure session parameters which will be used to create a session. - */ -public interface SessionParametersTemplate -{ - /** - * Set the initial bookmarks to be used in a session. - * First transaction in a session will ensure that server hosting is at least as up-to-date as the - * latest transaction referenced by the supplied bookmarks. - * - * @param bookmarks a series of initial bookmarks. Both {@code null} value and empty array - * are permitted, and indicate that the bookmarks do not exist or are unknown. - * @return this builder. - */ - SessionParametersTemplate withBookmarks( String... bookmarks ); - - /** - * Set the initial bookmarks to be used in a session. - * First transaction in a session will ensure that server hosting is at least as up-to-date as the - * latest transaction referenced by the supplied bookmarks. - * - * @param bookmarks initial references to some previous transactions. Both {@code null} value and empty iterable - * are permitted, and indicate that the bookmarks do not exist or are unknown. - * @return this builder - */ - SessionParametersTemplate withBookmarks( List bookmarks ); - - /** - * Set the type of access required by units of work in this session, - * e.g. {@link AccessMode#READ read access} or {@link AccessMode#WRITE write access}. - * - * @param mode access mode. - * @return this builder. - */ - SessionParametersTemplate withDefaultAccessMode( AccessMode mode ); - - /** - * Set the database that the newly created session is going to connect to. - * If the database name is not set or the database name is set to null, - * then the default database configured in the server configuration will be connected when the session is established. - * For servers that do not support multi-databases, this database value should not be set or could only be set to null. - * - * @param database the database the session going to connect to. - * @return this builder. - */ - SessionParametersTemplate withDatabase( String database ); -} diff --git a/driver/src/main/java/org/neo4j/driver/exceptions/FatalDiscoveryException.java b/driver/src/main/java/org/neo4j/driver/exceptions/FatalDiscoveryException.java new file mode 100644 index 0000000000..7357122161 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/exceptions/FatalDiscoveryException.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.exceptions; + +/** + * This error indicate a fatal problem to obtain routing tables such as the routing table for a specified database does not exist. + * This exception should not be retried. + * @since 2.0 + */ +public class FatalDiscoveryException extends ClientException +{ + public FatalDiscoveryException( String message ) + { + super( message ); + } + + public FatalDiscoveryException( String code, String message ) + { + super( code, message ); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/exceptions/SecurityException.java b/driver/src/main/java/org/neo4j/driver/exceptions/SecurityException.java index f9304e3fef..8d5052b4b9 100644 --- a/driver/src/main/java/org/neo4j/driver/exceptions/SecurityException.java +++ b/driver/src/main/java/org/neo4j/driver/exceptions/SecurityException.java @@ -24,7 +24,7 @@ * Restart of server/driver/cluster might be required to recover from this error. * @since 1.1 */ -public class SecurityException extends Neo4jException +public class SecurityException extends ClientException { public SecurityException( String code, String message ) { diff --git a/driver/src/main/java/org/neo4j/driver/internal/DirectConnectionProvider.java b/driver/src/main/java/org/neo4j/driver/internal/DirectConnectionProvider.java index 329414c0b5..6bfbbbf6d0 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/DirectConnectionProvider.java +++ b/driver/src/main/java/org/neo4j/driver/internal/DirectConnectionProvider.java @@ -21,7 +21,7 @@ import java.util.concurrent.CompletionStage; import org.neo4j.driver.AccessMode; -import org.neo4j.driver.internal.async.connection.DecoratedConnection; +import org.neo4j.driver.internal.async.connection.DirectConnection; import org.neo4j.driver.internal.spi.Connection; import org.neo4j.driver.internal.spi.ConnectionPool; import org.neo4j.driver.internal.spi.ConnectionProvider; @@ -47,13 +47,14 @@ public class DirectConnectionProvider implements ConnectionProvider @Override public CompletionStage acquireConnection( String databaseName, AccessMode mode ) { - return connectionPool.acquire( address ).thenApply( connection -> new DecoratedConnection( connection, databaseName, mode ) ); + return connectionPool.acquire( address ).thenApply( connection -> new DirectConnection( connection, databaseName, mode ) ); } @Override public CompletionStage verifyConnectivity() { - // we verify the connection by establishing the connection to the default database + // We verify the connection by establishing a connection with the remote server specified by the address. + // Database name will be ignored as no query is run in this connection and the connection is released immediately. return acquireConnection( ABSENT_DB_NAME, READ ).thenCompose( Connection::release ); } diff --git a/driver/src/main/java/org/neo4j/driver/internal/DriverFactory.java b/driver/src/main/java/org/neo4j/driver/internal/DriverFactory.java index 0d3bca4022..24edb602df 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/DriverFactory.java +++ b/driver/src/main/java/org/neo4j/driver/internal/DriverFactory.java @@ -27,6 +27,13 @@ import java.net.URI; import java.security.GeneralSecurityException; +import org.neo4j.driver.AuthToken; +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Config; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Logger; +import org.neo4j.driver.Logging; +import org.neo4j.driver.exceptions.ClientException; import org.neo4j.driver.internal.async.connection.BootstrapFactory; import org.neo4j.driver.internal.async.connection.ChannelConnector; import org.neo4j.driver.internal.async.connection.ChannelConnectorImpl; @@ -49,19 +56,11 @@ import org.neo4j.driver.internal.spi.ConnectionProvider; import org.neo4j.driver.internal.util.Clock; import org.neo4j.driver.internal.util.Futures; -import org.neo4j.driver.AuthToken; -import org.neo4j.driver.AuthTokens; -import org.neo4j.driver.Config; -import org.neo4j.driver.Driver; -import org.neo4j.driver.Logger; -import org.neo4j.driver.Logging; -import org.neo4j.driver.exceptions.ClientException; -import org.neo4j.driver.exceptions.ServiceUnavailableException; import org.neo4j.driver.net.ServerAddressResolver; import static java.lang.String.format; -import static org.neo4j.driver.internal.metrics.MetricsProvider.METRICS_DISABLED_PROVIDER; import static org.neo4j.driver.internal.cluster.IdentityResolver.IDENTITY_RESOLVER; +import static org.neo4j.driver.internal.metrics.MetricsProvider.METRICS_DISABLED_PROVIDER; import static org.neo4j.driver.internal.security.SecurityPlan.insecure; import static org.neo4j.driver.internal.util.ErrorUtil.addSuppressed; @@ -105,11 +104,7 @@ public final Driver newInstance ( URI uri, AuthToken authToken, RoutingSettings MetricsProvider metricsProvider = createDriverMetrics( config, createClock() ); ConnectionPool connectionPool = createConnectionPool( authToken, securityPlan, bootstrap, metricsProvider, config, ownsEventLoopGroup ); - InternalDriver driver = createDriver( uri, securityPlan, address, connectionPool, eventExecutorGroup, newRoutingSettings, retryLogic, metricsProvider, config ); - - verifyConnectivity( driver, connectionPool, config ); - - return driver; + return createDriver( uri, securityPlan, address, connectionPool, eventExecutorGroup, newRoutingSettings, retryLogic, metricsProvider, config ); } protected ConnectionPool createConnectionPool( AuthToken authToken, SecurityPlan securityPlan, Bootstrap bootstrap, @@ -366,30 +361,6 @@ private static void assertNoRoutingContext( URI uri, RoutingSettings routingSett } } - private static void verifyConnectivity( InternalDriver driver, ConnectionPool connectionPool, Config config ) - { - try - { - // block to verify connectivity, close connection pool if thread gets interrupted - Futures.blockingGet( driver.verifyConnectivity(), - () -> closeConnectionPoolOnThreadInterrupt( connectionPool, config.logging() ) ); - } - catch ( Throwable connectionError ) - { - if ( Thread.currentThread().isInterrupted() ) - { - // current thread has been interrupted while verifying connectivity - // connection pool should've been closed - throw new ServiceUnavailableException( "Unable to create driver. Thread has been interrupted.", - connectionError ); - } - - // we need to close the connection pool if driver creation threw exception - closeConnectionPoolAndSuppressError( connectionPool, connectionError ); - throw connectionError; - } - } - private static void closeConnectionPoolAndSuppressError( ConnectionPool connectionPool, Throwable mainError ) { try @@ -401,11 +372,4 @@ private static void closeConnectionPoolAndSuppressError( ConnectionPool connecti addSuppressed( mainError, closeError ); } } - - private static void closeConnectionPoolOnThreadInterrupt( ConnectionPool pool, Logging logging ) - { - Logger log = logging.getLog( Driver.class.getSimpleName() ); - log.warn( "Driver creation interrupted while verifying connectivity. Connection pool will be closed" ); - pool.close(); - } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/InternalDriver.java b/driver/src/main/java/org/neo4j/driver/internal/InternalDriver.java index ee38263667..bfe2af94db 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/InternalDriver.java +++ b/driver/src/main/java/org/neo4j/driver/internal/InternalDriver.java @@ -20,14 +20,12 @@ import java.util.concurrent.CompletionStage; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; import org.neo4j.driver.Driver; import org.neo4j.driver.Logger; import org.neo4j.driver.Logging; import org.neo4j.driver.Metrics; import org.neo4j.driver.Session; -import org.neo4j.driver.SessionParametersTemplate; import org.neo4j.driver.async.AsyncSession; import org.neo4j.driver.internal.async.InternalAsyncSession; import org.neo4j.driver.internal.async.NetworkSession; @@ -61,43 +59,37 @@ public class InternalDriver implements Driver @Override public Session session() { - return new InternalSession( newSession( SessionParameters.empty() ) ); + return new InternalSession( newSession( SessionConfig.empty() ) ); } @Override - public Session session( Consumer templateConsumer ) + public Session session( SessionConfig sessionConfig ) { - SessionParameters.Template template = SessionParameters.template(); - templateConsumer.accept( template ); - return new InternalSession( newSession( template.build() ) ); + return new InternalSession( newSession( sessionConfig ) ); } @Override public RxSession rxSession() { - return new InternalRxSession( newSession( SessionParameters.empty() ) ); + return new InternalRxSession( newSession( SessionConfig.empty() ) ); } @Override - public RxSession rxSession( Consumer templateConsumer ) + public RxSession rxSession( SessionConfig sessionConfig ) { - SessionParameters.Template template = SessionParameters.template(); - templateConsumer.accept( template ); - return new InternalRxSession( newSession( template.build() ) ); + return new InternalRxSession( newSession( sessionConfig ) ); } @Override public AsyncSession asyncSession() { - return new InternalAsyncSession( newSession( SessionParameters.empty() ) ); + return new InternalAsyncSession( newSession( SessionConfig.empty() ) ); } @Override - public AsyncSession asyncSession( Consumer templateConsumer ) + public AsyncSession asyncSession( SessionConfig sessionConfig ) { - SessionParameters.Template template = SessionParameters.template(); - templateConsumer.accept( template ); - return new InternalAsyncSession( newSession( template.build() ) ); + return new InternalAsyncSession( newSession( sessionConfig ) ); } @Override @@ -136,11 +128,18 @@ public final TypeSystem defaultTypeSystem() return InternalTypeSystem.TYPE_SYSTEM; } - public CompletionStage verifyConnectivity() + @Override + public CompletionStage verifyConnectivityAsync() { return sessionFactory.verifyConnectivity(); } + @Override + public void verifyConnectivity() + { + Futures.blockingGet( verifyConnectivityAsync() ); + } + /** * Get the underlying session factory. *

@@ -158,7 +157,7 @@ private static RuntimeException driverCloseException() return new IllegalStateException( "This driver instance has already been closed" ); } - public NetworkSession newSession( SessionParameters parameters ) + public NetworkSession newSession( SessionConfig parameters ) { assertOpen(); NetworkSession session = sessionFactory.newInstance( parameters ); diff --git a/driver/src/main/java/org/neo4j/driver/internal/SessionConfig.java b/driver/src/main/java/org/neo4j/driver/internal/SessionConfig.java new file mode 100644 index 0000000000..110e63a456 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/SessionConfig.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.Session; +import org.neo4j.driver.async.AsyncSession; +import org.neo4j.driver.reactive.RxSession; + +import static java.util.Objects.requireNonNull; +import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.ABSENT_DB_NAME; + +/** + * The session configurations used to configure a session. + */ +public class SessionConfig +{ + private static final SessionConfig EMPTY = builder().build(); + + private final List bookmarks; + private final AccessMode defaultAccessMode; + private final String database; + + private SessionConfig( Builder builder ) + { + this.bookmarks = builder.bookmarks; + this.defaultAccessMode = builder.defaultAccessMode; + this.database = builder.database; + } + + /** + * Creates a new {@link Builder} used to construct a configuration object. + * + * @return a session configuration builder. + */ + public static Builder builder() + { + return new Builder(); + } + + /** + * Returns a static {@link SessionConfig} with default values for a general purpose session. + * + * @return a session config for a general purpose session. + */ + public static SessionConfig empty() + { + return EMPTY; + } + + /** + * Returns a {@link SessionConfig} for the specified database + * @param database the database the session binds to. + * @return a session config for a session for the specified database. + */ + public static SessionConfig forDatabase( String database ) + { + return new Builder().withDatabase( database ).build(); + } + + /** + * Returns the initial bookmarks. + * First transaction in the session created with this {@link SessionConfig} + * will ensure that server hosting is at least as up-to-date as the + * latest transaction referenced by the supplied initial bookmarks. + * + * @return the initial bookmarks. + */ + public List bookmarks() + { + return bookmarks; + } + + /** + * The type of access required by units of work in this session, + * e.g. {@link AccessMode#READ read access} or {@link AccessMode#WRITE write access}. + * + * @return the access mode. + */ + public AccessMode defaultAccessMode() + { + return defaultAccessMode; + } + + /** + * The database where the session is going to connect to. + * + * @return the nullable database name where the session is going to connect to. + */ + public Optional database() + { + return Optional.ofNullable( database ); + } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) + { + return true; + } + if ( o == null || getClass() != o.getClass() ) + { + return false; + } + SessionConfig that = (SessionConfig) o; + return Objects.equals( bookmarks, that.bookmarks ) && defaultAccessMode == that.defaultAccessMode && database.equals( that.database ); + } + + @Override + public int hashCode() + { + return Objects.hash( bookmarks, defaultAccessMode, database ); + } + + @Override + public String toString() + { + return "SessionParameters{" + "bookmarks=" + bookmarks + ", defaultAccessMode=" + defaultAccessMode + ", database='" + database + '\'' + '}'; + } + + /** + * Builder used to configure {@link SessionConfig} which will be used to create a session. + */ + public static class Builder + { + private List bookmarks = null; + private AccessMode defaultAccessMode = AccessMode.WRITE; + private String database = null; + + private Builder() + { + } + + /** + * Set the initial bookmarks to be used in a session. + *

+ * First transaction in a session will ensure that server hosting is at least as up-to-date as the + * latest transaction referenced by the supplied bookmarks. + * The bookmarks can be obtained via {@link Session#lastBookmark()}, {@link AsyncSession#lastBookmark()}, + * and/or {@link RxSession#lastBookmark()}. + * + * @param bookmarks a series of initial bookmarks. + * Both {@code null} value and empty array + * are permitted, and indicate that the bookmarks do not exist or are unknown. + * @return this builder. + */ + public Builder withBookmarks( String... bookmarks ) + { + if ( bookmarks == null ) + { + // TODO bookmarks should not be null + this.bookmarks = null; + } + else + { + this.bookmarks = Arrays.asList( bookmarks ); + } + return this; + } + + /** + * Set the initial bookmarks to be used in a session. + * First transaction in a session will ensure that server hosting is at least as up-to-date as the + * latest transaction referenced by the supplied bookmarks. + * The bookmarks can be obtained via {@link Session#lastBookmark()}, {@link AsyncSession#lastBookmark()}, + * and/or {@link RxSession#lastBookmark()}. + * + * @param bookmarks initial references to some previous transactions. Both {@code null} value and empty iterable + * are permitted, and indicate that the bookmarks do not exist or are unknown. + * @return this builder + */ + public Builder withBookmarks( List bookmarks ) + { + this.bookmarks = bookmarks; + return this; + } + + /** + * Set the type of access required by units of work in this session, + * e.g. {@link AccessMode#READ read access} or {@link AccessMode#WRITE write access}. + * This access mode is used to route transactions in the session to the server who has the right to carry out the specified operations. + * + * @param mode access mode. + * @return this builder. + */ + public Builder withDefaultAccessMode( AccessMode mode ) + { + this.defaultAccessMode = mode; + return this; + } + + /** + * Set the database that the newly created session is going to connect to. + *

+ * For connecting to servers that support multi-databases, + * it is highly recommended to always set the database explicitly in the {@link SessionConfig} for each session. + * If the database name is not set, then session defaults to connecting to the default database configured in server configuration. + *

+ * For servers that do not support multi-databases, leave this database value unset. The only database will be used instead. + * + * @param database the database the session going to connect to. Provided value should not be {@code null}. + * @return this builder. + */ + public Builder withDatabase( String database ) + { + requireNonNull( database, "Database name should not be null." ); + if ( ABSENT_DB_NAME.equals( database ) ) + { + // Disallow users to use bolt internal value directly. To users, this is totally an illegal database name. + throw new IllegalArgumentException( String.format( "Illegal database name '%s'.", database ) ); + } + this.database = database; + return this; + } + + public SessionConfig build() + { + return new SessionConfig( this ); + } + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/SessionFactory.java b/driver/src/main/java/org/neo4j/driver/internal/SessionFactory.java index 2863980644..0e0aef283c 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/SessionFactory.java +++ b/driver/src/main/java/org/neo4j/driver/internal/SessionFactory.java @@ -24,7 +24,7 @@ public interface SessionFactory { - NetworkSession newInstance( SessionParameters parameters ); + NetworkSession newInstance( SessionConfig parameters ); CompletionStage verifyConnectivity(); diff --git a/driver/src/main/java/org/neo4j/driver/internal/SessionFactoryImpl.java b/driver/src/main/java/org/neo4j/driver/internal/SessionFactoryImpl.java index ed552c77e1..5bd2c2b9eb 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/SessionFactoryImpl.java +++ b/driver/src/main/java/org/neo4j/driver/internal/SessionFactoryImpl.java @@ -46,7 +46,7 @@ public class SessionFactoryImpl implements SessionFactory } @Override - public NetworkSession newInstance( SessionParameters parameters ) + public NetworkSession newInstance( SessionConfig parameters ) { BookmarksHolder bookmarksHolder = new DefaultBookmarksHolder( Bookmarks.from( parameters.bookmarks() ) ); return createSession( connectionProvider, retryLogic, parameters.database().orElse( ABSENT_DB_NAME ), parameters.defaultAccessMode(), bookmarksHolder, logging ); diff --git a/driver/src/main/java/org/neo4j/driver/internal/SessionParameters.java b/driver/src/main/java/org/neo4j/driver/internal/SessionParameters.java deleted file mode 100644 index d6b2257fa3..0000000000 --- a/driver/src/main/java/org/neo4j/driver/internal/SessionParameters.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (c) 2002-2019 "Neo4j," - * Neo4j Sweden AB [http://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 - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.neo4j.driver.internal; - -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -import org.neo4j.driver.AccessMode; -import org.neo4j.driver.SessionParametersTemplate; - -import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.ABSENT_DB_NAME; - -/** - * The session parameters used to configure a session. - */ -public class SessionParameters -{ - private static final SessionParameters EMPTY = template().build(); - - private final List bookmarks; - private final AccessMode defaultAccessMode; - private final String database; - - /** - * Creates a session parameter template. - * @return a session parameter template. - */ - public static Template template() - { - return new Template(); - } - - /** - * Returns a static {@link SessionParameters} with default values for a general purpose session. - * @return a session parameter for a general purpose session. - */ - public static SessionParameters empty() - { - return EMPTY; - } - - private SessionParameters( Template template ) - { - this.bookmarks = template.bookmarks; - this.defaultAccessMode = template.defaultAccessMode; - this.database = template.database; - } - - /** - * Returns the initial bookmarks. - * First transaction in the session created with this {@link SessionParameters} - * will ensure that server hosting is at least as up-to-date as the - * latest transaction referenced by the supplied initial bookmarks. - * @return the initial bookmarks. - */ - public List bookmarks() - { - return bookmarks; - } - - /** - * The type of access required by units of work in this session, - * e.g. {@link AccessMode#READ read access} or {@link AccessMode#WRITE write access}. - * @return the access mode. - */ - public AccessMode defaultAccessMode() - { - return defaultAccessMode; - } - - /** - * The database where the session is going to connect to. - * @return the nullable database name where the session is going to connect to. - */ - public Optional database() - { - return Optional.ofNullable( database ); - } - - @Override - public boolean equals( Object o ) - { - if ( this == o ) - { - return true; - } - if ( o == null || getClass() != o.getClass() ) - { - return false; - } - SessionParameters that = (SessionParameters) o; - return Objects.equals( bookmarks, that.bookmarks ) && defaultAccessMode == that.defaultAccessMode && database.equals( that.database ); - } - - @Override - public int hashCode() - { - return Objects.hash( bookmarks, defaultAccessMode, database ); - } - - @Override - public String toString() - { - return "SessionParameters{" + "bookmarks=" + bookmarks + ", defaultAccessMode=" + defaultAccessMode + ", database='" + database + '\'' + '}'; - } - - public static class Template implements SessionParametersTemplate - { - private List bookmarks = null; - private AccessMode defaultAccessMode = AccessMode.WRITE; - private String database = null; - - private Template() - { - } - - @Override - public Template withBookmarks( String... bookmarks ) - { - if ( bookmarks == null ) - { - this.bookmarks = null; - } - else - { - this.bookmarks = Arrays.asList( bookmarks ); - } - return this; - } - - @Override - public Template withBookmarks( List bookmarks ) - { - this.bookmarks = bookmarks; - return this; - } - - @Override - public Template withDefaultAccessMode( AccessMode mode ) - { - this.defaultAccessMode = mode; - return this; - } - - @Override - public Template withDatabase( String database ) - { - if ( ABSENT_DB_NAME.equals( database ) ) - { - // Disallow users to use bolt internal value directly. To users, this is totally an illegal database name. - throw new IllegalArgumentException( String.format( "Illegal database name '%s'.", database ) ); - } - this.database = database; - return this; - } - - SessionParameters build() - { - return new SessionParameters( this ); - } - } -} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/NetworkConnection.java b/driver/src/main/java/org/neo4j/driver/internal/async/NetworkConnection.java new file mode 100644 index 0000000000..588fe028f4 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/NetworkConnection.java @@ -0,0 +1,311 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.async; + +import io.netty.channel.Channel; +import io.netty.channel.pool.ChannelPool; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicReference; + +import org.neo4j.driver.internal.BoltServerAddress; +import org.neo4j.driver.internal.async.connection.ChannelAttributes; +import org.neo4j.driver.internal.async.inbound.InboundMessageDispatcher; +import org.neo4j.driver.internal.handlers.ChannelReleasingResetResponseHandler; +import org.neo4j.driver.internal.handlers.ResetResponseHandler; +import org.neo4j.driver.internal.messaging.BoltProtocol; +import org.neo4j.driver.internal.messaging.Message; +import org.neo4j.driver.internal.messaging.request.ResetMessage; +import org.neo4j.driver.internal.metrics.ListenerEvent; +import org.neo4j.driver.internal.metrics.MetricsListener; +import org.neo4j.driver.internal.spi.Connection; +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.internal.util.Clock; +import org.neo4j.driver.internal.util.ServerVersion; + +import static java.util.Collections.emptyMap; +import static org.neo4j.driver.internal.async.connection.ChannelAttributes.setTerminationReason; + +/** + * This connection represents a simple network connection to a remote server. + * It wraps a channel obtained from a connection pool. + * The life cycle of this connection start from the moment the channel is borrowed out of the pool + * and end at the time the connection is released back to the pool. + */ +public class NetworkConnection implements Connection +{ + private final Channel channel; + private final InboundMessageDispatcher messageDispatcher; + private final BoltServerAddress serverAddress; + private final ServerVersion serverVersion; + private final BoltProtocol protocol; + private final ChannelPool channelPool; + private final CompletableFuture releaseFuture; + private final Clock clock; + + private final AtomicReference status = new AtomicReference<>( Status.OPEN ); + private final MetricsListener metricsListener; + private final ListenerEvent inUseEvent; + + public NetworkConnection( Channel channel, ChannelPool channelPool, Clock clock, MetricsListener metricsListener ) + { + this.channel = channel; + this.messageDispatcher = ChannelAttributes.messageDispatcher( channel ); + this.serverAddress = ChannelAttributes.serverAddress( channel ); + this.serverVersion = ChannelAttributes.serverVersion( channel ); + this.protocol = BoltProtocol.forChannel( channel ); + this.channelPool = channelPool; + this.releaseFuture = new CompletableFuture<>(); + this.clock = clock; + this.metricsListener = metricsListener; + this.inUseEvent = metricsListener.createListenerEvent(); + metricsListener.afterConnectionCreated( this.serverAddress, this.inUseEvent ); + } + + @Override + public boolean isOpen() + { + return status.get() == Status.OPEN; + } + + @Override + public void enableAutoRead() + { + if ( isOpen() ) + { + setAutoRead( true ); + } + } + + @Override + public void disableAutoRead() + { + if ( isOpen() ) + { + setAutoRead( false ); + } + } + + @Override + public void flush() + { + if ( verifyOpen( null, null ) ) + { + flushInEventLoop(); + } + } + + @Override + public void write( Message message, ResponseHandler handler ) + { + if ( verifyOpen( handler, null ) ) + { + writeMessageInEventLoop( message, handler, false ); + } + } + + @Override + public void write( Message message1, ResponseHandler handler1, Message message2, ResponseHandler handler2 ) + { + if ( verifyOpen( handler1, handler2 ) ) + { + writeMessagesInEventLoop( message1, handler1, message2, handler2, false ); + } + } + + @Override + public void writeAndFlush( Message message, ResponseHandler handler ) + { + if ( verifyOpen( handler, null ) ) + { + writeMessageInEventLoop( message, handler, true ); + } + } + + @Override + public void writeAndFlush( Message message1, ResponseHandler handler1, Message message2, ResponseHandler handler2 ) + { + if ( verifyOpen( handler1, handler2 ) ) + { + writeMessagesInEventLoop( message1, handler1, message2, handler2, true ); + } + } + + @Override + public CompletionStage reset() + { + CompletableFuture result = new CompletableFuture<>(); + ResetResponseHandler handler = new ResetResponseHandler( messageDispatcher, result ); + writeResetMessageIfNeeded( handler, true ); + return result; + } + + @Override + public CompletionStage release() + { + if ( status.compareAndSet( Status.OPEN, Status.RELEASED ) ) + { + ChannelReleasingResetResponseHandler handler = new ChannelReleasingResetResponseHandler( channel, + channelPool, messageDispatcher, clock, releaseFuture ); + + writeResetMessageIfNeeded( handler, false ); + metricsListener.afterConnectionReleased( this.serverAddress, this.inUseEvent ); + } + return releaseFuture; + } + + @Override + public void terminateAndRelease( String reason ) + { + if ( status.compareAndSet( Status.OPEN, Status.TERMINATED ) ) + { + setTerminationReason( channel, reason ); + channel.close(); + channelPool.release( channel ); + releaseFuture.complete( null ); + metricsListener.afterConnectionReleased( this.serverAddress, this.inUseEvent ); + } + } + + @Override + public BoltServerAddress serverAddress() + { + return serverAddress; + } + + @Override + public ServerVersion serverVersion() + { + return serverVersion; + } + + @Override + public BoltProtocol protocol() + { + return protocol; + } + + private void writeResetMessageIfNeeded( ResponseHandler resetHandler, boolean isSessionReset ) + { + channel.eventLoop().execute( () -> + { + if ( isSessionReset && !isOpen() ) + { + resetHandler.onSuccess( emptyMap() ); + } + else + { + // auto-read could've been disabled, re-enable it to automatically receive response for RESET + setAutoRead( true ); + + messageDispatcher.enqueue( resetHandler ); + channel.writeAndFlush( ResetMessage.RESET, channel.voidPromise() ); + } + } ); + } + + private void flushInEventLoop() + { + channel.eventLoop().execute( channel::flush ); + } + + private void writeMessageInEventLoop( Message message, ResponseHandler handler, boolean flush ) + { + channel.eventLoop().execute( () -> + { + messageDispatcher.enqueue( handler ); + + if ( flush ) + { + channel.writeAndFlush( message, channel.voidPromise() ); + } + else + { + channel.write( message, channel.voidPromise() ); + } + } ); + } + + private void writeMessagesInEventLoop( Message message1, ResponseHandler handler1, Message message2, ResponseHandler handler2, boolean flush ) + { + channel.eventLoop().execute( () -> + { + messageDispatcher.enqueue( handler1 ); + messageDispatcher.enqueue( handler2 ); + + channel.write( message1, channel.voidPromise() ); + + if ( flush ) + { + channel.writeAndFlush( message2, channel.voidPromise() ); + } + else + { + channel.write( message2, channel.voidPromise() ); + } + } ); + } + + private void setAutoRead( boolean value ) + { + channel.config().setAutoRead( value ); + } + + private boolean verifyOpen( ResponseHandler handler1, ResponseHandler handler2 ) + { + Status connectionStatus = this.status.get(); + switch ( connectionStatus ) + { + case OPEN: + return true; + case RELEASED: + Exception error = new IllegalStateException( "Connection has been released to the pool and can't be used" ); + if ( handler1 != null ) + { + handler1.onFailure( error ); + } + if ( handler2 != null ) + { + handler2.onFailure( error ); + } + return false; + case TERMINATED: + Exception terminatedError = new IllegalStateException( "Connection has been terminated and can't be used" ); + if ( handler1 != null ) + { + handler1.onFailure( terminatedError ); + } + if ( handler2 != null ) + { + handler2.onFailure( terminatedError ); + } + return false; + default: + throw new IllegalStateException( "Unknown status: " + connectionStatus ); + } + } + + private enum Status + { + OPEN, + RELEASED, + TERMINATED + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/connection/DecoratedConnection.java b/driver/src/main/java/org/neo4j/driver/internal/async/connection/DecoratedConnection.java deleted file mode 100644 index 29cacaed80..0000000000 --- a/driver/src/main/java/org/neo4j/driver/internal/async/connection/DecoratedConnection.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (c) 2002-2019 "Neo4j," - * Neo4j Sweden AB [http://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 - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.neo4j.driver.internal.async.connection; - -import java.util.concurrent.CompletionStage; - -import org.neo4j.driver.internal.BoltServerAddress; -import org.neo4j.driver.internal.messaging.BoltProtocol; -import org.neo4j.driver.internal.messaging.Message; -import org.neo4j.driver.internal.spi.Connection; -import org.neo4j.driver.internal.spi.ResponseHandler; -import org.neo4j.driver.internal.util.ServerVersion; -import org.neo4j.driver.AccessMode; - -/** - * This is a connection with extra parameters such as database name and access mode - */ -public class DecoratedConnection implements Connection -{ - private final Connection delegate; - private final AccessMode mode; - private final String databaseName; - - public DecoratedConnection( Connection delegate, String databaseName, AccessMode mode ) - { - this.delegate = delegate; - this.mode = mode; - this.databaseName = databaseName; - } - - public Connection connection() - { - return delegate; - } - - @Override - public boolean isOpen() - { - return delegate.isOpen(); - } - - @Override - public void enableAutoRead() - { - delegate.enableAutoRead(); - } - - @Override - public void disableAutoRead() - { - delegate.disableAutoRead(); - } - - @Override - public void write( Message message, ResponseHandler handler ) - { - delegate.write( message, handler ); - } - - @Override - public void write( Message message1, ResponseHandler handler1, Message message2, ResponseHandler handler2 ) - { - delegate.write( message1, handler1, message2, handler2 ); - } - - @Override - public void writeAndFlush( Message message, ResponseHandler handler ) - { - delegate.writeAndFlush( message, handler ); - } - - @Override - public void writeAndFlush( Message message1, ResponseHandler handler1, Message message2, ResponseHandler handler2 ) - { - delegate.writeAndFlush( message1, handler1, message2, handler2 ); - } - - @Override - public CompletionStage reset() - { - return delegate.reset(); - } - - @Override - public CompletionStage release() - { - return delegate.release(); - } - - @Override - public void terminateAndRelease( String reason ) - { - delegate.terminateAndRelease( reason ); - } - - @Override - public BoltServerAddress serverAddress() - { - return delegate.serverAddress(); - } - - @Override - public ServerVersion serverVersion() - { - return delegate.serverVersion(); - } - - @Override - public BoltProtocol protocol() - { - return delegate.protocol(); - } - - @Override - public AccessMode mode() - { - return mode; - } - - @Override - public String databaseName() - { - return this.databaseName; - } - - @Override - public void flush() - { - delegate.flush(); - } -} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/connection/DirectConnection.java b/driver/src/main/java/org/neo4j/driver/internal/async/connection/DirectConnection.java index 70820796d0..2abeecf9ae 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/async/connection/DirectConnection.java +++ b/driver/src/main/java/org/neo4j/driver/internal/async/connection/DirectConnection.java @@ -18,287 +18,131 @@ */ package org.neo4j.driver.internal.async.connection; -import io.netty.channel.Channel; -import io.netty.channel.pool.ChannelPool; - -import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; -import java.util.concurrent.atomic.AtomicReference; import org.neo4j.driver.internal.BoltServerAddress; -import org.neo4j.driver.internal.async.inbound.InboundMessageDispatcher; -import org.neo4j.driver.internal.handlers.ChannelReleasingResetResponseHandler; -import org.neo4j.driver.internal.handlers.ResetResponseHandler; +import org.neo4j.driver.internal.DirectConnectionProvider; import org.neo4j.driver.internal.messaging.BoltProtocol; import org.neo4j.driver.internal.messaging.Message; -import org.neo4j.driver.internal.messaging.request.ResetMessage; -import org.neo4j.driver.internal.metrics.ListenerEvent; -import org.neo4j.driver.internal.metrics.MetricsListener; import org.neo4j.driver.internal.spi.Connection; import org.neo4j.driver.internal.spi.ResponseHandler; -import org.neo4j.driver.internal.util.Clock; import org.neo4j.driver.internal.util.ServerVersion; +import org.neo4j.driver.AccessMode; -import static java.util.Collections.emptyMap; -import static org.neo4j.driver.internal.async.connection.ChannelAttributes.setTerminationReason; - +/** + * This is a connection used by {@link DirectConnectionProvider} to connect to a remote database. + */ public class DirectConnection implements Connection { - private final Channel channel; - private final InboundMessageDispatcher messageDispatcher; - private final BoltServerAddress serverAddress; - private final ServerVersion serverVersion; - private final BoltProtocol protocol; - private final ChannelPool channelPool; - private final CompletableFuture releaseFuture; - private final Clock clock; + private final Connection delegate; + private final AccessMode mode; + private final String databaseName; - private final AtomicReference status = new AtomicReference<>( Status.OPEN ); - private final MetricsListener metricsListener; - private final ListenerEvent inUseEvent; + public DirectConnection( Connection delegate, String databaseName, AccessMode mode ) + { + this.delegate = delegate; + this.mode = mode; + this.databaseName = databaseName; + } - public DirectConnection( Channel channel, ChannelPool channelPool, Clock clock, MetricsListener metricsListener ) + public Connection connection() { - this.channel = channel; - this.messageDispatcher = ChannelAttributes.messageDispatcher( channel ); - this.serverAddress = ChannelAttributes.serverAddress( channel ); - this.serverVersion = ChannelAttributes.serverVersion( channel ); - this.protocol = BoltProtocol.forChannel( channel ); - this.channelPool = channelPool; - this.releaseFuture = new CompletableFuture<>(); - this.clock = clock; - this.metricsListener = metricsListener; - this.inUseEvent = metricsListener.createListenerEvent(); - metricsListener.afterConnectionCreated( this.serverAddress, this.inUseEvent ); + return delegate; } @Override public boolean isOpen() { - return status.get() == Status.OPEN; + return delegate.isOpen(); } @Override public void enableAutoRead() { - if ( isOpen() ) - { - setAutoRead( true ); - } + delegate.enableAutoRead(); } @Override public void disableAutoRead() { - if ( isOpen() ) - { - setAutoRead( false ); - } - } - - @Override - public void flush() - { - if ( verifyOpen( null, null ) ) - { - flushInEventLoop(); - } + delegate.disableAutoRead(); } @Override public void write( Message message, ResponseHandler handler ) { - if ( verifyOpen( handler, null ) ) - { - writeMessageInEventLoop( message, handler, false ); - } + delegate.write( message, handler ); } @Override public void write( Message message1, ResponseHandler handler1, Message message2, ResponseHandler handler2 ) { - if ( verifyOpen( handler1, handler2 ) ) - { - writeMessagesInEventLoop( message1, handler1, message2, handler2, false ); - } + delegate.write( message1, handler1, message2, handler2 ); } @Override public void writeAndFlush( Message message, ResponseHandler handler ) { - if ( verifyOpen( handler, null ) ) - { - writeMessageInEventLoop( message, handler, true ); - } + delegate.writeAndFlush( message, handler ); } @Override public void writeAndFlush( Message message1, ResponseHandler handler1, Message message2, ResponseHandler handler2 ) { - if ( verifyOpen( handler1, handler2 ) ) - { - writeMessagesInEventLoop( message1, handler1, message2, handler2, true ); - } + delegate.writeAndFlush( message1, handler1, message2, handler2 ); } @Override public CompletionStage reset() { - CompletableFuture result = new CompletableFuture<>(); - ResetResponseHandler handler = new ResetResponseHandler( messageDispatcher, result ); - writeResetMessageIfNeeded( handler, true ); - return result; + return delegate.reset(); } @Override public CompletionStage release() { - if ( status.compareAndSet( Status.OPEN, Status.RELEASED ) ) - { - ChannelReleasingResetResponseHandler handler = new ChannelReleasingResetResponseHandler( channel, - channelPool, messageDispatcher, clock, releaseFuture ); - - writeResetMessageIfNeeded( handler, false ); - metricsListener.afterConnectionReleased( this.serverAddress, this.inUseEvent ); - } - return releaseFuture; + return delegate.release(); } @Override public void terminateAndRelease( String reason ) { - if ( status.compareAndSet( Status.OPEN, Status.TERMINATED ) ) - { - setTerminationReason( channel, reason ); - channel.close(); - channelPool.release( channel ); - releaseFuture.complete( null ); - metricsListener.afterConnectionReleased( this.serverAddress, this.inUseEvent ); - } + delegate.terminateAndRelease( reason ); } @Override public BoltServerAddress serverAddress() { - return serverAddress; + return delegate.serverAddress(); } @Override public ServerVersion serverVersion() { - return serverVersion; + return delegate.serverVersion(); } @Override public BoltProtocol protocol() { - return protocol; - } - - private void writeResetMessageIfNeeded( ResponseHandler resetHandler, boolean isSessionReset ) - { - channel.eventLoop().execute( () -> - { - if ( isSessionReset && !isOpen() ) - { - resetHandler.onSuccess( emptyMap() ); - } - else - { - // auto-read could've been disabled, re-enable it to automatically receive response for RESET - setAutoRead( true ); - - messageDispatcher.enqueue( resetHandler ); - channel.writeAndFlush( ResetMessage.RESET, channel.voidPromise() ); - } - } ); + return delegate.protocol(); } - private void flushInEventLoop() - { - channel.eventLoop().execute( channel::flush ); - } - - private void writeMessageInEventLoop( Message message, ResponseHandler handler, boolean flush ) - { - channel.eventLoop().execute( () -> - { - messageDispatcher.enqueue( handler ); - - if ( flush ) - { - channel.writeAndFlush( message, channel.voidPromise() ); - } - else - { - channel.write( message, channel.voidPromise() ); - } - } ); - } - - private void writeMessagesInEventLoop( Message message1, ResponseHandler handler1, Message message2, ResponseHandler handler2, boolean flush ) - { - channel.eventLoop().execute( () -> - { - messageDispatcher.enqueue( handler1 ); - messageDispatcher.enqueue( handler2 ); - - channel.write( message1, channel.voidPromise() ); - - if ( flush ) - { - channel.writeAndFlush( message2, channel.voidPromise() ); - } - else - { - channel.write( message2, channel.voidPromise() ); - } - } ); - } - - private void setAutoRead( boolean value ) + @Override + public AccessMode mode() { - channel.config().setAutoRead( value ); + return mode; } - private boolean verifyOpen( ResponseHandler handler1, ResponseHandler handler2 ) + @Override + public String databaseName() { - Status connectionStatus = this.status.get(); - switch ( connectionStatus ) - { - case OPEN: - return true; - case RELEASED: - Exception error = new IllegalStateException( "Connection has been released to the pool and can't be used" ); - if ( handler1 != null ) - { - handler1.onFailure( error ); - } - if ( handler2 != null ) - { - handler2.onFailure( error ); - } - return false; - case TERMINATED: - Exception terminatedError = new IllegalStateException( "Connection has been terminated and can't be used" ); - if ( handler1 != null ) - { - handler1.onFailure( terminatedError ); - } - if ( handler2 != null ) - { - handler2.onFailure( terminatedError ); - } - return false; - default: - throw new IllegalStateException( "Unknown status: " + connectionStatus ); - } + return this.databaseName; } - private enum Status + @Override + public void flush() { - OPEN, - RELEASED, - TERMINATED + delegate.flush(); } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/connection/RoutingConnection.java b/driver/src/main/java/org/neo4j/driver/internal/async/connection/RoutingConnection.java index 16528529a3..856bc7766e 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/async/connection/RoutingConnection.java +++ b/driver/src/main/java/org/neo4j/driver/internal/async/connection/RoutingConnection.java @@ -30,15 +30,20 @@ import org.neo4j.driver.internal.util.ServerVersion; import org.neo4j.driver.AccessMode; +/** + * A connection used by the routing driver. + */ public class RoutingConnection implements Connection { private final Connection delegate; private final AccessMode accessMode; private final RoutingErrorHandler errorHandler; + private final String databaseName; - public RoutingConnection( Connection delegate, AccessMode accessMode, RoutingErrorHandler errorHandler ) + public RoutingConnection( Connection delegate, String databaseName, AccessMode accessMode, RoutingErrorHandler errorHandler ) { this.delegate = delegate; + this.databaseName = databaseName; this.accessMode = accessMode; this.errorHandler = errorHandler; } @@ -127,6 +132,19 @@ public void flush() delegate.flush(); } + @Override + public AccessMode mode() + { + return this.accessMode; + } + + @Override + public String databaseName() + { + return this.databaseName; + } + + private RoutingResponseHandler newRoutingResponseHandler( ResponseHandler handler ) { return new RoutingResponseHandler( handler, serverAddress(), accessMode, errorHandler ); diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/pool/ConnectionFactory.java b/driver/src/main/java/org/neo4j/driver/internal/async/pool/ConnectionFactory.java new file mode 100644 index 0000000000..b19962969f --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/pool/ConnectionFactory.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.async.pool; + +import io.netty.channel.Channel; + +import org.neo4j.driver.internal.spi.Connection; + +public interface ConnectionFactory +{ + Connection createConnection( Channel channel, ExtendedChannelPool pool ); +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/pool/ConnectionPoolImpl.java b/driver/src/main/java/org/neo4j/driver/internal/async/pool/ConnectionPoolImpl.java index 3c7576824c..cadd31efff 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/async/pool/ConnectionPoolImpl.java +++ b/driver/src/main/java/org/neo4j/driver/internal/async/pool/ConnectionPoolImpl.java @@ -40,7 +40,6 @@ import org.neo4j.driver.exceptions.ServiceUnavailableException; import org.neo4j.driver.internal.BoltServerAddress; import org.neo4j.driver.internal.async.connection.ChannelConnector; -import org.neo4j.driver.internal.async.connection.DirectConnection; import org.neo4j.driver.internal.metrics.ListenerEvent; import org.neo4j.driver.internal.metrics.MetricsListener; import org.neo4j.driver.internal.spi.Connection; @@ -57,22 +56,23 @@ public class ConnectionPoolImpl implements ConnectionPool private final NettyChannelTracker nettyChannelTracker; private final NettyChannelHealthChecker channelHealthChecker; private final PoolSettings settings; - private final Clock clock; private final Logger log; private final MetricsListener metricsListener; private final boolean ownsEventLoopGroup; private final ConcurrentMap pools = new ConcurrentHashMap<>(); private final AtomicBoolean closed = new AtomicBoolean(); + private final ConnectionFactory connectionFactory; - public ConnectionPoolImpl( ChannelConnector connector, Bootstrap bootstrap, PoolSettings settings, MetricsListener metricsListener, Logging logging, Clock clock, boolean ownsEventLoopGroup ) + public ConnectionPoolImpl( ChannelConnector connector, Bootstrap bootstrap, PoolSettings settings, MetricsListener metricsListener, Logging logging, + Clock clock, boolean ownsEventLoopGroup ) { - - this( connector, bootstrap, new NettyChannelTracker( metricsListener, bootstrap.config().group().next(), logging ), settings, metricsListener, logging, clock, ownsEventLoopGroup ); + this( connector, bootstrap, new NettyChannelTracker( metricsListener, bootstrap.config().group().next(), logging ), settings, metricsListener, logging, + clock, ownsEventLoopGroup, new NetworkConnectionFactory( clock, metricsListener ) ); } - ConnectionPoolImpl( ChannelConnector connector, Bootstrap bootstrap, NettyChannelTracker nettyChannelTracker, - PoolSettings settings, MetricsListener metricsListener, Logging logging, Clock clock, boolean ownsEventLoopGroup ) + public ConnectionPoolImpl( ChannelConnector connector, Bootstrap bootstrap, NettyChannelTracker nettyChannelTracker, PoolSettings settings, + MetricsListener metricsListener, Logging logging, Clock clock, boolean ownsEventLoopGroup, ConnectionFactory connectionFactory ) { this.connector = connector; this.bootstrap = bootstrap; @@ -80,9 +80,9 @@ public ConnectionPoolImpl( ChannelConnector connector, Bootstrap bootstrap, Pool this.channelHealthChecker = new NettyChannelHealthChecker( settings, clock, logging ); this.settings = settings; this.metricsListener = metricsListener; - this.clock = clock; this.log = logging.getLog( ConnectionPool.class.getSimpleName() ); this.ownsEventLoopGroup = ownsEventLoopGroup; + this.connectionFactory = connectionFactory; } @Override @@ -103,7 +103,7 @@ public CompletionStage acquire( BoltServerAddress address ) { processAcquisitionError( pool, address, error ); assertNotClosed( address, channel, pool ); - Connection connection = new DirectConnection( channel, pool, clock, metricsListener ); + Connection connection = connectionFactory.createConnection( channel, pool ); metricsListener.afterAcquiredOrCreated( address, acquireEvent ); return connection; @@ -127,7 +127,6 @@ public void retainAll( Set addressesToRetain ) { // address is not present in updated routing table and has no active connections // it's now safe to terminate corresponding connection pool and forget about it - ChannelPool pool = pools.remove( address ); if ( pool != null ) { diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/pool/NettyChannelTracker.java b/driver/src/main/java/org/neo4j/driver/internal/async/pool/NettyChannelTracker.java index eca8b655ac..bd6cf5ea17 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/async/pool/NettyChannelTracker.java +++ b/driver/src/main/java/org/neo4j/driver/internal/async/pool/NettyChannelTracker.java @@ -71,7 +71,7 @@ public void channelReleased( Channel channel ) @Override public void channelAcquired( Channel channel ) { - log.debug( "Channel [%s] acquired from the pool. Local address: %s, remote address: %s", + log.debug( "Channel [0x%s] acquired from the pool. Local address: %s, remote address: %s", channel.id(), channel.localAddress(), channel.remoteAddress() ); incrementInUse( channel ); @@ -87,10 +87,10 @@ public void channelCreated( Channel channel ) public void channelCreated( Channel channel, ListenerEvent creatingEvent ) { - log.debug( "Channel [%s] created. Local address: %s, remote address: %s", + log.debug( "Channel [0x%s] created. Local address: %s, remote address: %s", channel.id(), channel.localAddress(), channel.remoteAddress() ); - incrementInUse( channel ); + incrementIdle( channel ); metricsListener.afterCreated( serverAddress( channel ), creatingEvent ); allChannels.add( channel ); diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/pool/NetworkConnectionFactory.java b/driver/src/main/java/org/neo4j/driver/internal/async/pool/NetworkConnectionFactory.java new file mode 100644 index 0000000000..6cd9c00f28 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/pool/NetworkConnectionFactory.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.async.pool; + +import io.netty.channel.Channel; + +import org.neo4j.driver.internal.async.NetworkConnection; +import org.neo4j.driver.internal.metrics.MetricsListener; +import org.neo4j.driver.internal.spi.Connection; +import org.neo4j.driver.internal.util.Clock; + +public class NetworkConnectionFactory implements ConnectionFactory +{ + private final Clock clock; + private final MetricsListener metricsListener; + + public NetworkConnectionFactory( Clock clock, MetricsListener metricsListener ) + { + this.clock = clock; + this.metricsListener = metricsListener; + } + + @Override + public Connection createConnection( Channel channel, ExtendedChannelPool pool ) + { + return new NetworkConnection( channel, pool, clock, metricsListener ); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/ClusterComposition.java b/driver/src/main/java/org/neo4j/driver/internal/cluster/ClusterComposition.java index 50a4feb36a..80e2db1238 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/cluster/ClusterComposition.java +++ b/driver/src/main/java/org/neo4j/driver/internal/cluster/ClusterComposition.java @@ -21,24 +21,16 @@ import java.util.LinkedHashSet; import java.util.Objects; import java.util.Set; +import java.util.function.Function; -import org.neo4j.driver.internal.BoltServerAddress; import org.neo4j.driver.Record; import org.neo4j.driver.Value; -import java.util.function.Function; +import org.neo4j.driver.internal.BoltServerAddress; public final class ClusterComposition { private static final long MAX_TTL = Long.MAX_VALUE / 1000L; - private static final Function OF_BoltServerAddress = - new Function() - { - @Override - public BoltServerAddress apply( Value value ) - { - return new BoltServerAddress( value.asString() ); - } - }; + private static final Function OF_BoltServerAddress = value -> new BoltServerAddress( value.asString() ); private final Set readers; private final Set writers; diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/ClusterCompositionProvider.java b/driver/src/main/java/org/neo4j/driver/internal/cluster/ClusterCompositionProvider.java index 65a5c8961e..ff58366e75 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/cluster/ClusterCompositionProvider.java +++ b/driver/src/main/java/org/neo4j/driver/internal/cluster/ClusterCompositionProvider.java @@ -24,6 +24,5 @@ public interface ClusterCompositionProvider { - CompletionStage getClusterComposition( - CompletionStage connectionStage ); + CompletionStage getClusterComposition( CompletionStage connectionStage, String databaseName ); } diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/ClusterCompositionResponse.java b/driver/src/main/java/org/neo4j/driver/internal/cluster/ClusterCompositionResponse.java deleted file mode 100644 index 461e52cfdb..0000000000 --- a/driver/src/main/java/org/neo4j/driver/internal/cluster/ClusterCompositionResponse.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2002-2019 "Neo4j," - * Neo4j Sweden AB [http://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 - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.neo4j.driver.internal.cluster; - -public interface ClusterCompositionResponse -{ - ClusterComposition clusterComposition(); - - class Failure implements ClusterCompositionResponse - { - private final RuntimeException error; - - public Failure( RuntimeException t ) - { - this.error = t; - } - - @Override - public ClusterComposition clusterComposition() - { - throw this.error; - } - } - - class Success implements ClusterCompositionResponse - { - private final ClusterComposition cluster; - - public Success( ClusterComposition cluster ) - { - this.cluster = cluster; - } - - @Override - public ClusterComposition clusterComposition() - { - return cluster; - } - } -} diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/ClusterRoutingTable.java b/driver/src/main/java/org/neo4j/driver/internal/cluster/ClusterRoutingTable.java index a51141bb99..eb1d32d0a1 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/cluster/ClusterRoutingTable.java +++ b/driver/src/main/java/org/neo4j/driver/internal/cluster/ClusterRoutingTable.java @@ -23,9 +23,9 @@ import java.util.LinkedHashSet; import java.util.Set; +import org.neo4j.driver.AccessMode; import org.neo4j.driver.internal.BoltServerAddress; import org.neo4j.driver.internal.util.Clock; -import org.neo4j.driver.AccessMode; import static java.lang.String.format; import static java.util.Arrays.asList; @@ -35,21 +35,26 @@ public class ClusterRoutingTable implements RoutingTable private static final int MIN_ROUTERS = 1; private final Clock clock; - private volatile long expirationTimeout; + private volatile long expirationTimestamp; private final AddressSet readers; private final AddressSet writers; private final AddressSet routers; - public ClusterRoutingTable( Clock clock, BoltServerAddress... routingAddresses ) + private final String databaseName; // specifies the database this routing table is acquired for + private boolean preferInitialRouter; + + public ClusterRoutingTable( String ofDatabase, Clock clock, BoltServerAddress... routingAddresses ) { - this( clock ); + this( ofDatabase, clock ); routers.update( new LinkedHashSet<>( asList( routingAddresses ) ) ); } - private ClusterRoutingTable( Clock clock ) + private ClusterRoutingTable( String ofDatabase, Clock clock ) { + this.databaseName = ofDatabase; this.clock = clock; - this.expirationTimeout = clock.millis() - 1; + this.expirationTimestamp = clock.millis() - 1; + this.preferInitialRouter = true; this.readers = new AddressSet(); this.writers = new AddressSet(); @@ -59,19 +64,31 @@ private ClusterRoutingTable( Clock clock ) @Override public boolean isStaleFor( AccessMode mode ) { - return expirationTimeout < clock.millis() || + return expirationTimestamp < clock.millis() || routers.size() < MIN_ROUTERS || mode == AccessMode.READ && readers.size() == 0 || mode == AccessMode.WRITE && writers.size() == 0; } + @Override + public boolean hasBeenStaleFor( long extraTime ) + { + long totalTime = expirationTimestamp + extraTime; + if ( totalTime < 0 ) + { + totalTime = Long.MAX_VALUE; + } + return totalTime < clock.millis(); + } + @Override public synchronized void update( ClusterComposition cluster ) { - expirationTimeout = cluster.expirationTimestamp(); + expirationTimestamp = cluster.expirationTimestamp(); readers.update( cluster.readers() ); writers.update( cluster.writers() ); routers.update( cluster.routers() ); + preferInitialRouter = !cluster.hasWriters(); } @Override @@ -110,17 +127,26 @@ public Set servers() return servers; } + public String database() + { + return databaseName; + } + @Override - public void removeWriter( BoltServerAddress toRemove ) + public void forgetWriter( BoltServerAddress toRemove ) { writers.remove( toRemove ); } + @Override + public boolean preferInitialRouter() + { + return preferInitialRouter; + } @Override public synchronized String toString() { - return format( "Ttl %s, currentTime %s, routers %s, writers %s, readers %s", - expirationTimeout, clock.millis(), routers, writers, readers ); + return format( "Ttl %s, currentTime %s, routers %s, writers %s, readers %s, database '%s'", expirationTimestamp, clock.millis(), routers, writers, readers, databaseName ); } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/Rediscovery.java b/driver/src/main/java/org/neo4j/driver/internal/cluster/Rediscovery.java index cf230ed1ec..612675ae0b 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/cluster/Rediscovery.java +++ b/driver/src/main/java/org/neo4j/driver/internal/cluster/Rediscovery.java @@ -18,290 +18,11 @@ */ package org.neo4j.driver.internal.cluster; -import io.netty.util.concurrent.EventExecutorGroup; - -import java.net.UnknownHostException; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; -import java.util.concurrent.TimeUnit; -import java.util.stream.Stream; -import org.neo4j.driver.internal.BoltServerAddress; -import org.neo4j.driver.internal.spi.Connection; import org.neo4j.driver.internal.spi.ConnectionPool; -import org.neo4j.driver.internal.util.Futures; -import org.neo4j.driver.Logger; -import org.neo4j.driver.exceptions.SecurityException; -import org.neo4j.driver.exceptions.ServiceUnavailableException; -import org.neo4j.driver.net.ServerAddressResolver; - -import static java.lang.String.format; -import static java.util.Collections.emptySet; -import static java.util.concurrent.CompletableFuture.completedFuture; -import static java.util.stream.Collectors.toList; -import static org.neo4j.driver.internal.util.Futures.completedWithNull; -import static org.neo4j.driver.internal.util.Futures.failedFuture; -public class Rediscovery +public interface Rediscovery { - private static final String NO_ROUTERS_AVAILABLE = "Could not perform discovery. No routing servers available."; - - private final BoltServerAddress initialRouter; - private final RoutingSettings settings; - private final Logger logger; - private final ClusterCompositionProvider provider; - private final ServerAddressResolver resolver; - private final EventExecutorGroup eventExecutorGroup; - - private volatile boolean useInitialRouter; - - public Rediscovery( BoltServerAddress initialRouter, RoutingSettings settings, ClusterCompositionProvider provider, - EventExecutorGroup eventExecutorGroup, ServerAddressResolver resolver, Logger logger ) - { - this( initialRouter, settings, provider, resolver, eventExecutorGroup, logger, true ); - } - - // Test-only constructor - Rediscovery( BoltServerAddress initialRouter, RoutingSettings settings, ClusterCompositionProvider provider, - ServerAddressResolver resolver, EventExecutorGroup eventExecutorGroup, Logger logger, boolean useInitialRouter ) - { - this.initialRouter = initialRouter; - this.settings = settings; - this.logger = logger; - this.provider = provider; - this.resolver = resolver; - this.eventExecutorGroup = eventExecutorGroup; - this.useInitialRouter = useInitialRouter; - } - - /** - * Given the current routing table and connection pool, use the connection composition provider to fetch a new - * cluster composition, which would be used to update the routing table and connection pool. - * - * @param routingTable current routing table. - * @param connectionPool connection pool. - * @return new cluster composition. - */ - public CompletionStage lookupClusterComposition( RoutingTable routingTable, - ConnectionPool connectionPool ) - { - CompletableFuture result = new CompletableFuture<>(); - lookupClusterComposition( routingTable, connectionPool, 0, 0, result ); - return result; - } - - private void lookupClusterComposition( RoutingTable routingTable, ConnectionPool pool, - int failures, long previousDelay, CompletableFuture result ) - { - lookup( routingTable, pool ).whenComplete( ( composition, completionError ) -> - { - Throwable error = Futures.completionExceptionCause( completionError ); - if ( error != null ) - { - result.completeExceptionally( error ); - } - else if ( composition != null ) - { - result.complete( composition ); - } - else - { - int newFailures = failures + 1; - if ( newFailures >= settings.maxRoutingFailures() ) - { - result.completeExceptionally( new ServiceUnavailableException( NO_ROUTERS_AVAILABLE ) ); - } - else - { - long nextDelay = Math.max( settings.retryTimeoutDelay(), previousDelay * 2 ); - logger.info( "Unable to fetch new routing table, will try again in " + nextDelay + "ms" ); - eventExecutorGroup.next().schedule( - () -> lookupClusterComposition( routingTable, pool, newFailures, nextDelay, result ), - nextDelay, TimeUnit.MILLISECONDS - ); - } - } - } ); - } - - private CompletionStage lookup( RoutingTable routingTable, ConnectionPool connectionPool ) - { - CompletionStage compositionStage; - - if ( useInitialRouter ) - { - compositionStage = lookupOnInitialRouterThenOnKnownRouters( routingTable, connectionPool ); - useInitialRouter = false; - } - else - { - compositionStage = lookupOnKnownRoutersThenOnInitialRouter( routingTable, connectionPool ); - } - - return compositionStage.whenComplete( ( composition, error ) -> - { - if ( composition != null && !composition.hasWriters() ) - { - useInitialRouter = true; - } - } ); - } - - private CompletionStage lookupOnKnownRoutersThenOnInitialRouter( RoutingTable routingTable, - ConnectionPool connectionPool ) - { - Set seenServers = new HashSet<>(); - return lookupOnKnownRouters( routingTable, connectionPool, seenServers ).thenCompose( composition -> - { - if ( composition != null ) - { - return completedFuture( composition ); - } - return lookupOnInitialRouter( routingTable, connectionPool, seenServers ); - } ); - } - - private CompletionStage lookupOnInitialRouterThenOnKnownRouters( RoutingTable routingTable, - ConnectionPool connectionPool ) - { - Set seenServers = emptySet(); - return lookupOnInitialRouter( routingTable, connectionPool, seenServers ).thenCompose( composition -> - { - if ( composition != null ) - { - return completedFuture( composition ); - } - return lookupOnKnownRouters( routingTable, connectionPool, new HashSet<>() ); - } ); - } - - private CompletionStage lookupOnKnownRouters( RoutingTable routingTable, - ConnectionPool connectionPool, Set seenServers ) - { - BoltServerAddress[] addresses = routingTable.routers().toArray(); - - CompletableFuture result = completedWithNull(); - for ( BoltServerAddress address : addresses ) - { - result = result.thenCompose( composition -> - { - if ( composition != null ) - { - return completedFuture( composition ); - } - else - { - return lookupOnRouter( address, routingTable, connectionPool ) - .whenComplete( ( ignore, error ) -> seenServers.add( address ) ); - } - } ); - } - return result; - } - - private CompletionStage lookupOnInitialRouter( RoutingTable routingTable, - ConnectionPool connectionPool, Set seenServers ) - { - List addresses; - try - { - addresses = resolve( initialRouter ); - } - catch ( Throwable error ) - { - return failedFuture( error ); - } - addresses.removeAll( seenServers ); - - CompletableFuture result = completedWithNull(); - for ( BoltServerAddress address : addresses ) - { - result = result.thenCompose( composition -> - { - if ( composition != null ) - { - return completedFuture( composition ); - } - return lookupOnRouter( address, routingTable, connectionPool ); - } ); - } - return result; - } - - private CompletionStage lookupOnRouter( BoltServerAddress routerAddress, - RoutingTable routingTable, ConnectionPool connectionPool ) - { - CompletionStage connectionStage = connectionPool.acquire( routerAddress ); - - return provider.getClusterComposition( connectionStage ).handle( ( response, error ) -> - { - Throwable cause = Futures.completionExceptionCause( error ); - if ( cause != null ) - { - return handleRoutingProcedureError( cause, routingTable, routerAddress ); - } - else - { - return handleClusterComposition( routerAddress, response ); - } - } ); - } - - private ClusterComposition handleRoutingProcedureError( Throwable error, RoutingTable routingTable, - BoltServerAddress routerAddress ) - { - if ( error instanceof SecurityException ) - { - // auth error happened, terminate the discovery procedure immediately - throw new CompletionException( error ); - } - else - { - // connection turned out to be broken - logger.info( format( "Failed to connect to routing server '%s'.", routerAddress ), error ); - routingTable.forget( routerAddress ); - return null; - } - } - - private ClusterComposition handleClusterComposition( BoltServerAddress routerAddress, ClusterCompositionResponse response ) - { - ClusterComposition result = null; - - try - { - result = response.clusterComposition(); - } - catch ( Exception exc ) - { - logger.warn( format( "Unable to process routing table received from '%s'.", routerAddress ), exc ); - } - - return result; - } - - private List resolve( BoltServerAddress address ) - { - return resolver.resolve( address ) - .stream() - .flatMap( resolved -> resolveAll( BoltServerAddress.from( resolved ) ) ) - .collect( toList() ); // collect to list to preserve the order - } - - private Stream resolveAll( BoltServerAddress address ) - { - try - { - return address.resolveAll().stream(); - } - catch ( UnknownHostException e ) - { - logger.error( "Failed to resolve address `" + address + "` to IPs due to error: " + e.getMessage(), e ); - return Stream.of( address ); - } - } + CompletionStage lookupClusterComposition( RoutingTable routingTable, ConnectionPool connectionPool ); } diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/RediscoveryImpl.java b/driver/src/main/java/org/neo4j/driver/internal/cluster/RediscoveryImpl.java new file mode 100644 index 0000000000..1d9449ae7b --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/cluster/RediscoveryImpl.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.cluster; + +import io.netty.util.concurrent.EventExecutorGroup; + +import java.net.UnknownHostException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import org.neo4j.driver.Logger; +import org.neo4j.driver.exceptions.FatalDiscoveryException; +import org.neo4j.driver.exceptions.SecurityException; +import org.neo4j.driver.exceptions.ServiceUnavailableException; +import org.neo4j.driver.internal.BoltServerAddress; +import org.neo4j.driver.internal.spi.Connection; +import org.neo4j.driver.internal.spi.ConnectionPool; +import org.neo4j.driver.internal.util.Futures; +import org.neo4j.driver.net.ServerAddressResolver; + +import static java.lang.String.format; +import static java.util.Collections.emptySet; +import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.stream.Collectors.toList; +import static org.neo4j.driver.internal.util.Futures.completedWithNull; +import static org.neo4j.driver.internal.util.Futures.failedFuture; + +/** + * This class is used by all router tables to perform discovery. + * In other words, the methods in this class could be called by multiple threads concurrently. + */ +public class RediscoveryImpl implements Rediscovery +{ + private static final String NO_ROUTERS_AVAILABLE = "Could not perform discovery for database '%s'. No routing server available."; + + private final BoltServerAddress initialRouter; + private final RoutingSettings settings; + private final Logger logger; + private final ClusterCompositionProvider provider; + private final ServerAddressResolver resolver; + private final EventExecutorGroup eventExecutorGroup; + + public RediscoveryImpl( BoltServerAddress initialRouter, RoutingSettings settings, ClusterCompositionProvider provider, + EventExecutorGroup eventExecutorGroup, ServerAddressResolver resolver, Logger logger ) + { + this.initialRouter = initialRouter; + this.settings = settings; + this.logger = logger; + this.provider = provider; + this.resolver = resolver; + this.eventExecutorGroup = eventExecutorGroup; + } + + /** + * Given a database and its current routing table, and the global connection pool, use the global cluster composition provider to fetch a new + * cluster composition, which would be used to update the routing table of the given database and global connection pool. + * + * @param routingTable current routing table of the given database. + * @param connectionPool connection pool. + * @return new cluster composition. + */ + @Override + public CompletionStage lookupClusterComposition( RoutingTable routingTable, ConnectionPool connectionPool ) + { + CompletableFuture result = new CompletableFuture<>(); + lookupClusterComposition( routingTable, connectionPool, 0, 0, result ); + return result; + } + + private void lookupClusterComposition( RoutingTable routingTable, ConnectionPool pool, + int failures, long previousDelay, CompletableFuture result ) + { + lookup( routingTable, pool ).whenComplete( ( composition, completionError ) -> + { + Throwable error = Futures.completionExceptionCause( completionError ); + if ( error != null ) + { + result.completeExceptionally( error ); + } + else if ( composition != null ) + { + result.complete( composition ); + } + else + { + int newFailures = failures + 1; + if ( newFailures >= settings.maxRoutingFailures() ) + { + result.completeExceptionally( new ServiceUnavailableException( String.format( NO_ROUTERS_AVAILABLE, routingTable.database() ) ) ); + } + else + { + long nextDelay = Math.max( settings.retryTimeoutDelay(), previousDelay * 2 ); + logger.info( "Unable to fetch new routing table, will try again in " + nextDelay + "ms" ); + eventExecutorGroup.next().schedule( + () -> lookupClusterComposition( routingTable, pool, newFailures, nextDelay, result ), + nextDelay, TimeUnit.MILLISECONDS + ); + } + } + } ); + } + + private CompletionStage lookup( RoutingTable routingTable, ConnectionPool connectionPool ) + { + CompletionStage compositionStage; + + if ( routingTable.preferInitialRouter() ) + { + compositionStage = lookupOnInitialRouterThenOnKnownRouters( routingTable, connectionPool ); + } + else + { + compositionStage = lookupOnKnownRoutersThenOnInitialRouter( routingTable, connectionPool ); + } + + return compositionStage; + } + + private CompletionStage lookupOnKnownRoutersThenOnInitialRouter( RoutingTable routingTable, + ConnectionPool connectionPool ) + { + Set seenServers = new HashSet<>(); + return lookupOnKnownRouters( routingTable, connectionPool, seenServers ).thenCompose( composition -> + { + if ( composition != null ) + { + return completedFuture( composition ); + } + return lookupOnInitialRouter( routingTable, connectionPool, seenServers ); + } ); + } + + private CompletionStage lookupOnInitialRouterThenOnKnownRouters( RoutingTable routingTable, + ConnectionPool connectionPool ) + { + Set seenServers = emptySet(); + return lookupOnInitialRouter( routingTable, connectionPool, seenServers ).thenCompose( composition -> + { + if ( composition != null ) + { + return completedFuture( composition ); + } + return lookupOnKnownRouters( routingTable, connectionPool, new HashSet<>() ); + } ); + } + + private CompletionStage lookupOnKnownRouters( RoutingTable routingTable, + ConnectionPool connectionPool, Set seenServers ) + { + BoltServerAddress[] addresses = routingTable.routers().toArray(); + + CompletableFuture result = completedWithNull(); + for ( BoltServerAddress address : addresses ) + { + result = result.thenCompose( composition -> + { + if ( composition != null ) + { + return completedFuture( composition ); + } + else + { + return lookupOnRouter( address, routingTable, connectionPool ) + .whenComplete( ( ignore, error ) -> seenServers.add( address ) ); + } + } ); + } + return result; + } + + private CompletionStage lookupOnInitialRouter( RoutingTable routingTable, + ConnectionPool connectionPool, Set seenServers ) + { + List addresses; + try + { + addresses = resolve( initialRouter ); + } + catch ( Throwable error ) + { + return failedFuture( error ); + } + addresses.removeAll( seenServers ); + + CompletableFuture result = completedWithNull(); + for ( BoltServerAddress address : addresses ) + { + result = result.thenCompose( composition -> + { + if ( composition != null ) + { + return completedFuture( composition ); + } + return lookupOnRouter( address, routingTable, connectionPool ); + } ); + } + return result; + } + + private CompletionStage lookupOnRouter( BoltServerAddress routerAddress, + RoutingTable routingTable, ConnectionPool connectionPool ) + { + CompletionStage connectionStage = connectionPool.acquire( routerAddress ); + + return provider.getClusterComposition( connectionStage, routingTable.database() ).handle( ( response, error ) -> + { + Throwable cause = Futures.completionExceptionCause( error ); + if ( cause != null ) + { + return handleRoutingProcedureError( cause, routingTable, routerAddress ); + } + else + { + return response; + } + } ); + } + + private ClusterComposition handleRoutingProcedureError( Throwable error, RoutingTable routingTable, + BoltServerAddress routerAddress ) + { + if ( error instanceof SecurityException || error instanceof FatalDiscoveryException ) + { + // auth error or routing error happened, terminate the discovery procedure immediately + throw new CompletionException( error ); + } + + // Retriable error happened during discovery. + logger.warn( format( "Failed to update routing table with server '%s'.", routerAddress ), error ); + routingTable.forget( routerAddress ); + return null; + } + + private List resolve( BoltServerAddress address ) + { + return resolver.resolve( address ) + .stream() + .flatMap( resolved -> resolveAll( BoltServerAddress.from( resolved ) ) ) + .collect( toList() ); // collect to list to preserve the order + } + + private Stream resolveAll( BoltServerAddress address ) + { + try + { + return address.resolveAll().stream(); + } + catch ( UnknownHostException e ) + { + logger.error( "Failed to resolve address `" + address + "` to IPs due to error: " + e.getMessage(), e ); + return Stream.of( address ); + } + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingProcedureClusterCompositionProvider.java b/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingProcedureClusterCompositionProvider.java index 260fad0fae..ef36497fc4 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingProcedureClusterCompositionProvider.java +++ b/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingProcedureClusterCompositionProvider.java @@ -19,15 +19,15 @@ package org.neo4j.driver.internal.cluster; import java.util.List; +import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; -import org.neo4j.driver.internal.spi.Connection; -import org.neo4j.driver.internal.util.Clock; import org.neo4j.driver.Record; import org.neo4j.driver.Statement; import org.neo4j.driver.exceptions.ProtocolException; -import org.neo4j.driver.exceptions.ServiceUnavailableException; import org.neo4j.driver.exceptions.value.ValueException; +import org.neo4j.driver.internal.spi.Connection; +import org.neo4j.driver.internal.util.Clock; import static java.lang.String.format; @@ -50,22 +50,19 @@ public RoutingProcedureClusterCompositionProvider( Clock clock, RoutingSettings } @Override - public CompletionStage getClusterComposition( - CompletionStage connectionStage ) + public CompletionStage getClusterComposition( CompletionStage connectionStage, String databaseName ) { - return routingProcedureRunner.run( connectionStage ) + return routingProcedureRunner.run( connectionStage, databaseName ) .thenApply( this::processRoutingResponse ); } - private ClusterCompositionResponse processRoutingResponse( RoutingProcedureResponse response ) + private ClusterComposition processRoutingResponse( RoutingProcedureResponse response ) { if ( !response.isSuccess() ) { - return new ClusterCompositionResponse.Failure( new ServiceUnavailableException( format( - "Failed to run '%s' on server. " + - "Please make sure that there is a Neo4j 3.1+ causal cluster up running.", - invokedProcedureString( response ) ), response.error() - ) ); + throw new CompletionException( format( + "Failed to run '%s' on server. Please make sure that there is a Neo4j server or cluster up running.", + invokedProcedureString( response ) ), response.error() ); } List records = response.records(); @@ -75,9 +72,9 @@ private ClusterCompositionResponse processRoutingResponse( RoutingProcedureRespo // the record size is wrong if ( records.size() != 1 ) { - return new ClusterCompositionResponse.Failure( new ProtocolException( format( + throw new ProtocolException( format( PROTOCOL_ERROR_MESSAGE + "records received '%s' is too few or too many.", - invokedProcedureString( response ), records.size() ) ) ); + invokedProcedureString( response ), records.size() ) ); } // failed to parse the record @@ -88,21 +85,21 @@ private ClusterCompositionResponse processRoutingResponse( RoutingProcedureRespo } catch ( ValueException e ) { - return new ClusterCompositionResponse.Failure( new ProtocolException( format( + throw new ProtocolException( format( PROTOCOL_ERROR_MESSAGE + "unparsable record received.", - invokedProcedureString( response ) ), e ) ); + invokedProcedureString( response ) ), e ); } // the cluster result is not a legal reply if ( !cluster.hasRoutersAndReaders() ) { - return new ClusterCompositionResponse.Failure( new ProtocolException( format( + throw new ProtocolException( format( PROTOCOL_ERROR_MESSAGE + "no router or reader found in response.", - invokedProcedureString( response ) ) ) ); + invokedProcedureString( response ) ) ); } // all good - return new ClusterCompositionResponse.Success( cluster ); + return cluster; } private static String invokedProcedureString( RoutingProcedureResponse response ) diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingProcedureRunner.java b/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingProcedureRunner.java index 1efef184c6..c708d60b8f 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingProcedureRunner.java +++ b/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingProcedureRunner.java @@ -19,30 +19,34 @@ package org.neo4j.driver.internal.cluster; import java.util.List; +import java.util.Objects; import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; import org.neo4j.driver.AccessMode; -import org.neo4j.driver.internal.BookmarksHolder; -import org.neo4j.driver.internal.async.connection.DecoratedConnection; -import org.neo4j.driver.internal.spi.Connection; -import org.neo4j.driver.internal.util.Futures; -import org.neo4j.driver.internal.util.ServerVersion; import org.neo4j.driver.Record; import org.neo4j.driver.Statement; -import org.neo4j.driver.async.StatementResultCursor; import org.neo4j.driver.TransactionConfig; +import org.neo4j.driver.async.StatementResultCursor; import org.neo4j.driver.exceptions.ClientException; +import org.neo4j.driver.exceptions.FatalDiscoveryException; +import org.neo4j.driver.internal.BookmarksHolder; +import org.neo4j.driver.internal.async.connection.DirectConnection; +import org.neo4j.driver.internal.spi.Connection; +import org.neo4j.driver.internal.util.Futures; +import org.neo4j.driver.internal.util.ServerVersion; -import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.ABSENT_DB_NAME; -import static org.neo4j.driver.internal.util.ServerVersion.v3_2_0; import static org.neo4j.driver.Values.parameters; +import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.ABSENT_DB_NAME; +import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.SYSTEM_DB_NAME; +import static org.neo4j.driver.internal.util.ServerVersion.v4_0_0; public class RoutingProcedureRunner { - static final String GET_SERVERS = "dbms.cluster.routing.getServers"; - static final String GET_ROUTING_TABLE_PARAM = "context"; - static final String GET_ROUTING_TABLE = "dbms.cluster.routing.getRoutingTable($" + GET_ROUTING_TABLE_PARAM + ")"; + static final String ROUTING_CONTEXT = "context"; + static final String GET_ROUTING_TABLE = "dbms.cluster.routing.getRoutingTable($" + ROUTING_CONTEXT + ")"; + static final String DATABASE_NAME = "database"; + static final String MULTI_DB_GET_ROUTING_TABLE = String.format( "dbms.routing.getRoutingTable($%s, $%s)", ROUTING_CONTEXT, DATABASE_NAME ); private final RoutingContext context; @@ -51,13 +55,14 @@ public RoutingProcedureRunner( RoutingContext context ) this.context = context; } - public CompletionStage run( CompletionStage connectionStage ) + public CompletionStage run( CompletionStage connectionStage, String databaseName ) { return connectionStage.thenCompose( connection -> { - // Routing procedure will be called on the default database - DecoratedConnection delegate = new DecoratedConnection( connection, ABSENT_DB_NAME, AccessMode.WRITE ); - Statement procedure = procedureStatement( delegate.serverVersion() ); + ServerVersion serverVersion = connection.serverVersion(); + // As the connection can connect to any router (a.k.a. any core members), this connection strictly speaking is a read connection. + DirectConnection delegate = connection( serverVersion, connection ); + Statement procedure = procedureStatement( serverVersion, databaseName ); return runProcedure( delegate, procedure ) .thenCompose( records -> releaseConnection( delegate, records ) ) .handle( ( records, error ) -> processProcedureResponse( procedure, records, error ) ); @@ -71,16 +76,43 @@ CompletionStage> runProcedure( Connection connection, Statement pro .asyncResult().thenCompose( StatementResultCursor::listAsync ); } - private Statement procedureStatement( ServerVersion serverVersion ) + private DirectConnection connection( ServerVersion serverVersion, Connection connection ) { - if ( serverVersion.greaterThanOrEqual( v3_2_0 ) ) + if ( serverVersion.greaterThanOrEqual( v4_0_0 )) { - return new Statement( "CALL " + GET_ROUTING_TABLE, - parameters( GET_ROUTING_TABLE_PARAM, context.asMap() ) ); + return new DirectConnection( connection, SYSTEM_DB_NAME, AccessMode.READ ); } else { - return new Statement( "CALL " + GET_SERVERS ); + return new DirectConnection( connection, ABSENT_DB_NAME, AccessMode.WRITE ); + } + } + + private Statement procedureStatement( ServerVersion serverVersion, String databaseName ) + { + /* + * For v4.0+ databases, call procedure to get routing table for the database specified. + * For database version lower than 4.0, the database name will be ignored. + */ + if ( Objects.equals( ABSENT_DB_NAME, databaseName ) ) + { + databaseName = null; + } + + if ( serverVersion.greaterThanOrEqual( v4_0_0 ) ) + { + return new Statement( "CALL " + MULTI_DB_GET_ROUTING_TABLE, + parameters( ROUTING_CONTEXT, context.asMap(), DATABASE_NAME, databaseName ) ); + } + else + { + if ( databaseName != null ) + { + throw new FatalDiscoveryException( String.format( "Refreshing routing table for multi-databases is not supported in server version lower than 4.0. " + + "Current server version: %s. Database name: `%s`", serverVersion, databaseName ) ); + } + return new Statement( "CALL " + GET_ROUTING_TABLE, + parameters( ROUTING_CONTEXT, context.asMap() ) ); } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingTable.java b/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingTable.java index 91cff535fc..89278a4a82 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingTable.java +++ b/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingTable.java @@ -27,6 +27,8 @@ public interface RoutingTable { boolean isStaleFor( AccessMode mode ); + boolean hasBeenStaleFor( long staleRoutingTableTimeout ); + void update( ClusterComposition cluster ); void forget( BoltServerAddress address ); @@ -39,5 +41,9 @@ public interface RoutingTable Set servers(); - void removeWriter( BoltServerAddress toRemove ); + String database(); + + void forgetWriter( BoltServerAddress toRemove ); + + boolean preferInitialRouter(); } diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingTableHandler.java b/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingTableHandler.java new file mode 100644 index 0000000000..4cc406b222 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingTableHandler.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.cluster; + +import java.time.Duration; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.Logger; +import org.neo4j.driver.internal.BoltServerAddress; +import org.neo4j.driver.internal.RoutingErrorHandler; +import org.neo4j.driver.internal.spi.ConnectionPool; +import org.neo4j.driver.internal.util.Futures; + +import static java.util.concurrent.CompletableFuture.completedFuture; + +public class RoutingTableHandler implements RoutingErrorHandler +{ + private final RoutingTable routingTable; + private final String databaseName; + private final RoutingTableRegistry routingTableRegistry; + private volatile CompletableFuture refreshRoutingTableFuture; + private final ConnectionPool connectionPool; + private final Rediscovery rediscovery; + private final Logger log; + + // This defines how long we shall wait before trimming a routing table from routing tables after it is stale. + // TODO make this a configuration option + public static final Duration STALE_ROUTING_TABLE_PURGE_TIMEOUT = Duration.ofSeconds( 30 ); + + public RoutingTableHandler( RoutingTable routingTable, Rediscovery rediscovery, ConnectionPool connectionPool, RoutingTableRegistry routingTableRegistry, Logger log ) + { + this.routingTable = routingTable; + this.databaseName = routingTable.database(); + this.rediscovery = rediscovery; + this.connectionPool = connectionPool; + this.routingTableRegistry = routingTableRegistry; + this.log = log; + } + + @Override + public void onConnectionFailure( BoltServerAddress address ) + { + // remove server from the routing table, to prevent concurrent threads from making connections to this address + routingTable.forget( address ); + } + + @Override + public void onWriteFailure( BoltServerAddress address ) + { + routingTable.forgetWriter( address ); + } + + synchronized CompletionStage refreshRoutingTable( AccessMode mode ) + { + if ( refreshRoutingTableFuture != null ) + { + // refresh is already happening concurrently, just use it's result + return refreshRoutingTableFuture; + } + else if ( routingTable.isStaleFor( mode ) ) + { + // existing routing table is not fresh and should be updated + log.info( "Routing table for database '%s' is stale. %s", databaseName, routingTable ); + + CompletableFuture resultFuture = new CompletableFuture<>(); + refreshRoutingTableFuture = resultFuture; + + rediscovery.lookupClusterComposition( routingTable, connectionPool ) + .whenComplete( ( composition, completionError ) -> + { + Throwable error = Futures.completionExceptionCause( completionError ); + if ( error != null ) + { + clusterCompositionLookupFailed( error ); + } + else + { + freshClusterCompositionFetched( composition ); + } + } ); + + return resultFuture; + } + else + { + // existing routing table is fresh, use it + return completedFuture( routingTable ); + } + } + + private synchronized void freshClusterCompositionFetched( ClusterComposition composition ) + { + try + { + routingTable.update( composition ); + routingTableRegistry.purgeAged(); + connectionPool.retainAll( routingTableRegistry.allServers() ); + + log.info( "Updated routing table for database '%s'. %s", databaseName, routingTable ); + + CompletableFuture routingTableFuture = refreshRoutingTableFuture; + refreshRoutingTableFuture = null; + routingTableFuture.complete( routingTable ); + } + catch ( Throwable error ) + { + clusterCompositionLookupFailed( error ); + } + } + + private synchronized void clusterCompositionLookupFailed( Throwable error ) + { + log.error( String.format( "Failed to update routing table for database '%s'. Current routing table: %s.", databaseName, routingTable ), error ); + routingTableRegistry.remove( databaseName ); + CompletableFuture routingTableFuture = refreshRoutingTableFuture; + refreshRoutingTableFuture = null; + routingTableFuture.completeExceptionally( error ); + } + + // This method cannot be synchronized as it will be visited by all routing table handler's threads concurrently + public Set servers() + { + return routingTable.servers(); + } + + // This method cannot be synchronized as it will be visited by all routing table handler's threads concurrently + public boolean isRoutingTableAged() + { + return refreshRoutingTableFuture == null && routingTable.hasBeenStaleFor( STALE_ROUTING_TABLE_PURGE_TIMEOUT.toMillis() ); + } + + // for testing only + public RoutingTable routingTable() + { + return routingTable; + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingTableRegistry.java b/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingTableRegistry.java new file mode 100644 index 0000000000..d4bf878a5c --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingTableRegistry.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.cluster; + +import java.util.Set; +import java.util.concurrent.CompletionStage; + +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.internal.BoltServerAddress; + +/** + * A generic interface to access all routing tables as a whole. + * It also provides methods to obtain a routing table or manage a routing table for a specified database. + */ +public interface RoutingTableRegistry +{ + /** + * Fresh the routing table for the database with given access mode. + * For server version lower than 4.0, the database name will be ignored while refreshing routing table. + * @return The future of a new routing table handler. + */ + CompletionStage refreshRoutingTable( String databaseName, AccessMode mode ); + + /** + * @return all servers in the registry + */ + Set allServers(); + + /** + * Removes a routing table of the given database from registry. + */ + void remove( String databaseName ); + + /** + * Removes all routing tables that has been not used for a long time. + */ + void purgeAged(); +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingTableRegistryImpl.java b/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingTableRegistryImpl.java new file mode 100644 index 0000000000..637dcc2a06 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingTableRegistryImpl.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.cluster; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.Logger; +import org.neo4j.driver.internal.BoltServerAddress; +import org.neo4j.driver.internal.spi.ConnectionPool; +import org.neo4j.driver.internal.util.Clock; + +public class RoutingTableRegistryImpl implements RoutingTableRegistry +{ + private final ConcurrentMap routingTableHandlers; + private final RoutingTableHandlerFactory factory; + private final Logger logger; + + public RoutingTableRegistryImpl( ConnectionPool connectionPool, Rediscovery rediscovery, Clock clock, Logger logger ) + { + this( new ConcurrentHashMap<>(), new RoutingTableHandlerFactory( connectionPool, rediscovery, clock, logger ), logger ); + } + + RoutingTableRegistryImpl( ConcurrentMap routingTableHandlers, RoutingTableHandlerFactory factory, Logger logger ) + { + this.factory = factory; + this.routingTableHandlers = routingTableHandlers; + this.logger = logger; + } + + @Override + public CompletionStage refreshRoutingTable( String databaseName, AccessMode mode ) + { + RoutingTableHandler handler = getOrCreate( databaseName ); + return handler.refreshRoutingTable( mode ).thenApply( ignored -> handler ); + } + + @Override + public Set allServers() + { + // obviously we just had a snapshot of all servers in all routing tables + // after we read it, the set could already be changed. + Set servers = new HashSet<>(); + for ( RoutingTableHandler tableHandler : routingTableHandlers.values() ) + { + servers.addAll( tableHandler.servers() ); + } + return servers; + } + + @Override + public void remove( String databaseName ) + { + routingTableHandlers.remove( databaseName ); + logger.debug( "Routing table handler for database '%s' is removed.", databaseName ); + } + + @Override + public void purgeAged() + { + routingTableHandlers.forEach( ( databaseName, handler ) -> { + if ( handler.isRoutingTableAged() ) + { + logger.info( "Routing table handler for database '%s' is removed because it has not been used for a long time. Routing table: %s", + databaseName, handler.routingTable() ); + routingTableHandlers.remove( databaseName ); + } + } ); + } + + // For tests + public boolean contains( String databaseName ) + { + return routingTableHandlers.containsKey( databaseName ); + } + + private RoutingTableHandler getOrCreate( String databaseName ) + { + return routingTableHandlers.computeIfAbsent( databaseName, name -> { + RoutingTableHandler handler = factory.newInstance( name, this ); + logger.debug( "Routing table handler for database '%s' is added.", databaseName ); + return handler; + } ); + } + + static class RoutingTableHandlerFactory + { + private final ConnectionPool connectionPool; + private final Rediscovery rediscovery; + private final Logger log; + private final Clock clock; + + RoutingTableHandlerFactory( ConnectionPool connectionPool, Rediscovery rediscovery, Clock clock, Logger log ) + { + this.connectionPool = connectionPool; + this.rediscovery = rediscovery; + this.clock = clock; + this.log = log; + } + + RoutingTableHandler newInstance( String databaseName, RoutingTableRegistry allTables ) + { + ClusterRoutingTable routingTable = new ClusterRoutingTable( databaseName, clock ); + return new RoutingTableHandler( routingTable, rediscovery, connectionPool, allTables, log ); + } + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/loadbalancing/LoadBalancer.java b/driver/src/main/java/org/neo4j/driver/internal/cluster/loadbalancing/LoadBalancer.java index e3ceaf2e25..c51583e259 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/cluster/loadbalancing/LoadBalancer.java +++ b/driver/src/main/java/org/neo4j/driver/internal/cluster/loadbalancing/LoadBalancer.java @@ -23,70 +23,54 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.Logger; +import org.neo4j.driver.Logging; +import org.neo4j.driver.exceptions.ServiceUnavailableException; +import org.neo4j.driver.exceptions.SessionExpiredException; import org.neo4j.driver.internal.BoltServerAddress; -import org.neo4j.driver.internal.RoutingErrorHandler; -import org.neo4j.driver.internal.async.connection.DecoratedConnection; import org.neo4j.driver.internal.async.connection.RoutingConnection; import org.neo4j.driver.internal.cluster.AddressSet; -import org.neo4j.driver.internal.cluster.ClusterComposition; import org.neo4j.driver.internal.cluster.ClusterCompositionProvider; -import org.neo4j.driver.internal.cluster.ClusterRoutingTable; import org.neo4j.driver.internal.cluster.Rediscovery; +import org.neo4j.driver.internal.cluster.RediscoveryImpl; import org.neo4j.driver.internal.cluster.RoutingProcedureClusterCompositionProvider; import org.neo4j.driver.internal.cluster.RoutingSettings; import org.neo4j.driver.internal.cluster.RoutingTable; +import org.neo4j.driver.internal.cluster.RoutingTableRegistry; +import org.neo4j.driver.internal.cluster.RoutingTableRegistryImpl; import org.neo4j.driver.internal.spi.Connection; import org.neo4j.driver.internal.spi.ConnectionPool; import org.neo4j.driver.internal.spi.ConnectionProvider; import org.neo4j.driver.internal.util.Clock; import org.neo4j.driver.internal.util.Futures; -import org.neo4j.driver.AccessMode; -import org.neo4j.driver.Logger; -import org.neo4j.driver.Logging; -import org.neo4j.driver.exceptions.ServiceUnavailableException; -import org.neo4j.driver.exceptions.SessionExpiredException; import org.neo4j.driver.net.ServerAddressResolver; import static java.lang.String.format; -import static java.util.concurrent.CompletableFuture.completedFuture; +import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.ABSENT_DB_NAME; -public class LoadBalancer implements ConnectionProvider, RoutingErrorHandler +public class LoadBalancer implements ConnectionProvider { private static final String LOAD_BALANCER_LOG_NAME = "LoadBalancer"; - private final ConnectionPool connectionPool; - private final RoutingTable routingTable; - private final Rediscovery rediscovery; + private final RoutingTableRegistry routingTables; private final LoadBalancingStrategy loadBalancingStrategy; private final EventExecutorGroup eventExecutorGroup; private final Logger log; - private CompletableFuture refreshRoutingTableFuture; - public LoadBalancer( BoltServerAddress initialRouter, RoutingSettings settings, ConnectionPool connectionPool, EventExecutorGroup eventExecutorGroup, Clock clock, Logging logging, LoadBalancingStrategy loadBalancingStrategy, ServerAddressResolver resolver ) { - this( connectionPool, new ClusterRoutingTable( clock, initialRouter ), - createRediscovery( initialRouter, settings, eventExecutorGroup, resolver, clock, logging ), + this( connectionPool, createRoutingTables( connectionPool, eventExecutorGroup, initialRouter, resolver, settings, clock, logging ), loadBalancerLogger( logging ), loadBalancingStrategy, eventExecutorGroup ); } - // Used only in testing - LoadBalancer( ConnectionPool connectionPool, RoutingTable routingTable, Rediscovery rediscovery, - EventExecutorGroup eventExecutorGroup, Logging logging ) - { - this( connectionPool, routingTable, rediscovery, loadBalancerLogger( logging ), - new LeastConnectedLoadBalancingStrategy( connectionPool, logging ), - eventExecutorGroup ); - } - - private LoadBalancer( ConnectionPool connectionPool, RoutingTable routingTable, Rediscovery rediscovery, - Logger log, LoadBalancingStrategy loadBalancingStrategy, EventExecutorGroup eventExecutorGroup ) + LoadBalancer( ConnectionPool connectionPool, RoutingTableRegistry routingTables, Logger log, LoadBalancingStrategy loadBalancingStrategy, + EventExecutorGroup eventExecutorGroup ) { this.connectionPool = connectionPool; - this.routingTable = routingTable; - this.rediscovery = rediscovery; + this.routingTables = routingTables; this.loadBalancingStrategy = loadBalancingStrategy; this.eventExecutorGroup = eventExecutorGroup; this.log = log; @@ -95,28 +79,27 @@ private LoadBalancer( ConnectionPool connectionPool, RoutingTable routingTable, @Override public CompletionStage acquireConnection( String databaseName, AccessMode mode ) { - return freshRoutingTable( mode ) - .thenCompose( routingTable -> acquire( mode, routingTable ) ) - .thenApply( connection -> new RoutingConnection( connection, mode, this ) ) - .thenApply( connection -> new DecoratedConnection( connection, databaseName, mode ) ); + return routingTables.refreshRoutingTable( databaseName, mode ) + .thenCompose( handler -> acquire( mode, handler.routingTable() ) + .thenApply( connection -> new RoutingConnection( connection, databaseName, mode, handler ) ) ); } @Override public CompletionStage verifyConnectivity() { - return freshRoutingTable( AccessMode.READ ).thenApply( routingTable -> null ); - } - - @Override - public void onConnectionFailure( BoltServerAddress address ) - { - forget( address ); - } - - @Override - public void onWriteFailure( BoltServerAddress address ) - { - routingTable.removeWriter( address ); + return routingTables.refreshRoutingTable( ABSENT_DB_NAME, AccessMode.READ ).handle( ( ignored, error ) -> { + if ( error != null ) + { + Throwable cause = Futures.completionExceptionCause( error ); + if ( cause instanceof ServiceUnavailableException ) + { + throw Futures.asCompletionException( new ServiceUnavailableException( + "Unable to connect to database, ensure the database is running and that there is a working network connection to it.", cause ) ); + } + throw Futures.asCompletionException( cause ); + } + return null; + } ); } @Override @@ -125,86 +108,15 @@ public CompletionStage close() return connectionPool.close(); } - private synchronized void forget( BoltServerAddress address ) - { - // remove from the routing table, to prevent concurrent threads from making connections to this address - routingTable.forget( address ); - } - - private synchronized CompletionStage freshRoutingTable( AccessMode mode ) - { - if ( refreshRoutingTableFuture != null ) - { - // refresh is already happening concurrently, just use it's result - return refreshRoutingTableFuture; - } - else if ( routingTable.isStaleFor( mode ) ) - { - // existing routing table is not fresh and should be updated - log.info( "Routing table is stale. %s", routingTable ); - - CompletableFuture resultFuture = new CompletableFuture<>(); - refreshRoutingTableFuture = resultFuture; - - rediscovery.lookupClusterComposition( routingTable, connectionPool ) - .whenComplete( ( composition, completionError ) -> - { - Throwable error = Futures.completionExceptionCause( completionError ); - if ( error != null ) - { - clusterCompositionLookupFailed( error ); - } - else - { - freshClusterCompositionFetched( composition ); - } - } ); - - return resultFuture; - } - else - { - // existing routing table is fresh, use it - return completedFuture( routingTable ); - } - } - - private synchronized void freshClusterCompositionFetched( ClusterComposition composition ) - { - try - { - routingTable.update( composition ); - connectionPool.retainAll( routingTable.servers() ); - - log.info( "Updated routing table. %s", routingTable ); - - CompletableFuture routingTableFuture = refreshRoutingTableFuture; - refreshRoutingTableFuture = null; - routingTableFuture.complete( routingTable ); - } - catch ( Throwable error ) - { - clusterCompositionLookupFailed( error ); - } - } - - private synchronized void clusterCompositionLookupFailed( Throwable error ) - { - log.error( "Failed to update routing table. Current routing table: " + routingTable, error ); - CompletableFuture routingTableFuture = refreshRoutingTableFuture; - refreshRoutingTableFuture = null; - routingTableFuture.completeExceptionally( error ); - } - private CompletionStage acquire( AccessMode mode, RoutingTable routingTable ) { AddressSet addresses = addressSet( mode, routingTable ); CompletableFuture result = new CompletableFuture<>(); - acquire( mode, addresses, result ); + acquire( mode, routingTable, addresses, result ); return result; } - private void acquire( AccessMode mode, AddressSet addresses, CompletableFuture result ) + private void acquire( AccessMode mode, RoutingTable routingTable, AddressSet addresses, CompletableFuture result ) { BoltServerAddress address = selectAddress( mode, addresses ); @@ -225,8 +137,8 @@ private void acquire( AccessMode mode, AddressSet addresses, CompletableFuture acquire( mode, addresses, result ) ); + routingTable.forget( address ); + eventExecutorGroup.next().execute( () -> acquire( mode, routingTable, addresses, result ) ); } else { @@ -268,12 +180,19 @@ private BoltServerAddress selectAddress( AccessMode mode, AddressSet servers ) } } - private static Rediscovery createRediscovery( BoltServerAddress initialRouter, RoutingSettings settings, - EventExecutorGroup eventExecutorGroup, ServerAddressResolver resolver, Clock clock, Logging logging ) + private static RoutingTableRegistry createRoutingTables( ConnectionPool connectionPool, EventExecutorGroup eventExecutorGroup, BoltServerAddress initialRouter, + ServerAddressResolver resolver, RoutingSettings settings, Clock clock, Logging logging ) { Logger log = loadBalancerLogger( logging ); + Rediscovery rediscovery = createRediscovery( eventExecutorGroup, initialRouter, resolver, settings, clock, log ); + return new RoutingTableRegistryImpl( connectionPool, rediscovery, clock, log ); + } + + private static Rediscovery createRediscovery( EventExecutorGroup eventExecutorGroup, BoltServerAddress initialRouter, ServerAddressResolver resolver, + RoutingSettings settings, Clock clock, Logger log ) + { ClusterCompositionProvider clusterCompositionProvider = new RoutingProcedureClusterCompositionProvider( clock, settings ); - return new Rediscovery( initialRouter, settings, clusterCompositionProvider, eventExecutorGroup, resolver, log ); + return new RediscoveryImpl( initialRouter, settings, clusterCompositionProvider, eventExecutorGroup, resolver, log ); } private static Logger loadBalancerLogger( Logging logging ) diff --git a/driver/src/main/java/org/neo4j/driver/internal/logging/ConsoleLogging.java b/driver/src/main/java/org/neo4j/driver/internal/logging/ConsoleLogging.java index a817401c33..ca673b32b5 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/logging/ConsoleLogging.java +++ b/driver/src/main/java/org/neo4j/driver/internal/logging/ConsoleLogging.java @@ -52,11 +52,11 @@ public Logger getLog( String name ) return new ConsoleLogger( name, level ); } - static class ConsoleLogger extends JULogger + public static class ConsoleLogger extends JULogger { private final ConsoleHandler handler; - ConsoleLogger( String name, Level level ) + public ConsoleLogger( String name, Level level ) { super( name, level ); java.util.logging.Logger logger = java.util.logging.Logger.getLogger( name ); diff --git a/driver/src/main/java/org/neo4j/driver/internal/messaging/request/MultiDatabaseUtil.java b/driver/src/main/java/org/neo4j/driver/internal/messaging/request/MultiDatabaseUtil.java index ea810d7c68..cde07106fe 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/messaging/request/MultiDatabaseUtil.java +++ b/driver/src/main/java/org/neo4j/driver/internal/messaging/request/MultiDatabaseUtil.java @@ -24,14 +24,15 @@ public final class MultiDatabaseUtil { - public static final String ABSENT_DB_NAME = ""; + public static final String ABSENT_DB_NAME = ""; // TODO _default + public static final String SYSTEM_DB_NAME = "system"; // TODO _dbms - public static void assertEmptyDatabaseName( String databaseName, int version ) + public static void assertEmptyDatabaseName( String databaseName, int boltVersion ) { if ( !Objects.equals( ABSENT_DB_NAME, databaseName ) ) { throw new ClientException( String.format( "Database name parameter for selecting database is not supported in Bolt Protocol Version %s. " + - "Database name: `%s`", version, databaseName ) ); + "Database name: `%s`", boltVersion, databaseName ) ); } } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/metrics/MetricsListener.java b/driver/src/main/java/org/neo4j/driver/internal/metrics/MetricsListener.java index 0d5e9288c1..71a57e2ddc 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/metrics/MetricsListener.java +++ b/driver/src/main/java/org/neo4j/driver/internal/metrics/MetricsListener.java @@ -21,7 +21,7 @@ import java.util.concurrent.TimeUnit; import org.neo4j.driver.internal.BoltServerAddress; -import org.neo4j.driver.internal.async.connection.DirectConnection; +import org.neo4j.driver.internal.async.NetworkConnection; import org.neo4j.driver.internal.async.pool.ConnectionPoolImpl; import org.neo4j.driver.Config; @@ -82,14 +82,14 @@ public interface MetricsListener /** * After acquiring or creating a new netty channel from pool successfully. * @param serverAddress the server the netty channel binds to. - * @param inUseEvent a connection listener registered with a {@link DirectConnection} when created. + * @param inUseEvent a connection listener registered with a {@link NetworkConnection} when created. */ void afterConnectionCreated( BoltServerAddress serverAddress, ListenerEvent inUseEvent ); /** * After releasing a netty channel back to pool successfully. * @param serverAddress the server the netty channel binds to. - * @param inUseEvent a connection listener registered with a {@link DirectConnection} when destroyed. + * @param inUseEvent a connection listener registered with a {@link NetworkConnection} when destroyed. */ void afterConnectionReleased( BoltServerAddress serverAddress, ListenerEvent inUseEvent ); diff --git a/driver/src/main/java/org/neo4j/driver/internal/util/ErrorUtil.java b/driver/src/main/java/org/neo4j/driver/internal/util/ErrorUtil.java index 89093d763f..3e448040cb 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/util/ErrorUtil.java +++ b/driver/src/main/java/org/neo4j/driver/internal/util/ErrorUtil.java @@ -27,6 +27,7 @@ import org.neo4j.driver.exceptions.ClientException; import org.neo4j.driver.exceptions.DatabaseException; import org.neo4j.driver.exceptions.Neo4jException; +import org.neo4j.driver.exceptions.FatalDiscoveryException; import org.neo4j.driver.exceptions.ServiceUnavailableException; import org.neo4j.driver.exceptions.TransientException; @@ -62,6 +63,10 @@ public static Neo4jException newNeo4jError( String code, String message ) { return new AuthenticationException( code, message ); } + else if ( code.equalsIgnoreCase( "Neo.ClientError.Database.DatabaseNotFound" ) ) + { + return new FatalDiscoveryException( code, message ); + } else { return new ClientException( code, message ); diff --git a/driver/src/main/java/org/neo4j/driver/summary/ServerInfo.java b/driver/src/main/java/org/neo4j/driver/summary/ServerInfo.java index fdd3832a94..7c8164a894 100644 --- a/driver/src/main/java/org/neo4j/driver/summary/ServerInfo.java +++ b/driver/src/main/java/org/neo4j/driver/summary/ServerInfo.java @@ -33,7 +33,7 @@ public interface ServerInfo /** * Returns a string telling which version of the server the query was executed. * Supported since neo4j 3.1. - * @return The server version of null if not available. + * @return The server version. */ String version(); } diff --git a/driver/src/main/resources/META-INF/native-image/io.netty/transport/native-image.properties b/driver/src/main/resources/META-INF/native-image/io.netty/transport/native-image.properties new file mode 100644 index 0000000000..260b13af36 --- /dev/null +++ b/driver/src/main/resources/META-INF/native-image/io.netty/transport/native-image.properties @@ -0,0 +1,9 @@ +Args = -H:ReflectionConfigurationResources=${.}/reflection-config.json \ + -H:EnableURLProtocols=http,https --enable-all-security-services -H:+JNI \ + --initialize-at-run-time=org.neo4j.driver.internal.shaded.io.netty.handler.ssl.JdkNpnApplicationProtocolNegotiator \ + --initialize-at-run-time=org.neo4j.driver.internal.shaded.io.netty.handler.ssl.ReferenceCountedOpenSslEngine \ + --initialize-at-run-time=org.neo4j.driver.internal.shaded.io.netty.handler.ssl.util.ThreadLocalInsecureRandom \ + --initialize-at-run-time=org.neo4j.driver.internal.shaded.io.netty.handler.ssl.ConscryptAlpnSslEngine \ + --initialize-at-run-time=org.neo4j.driver.internal.shaded.io.netty.util.internal.logging.Log4JLogger \ + -Dio.netty.noUnsafe=true \ + -Dio.netty.leakDetection.level=DISABLED diff --git a/driver/src/main/resources/META-INF/native-image/io.netty/transport/reflection-config.json b/driver/src/main/resources/META-INF/native-image/io.netty/transport/reflection-config.json new file mode 100644 index 0000000000..f823cca09d --- /dev/null +++ b/driver/src/main/resources/META-INF/native-image/io.netty/transport/reflection-config.json @@ -0,0 +1,14 @@ +[ + { + "name": "org.neo4j.driver.internal.shaded.io.netty.channel.socket.nio.NioSocketChannel", + "methods": [ + { "name": "", "parameterTypes": [] } + ] + }, + { + "name": "org.neo4j.driver.internal.shaded.io.netty.channel.socket.nio.NioServerSocketChannel", + "methods": [ + { "name": "", "parameterTypes": [] } + ] + } +] diff --git a/driver/src/test/java/org/neo4j/driver/GraphDatabaseTest.java b/driver/src/test/java/org/neo4j/driver/GraphDatabaseTest.java index d40974f7ae..91364dd0bd 100644 --- a/driver/src/test/java/org/neo4j/driver/GraphDatabaseTest.java +++ b/driver/src/test/java/org/neo4j/driver/GraphDatabaseTest.java @@ -32,6 +32,7 @@ import static java.util.Arrays.asList; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.hamcrest.core.IsEqual.equalTo; import static org.hamcrest.junit.MatcherAssert.assertThat; @@ -60,6 +61,7 @@ void boltSchemeShouldInstantiateDirectDriver() throws Exception // When Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); + driver.verifyConnectivity(); // Then assertThat( driver, is( directDriver() ) ); @@ -78,6 +80,7 @@ void boltPlusDiscoverySchemeShouldInstantiateClusterDriver() throws Exception // When Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); + driver.verifyConnectivity(); // Then assertThat( driver, is( clusterDriver() ) ); @@ -146,9 +149,10 @@ void shouldRespondToInterruptsWhenConnectingToUnresponsiveServer() throws Except // setup other thread to interrupt current thread when it blocks TestUtil.interruptWhenInWaitingState( Thread.currentThread() ); + final Driver driver = GraphDatabase.driver( "bolt://localhost:" + serverSocket.getLocalPort() ); try { - assertThrows( ServiceUnavailableException.class, () -> GraphDatabase.driver( "bolt://localhost:" + serverSocket.getLocalPort() ) ); + assertThrows( ServiceUnavailableException.class, driver::verifyConnectivity ); } finally { @@ -158,6 +162,33 @@ void shouldRespondToInterruptsWhenConnectingToUnresponsiveServer() throws Except } } + @Test + void shouldPrintNiceErrorWhenConnectingToUnresponsiveServer() throws Exception + { + int localPort = -1; + try ( ServerSocket serverSocket = new ServerSocket( 0 ) ) + { + localPort = serverSocket.getLocalPort(); + } + final Driver driver = GraphDatabase.driver( "bolt://localhost:" + localPort, INSECURE_CONFIG ); + final ServiceUnavailableException error = assertThrows( ServiceUnavailableException.class, driver::verifyConnectivity ); + assertThat( error.getMessage(), containsString( "Unable to connect to" ) ); + } + + @Test + void shouldPrintNiceRoutingErrorWhenConnectingToUnresponsiveServer() throws Exception + { + int localPort = -1; + try ( ServerSocket serverSocket = new ServerSocket( 0 ) ) + { + localPort = serverSocket.getLocalPort(); + } + final Driver driver = GraphDatabase.driver( "neo4j://localhost:" + localPort, INSECURE_CONFIG ); + final ServiceUnavailableException error = assertThrows( ServiceUnavailableException.class, driver::verifyConnectivity ); + error.printStackTrace(); + assertThat( error.getMessage(), containsString( "Unable to connect to" ) ); + } + @Test void shouldFailToCreateUnencryptedDriverWhenServerDoesNotRespond() throws IOException { @@ -176,9 +207,9 @@ private static void testFailureWhenServerDoesNotRespond( boolean encrypted ) thr { int connectionTimeoutMillis = 1_000; Config config = createConfig( encrypted, connectionTimeoutMillis ); + final Driver driver = GraphDatabase.driver( URI.create( "bolt://localhost:" + server.getLocalPort() ), config ); - ServiceUnavailableException e = assertThrows( ServiceUnavailableException.class, - () -> GraphDatabase.driver( URI.create( "bolt://localhost:" + server.getLocalPort() ), config ) ); + ServiceUnavailableException e = assertThrows( ServiceUnavailableException.class, driver::verifyConnectivity ); assertEquals( e.getMessage(), "Unable to establish connection in " + connectionTimeoutMillis + "ms" ); } } diff --git a/driver/src/test/java/org/neo4j/driver/integration/BookmarkIT.java b/driver/src/test/java/org/neo4j/driver/integration/BookmarkIT.java index 83e54d0665..8becbccb51 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/BookmarkIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/BookmarkIT.java @@ -38,6 +38,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.neo4j.driver.internal.SessionConfig.builder; @ParallelizableIT class BookmarkIT @@ -74,7 +75,7 @@ void shouldThrowForInvalidBookmark() { String invalidBookmark = "hi, this is an invalid bookmark"; - try ( Session session = driver.session( t -> t.withBookmarks( invalidBookmark ) ) ) + try ( Session session = driver.session( builder().withBookmarks( invalidBookmark ).build() ) ) { assertThrows( ClientException.class, session::beginTransaction ); } @@ -170,7 +171,7 @@ void bookmarkIsUpdatedOnEveryCommittedTx() void createSessionWithInitialBookmark() { String bookmark = "TheBookmark"; - try ( Session session = driver.session( t -> t.withBookmarks( bookmark ) ) ) + try ( Session session = driver.session( builder().withBookmarks( bookmark ).build() ) ) { assertEquals( bookmark, session.lastBookmark() ); } @@ -180,7 +181,7 @@ void createSessionWithInitialBookmark() void createSessionWithAccessModeAndInitialBookmark() { String bookmark = "TheBookmark"; - try ( Session session = driver.session( t -> t.withBookmarks( bookmark ) ) ) + try ( Session session = driver.session( builder().withBookmarks( bookmark ).build() ) ) { assertEquals( bookmark, session.lastBookmark() ); } diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/connection/ChannelConnectorImplIT.java b/driver/src/test/java/org/neo4j/driver/integration/ChannelConnectorImplIT.java similarity index 97% rename from driver/src/test/java/org/neo4j/driver/internal/async/connection/ChannelConnectorImplIT.java rename to driver/src/test/java/org/neo4j/driver/integration/ChannelConnectorImplIT.java index 53aabadafc..806fdc80e4 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/async/connection/ChannelConnectorImplIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/ChannelConnectorImplIT.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.neo4j.driver.internal.async.connection; +package org.neo4j.driver.integration; import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; @@ -42,6 +42,9 @@ import org.neo4j.driver.exceptions.ServiceUnavailableException; import org.neo4j.driver.internal.BoltServerAddress; import org.neo4j.driver.internal.ConnectionSettings; +import org.neo4j.driver.internal.async.connection.BootstrapFactory; +import org.neo4j.driver.internal.async.connection.ChannelConnector; +import org.neo4j.driver.internal.async.connection.ChannelConnectorImpl; import org.neo4j.driver.internal.async.inbound.ConnectTimeoutHandler; import org.neo4j.driver.internal.security.SecurityPlan; import org.neo4j.driver.internal.util.FakeClock; diff --git a/driver/src/test/java/org/neo4j/driver/integration/CredentialsIT.java b/driver/src/test/java/org/neo4j/driver/integration/CredentialsIT.java index d1768e4748..3e0fa0359a 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/CredentialsIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/CredentialsIT.java @@ -81,11 +81,11 @@ void shouldBePossibleToChangePassword() throws Exception } // verify old password does not work - assertThrows( AuthenticationException.class, - () -> GraphDatabase.driver( neo4j.uri(), AuthTokens.basic( "neo4j", PASSWORD ) ) ); + final Driver badDriver = GraphDatabase.driver( CredentialsIT.neo4j.uri(), basic( "neo4j", PASSWORD ) ); + assertThrows( AuthenticationException.class, badDriver::verifyConnectivity ); // verify new password works - try ( Driver driver = GraphDatabase.driver( neo4j.uri(), AuthTokens.basic( "neo4j", newPassword ) ); + try ( Driver driver = GraphDatabase.driver( CredentialsIT.neo4j.uri(), AuthTokens.basic( "neo4j", newPassword ) ); Session session = driver.session() ) { session.run( "RETURN 2" ).consume(); @@ -177,6 +177,7 @@ private void testDriverFailureOnWrongCredentials( String uri ) Config config = Config.builder().withLogging( DEV_NULL_LOGGING ).build(); AuthToken authToken = AuthTokens.basic( "neo4j", "wrongSecret" ); - assertThrows( AuthenticationException.class, () -> GraphDatabase.driver( uri, authToken, config ) ); + final Driver driver = GraphDatabase.driver( uri, authToken, config ); + assertThrows( AuthenticationException.class, driver::verifyConnectivity ); } } diff --git a/driver/src/test/java/org/neo4j/driver/integration/DirectDriverIT.java b/driver/src/test/java/org/neo4j/driver/integration/DirectDriverIT.java index f3b2d921f0..3ab384701b 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/DirectDriverIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/DirectDriverIT.java @@ -30,8 +30,6 @@ import org.neo4j.driver.Session; import org.neo4j.driver.StatementResult; import org.neo4j.driver.internal.BoltServerAddress; -import org.neo4j.driver.internal.util.EnabledOnNeo4jWith; -import org.neo4j.driver.internal.util.Neo4jFeature; import org.neo4j.driver.util.DatabaseExtension; import org.neo4j.driver.util.ParallelizableIT; @@ -39,7 +37,6 @@ import static org.hamcrest.core.IsEqual.equalTo; import static org.hamcrest.junit.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.neo4j.driver.internal.util.Matchers.clusterDriver; import static org.neo4j.driver.internal.util.Matchers.directDriverWithAddress; @ParallelizableIT @@ -59,22 +56,6 @@ void closeDriver() } } - @Test - @EnabledOnNeo4jWith( Neo4jFeature.BOLT_V4 ) - void shouldBeAbleToConnectSingleInstanceWithNeo4jScheme() throws Throwable - { - URI uri = URI.create( String.format( "neo4j://%s:%s", neo4j.uri().getHost(), neo4j.uri().getPort() ) ); - BoltServerAddress address = new BoltServerAddress( neo4j.uri() ); - - try ( Driver driver = GraphDatabase.driver( uri, neo4j.authToken() ); Session session = driver.session() ) - { - assertThat( driver, is( clusterDriver() ) ); - - StatementResult result = session.run( "RETURN 1" ); - assertThat( result.single().get( 0 ).asInt(), CoreMatchers.equalTo( 1 ) ); - } - } - @Test void shouldAllowIPv6Address() { diff --git a/driver/src/test/java/org/neo4j/driver/integration/DriverCloseIT.java b/driver/src/test/java/org/neo4j/driver/integration/DriverCloseIT.java index ecbedcad6f..fb2a4dc244 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/DriverCloseIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/DriverCloseIT.java @@ -29,6 +29,7 @@ import org.neo4j.driver.util.ParallelizableIT; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.neo4j.driver.internal.SessionConfig.builder; @ParallelizableIT class DriverCloseIT @@ -63,7 +64,7 @@ void sessionWithModeThrowsForClosedDriver() driver.close(); - assertThrows( IllegalStateException.class, () -> driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ) ); + assertThrows( IllegalStateException.class, () -> driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) ); } @Test diff --git a/driver/src/test/java/org/neo4j/driver/integration/EncryptionIT.java b/driver/src/test/java/org/neo4j/driver/integration/EncryptionIT.java index 2c9bc16517..eac6b9f009 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/EncryptionIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/EncryptionIT.java @@ -119,7 +119,7 @@ private void testMismatchingEncryption( BoltTlsLevel tlsLevel, boolean driverEnc Config config = newConfig( driverEncrypted ); RuntimeException e = assertThrows( RuntimeException.class, - () -> GraphDatabase.driver( neo4j.uri(), neo4j.authToken(), config ).close() ); + () -> GraphDatabase.driver( neo4j.uri(), neo4j.authToken(), config ).verifyConnectivity() ); // pre 3.1 neo4j throws different exception when encryption required but not used if ( neo4jVersion.lessThan( v3_1_0 ) && tlsLevel == BoltTlsLevel.REQUIRED ) diff --git a/driver/src/test/java/org/neo4j/driver/integration/ErrorIT.java b/driver/src/test/java/org/neo4j/driver/integration/ErrorIT.java index 8537f9c9bb..27b7cfbff0 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/ErrorIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/ErrorIT.java @@ -33,13 +33,6 @@ import java.util.function.Consumer; import java.util.stream.Stream; -import org.neo4j.driver.internal.cluster.RoutingSettings; -import org.neo4j.driver.internal.messaging.response.FailureMessage; -import org.neo4j.driver.internal.retry.RetrySettings; -import org.neo4j.driver.internal.util.io.ChannelTrackingDriverFactory; -import org.neo4j.driver.internal.util.io.ChannelTrackingDriverFactoryWithFailingMessageFormat; -import org.neo4j.driver.internal.util.FailingMessageFormat; -import org.neo4j.driver.internal.util.FakeClock; import org.neo4j.driver.AuthToken; import org.neo4j.driver.Config; import org.neo4j.driver.Driver; @@ -49,6 +42,13 @@ import org.neo4j.driver.Transaction; import org.neo4j.driver.exceptions.ClientException; import org.neo4j.driver.exceptions.ServiceUnavailableException; +import org.neo4j.driver.internal.cluster.RoutingSettings; +import org.neo4j.driver.internal.messaging.response.FailureMessage; +import org.neo4j.driver.internal.retry.RetrySettings; +import org.neo4j.driver.internal.util.FailingMessageFormat; +import org.neo4j.driver.internal.util.FakeClock; +import org.neo4j.driver.internal.util.io.ChannelTrackingDriverFactory; +import org.neo4j.driver.internal.util.io.ChannelTrackingDriverFactoryWithFailingMessageFormat; import org.neo4j.driver.util.ParallelizableIT; import org.neo4j.driver.util.SessionExtension; @@ -143,7 +143,8 @@ void shouldAllowNewTransactionAfterRecoverableError() @Test void shouldExplainConnectionError() { - ServiceUnavailableException e = assertThrows( ServiceUnavailableException.class, () -> GraphDatabase.driver( "bolt://localhost:7777" ) ); + final Driver driver = GraphDatabase.driver( "bolt://localhost:7777" ); + ServiceUnavailableException e = assertThrows( ServiceUnavailableException.class, driver::verifyConnectivity ); assertEquals( "Unable to connect to localhost:7777, ensure the database is running " + "and that there is a working network connection to it.", e.getMessage() ); @@ -179,7 +180,8 @@ void shouldGetHelpfulErrorWhenTryingToConnectToHttpPort() throws Throwable Config config = Config.builder().withoutEncryption().build(); - ClientException e = assertThrows( ClientException.class, () -> GraphDatabase.driver( "bolt://localhost:" + session.httpPort(), config ) ); + final Driver driver = GraphDatabase.driver( "bolt://localhost:" + session.httpPort(), config ); + ClientException e = assertThrows( ClientException.class, driver::verifyConnectivity ); assertEquals( "Server responded HTTP. Make sure you are not trying to connect to the http endpoint " + "(HTTP defaults to port 7474 whereas BOLT defaults to port 7687)", e.getMessage() ); } @@ -264,25 +266,29 @@ private Throwable testChannelErrorHandling( Consumer messa Config config = Config.builder().withLogging( DEV_NULL_LOGGING ).build(); Throwable queryError = null; - try ( Driver driver = driverFactory.newInstance( uri, authToken, routingSettings, retrySettings, config ); - Session session = driver.session() ) + try ( Driver driver = driverFactory.newInstance( uri, authToken, routingSettings, retrySettings, config ) ) { - messageFormatSetup.accept( driverFactory.getFailingMessageFormat() ); - - try + driver.verifyConnectivity(); + try(Session session = driver.session() ) { - session.run( "RETURN 1" ).consume(); - fail( "Exception expected" ); + messageFormatSetup.accept( driverFactory.getFailingMessageFormat() ); + + try + { + session.run( "RETURN 1" ).consume(); + fail( "Exception expected" ); + } + catch ( Throwable error ) + { + queryError = error; + } + + assertSingleChannelIsClosed( driverFactory ); + assertNewQueryCanBeExecuted( session, driverFactory ); } - catch ( Throwable error ) - { - queryError = error; - } - - assertSingleChannelIsClosed( driverFactory ); - assertNewQueryCanBeExecuted( session, driverFactory ); } + return queryError; } diff --git a/driver/src/test/java/org/neo4j/driver/integration/ExplicitTransactionIT.java b/driver/src/test/java/org/neo4j/driver/integration/ExplicitTransactionIT.java index d51afb8bc2..1882b03cad 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/ExplicitTransactionIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/ExplicitTransactionIT.java @@ -37,7 +37,7 @@ import org.neo4j.driver.exceptions.ClientException; import org.neo4j.driver.exceptions.ServiceUnavailableException; import org.neo4j.driver.internal.InternalDriver; -import org.neo4j.driver.internal.SessionParameters; +import org.neo4j.driver.internal.SessionConfig; import org.neo4j.driver.internal.async.ExplicitTransaction; import org.neo4j.driver.internal.async.NetworkSession; import org.neo4j.driver.internal.cluster.RoutingSettings; @@ -70,7 +70,7 @@ class ExplicitTransactionIT @BeforeEach void setUp() { - session = ((InternalDriver) neo4j.driver()).newSession( SessionParameters.empty() ); + session = ((InternalDriver) neo4j.driver()).newSession( SessionConfig.empty() ); } @AfterEach @@ -249,7 +249,7 @@ private void testCommitAndRollbackFailurePropagation( boolean commit ) try ( Driver driver = driverFactory.newInstance( neo4j.uri(), neo4j.authToken(), RoutingSettings.DEFAULT, RetrySettings.DEFAULT, config ) ) { - NetworkSession session = ((InternalDriver) driver).newSession( SessionParameters.empty() ); + NetworkSession session = ((InternalDriver) driver).newSession( SessionConfig.empty() ); { ExplicitTransaction tx = beginTransaction( session ); diff --git a/driver/src/test/java/org/neo4j/driver/integration/NestedQueriesIT.java b/driver/src/test/java/org/neo4j/driver/integration/NestedQueriesIT.java index 9d2f8a88d1..fde6408450 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/NestedQueriesIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/NestedQueriesIT.java @@ -25,6 +25,8 @@ import org.neo4j.driver.util.DatabaseExtension; import org.neo4j.driver.util.ParallelizableIT; +import static org.neo4j.driver.internal.SessionConfig.builder; + @ParallelizableIT public class NestedQueriesIT implements NestedQueries { @@ -34,6 +36,6 @@ public class NestedQueriesIT implements NestedQueries @Override public Session newSession( AccessMode mode ) { - return server.driver().session( t -> t.withDefaultAccessMode( mode ) ); + return server.driver().session( builder().withDefaultAccessMode( mode ).build() ); } } diff --git a/driver/src/test/java/org/neo4j/driver/integration/ResultStreamIT.java b/driver/src/test/java/org/neo4j/driver/integration/ResultStreamIT.java index b9d04a7e17..6e35003b16 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/ResultStreamIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/ResultStreamIT.java @@ -27,7 +27,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.IntStream; -import org.neo4j.driver.internal.util.DisabledOnNeo4jWith; import org.neo4j.driver.Record; import org.neo4j.driver.StatementResult; import org.neo4j.driver.Transaction; @@ -286,7 +285,7 @@ void shouldConvertEventuallyFailingStatementResultToStream() List seen = new ArrayList<>(); ClientException e = assertThrows( ClientException.class, - () -> session.run( "UNWIND range(5, 0, -1) AS x RETURN x / x" ) + () -> session.run( "CYPHER runtime=interpreted UNWIND range(5, 0, -1) AS x RETURN x / x" ) .stream() .forEach( record -> seen.add( record.get( 0 ).asInt() ) ) ); diff --git a/driver/src/test/java/org/neo4j/driver/internal/RoutingDriverBoltKitTest.java b/driver/src/test/java/org/neo4j/driver/integration/RoutingDriverBoltKitTest.java similarity index 85% rename from driver/src/test/java/org/neo4j/driver/internal/RoutingDriverBoltKitTest.java rename to driver/src/test/java/org/neo4j/driver/integration/RoutingDriverBoltKitTest.java index d2cfb2fa45..6b4f329925 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/RoutingDriverBoltKitTest.java +++ b/driver/src/test/java/org/neo4j/driver/integration/RoutingDriverBoltKitTest.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.neo4j.driver.internal; +package org.neo4j.driver.integration; import org.junit.jupiter.api.Test; @@ -38,7 +38,6 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.GraphDatabase; import org.neo4j.driver.Logger; -import org.neo4j.driver.Logging; import org.neo4j.driver.Record; import org.neo4j.driver.Session; import org.neo4j.driver.StatementResult; @@ -47,6 +46,7 @@ import org.neo4j.driver.exceptions.ServiceUnavailableException; import org.neo4j.driver.exceptions.SessionExpiredException; import org.neo4j.driver.exceptions.TransientException; +import org.neo4j.driver.internal.DriverFactory; import org.neo4j.driver.internal.cluster.RoutingSettings; import org.neo4j.driver.internal.retry.RetrySettings; import org.neo4j.driver.internal.util.DriverFactoryWithClock; @@ -72,12 +72,12 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.neo4j.driver.Logging.none; +import static org.neo4j.driver.internal.SessionConfig.builder; +import static org.neo4j.driver.util.StubServer.INSECURE_CONFIG; +import static org.neo4j.driver.util.StubServer.insecureBuilder; class RoutingDriverBoltKitTest { - private static final Config config = Config.builder().withoutEncryption().withLogging( none() ).build(); - @Test void shouldHandleAcquireReadSession() throws IOException, InterruptedException, StubServer.ForceKilled { @@ -87,7 +87,8 @@ void shouldHandleAcquireReadSession() throws IOException, InterruptedException, //START a read server StubServer readServer = StubServer.start( "read_server_v3_read.script", 9005 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); - try ( Driver driver = GraphDatabase.driver( uri, config ); Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ) ) + + try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) { List result = session.run( "MATCH (n) RETURN n.name" ).list( record -> record.get( "n.name" ).asString() ); @@ -107,7 +108,9 @@ void shouldHandleAcquireReadTransaction() throws IOException, InterruptedExcepti //START a read server StubServer readServer = StubServer.start( "read_server_v3_read_tx.script", 9005 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); - try ( Driver driver = GraphDatabase.driver( uri, config ); Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ) ) + try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) + { List result = session.readTransaction( tx -> tx.run( "MATCH (n) RETURN n.name" ) ) .list( record -> record.get( "n.name" ).asString() ); @@ -128,7 +131,8 @@ void shouldHandleAcquireReadSessionAndTransaction() throws IOException, Interrup //START a read server StubServer readServer = StubServer.start( "read_server_v3_read_tx.script", 9005 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); - try ( Driver driver = GraphDatabase.driver( uri, config ); Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ); + try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ); Transaction tx = session.beginTransaction() ) { List result = tx.run( "MATCH (n) RETURN n.name" ).list( record -> record.get( "n.name" ).asString() ); @@ -151,12 +155,12 @@ void shouldRoundRobinReadServers() throws IOException, InterruptedException, Stu StubServer readServer1 = StubServer.start( "read_server_v3_read.script", 9005 ); StubServer readServer2 = StubServer.start( "read_server_v3_read.script", 9006 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); - try ( Driver driver = GraphDatabase.driver( uri, config ) ) + try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ) ) { // Run twice, one on each read server for ( int i = 0; i < 2; i++ ) { - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ) ) + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) { assertThat( session.run( "MATCH (n) RETURN n.name" ).list( record -> record.get( "n.name" ).asString() ), equalTo( asList( "Bob", "Alice", "Tina" ) ) ); @@ -179,12 +183,12 @@ void shouldRoundRobinReadServersWhenUsingTransaction() throws IOException, Inter StubServer readServer1 = StubServer.start( "read_server_v3_read_tx.script", 9005 ); StubServer readServer2 = StubServer.start( "read_server_v3_read_tx.script", 9006 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); - try ( Driver driver = GraphDatabase.driver( uri, config ) ) + try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ) ) { // Run twice, one on each read server for ( int i = 0; i < 2; i++ ) { - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ); Transaction tx = session.beginTransaction() ) + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ); Transaction tx = session.beginTransaction() ) { assertThat( tx.run( "MATCH (n) RETURN n.name" ).list( record -> record.get( "n.name" ).asString() ), equalTo( asList( "Bob", "Alice", "Tina" ) ) ); @@ -205,17 +209,19 @@ void shouldThrowSessionExpiredIfReadServerDisappears() throws IOException, Inter StubServer server = StubServer.start( "acquire_endpoints_v3.script", 9001 ); //START a read server - StubServer.start( "dead_read_server.script", 9005 ); + final StubServer readServer = StubServer.start( "dead_read_server.script", 9005 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); //Expect assertThrows( SessionExpiredException.class, () -> { - try ( Driver driver = GraphDatabase.driver( uri, config ); Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ) ) + try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) { session.run( "MATCH (n) RETURN n.name" ); } } ); assertThat( server.exitStatus(), equalTo( 0 ) ); + assertThat( readServer.exitStatus(), equalTo( 0 ) ); } @Test @@ -225,12 +231,13 @@ void shouldThrowSessionExpiredIfReadServerDisappearsWhenUsingTransaction() throw StubServer server = StubServer.start( "acquire_endpoints_v3.script", 9001 ); //START a read server - StubServer.start( "dead_read_server.script", 9005 ); + final StubServer readServer = StubServer.start( "dead_read_server_tx.script", 9005 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); //Expect SessionExpiredException e = assertThrows( SessionExpiredException.class, () -> { - try ( Driver driver = GraphDatabase.driver( uri, config ); Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ); + try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ); Transaction tx = session.beginTransaction() ) { tx.run( "MATCH (n) RETURN n.name" ); @@ -239,6 +246,7 @@ void shouldThrowSessionExpiredIfReadServerDisappearsWhenUsingTransaction() throw } ); assertEquals( "Server at 127.0.0.1:9005 is no longer available", e.getMessage() ); assertThat( server.exitStatus(), equalTo( 0 ) ); + assertThat( readServer.exitStatus(), equalTo( 0 ) ); } @Test @@ -247,18 +255,20 @@ void shouldThrowSessionExpiredIfWriteServerDisappears() throws IOException, Inte // Given StubServer server = StubServer.start( "acquire_endpoints_v3.script", 9001 ); - //START a dead write servers - StubServer.start( "dead_read_server.script", 9007 ); + //START a dead write server + final StubServer writeServer = StubServer.start( "dead_write_server.script", 9007 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); //Expect - try ( Driver driver = GraphDatabase.driver( uri, config ); Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ) ) + try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) ) { - assertThrows( SessionExpiredException.class, () -> session.run( "MATCH (n) RETURN n.name" ).consume() ); + assertThrows( SessionExpiredException.class, () -> session.run( "CREATE (n {name:'Bob'})" ).consume() ); } finally { assertThat( server.exitStatus(), equalTo( 0 ) ); + assertThat( writeServer.exitStatus(), equalTo( 0 ) ); } } @@ -269,11 +279,12 @@ void shouldThrowSessionExpiredIfWriteServerDisappearsWhenUsingTransaction() thro StubServer server = StubServer.start( "acquire_endpoints_v3.script", 9001 ); //START a dead write servers - StubServer.start( "dead_read_server.script", 9007 ); + final StubServer writeServer = StubServer.start( "dead_read_server_tx.script", 9007 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); //Expect - try ( Driver driver = GraphDatabase.driver( uri, config ); Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ); + try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ); Transaction tx = session.beginTransaction() ) { assertThrows( SessionExpiredException.class, () -> tx.run( "MATCH (n) RETURN n.name" ).consume() ); @@ -282,6 +293,7 @@ void shouldThrowSessionExpiredIfWriteServerDisappearsWhenUsingTransaction() thro finally { assertThat( server.exitStatus(), equalTo( 0 ) ); + assertThat( writeServer.exitStatus(), equalTo( 0 ) ); } } @@ -294,7 +306,8 @@ void shouldHandleAcquireWriteSession() throws IOException, InterruptedException, //START a write server StubServer writeServer = StubServer.start( "write_server_v3_write.script", 9007 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); - try ( Driver driver = GraphDatabase.driver( uri, config ); Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ) ) + try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) ) { session.run( "CREATE (n {name:'Bob'})" ); } @@ -312,7 +325,7 @@ void shouldHandleAcquireWriteTransaction() throws IOException, InterruptedExcept //START a write server StubServer writeServer = StubServer.start( "write_server_v3_write_tx.script", 9007 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); - try ( Driver driver = GraphDatabase.driver( uri, config ); Session session = driver.session() ) + try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); Session session = driver.session() ) { session.writeTransaction( t -> t.run( "CREATE (n {name:'Bob'})" ) ); } @@ -330,7 +343,8 @@ void shouldHandleAcquireWriteSessionAndTransaction() throws IOException, Interru //START a write server StubServer writeServer = StubServer.start( "write_server_v3_write_tx.script", 9007 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); - try ( Driver driver = GraphDatabase.driver( uri, config ); Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ); + try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ); Transaction tx = session.beginTransaction() ) { tx.run( "CREATE (n {name:'Bob'})" ); @@ -351,7 +365,7 @@ void shouldRoundRobinWriteSessions() throws IOException, InterruptedException, S StubServer writeServer1 = StubServer.start( "write_server_v3_write.script", 9007 ); StubServer writeServer2 = StubServer.start( "write_server_v3_write.script", 9008 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); - try ( Driver driver = GraphDatabase.driver( uri, config ) ) + try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ) ) { for ( int i = 0; i < 2; i++ ) { @@ -377,7 +391,7 @@ void shouldRoundRobinWriteSessionsInTransaction() throws Exception StubServer writeServer1 = StubServer.start( "write_server_v3_write_tx.script", 9007 ); StubServer writeServer2 = StubServer.start( "write_server_v3_write_tx.script", 9008 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); - try ( Driver driver = GraphDatabase.driver( uri, config ) ) + try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ) ) { for ( int i = 0; i < 2; i++ ) { @@ -400,9 +414,10 @@ void shouldFailOnNonDiscoverableServer() throws IOException, InterruptedExceptio // Given StubServer.start( "discover_not_supported.script", 9001 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); + final Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); //Expect - assertThrows( ServiceUnavailableException.class, () -> GraphDatabase.driver( uri, config ) ); + assertThrows( ServiceUnavailableException.class, driver::verifyConnectivity ); } @Test @@ -411,9 +426,10 @@ void shouldFailRandomFailureInGetServers() throws IOException, InterruptedExcept // Given StubServer.start( "discover_failed.script", 9001 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); + final Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); //Expect - assertThrows( ServiceUnavailableException.class, () -> GraphDatabase.driver( uri, config ) ); + assertThrows( ServiceUnavailableException.class, driver::verifyConnectivity ); } @Test @@ -425,9 +441,9 @@ void shouldHandleLeaderSwitchWhenWriting() throws IOException, InterruptedExcept //START a write server that doesn't accept writes StubServer.start( "not_able_to_write_server.script", 9007 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); - Driver driver = GraphDatabase.driver( uri, config ); + Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); boolean failed = false; - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ) ) + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) ) { session.run( "CREATE ()" ).consume(); } @@ -452,9 +468,9 @@ void shouldHandleLeaderSwitchWhenWritingWithoutConsuming() throws IOException, I //START a write server that doesn't accept writes StubServer.start( "not_able_to_write_server.script", 9007 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); - Driver driver = GraphDatabase.driver( uri, config ); + Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); boolean failed = false; - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ) ) + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) ) { session.run( "CREATE ()" ); } @@ -479,9 +495,9 @@ void shouldHandleLeaderSwitchWhenWritingInTransaction() throws IOException, Inte //START a write server that doesn't accept writes StubServer.start( "not_able_to_write_server.script", 9007 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); - Driver driver = GraphDatabase.driver( uri, config ); + Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); boolean failed = false; - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ); Transaction tx = session.beginTransaction() ) + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ); Transaction tx = session.beginTransaction() ) { tx.run( "CREATE ()" ).consume(); } @@ -503,8 +519,8 @@ void shouldSendInitialBookmark() throws Exception StubServer router = StubServer.start( "acquire_endpoints_v3.script", 9001 ); StubServer writer = StubServer.start( "write_tx_with_bookmarks.script", 9007 ); - try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9001", config ); - Session session = driver.session( t -> t.withBookmarks( "OldBookmark" ) ) ) + try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9001", INSECURE_CONFIG ); + Session session = driver.session( builder().withBookmarks( "OldBookmark" ).build() ) ) { try ( Transaction tx = session.beginTransaction() ) { @@ -525,8 +541,8 @@ void shouldUseWriteSessionModeAndInitialBookmark() throws Exception StubServer router = StubServer.start( "acquire_endpoints_v3.script", 9001 ); StubServer writer = StubServer.start( "write_tx_with_bookmarks.script", 9008 ); - try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9001", config ); - Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ).withBookmarks( "OldBookmark" ) ) ) + try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9001", INSECURE_CONFIG ); + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).withBookmarks( "OldBookmark" ).build() ) ) { try ( Transaction tx = session.beginTransaction() ) { @@ -547,8 +563,8 @@ void shouldUseReadSessionModeAndInitialBookmark() throws Exception StubServer router = StubServer.start( "acquire_endpoints_v3.script", 9001 ); StubServer writer = StubServer.start( "read_tx_with_bookmarks.script", 9005 ); - try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9001", config ); - Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ).withBookmarks( "OldBookmark" ) ) ) + try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9001", INSECURE_CONFIG ); + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).withBookmarks( "OldBookmark" ).build() ) ) { try ( Transaction tx = session.beginTransaction() ) { @@ -572,8 +588,8 @@ void shouldPassBookmarkFromTransactionToTransaction() throws Exception StubServer router = StubServer.start( "acquire_endpoints_v3.script", 9001 ); StubServer writer = StubServer.start( "write_read_tx_with_bookmarks.script", 9007 ); - try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9001", config ); - Session session = driver.session( t -> t.withBookmarks( "BookmarkA" ) ) ) + try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9001", INSECURE_CONFIG ); + Session session = driver.session( builder().withBookmarks( "BookmarkA" ).build() ) ) { try ( Transaction tx = session.beginTransaction() ) { @@ -602,7 +618,7 @@ void shouldPassBookmarkFromTransactionToTransaction() throws Exception void shouldRetryReadTransactionUntilSuccess() throws Exception { StubServer router = StubServer.start( "acquire_endpoints_v3.script", 9001 ); - StubServer brokenReader = StubServer.start( "dead_read_server.script", 9005 ); + StubServer brokenReader = StubServer.start( "dead_read_server_tx.script", 9005 ); StubServer reader = StubServer.start( "read_server_v3_read_tx.script", 9006 ); try ( Driver driver = newDriverWithSleeplessClock( "neo4j://127.0.0.1:9001" ); Session session = driver.session() ) @@ -658,8 +674,9 @@ void shouldRetryWriteTransactionUntilSuccessWithWhenLeaderIsRemoved() throws Exc StubServer writer = StubServer.start( "write_server_v3_write_tx.script", 9008 ); Logger logger = mock( Logger.class ); - Config config = Config.builder().withoutEncryption().withLogging( mockedLogging( logger ) ).build(); - try ( Driver driver = newDriverWithSleeplessClock( "neo4j://127.0.0.1:9001", config ); Session session = driver.session() ) + Config config = insecureBuilder().withLogging( ignored -> logger ).build(); + try ( Driver driver = newDriverWithSleeplessClock( "neo4j://127.0.0.1:9001", config ); + Session session = driver.session() ) { AtomicInteger invocations = new AtomicInteger(); List records = session.writeTransaction( queryWork( "CREATE (n {name:'Bob'})", invocations ) ); @@ -691,8 +708,9 @@ void shouldRetryWriteTransactionUntilSuccessWithWhenLeaderIsRemovedV3() throws E StubServer writer = StubServer.start( "write_server_v3_write_tx.script", 9008 ); Logger logger = mock( Logger.class ); - Config config = Config.builder().withoutEncryption().withLogging( mockedLogging( logger ) ).build(); - try ( Driver driver = newDriverWithSleeplessClock( "neo4j://127.0.0.1:9001", config ); Session session = driver.session() ) + Config config = insecureBuilder().withLogging( ignored -> logger ).build(); + try ( Driver driver = newDriverWithSleeplessClock( "neo4j://127.0.0.1:9001", config ); + Session session = driver.session() ) { AtomicInteger invocations = new AtomicInteger(); List records = session.writeTransaction( queryWork( "CREATE (n {name:'Bob'})", invocations ) ); @@ -715,10 +733,11 @@ void shouldRetryWriteTransactionUntilSuccessWithWhenLeaderIsRemovedV3() throws E void shouldRetryReadTransactionUntilFailure() throws Exception { StubServer router = StubServer.start( "acquire_endpoints_v3.script", 9001 ); - StubServer brokenReader1 = StubServer.start( "dead_read_server.script", 9005 ); - StubServer brokenReader2 = StubServer.start( "dead_read_server.script", 9006 ); + StubServer brokenReader1 = StubServer.start( "dead_read_server_tx.script", 9005 ); + StubServer brokenReader2 = StubServer.start( "dead_read_server_tx.script", 9006 ); - try ( Driver driver = newDriverWithFixedRetries( "neo4j://127.0.0.1:9001", 1 ); Session session = driver.session() ) + try ( Driver driver = newDriverWithFixedRetries( "neo4j://127.0.0.1:9001", 1 ); + Session session = driver.session() ) { AtomicInteger invocations = new AtomicInteger(); assertThrows( SessionExpiredException.class, () -> session.readTransaction( queryWork( "MATCH (n) RETURN n.name", invocations ) ) ); @@ -757,8 +776,8 @@ void shouldRetryWriteTransactionUntilFailure() throws Exception void shouldRetryReadTransactionAndPerformRediscoveryUntilSuccess() throws Exception { StubServer router1 = StubServer.start( "acquire_endpoints_v3.script", 9010 ); - StubServer brokenReader1 = StubServer.start( "dead_read_server.script", 9005 ); - StubServer brokenReader2 = StubServer.start( "dead_read_server.script", 9006 ); + StubServer brokenReader1 = StubServer.start( "dead_read_server_tx.script", 9005 ); + StubServer brokenReader2 = StubServer.start( "dead_read_server_tx.script", 9006 ); StubServer router2 = StubServer.start( "discover_servers.script", 9003 ); StubServer reader = StubServer.start( "read_server_v3_read_tx.script", 9004 ); @@ -813,9 +832,10 @@ void shouldUseInitialRouterForRediscoveryWhenAllOtherRoutersAreDead() throws Exc // initial router does not have itself in the returned set of routers StubServer router = StubServer.start( "acquire_endpoints_v3.script", 9010 ); - try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9010", config ) ) + try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9010", INSECURE_CONFIG ) ) { - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ) ) + driver.verifyConnectivity(); + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) { // restart router on the same port with different script that contains itself as reader assertEquals( 0, router.exitStatus() ); @@ -835,7 +855,7 @@ void shouldInvokeProcedureGetRoutingTableWhenServerVersionPermits() throws Excep // stub server is both a router and reader StubServer server = StubServer.start( "get_routing_table.script", 9001 ); - try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9001", config ); Session session = driver.session() ) + try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9001", INSECURE_CONFIG ); Session session = driver.session() ) { List records = session.run( "MATCH (n) RETURN n.name AS name" ).list(); assertEquals( 3, records.size() ); @@ -856,7 +876,7 @@ void shouldSendRoutingContextToServer() throws Exception StubServer server = StubServer.start( "get_routing_table_with_context.script", 9001 ); URI uri = URI.create( "neo4j://127.0.0.1:9001/?policy=my_policy®ion=china" ); - try ( Driver driver = GraphDatabase.driver( uri, config ); Session session = driver.session() ) + try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); Session session = driver.session() ) { List records = session.run( "MATCH (n) RETURN n.name AS name" ).list(); assertEquals( 2, records.size() ); @@ -876,7 +896,7 @@ void shouldServeReadsButFailWritesWhenNoWritersAvailable() throws Exception StubServer router2 = StubServer.start( "discover_no_writers.script", 9004 ); StubServer reader = StubServer.start( "read_server_v3_read_tx.script", 9003 ); - try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9010", config ); + try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9010", INSECURE_CONFIG ); Session session = driver.session() ) { assertEquals( asList( "Bob", "Alice", "Tina" ), readStrings( "MATCH (n) RETURN n.name", session ) ); @@ -901,15 +921,19 @@ void shouldAcceptRoutingTableWithoutWritersAndThenRediscover() throws Exception StubServer reader = StubServer.start( "read_server_v3_read_tx.script", 9003 ); StubServer writer = StubServer.start( "write_with_bookmarks.script", 9007 ); - try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9010", config ); Session session = driver.session() ) + try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9010", INSECURE_CONFIG ) ) { - // start another router which knows about writes, use same address as the initial router - router2 = StubServer.start( "acquire_endpoints_v3.script", 9010 ); + driver.verifyConnectivity(); + try ( Session session = driver.session() ) + { + // start another router which knows about writes, use same address as the initial router + router2 = StubServer.start( "acquire_endpoints_v3.script", 9010 ); - assertEquals( asList( "Bob", "Alice", "Tina" ), readStrings( "MATCH (n) RETURN n.name", session ) ); + assertEquals( asList( "Bob", "Alice", "Tina" ), readStrings( "MATCH (n) RETURN n.name", session ) ); - StatementResult createResult = session.run( "CREATE (n {name:'Bob'})" ); - assertFalse( createResult.hasNext() ); + StatementResult createResult = session.run( "CREATE (n {name:'Bob'})" ); + assertFalse( createResult.hasNext() ); + } } finally { @@ -928,8 +952,8 @@ void shouldTreatRoutingTableWithSingleRouterAsValid() throws Exception StubServer reader1 = StubServer.start( "read_server_v3_read.script", 9003 ); StubServer reader2 = StubServer.start( "read_server_v3_read.script", 9004 ); - try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9010", config ); - Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ) ) + try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9010", INSECURE_CONFIG ); + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) { // returned routing table contains only one router, this should be fine and we should be able to // read multiple times without additional rediscovery @@ -960,7 +984,7 @@ void shouldSendMultipleBookmarks() throws Exception asList( "neo4j:bookmark:v1:tx5", "neo4j:bookmark:v1:tx29", "neo4j:bookmark:v1:tx94", "neo4j:bookmark:v1:tx56", "neo4j:bookmark:v1:tx16", "neo4j:bookmark:v1:tx68" ); - try ( Driver driver = GraphDatabase.driver( "neo4j://localhost:9001", config ); Session session = driver.session( t -> t.withBookmarks( bookmarks ) ) ) + try ( Driver driver = GraphDatabase.driver( "neo4j://localhost:9001", INSECURE_CONFIG ); Session session = driver.session( builder().withBookmarks( bookmarks ).build() ) ) { try ( Transaction tx = session.beginTransaction() ) { @@ -1013,9 +1037,10 @@ void shouldFailInitialDiscoveryWhenConfiguredResolverThrows() ServerAddressResolver resolver = mock( ServerAddressResolver.class ); when( resolver.resolve( any( ServerAddress.class ) ) ).thenThrow( new RuntimeException( "Resolution failure!" ) ); - Config config = Config.builder().withLogging( none() ).withoutEncryption().withResolver( resolver ).build(); + Config config = insecureBuilder().withResolver( resolver ).build(); + final Driver driver = GraphDatabase.driver( "neo4j://my.server.com:9001", config ); - RuntimeException error = assertThrows( RuntimeException.class, () -> GraphDatabase.driver( "neo4j://my.server.com:9001", config ) ); + RuntimeException error = assertThrows( RuntimeException.class, driver::verifyConnectivity ); assertEquals( "Resolution failure!", error.getMessage() ); verify( resolver ).resolve( ServerAddress.of( "my.server.com", 9001 ) ); } @@ -1043,7 +1068,7 @@ void shouldUseResolverDuringRediscoveryWhenExistingRoutersFail() throws Exceptio throw new AssertionError(); }; - Config config = Config.builder().withLogging( none() ).withoutEncryption().withResolver( resolver ).build(); + Config config = insecureBuilder().withResolver( resolver ).build(); try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9001", config ) ) { @@ -1072,17 +1097,15 @@ void useSessionAfterDriverIsClosed() throws Exception StubServer router = StubServer.start( "acquire_endpoints_v3.script", 9001 ); StubServer readServer = StubServer.start( "read_server_v3_read.script", 9005 ); - Config config = Config.builder().withoutEncryption().withLogging( none() ).build(); - - try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9001", config ) ) + try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9001", INSECURE_CONFIG ) ) { - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ) ) + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) { List records = session.run( "MATCH (n) RETURN n.name" ).list(); assertEquals( 3, records.size() ); } - Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ); + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ); driver.close(); @@ -1105,7 +1128,7 @@ void shouldRevertToInitialRouterIfKnownRouterThrowsProtocolErrors() throws Excep return addresses; }; - Config config = Config.builder().withoutEncryption().withResolver( resolver ).build(); + Config config = insecureBuilder().withResolver( resolver ).build(); StubServer router1 = StubServer.start( "acquire_endpoints_v3_point_to_empty_router_and_exit.script", 9001 ); StubServer router2 = StubServer.start( "acquire_endpoints_v3_empty.script", 9004 ); @@ -1114,7 +1137,7 @@ void shouldRevertToInitialRouterIfKnownRouterThrowsProtocolErrors() throws Excep try ( Driver driver = GraphDatabase.driver( "neo4j://my.virtual.host:8080", config ) ) { - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ) ) + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) { List records = session.readTransaction( tx -> tx.run( "MATCH (n) RETURN n.name" ) ).list(); assertEquals( 3, records.size() ); @@ -1137,13 +1160,13 @@ private static Driver newDriverWithSleeplessClock( String uriString, Config conf private static Driver newDriverWithSleeplessClock( String uriString ) { - return newDriverWithSleeplessClock( uriString, config ); + return newDriverWithSleeplessClock( uriString, INSECURE_CONFIG ); } private static Driver newDriverWithFixedRetries( String uriString, int retries ) { DriverFactory driverFactory = new DriverFactoryWithFixedRetryLogic( retries ); - return newDriver( uriString, driverFactory, config ); + return newDriver( uriString, driverFactory, INSECURE_CONFIG ); } private static Driver newDriver( String uriString, DriverFactory driverFactory, Config config ) @@ -1175,16 +1198,8 @@ private static List readStrings( final String query, Session session ) } ); } - private static Logging mockedLogging( Logger logger ) + static class PortBasedServerAddressComparator implements Comparator { - Logging logging = mock( Logging.class ); - when( logging.getLog( any() ) ).thenReturn( logger ); - return logging; - } - - private static class PortBasedServerAddressComparator implements Comparator - { - @Override public int compare( ServerAddress a1, ServerAddress a2 ) { diff --git a/driver/src/test/java/org/neo4j/driver/integration/RoutingDriverIT.java b/driver/src/test/java/org/neo4j/driver/integration/RoutingDriverIT.java new file mode 100644 index 0000000000..e5e51ee798 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/integration/RoutingDriverIT.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.integration; + +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.net.URI; + +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; +import org.neo4j.driver.Session; +import org.neo4j.driver.StatementResult; +import org.neo4j.driver.internal.util.EnabledOnNeo4jWith; +import org.neo4j.driver.internal.util.Neo4jFeature; +import org.neo4j.driver.util.DatabaseExtension; +import org.neo4j.driver.util.ParallelizableIT; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.junit.MatcherAssert.assertThat; +import static org.neo4j.driver.internal.SessionConfig.forDatabase; +import static org.neo4j.driver.internal.util.Matchers.clusterDriver; + +@ParallelizableIT +@EnabledOnNeo4jWith( Neo4jFeature.BOLT_V4 ) +class RoutingDriverIT +{ + @RegisterExtension + static final DatabaseExtension neo4j = new DatabaseExtension(); + + @Test + void shouldBeAbleToConnectSingleInstanceWithNeo4jScheme() throws Throwable + { + URI uri = URI.create( String.format( "neo4j://%s:%s", neo4j.uri().getHost(), neo4j.uri().getPort() ) ); + + try ( Driver driver = GraphDatabase.driver( uri, neo4j.authToken() ); + Session session = driver.session() ) + { + assertThat( driver, is( clusterDriver() ) ); + + StatementResult result = session.run( "RETURN 1" ); + assertThat( result.single().get( 0 ).asInt(), CoreMatchers.equalTo( 1 ) ); + } + } + + @Test + void shouldBeAbleToRunQueryOnNeo4j() throws Throwable + { + URI uri = URI.create( String.format( "neo4j://%s:%s", neo4j.uri().getHost(), neo4j.uri().getPort() ) ); + try ( Driver driver = GraphDatabase.driver( uri, neo4j.authToken() ); + Session session = driver.session( forDatabase( "neo4j" ) ) ) + { + assertThat( driver, is( clusterDriver() ) ); + + StatementResult result = session.run( "RETURN 1" ); + assertThat( result.single().get( 0 ).asInt(), CoreMatchers.equalTo( 1 ) ); + } + } +} diff --git a/driver/src/test/java/org/neo4j/driver/integration/RoutingDriverMultidatabaseBoltKitTest.java b/driver/src/test/java/org/neo4j/driver/integration/RoutingDriverMultidatabaseBoltKitTest.java new file mode 100644 index 0000000000..35aeee5461 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/integration/RoutingDriverMultidatabaseBoltKitTest.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.integration; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URI; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.Config; +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; +import org.neo4j.driver.Record; +import org.neo4j.driver.Session; +import org.neo4j.driver.exceptions.FatalDiscoveryException; +import org.neo4j.driver.exceptions.ServiceUnavailableException; +import org.neo4j.driver.integration.RoutingDriverBoltKitTest.PortBasedServerAddressComparator; +import org.neo4j.driver.net.ServerAddress; +import org.neo4j.driver.net.ServerAddressResolver; +import org.neo4j.driver.util.StubServer; + +import static java.util.Arrays.asList; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.hamcrest.junit.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.neo4j.driver.internal.SessionConfig.builder; +import static org.neo4j.driver.util.StubServer.INSECURE_CONFIG; +import static org.neo4j.driver.util.StubServer.insecureBuilder; + +class RoutingDriverMultidatabaseBoltKitTest +{ + @Test + void shouldDiscoverForDatabase() throws IOException, InterruptedException, StubServer.ForceKilled + { + // Given + StubServer router = StubServer.start( "acquire_endpoints_v4.script", 9001 ); + //START a read server + StubServer reader = StubServer.start( "read_server_v4_read.script", 9005 ); + URI uri = URI.create( "neo4j://127.0.0.1:9001" ); + try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).withDatabase( "myDatabase" ).build() ) ) + { + List result = session.run( "MATCH (n) RETURN n.name" ).list( record -> record.get( "n.name" ).asString() ); + + assertThat( result, equalTo( asList( "Bob", "Alice", "Tina" ) ) ); + } + // Finally + assertThat( router.exitStatus(), equalTo( 0 ) ); + assertThat( reader.exitStatus(), equalTo( 0 ) ); + } + + @Test + void shouldRetryOnEmptyDiscoveryResult() throws IOException, InterruptedException, StubServer.ForceKilled + { + ServerAddressResolver resolver = a -> { + SortedSet addresses = new TreeSet<>( new PortBasedServerAddressComparator() ); + addresses.add( ServerAddress.of( "127.0.0.1", 9001 ) ); + addresses.add( ServerAddress.of( "127.0.0.1", 9002 ) ); + return addresses; + }; + + StubServer emptyRouter = StubServer.start( "acquire_endpoints_v4_empty.script", 9001 ); + StubServer realRouter = StubServer.start( "acquire_endpoints_v4.script", 9002 ); + StubServer reader = StubServer.start( "read_server_v4_read.script", 9005 ); + + Config config = insecureBuilder().withResolver( resolver ).build(); + try ( Driver driver = GraphDatabase.driver( "neo4j://my.virtual.host:8080", config ); + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).withDatabase( "myDatabase" ).build() ) ) + { + List result = session.run( "MATCH (n) RETURN n.name" ).list( record -> record.get( "n.name" ).asString() ); + + assertThat( result, equalTo( asList( "Bob", "Alice", "Tina" ) ) ); + } + // Finally + assertThat( emptyRouter.exitStatus(), equalTo( 0 ) ); + assertThat( realRouter.exitStatus(), equalTo( 0 ) ); + assertThat( reader.exitStatus(), equalTo( 0 ) ); + } + + @Test + void shouldThrowRoutingErrorIfDatabaseNotFound() throws IOException, InterruptedException, StubServer.ForceKilled + { + // Given + StubServer server = StubServer.start( "acquire_endpoints_v4_database_not_found.script", 9001 ); + + URI uri = URI.create( "neo4j://127.0.0.1:9001" ); + try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).withDatabase( "myDatabase" ).build() ) ) + { + final FatalDiscoveryException error = assertThrows( FatalDiscoveryException.class, () -> { + session.run( "MATCH (n) RETURN n.name" ); + } ); + + assertThat( error.code(), equalTo( "Neo.ClientError.Database.DatabaseNotFound" ) ); + } + // Finally + assertThat( server.exitStatus(), equalTo( 0 ) ); + } + + @Test + void shouldBeAbleToServeReachableDatabase() throws IOException, InterruptedException, StubServer.ForceKilled + { + // Given + StubServer router = StubServer.start( "acquire_endpoints_v4_multi_db.script", 9001 ); + StubServer readServer = StubServer.start( "read_server_v4_read.script", 9005 ); + + URI uri = URI.create( "neo4j://127.0.0.1:9001" ); + + try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ) ) + { + try( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).withDatabase( "Unreachable" ).build() ) ) + { + final ServiceUnavailableException error = assertThrows( ServiceUnavailableException.class, () -> { + session.run( "MATCH (n) RETURN n.name" ); + } ); + + assertThat( error.getMessage(), containsString( "Could not perform discovery for database 'Unreachable'" ) ); + } + + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).withDatabase( "myDatabase" ).build() ) ) + { + List result = session.run( "MATCH (n) RETURN n.name" ).list( record -> record.get( "n.name" ).asString() ); + + assertThat( result, equalTo( asList( "Bob", "Alice", "Tina" ) ) ); + } + } + // Finally + assertThat( router.exitStatus(), equalTo( 0 ) ); + assertThat( readServer.exitStatus(), equalTo( 0 ) ); + } + + + @Test + void shouldVerifyConnectivityOnDriverCreation() throws Throwable + { + StubServer router = StubServer.start( "acquire_endpoints_v4_verify_connectivity.script", 9001 ); + StubServer readServer = StubServer.start( "read_server_v4_read.script", 9005 ); + + URI uri = URI.create( "neo4j://127.0.0.1:9001" ); + try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ) ) + { + driver.verifyConnectivity(); + try ( Session session = driver.session( builder().withDatabase( "myDatabase" ).withDefaultAccessMode( AccessMode.READ ).build() ) ) + { + List records = session.run( "MATCH (n) RETURN n.name" ).list(); + assertEquals( 3, records.size() ); + } + + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ); + + driver.close(); + + assertThrows( IllegalStateException.class, () -> session.run( "MATCH (n) RETURN n.name" ) ); + } + finally + { + assertEquals( 0, readServer.exitStatus() ); + assertEquals( 0, router.exitStatus() ); + } + } +} diff --git a/driver/src/test/java/org/neo4j/driver/integration/SessionIT.java b/driver/src/test/java/org/neo4j/driver/integration/SessionIT.java index 18ca46eeba..c5316fc79c 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/SessionIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/SessionIT.java @@ -59,8 +59,8 @@ import org.neo4j.driver.internal.util.DriverFactoryWithFixedRetryLogic; import org.neo4j.driver.internal.util.DriverFactoryWithOneEventLoopThread; import org.neo4j.driver.internal.util.EnabledOnNeo4jWith; -import org.neo4j.driver.reactive.RxStatementResult; import org.neo4j.driver.reactive.RxSession; +import org.neo4j.driver.reactive.RxStatementResult; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.summary.StatementType; import org.neo4j.driver.util.DatabaseExtension; @@ -88,6 +88,8 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.neo4j.driver.Values.parameters; +import static org.neo4j.driver.internal.SessionConfig.builder; +import static org.neo4j.driver.internal.SessionConfig.forDatabase; import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; import static org.neo4j.driver.internal.util.Matchers.arithmeticError; import static org.neo4j.driver.internal.util.Matchers.connectionAcquisitionTimeoutError; @@ -152,7 +154,7 @@ void shouldHandleNullAuthToken() // null auth token should be interpreted as AuthTokens.none() and fail driver creation // because server expects basic auth - assertThrows( AuthenticationException.class, () -> GraphDatabase.driver( neo4j.uri(), token ) ); + assertThrows( AuthenticationException.class, () -> GraphDatabase.driver( neo4j.uri(), token ).verifyConnectivity() ); } @Test @@ -866,7 +868,7 @@ void shouldAllowConsumingRecordsAfterFailureInSessionClose() { Session session = neo4j.driver().session(); - StatementResult result = session.run( "UNWIND [2, 4, 8, 0] AS x RETURN 32 / x" ); + StatementResult result = session.run( "CYPHER runtime=interpreted UNWIND [2, 4, 8, 0] AS x RETURN 32 / x" ); ClientException e = assertThrows( ClientException.class, session::close ); assertThat( e, is( arithmeticError() ) ); @@ -1224,7 +1226,7 @@ void shouldErrorWhenTryingToUseRxAPIWithoutBoltV4() throws Throwable void shouldErrorWhenTryingToUseDatabaseNameWithoutBoltV4() throws Throwable { // Given - Session session = neo4j.driver().session( t -> t.withDatabase( "foo" ) ); + Session session = neo4j.driver().session( forDatabase( "foo" ) ); // When trying to run the query on a server that is using a protocol that is lower than V4 ClientException error = assertThrows( ClientException.class, () -> session.run( "RETURN 1" ) ); @@ -1237,7 +1239,7 @@ void shouldErrorWhenTryingToUseDatabaseNameWithoutBoltV4() throws Throwable void shouldErrorWhenTryingToUseDatabaseNameWithoutBoltV4UsingTx() throws Throwable { // Given - Session session = neo4j.driver().session( t -> t.withDatabase( "foo" ) ); + Session session = neo4j.driver().session( forDatabase( "foo" ) ); // When trying to run the query on a server that is using a protocol that is lower than V4 ClientException error = assertThrows( ClientException.class, session::beginTransaction ); @@ -1250,7 +1252,7 @@ void shouldErrorWhenTryingToUseDatabaseNameWithoutBoltV4UsingTx() throws Throwab void shouldAllowDatabaseName() throws Throwable { // Given - try( Session session = neo4j.driver().session( t -> t.withDatabase( "neo4j" ) ) ) + try( Session session = neo4j.driver().session( forDatabase( "neo4j" ) ) ) { StatementResult result = session.run( "RETURN 1" ); assertThat( result.single().get( 0 ).asInt(), equalTo( 1 ) ); @@ -1261,7 +1263,7 @@ void shouldAllowDatabaseName() throws Throwable @EnabledOnNeo4jWith( BOLT_V4 ) void shouldAllowDatabaseNameUsingTx() throws Throwable { - try ( Session session = neo4j.driver().session( t -> t.withDatabase( "neo4j" ) ); + try ( Session session = neo4j.driver().session( forDatabase( "neo4j" ) ); Transaction transaction = session.beginTransaction() ) { StatementResult result = transaction.run( "RETURN 1" ); @@ -1273,7 +1275,7 @@ void shouldAllowDatabaseNameUsingTx() throws Throwable @EnabledOnNeo4jWith( BOLT_V4 ) void shouldAllowDatabaseNameUsingTxWithRetries() throws Throwable { - try ( Session session = neo4j.driver().session( t -> t.withDatabase( "neo4j" ) ) ) + try ( Session session = neo4j.driver().session( forDatabase( "neo4j" ) ) ) { int num = session.readTransaction( tx -> tx.run( "RETURN 1" ).single().get( 0 ).asInt() ); assertThat( num, equalTo( 1 ) ); @@ -1284,14 +1286,14 @@ void shouldAllowDatabaseNameUsingTxWithRetries() throws Throwable @EnabledOnNeo4jWith( BOLT_V4 ) void shouldErrorDatabaseWhenDatabaseIsAbsent() throws Throwable { - Session session = neo4j.driver().session( t -> t.withDatabase( "foo" ) ); + Session session = neo4j.driver().session( forDatabase( "foo" ) ); ClientException error = assertThrows( ClientException.class, () -> { StatementResult result = session.run( "RETURN 1" ); result.consume(); } ); - assertThat( error.getMessage(), containsString( "Database does not exists. Database name: 'foo'" ) ); + assertThat( error.getMessage(), containsString( "Database does not exist. Database name: 'foo'" ) ); session.close(); } @@ -1300,7 +1302,7 @@ void shouldErrorDatabaseWhenDatabaseIsAbsent() throws Throwable void shouldErrorDatabaseNameUsingTxWhenDatabaseIsAbsent() throws Throwable { // Given - Session session = neo4j.driver().session( t -> t.withDatabase( "foo" ) ); + Session session = neo4j.driver().session( forDatabase( "foo" ) ); // When trying to run the query on a server that is using a protocol that is lower than V4 ClientException error = assertThrows( ClientException.class, () -> { @@ -1308,7 +1310,7 @@ void shouldErrorDatabaseNameUsingTxWhenDatabaseIsAbsent() throws Throwable StatementResult result = transaction.run( "RETURN 1" ); result.consume(); }); - assertThat( error.getMessage(), containsString( "Database does not exists. Database name: 'foo'" ) ); + assertThat( error.getMessage(), containsString( "Database does not exist. Database name: 'foo'" ) ); session.close(); } @@ -1317,13 +1319,13 @@ void shouldErrorDatabaseNameUsingTxWhenDatabaseIsAbsent() throws Throwable void shouldErrorDatabaseNameUsingTxWithRetriesWhenDatabaseIsAbsent() throws Throwable { // Given - Session session = neo4j.driver().session( t -> t.withDatabase( "foo" ) ); + Session session = neo4j.driver().session( forDatabase( "foo" ) ); // When trying to run the query on a database that does not exist ClientException error = assertThrows( ClientException.class, () -> { session.readTransaction( tx -> tx.run( "RETURN 1" ).consume() ); }); - assertThat( error.getMessage(), containsString( "Database does not exists. Database name: 'foo'" ) ); + assertThat( error.getMessage(), containsString( "Database does not exist. Database name: 'foo'" ) ); session.close(); } @@ -1339,7 +1341,7 @@ private void testExecuteReadTx( AccessMode sessionMode ) } // read previously committed data - try ( Session session = driver.session( t -> t.withDefaultAccessMode( sessionMode ) ) ) + try ( Session session = driver.session( builder().withDefaultAccessMode( sessionMode ).build() ) ) { Set names = session.readTransaction( tx -> { @@ -1361,7 +1363,7 @@ private void testExecuteWriteTx( AccessMode sessionMode ) Driver driver = neo4j.driver(); // write some test data - try ( Session session = driver.session( t -> t.withDefaultAccessMode( sessionMode ) ) ) + try ( Session session = driver.session( builder().withDefaultAccessMode( sessionMode ).build() ) ) { String material = session.writeTransaction( tx -> { @@ -1386,7 +1388,7 @@ private void testTxRollbackWhenFunctionThrows( AccessMode sessionMode ) { Driver driver = neo4j.driver(); - try ( Session session = driver.session( t -> t.withDefaultAccessMode( sessionMode ) ) ) + try ( Session session = driver.session( builder().withDefaultAccessMode( sessionMode ).build() ) ) { assertThrows( ClientException.class, () -> session.writeTransaction( tx -> diff --git a/driver/src/test/java/org/neo4j/driver/integration/TrustCustomCertificateIT.java b/driver/src/test/java/org/neo4j/driver/integration/TrustCustomCertificateIT.java index cab213a607..f15e4387aa 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/TrustCustomCertificateIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/TrustCustomCertificateIT.java @@ -75,7 +75,8 @@ void shouldRejectServerWithUntrustedCertificate() throws Throwable CertificateKeyPair certificateAndKey = createNewCertificateAndKey(); // When & Then - SecurityException error = assertThrows( SecurityException.class, () -> createDriverWithCustomCertificate( certificateAndKey.cert() ) ); + final Driver driver = createDriverWithCustomCertificate( certificateAndKey.cert() ); + SecurityException error = assertThrows( SecurityException.class, driver::verifyConnectivity ); } private void shouldBeAbleToRunCypher( Supplier driverSupplier ) diff --git a/driver/src/test/java/org/neo4j/driver/integration/TrustOnFirstUseIT.java b/driver/src/test/java/org/neo4j/driver/integration/TrustOnFirstUseIT.java index 164d99bba3..3446841ebf 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/TrustOnFirstUseIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/TrustOnFirstUseIT.java @@ -114,7 +114,8 @@ private Driver createDriverWithKnownHostsFile( File knownHostsFile ) private void shouldFailToRunCypherWithAMeaningfulError( Supplier driverSupplier ) { - SecurityException exception = assertThrows( SecurityException.class, driverSupplier::get ); + final Driver driver = driverSupplier.get(); + SecurityException exception = assertThrows( SecurityException.class, driver::verifyConnectivity ); Throwable rootCause = getRootCause( exception ); assertThat( rootCause.toString(), containsString( "Unable to connect to neo4j at `localhost:" + neo4j.boltPort() + "`, " + diff --git a/driver/src/test/java/org/neo4j/driver/integration/AsyncSessionIT.java b/driver/src/test/java/org/neo4j/driver/integration/async/AsyncSessionIT.java similarity index 98% rename from driver/src/test/java/org/neo4j/driver/integration/AsyncSessionIT.java rename to driver/src/test/java/org/neo4j/driver/integration/async/AsyncSessionIT.java index 2c5bb64efe..b5b95330ca 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/AsyncSessionIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/async/AsyncSessionIT.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.neo4j.driver.integration; +package org.neo4j.driver.integration.async; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -73,6 +73,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.neo4j.driver.Values.parameters; +import static org.neo4j.driver.internal.SessionConfig.builder; import static org.neo4j.driver.internal.util.Futures.failedFuture; import static org.neo4j.driver.internal.util.Iterables.single; import static org.neo4j.driver.internal.util.Matchers.arithmeticError; @@ -156,7 +157,7 @@ void shouldFailForIncorrectQuery() @Test void shouldFailWhenQueryFailsAtRuntime() { - StatementResultCursor cursor = await( session.runAsync( "UNWIND [1, 2, 0] AS x RETURN 10 / x" ) ); + StatementResultCursor cursor = await( session.runAsync( "CYPHER runtime=interpreted UNWIND [1, 2, 0] AS x RETURN 10 / x" ) ); Record record1 = await( cursor.nextAsync() ); assertNotNull( record1 ); @@ -598,7 +599,7 @@ void shouldRunAfterRunFailureToAcquireConnection() @DisabledOnNeo4jWith( BOLT_V3 ) void shouldRunAfterBeginTxFailureOnBookmark() { - session = neo4j.driver().asyncSession( t -> t.withBookmarks( "Illegal Bookmark" ) ); + session = neo4j.driver().asyncSession( builder().withBookmarks( "Illegal Bookmark" ).build() ); assertThrows( ClientException.class, () -> await( session.beginTransactionAsync() ) ); @@ -610,7 +611,7 @@ void shouldRunAfterBeginTxFailureOnBookmark() @Test void shouldNotBeginTxAfterBeginTxFailureOnBookmark() { - session = neo4j.driver().asyncSession( t -> t.withBookmarks( "Illegal Bookmark" ) ); + session = neo4j.driver().asyncSession( builder().withBookmarks( "Illegal Bookmark" ).build() ); assertThrows( ClientException.class, () -> await( session.beginTransactionAsync() ) ); assertThrows( ClientException.class, () -> await( session.beginTransactionAsync() ) ); } @@ -619,7 +620,7 @@ void shouldNotBeginTxAfterBeginTxFailureOnBookmark() @EnabledOnNeo4jWith( BOLT_V3 ) void shouldNotRunAfterBeginTxFailureOnBookmark() { - session = neo4j.driver().asyncSession( t -> t.withBookmarks( "Illegal Bookmark" ) ); + session = neo4j.driver().asyncSession( builder().withBookmarks( "Illegal Bookmark" ).build() ); assertThrows( ClientException.class, () -> await( session.beginTransactionAsync() ) ); StatementResultCursor cursor = await( session.runAsync( "RETURN 'Hello!'" ) ); assertThrows( ClientException.class, () -> await( cursor.singleAsync() ) ); diff --git a/driver/src/test/java/org/neo4j/driver/integration/AsyncTransactionIT.java b/driver/src/test/java/org/neo4j/driver/integration/async/AsyncTransactionIT.java similarity index 99% rename from driver/src/test/java/org/neo4j/driver/integration/AsyncTransactionIT.java rename to driver/src/test/java/org/neo4j/driver/integration/async/AsyncTransactionIT.java index 6faca07fab..ce2578182e 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/AsyncTransactionIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/async/AsyncTransactionIT.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.neo4j.driver.integration; +package org.neo4j.driver.integration.async; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -62,6 +62,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.neo4j.driver.Values.parameters; +import static org.neo4j.driver.internal.SessionConfig.builder; import static org.neo4j.driver.internal.util.Iterables.single; import static org.neo4j.driver.internal.util.Matchers.containsResultAvailableAfterAndResultConsumedAfter; import static org.neo4j.driver.internal.util.Matchers.syntaxError; @@ -297,7 +298,7 @@ void shouldNotAllowNewStatementsAfterAnIncorrectStatement() @Test void shouldFailBoBeginTxWithInvalidBookmark() { - AsyncSession session = neo4j.driver().asyncSession( t -> t.withBookmarks( "InvalidBookmark" ) ); + AsyncSession session = neo4j.driver().asyncSession( builder().withBookmarks( "InvalidBookmark" ).build() ); ClientException e = assertThrows( ClientException.class, () -> await( session.beginTransactionAsync() ) ); assertThat( e.getMessage(), containsString( "InvalidBookmark" ) ); diff --git a/driver/src/test/java/org/neo4j/driver/integration/reactive/RxStatementResultIT.java b/driver/src/test/java/org/neo4j/driver/integration/reactive/RxStatementResultIT.java index 3391849450..936ef840de 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/reactive/RxStatementResultIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/reactive/RxStatementResultIT.java @@ -27,8 +27,8 @@ import org.neo4j.driver.Record; import org.neo4j.driver.exceptions.ClientException; import org.neo4j.driver.internal.util.EnabledOnNeo4jWith; -import org.neo4j.driver.reactive.RxStatementResult; import org.neo4j.driver.reactive.RxSession; +import org.neo4j.driver.reactive.RxStatementResult; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.summary.StatementType; import org.neo4j.driver.util.DatabaseExtension; @@ -285,7 +285,7 @@ void shouldStreamCorrectRecordsBackBeforeError() { RxSession session = neo4j.driver().rxSession(); - RxStatementResult result = session.run( "UNWIND range(5, 0, -1) AS x RETURN x / x" ); + RxStatementResult result = session.run( "CYPHER runtime=interpreted UNWIND range(5, 0, -1) AS x RETURN x / x" ); StepVerifier.create( Flux.from( result.records() ).map( record -> record.get( 0 ).asInt() ) ) .expectNext( 1 ) .expectNext( 1 ) diff --git a/driver/src/test/java/org/neo4j/driver/integration/reactive/RxTransactionIT.java b/driver/src/test/java/org/neo4j/driver/integration/reactive/RxTransactionIT.java index 3b635e92fb..d3b7cfbd08 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/reactive/RxTransactionIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/reactive/RxTransactionIT.java @@ -68,6 +68,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.neo4j.driver.Values.parameters; +import static org.neo4j.driver.internal.SessionConfig.builder; import static org.neo4j.driver.internal.util.Iterables.single; import static org.neo4j.driver.internal.util.Matchers.containsResultAvailableAfterAndResultConsumedAfter; import static org.neo4j.driver.internal.util.Matchers.syntaxError; @@ -246,7 +247,7 @@ void shouldNotAllowNewStatementsAfterAnIncorrectStatement() @Test void shouldFailBoBeginTxWithInvalidBookmark() { - RxSession session = neo4j.driver().rxSession( t ->t.withBookmarks( "InvalidBookmark" ) ); + RxSession session = neo4j.driver().rxSession( builder().withBookmarks( "InvalidBookmark" ).build() ); ClientException e = assertThrows( ClientException.class, () -> await( session.beginTransaction() ) ); assertThat( e.getMessage(), containsString( "InvalidBookmark" ) ); diff --git a/driver/src/test/java/org/neo4j/driver/internal/DirectConnectionProviderTest.java b/driver/src/test/java/org/neo4j/driver/internal/DirectConnectionProviderTest.java index 37ac1632ae..0a11a51ed9 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/DirectConnectionProviderTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/DirectConnectionProviderTest.java @@ -27,7 +27,7 @@ import java.util.stream.Stream; import org.neo4j.driver.AccessMode; -import org.neo4j.driver.internal.async.connection.DecoratedConnection; +import org.neo4j.driver.internal.async.connection.DirectConnection; import org.neo4j.driver.internal.spi.Connection; import org.neo4j.driver.internal.spi.ConnectionPool; @@ -57,12 +57,12 @@ void acquiresConnectionsFromThePool() DirectConnectionProvider provider = new DirectConnectionProvider( address, pool ); Connection acquired1 = await( provider.acquireConnection( ABSENT_DB_NAME, READ ) ); - assertThat( acquired1, instanceOf( DecoratedConnection.class ) ); - assertSame( connection1, ((DecoratedConnection) acquired1).connection() ); + assertThat( acquired1, instanceOf( DirectConnection.class ) ); + assertSame( connection1, ((DirectConnection) acquired1).connection() ); Connection acquired2 = await( provider.acquireConnection( ABSENT_DB_NAME, WRITE ) ); - assertThat( acquired2, instanceOf( DecoratedConnection.class ) ); - assertSame( connection2, ((DecoratedConnection) acquired2).connection() ); + assertThat( acquired2, instanceOf( DirectConnection.class ) ); + assertSame( connection2, ((DirectConnection) acquired2).connection() ); } @ParameterizedTest @@ -110,8 +110,8 @@ void shouldIgnoreDatabaseNameAndAccessModeWhenObtainConnectionFromPool() throws DirectConnectionProvider provider = new DirectConnectionProvider( address, pool ); Connection acquired1 = await( provider.acquireConnection( ABSENT_DB_NAME, READ ) ); - assertThat( acquired1, instanceOf( DecoratedConnection.class ) ); - assertSame( connection, ((DecoratedConnection) acquired1).connection() ); + assertThat( acquired1, instanceOf( DirectConnection.class ) ); + assertSame( connection, ((DirectConnection) acquired1).connection() ); verify( pool ).acquire( address ); } diff --git a/driver/src/test/java/org/neo4j/driver/internal/DirectDriverBoltKitTest.java b/driver/src/test/java/org/neo4j/driver/internal/DirectDriverBoltKitTest.java index 27723cfe42..2d046d3d23 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/DirectDriverBoltKitTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/DirectDriverBoltKitTest.java @@ -56,6 +56,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.neo4j.driver.internal.SessionConfig.builder; +import static org.neo4j.driver.internal.SessionConfig.forDatabase; import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; import static org.neo4j.driver.Values.parameters; import static org.neo4j.driver.util.StubServer.INSECURE_CONFIG; @@ -92,7 +94,7 @@ void shouldSendMultipleBookmarks() throws Exception "neo4j:bookmark:v1:tx68" ); try ( Driver driver = GraphDatabase.driver( "bolt://localhost:9001", INSECURE_CONFIG ); - Session session = driver.session( t -> t.withBookmarks( bookmarks ) ) ) + Session session = driver.session( builder().withBookmarks( bookmarks ).build() ) ) { try ( Transaction tx = session.beginTransaction() ) { @@ -151,7 +153,7 @@ void shouldSendReadAccessModeInStatementMetadata() throws Exception try ( Driver driver = GraphDatabase.driver( "bolt://localhost:9001", INSECURE_CONFIG ); - Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ) ) + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) { List names = session.run( "MATCH (n) RETURN n.name" ).list( record -> record.get( 0 ).asString() ); assertEquals( asList( "Foo", "Bar" ), names ); @@ -168,7 +170,7 @@ void shouldNotSendWriteAccessModeInStatementMetadata() throws Exception StubServer server = StubServer.start( "hello_run_exit.script", 9001 ); try ( Driver driver = GraphDatabase.driver( "bolt://localhost:9001", INSECURE_CONFIG ); - Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ) ) + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) ) { List names = session.run( "MATCH (n) RETURN n.name" ).list( record -> record.get( 0 ).asString() ); assertEquals( asList( "Foo", "Bar" ), names ); @@ -239,7 +241,7 @@ void shouldThrowCorrectErrorOnRunFailure() throws Throwable StubServer server = StubServer.start( "database_shutdown.script", 9001 ); try ( Driver driver = GraphDatabase.driver( "bolt://localhost:9001", INSECURE_CONFIG ); - Session session = driver.session( t -> t.withBookmarks( "neo4j:bookmark:v1:tx0" ) ); + Session session = driver.session( builder().withBookmarks( "neo4j:bookmark:v1:tx0" ).build() ); // has to enforce to flush BEGIN to have tx started. Transaction transaction = session.beginTransaction() ) { @@ -283,7 +285,7 @@ void shouldAllowDatabaseNameInSessionRun() throws Throwable StubServer server = StubServer.start( "read_server_v4_read.script", 9001 ); try ( Driver driver = GraphDatabase.driver( "bolt://localhost:9001", INSECURE_CONFIG ); - Session session = driver.session( t -> t.withDatabase( "myDatabase" ).withDefaultAccessMode( AccessMode.READ ) ) ) + Session session = driver.session( builder().withDatabase( "myDatabase" ).withDefaultAccessMode( AccessMode.READ ).build() ) ) { final StatementResult result = session.run( "MATCH (n) RETURN n.name" ); result.consume(); @@ -300,7 +302,7 @@ void shouldAllowDatabaseNameInBeginTransaction() throws Throwable StubServer server = StubServer.start( "read_server_v4_read_tx.script", 9001 ); try ( Driver driver = GraphDatabase.driver( "bolt://localhost:9001", INSECURE_CONFIG ); - Session session = driver.session( t -> t.withDatabase( "myDatabase" ) ) ) + Session session = driver.session( forDatabase( "myDatabase" ) ) ) { session.readTransaction( tx -> tx.run( "MATCH (n) RETURN n.name" ).summary() ); } diff --git a/driver/src/test/java/org/neo4j/driver/internal/DriverFactoryTest.java b/driver/src/test/java/org/neo4j/driver/internal/DriverFactoryTest.java index 5d8fd7429f..a2b7189994 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/DriverFactoryTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/DriverFactoryTest.java @@ -31,9 +31,8 @@ import org.neo4j.driver.AuthTokens; import org.neo4j.driver.Config; import org.neo4j.driver.Driver; -import org.neo4j.driver.exceptions.ServiceUnavailableException; -import org.neo4j.driver.internal.async.NetworkSession; import org.neo4j.driver.internal.async.LeakLoggingNetworkSession; +import org.neo4j.driver.internal.async.NetworkSession; import org.neo4j.driver.internal.async.connection.BootstrapFactory; import org.neo4j.driver.internal.cluster.RoutingSettings; import org.neo4j.driver.internal.cluster.loadbalancing.LoadBalancer; @@ -52,15 +51,15 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.junit.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.neo4j.driver.Config.defaultConfig; -import static org.neo4j.driver.internal.SessionParameters.empty; +import static org.neo4j.driver.internal.SessionConfig.empty; import static org.neo4j.driver.internal.metrics.MetricsProvider.METRICS_DISABLED_PROVIDER; import static org.neo4j.driver.internal.util.Futures.completedWithNull; import static org.neo4j.driver.internal.util.Futures.failedFuture; @@ -126,7 +125,7 @@ void usesLeakLoggingSessionFactoryWhenConfigured( String uri ) @ParameterizedTest @MethodSource( "testUris" ) - void shouldVerifyConnectivity( String uri ) + void shouldNotVerifyConnectivity( String uri ) { SessionFactory sessionFactory = mock( SessionFactory.class ); when( sessionFactory.verifyConnectivity() ).thenReturn( completedWithNull() ); @@ -136,24 +135,10 @@ void shouldVerifyConnectivity( String uri ) try ( Driver driver = createDriver( uri, driverFactory ) ) { assertNotNull( driver ); - verify( sessionFactory ).verifyConnectivity(); + verify( sessionFactory, never() ).verifyConnectivity(); } } - @ParameterizedTest - @MethodSource( "testUris" ) - void shouldThrowWhenUnableToVerifyConnectivity( String uri ) - { - SessionFactory sessionFactory = mock( SessionFactory.class ); - ServiceUnavailableException error = new ServiceUnavailableException( "Hello" ); - when( sessionFactory.verifyConnectivity() ).thenReturn( failedFuture( error ) ); - when( sessionFactory.close() ).thenReturn( completedWithNull() ); - DriverFactoryWithSessions driverFactory = new DriverFactoryWithSessions( sessionFactory ); - - ServiceUnavailableException e = assertThrows( ServiceUnavailableException.class, () -> createDriver( uri, driverFactory ) ); - assertEquals( e.getMessage(), "Hello" ); - } - @Test void shouldNotCreateDriverMetrics() { @@ -237,7 +222,7 @@ private static class SessionFactoryCapturingDriverFactory extends DriverFactory protected InternalDriver createDriver( SecurityPlan securityPlan, SessionFactory sessionFactory, MetricsProvider metricsProvider, Config config ) { InternalDriver driver = mock( InternalDriver.class ); - when( driver.verifyConnectivity() ).thenReturn( completedWithNull() ); + when( driver.verifyConnectivityAsync() ).thenReturn( completedWithNull() ); return driver; } diff --git a/driver/src/test/java/org/neo4j/driver/internal/InternalDriverTest.java b/driver/src/test/java/org/neo4j/driver/internal/InternalDriverTest.java index 90048b83d7..28dae12505 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/InternalDriverTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/InternalDriverTest.java @@ -22,13 +22,14 @@ import java.util.concurrent.CompletableFuture; +import org.neo4j.driver.Config; +import org.neo4j.driver.Metrics; +import org.neo4j.driver.exceptions.ClientException; +import org.neo4j.driver.exceptions.ServiceUnavailableException; import org.neo4j.driver.internal.metrics.InternalMetrics; import org.neo4j.driver.internal.metrics.MetricsProvider; import org.neo4j.driver.internal.security.SecurityPlan; import org.neo4j.driver.internal.util.Clock; -import org.neo4j.driver.Config; -import org.neo4j.driver.Metrics; -import org.neo4j.driver.exceptions.ClientException; import static org.junit.Assert.assertTrue; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -40,6 +41,7 @@ import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; import static org.neo4j.driver.internal.metrics.MetricsProvider.METRICS_DISABLED_PROVIDER; import static org.neo4j.driver.internal.util.Futures.completedWithNull; +import static org.neo4j.driver.internal.util.Futures.failedFuture; import static org.neo4j.driver.util.TestUtil.await; class InternalDriverTest @@ -76,7 +78,19 @@ void shouldVerifyConnectivity() InternalDriver driver = newDriver( sessionFactory ); - assertEquals( connectivityStage, driver.verifyConnectivity() ); + assertEquals( connectivityStage, driver.verifyConnectivityAsync() ); + } + + @Test + void shouldThrowWhenUnableToVerifyConnectivity() + { + SessionFactory sessionFactory = mock( SessionFactory.class ); + ServiceUnavailableException error = new ServiceUnavailableException( "Hello" ); + when( sessionFactory.verifyConnectivity() ).thenReturn( failedFuture( error ) ); + InternalDriver driver = newDriver( sessionFactory ); + + ServiceUnavailableException e = assertThrows( ServiceUnavailableException.class, () -> await( driver.verifyConnectivityAsync() ) ); + assertEquals( e.getMessage(), "Hello" ); } @Test diff --git a/driver/src/test/java/org/neo4j/driver/internal/SessionParametersTest.java b/driver/src/test/java/org/neo4j/driver/internal/SessionConfigTest.java similarity index 71% rename from driver/src/test/java/org/neo4j/driver/internal/SessionParametersTest.java rename to driver/src/test/java/org/neo4j/driver/internal/SessionConfigTest.java index 9a002e64d5..065853c955 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/SessionParametersTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/SessionConfigTest.java @@ -37,16 +37,16 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.neo4j.driver.internal.SessionParameters.empty; -import static org.neo4j.driver.internal.SessionParameters.template; +import static org.neo4j.driver.internal.SessionConfig.empty; +import static org.neo4j.driver.internal.SessionConfig.builder; import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.ABSENT_DB_NAME; -class SessionParametersTest +class SessionConfigTest { @Test void shouldReturnDefaultValues() throws Throwable { - SessionParameters parameters = empty(); + SessionConfig parameters = empty(); Assert.assertEquals( AccessMode.WRITE, parameters.defaultAccessMode() ); assertFalse( parameters.database().isPresent() ); @@ -57,7 +57,7 @@ void shouldReturnDefaultValues() throws Throwable @EnumSource( AccessMode.class ) void shouldChangeAccessMode( AccessMode mode ) throws Throwable { - SessionParameters parameters = template().withDefaultAccessMode( mode ).build(); + SessionConfig parameters = builder().withDefaultAccessMode( mode ).build(); assertEquals( mode, parameters.defaultAccessMode() ); } @@ -65,54 +65,52 @@ void shouldChangeAccessMode( AccessMode mode ) throws Throwable @ValueSource( strings = {"foo", "data", "my awesome database", " "} ) void shouldChangeDatabaseName( String databaseName ) { - SessionParameters parameters = template().withDatabase( databaseName ).build(); + SessionConfig parameters = builder().withDatabase( databaseName ).build(); assertTrue( parameters.database().isPresent() ); assertEquals( databaseName, parameters.database().get() ); } @Test - void shouldAllowNullDatabaseName() throws Throwable + void shouldNotAllowNullDatabaseName() throws Throwable { - SessionParameters parameters = template().withDatabase( null ).build(); - assertFalse( parameters.database().isPresent() ); - assertEquals( "", parameters.database().orElse( ABSENT_DB_NAME ) ); + assertThrows( NullPointerException.class, () -> builder().withDatabase( null ) ); } @ParameterizedTest @ValueSource( strings = {"", ABSENT_DB_NAME} ) void shouldForbiddenEmptyStringDatabaseName( String databaseName ) throws Throwable { - IllegalArgumentException error = assertThrows( IllegalArgumentException.class, () -> template().withDatabase( databaseName ).build()); + IllegalArgumentException error = assertThrows( IllegalArgumentException.class, () -> builder().withDatabase( databaseName ) ); assertThat( error.getMessage(), equalTo( "Illegal database name ''." ) ); } @Test void shouldAcceptNullBookmarks() throws Throwable { - SessionParameters parameters = template().withBookmarks( (String[]) null ).build(); + SessionConfig parameters = builder().withBookmarks( (String[]) null ).build(); assertNull( parameters.bookmarks() ); - SessionParameters parameters2 = template().withBookmarks( (List) null ).build(); + SessionConfig parameters2 = builder().withBookmarks( (List) null ).build(); assertNull( parameters2.bookmarks() ); } @Test void shouldAcceptEmptyBookmarks() throws Throwable { - SessionParameters parameters = template().withBookmarks().build(); + SessionConfig parameters = builder().withBookmarks().build(); assertEquals( emptyList(), parameters.bookmarks() ); - SessionParameters parameters2 = template().withBookmarks( emptyList() ).build(); + SessionConfig parameters2 = builder().withBookmarks( emptyList() ).build(); assertEquals( emptyList(), parameters2.bookmarks() ); } @Test void shouldAcceptBookmarks() throws Throwable { - SessionParameters parameters = template().withBookmarks( "one", "two" ).build(); + SessionConfig parameters = builder().withBookmarks( "one", "two" ).build(); assertThat( parameters.bookmarks(), equalTo( Arrays.asList( "one", "two" ) ) ); - SessionParameters parameters2 = template().withBookmarks( Arrays.asList( "one", "two" ) ).build(); + SessionConfig parameters2 = builder().withBookmarks( Arrays.asList( "one", "two" ) ).build(); assertThat( parameters2.bookmarks(), equalTo( Arrays.asList( "one", "two" ) ) ); } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/SessionFactoryImplTest.java b/driver/src/test/java/org/neo4j/driver/internal/SessionFactoryImplTest.java index 80c69515a6..5b3a95e033 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/SessionFactoryImplTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/SessionFactoryImplTest.java @@ -30,7 +30,7 @@ import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.junit.MatcherAssert.assertThat; import static org.mockito.Mockito.mock; -import static org.neo4j.driver.internal.SessionParameters.template; +import static org.neo4j.driver.internal.SessionConfig.builder; import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; class SessionFactoryImplTest @@ -41,10 +41,10 @@ void createsNetworkSessions() Config config = Config.builder().withLogging( DEV_NULL_LOGGING ).build(); SessionFactory factory = newSessionFactory( config ); - NetworkSession readSession = factory.newInstance( template().withDefaultAccessMode( AccessMode.READ ).build() ); + NetworkSession readSession = factory.newInstance( builder().withDefaultAccessMode( AccessMode.READ ).build() ); assertThat( readSession, instanceOf( NetworkSession.class ) ); - NetworkSession writeSession = factory.newInstance( template().withDefaultAccessMode( AccessMode.WRITE ).build() ); + NetworkSession writeSession = factory.newInstance( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ); assertThat( writeSession, instanceOf( NetworkSession.class ) ); } @@ -54,10 +54,10 @@ void createsLeakLoggingNetworkSessions() Config config = Config.builder().withLogging( DEV_NULL_LOGGING ).withLeakedSessionsLogging().build(); SessionFactory factory = newSessionFactory( config ); - NetworkSession readSession = factory.newInstance( template().withDefaultAccessMode( AccessMode.READ ).build() ); + NetworkSession readSession = factory.newInstance( builder().withDefaultAccessMode( AccessMode.READ ).build() ); assertThat( readSession, instanceOf( LeakLoggingNetworkSession.class ) ); - NetworkSession writeSession = factory.newInstance( template().withDefaultAccessMode( AccessMode.WRITE ).build() ); + NetworkSession writeSession = factory.newInstance( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ); assertThat( writeSession, instanceOf( LeakLoggingNetworkSession.class ) ); } diff --git a/driver/src/test/java/org/neo4j/driver/internal/TrustedServerProductTest.java b/driver/src/test/java/org/neo4j/driver/internal/TrustedServerProductTest.java index ed23af0092..cca815ce31 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/TrustedServerProductTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/TrustedServerProductTest.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test; import org.neo4j.driver.Config; +import org.neo4j.driver.Driver; import org.neo4j.driver.GraphDatabase; import org.neo4j.driver.exceptions.UntrustedServerException; import org.neo4j.driver.util.StubServer; @@ -42,7 +43,8 @@ class TrustedServerProductTest void shouldRejectConnectionsToNonNeo4jServers() throws Exception { StubServer server = StubServer.start( "untrusted_server.script", 9001 ); - assertThrows( UntrustedServerException.class, () -> GraphDatabase.driver( "bolt://127.0.0.1:9001", config )); + final Driver driver = GraphDatabase.driver( "bolt://127.0.0.1:9001", config ); + assertThrows( UntrustedServerException.class, driver::verifyConnectivity ); assertThat( server.exitStatus(), equalTo( 0 ) ); } diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/connection/DirectConnectionTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/NetworkConnectionTest.java similarity index 88% rename from driver/src/test/java/org/neo4j/driver/internal/async/connection/DirectConnectionTest.java rename to driver/src/test/java/org/neo4j/driver/internal/async/NetworkConnectionTest.java index 08a65be48a..a7ebac3cb6 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/async/connection/DirectConnectionTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/async/NetworkConnectionTest.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.neo4j.driver.internal.async.connection; +package org.neo4j.driver.internal.async; import io.netty.channel.Channel; import io.netty.channel.DefaultEventLoop; @@ -37,6 +37,7 @@ import java.util.function.Consumer; import org.neo4j.driver.internal.BoltServerAddress; +import org.neo4j.driver.internal.async.connection.ChannelAttributes; import org.neo4j.driver.internal.async.inbound.InboundMessageDispatcher; import org.neo4j.driver.internal.handlers.NoOpResponseHandler; import org.neo4j.driver.internal.messaging.request.RunMessage; @@ -66,7 +67,7 @@ import static org.neo4j.driver.util.DaemonThreadFactory.daemon; import static org.neo4j.driver.util.TestUtil.DEFAULT_TEST_PROTOCOL_VERSION; -class DirectConnectionTest +class NetworkConnectionTest { private static final NoOpResponseHandler NO_OP_HANDLER = NoOpResponseHandler.INSTANCE; @@ -82,14 +83,14 @@ void tearDown() throws Exception @Test void shouldBeOpenAfterCreated() { - DirectConnection connection = newConnection( newChannel() ); + NetworkConnection connection = newConnection( newChannel() ); assertTrue( connection.isOpen() ); } @Test void shouldNotBeOpenAfterRelease() { - DirectConnection connection = newConnection( newChannel() ); + NetworkConnection connection = newConnection( newChannel() ); connection.release(); assertFalse( connection.isOpen() ); } @@ -98,7 +99,7 @@ void shouldNotBeOpenAfterRelease() void shouldSendResetOnRelease() { EmbeddedChannel channel = newChannel(); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); connection.release(); channel.runPendingTasks(); @@ -130,7 +131,7 @@ void shouldWriteAndFlushInEventLoopThread() throws Exception @Test void shouldWriteForceReleaseInEventLoopThread() throws Exception { - testWriteInEventLoop( "ReleaseTestEventLoop", DirectConnection::release ); + testWriteInEventLoop( "ReleaseTestEventLoop", NetworkConnection::release ); } @Test @@ -140,7 +141,7 @@ void shouldFlushInEventLoopThread() throws Exception initializeEventLoop( channel, "Flush" ); ChannelAttributes.setProtocolVersion( channel, DEFAULT_TEST_PROTOCOL_VERSION ); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); connection.flush(); shutdownEventLoop(); @@ -153,7 +154,7 @@ void shouldEnableAutoReadWhenReleased() EmbeddedChannel channel = newChannel(); channel.config().setAutoRead( false ); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); connection.release(); channel.runPendingTasks(); @@ -167,7 +168,7 @@ void shouldNotDisableAutoReadWhenReleased() EmbeddedChannel channel = newChannel(); channel.config().setAutoRead( true ); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); connection.release(); connection.disableAutoRead(); // does nothing on released connection @@ -178,7 +179,7 @@ void shouldNotDisableAutoReadWhenReleased() void shouldWriteSingleMessage() { EmbeddedChannel channel = newChannel(); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); connection.write( PULL_ALL, NO_OP_HANDLER ); @@ -192,7 +193,7 @@ void shouldWriteSingleMessage() void shouldWriteMultipleMessage() { EmbeddedChannel channel = newChannel(); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); connection.write( PULL_ALL, NO_OP_HANDLER, RESET, NO_OP_HANDLER ); @@ -207,7 +208,7 @@ void shouldWriteMultipleMessage() void shouldWriteAndFlushSingleMessage() { EmbeddedChannel channel = newChannel(); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); connection.writeAndFlush( PULL_ALL, NO_OP_HANDLER ); channel.runPendingTasks(); // writeAndFlush is scheduled to execute in the event loop thread, trigger its execution @@ -220,7 +221,7 @@ void shouldWriteAndFlushSingleMessage() void shouldWriteAndFlushMultipleMessage() { EmbeddedChannel channel = newChannel(); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); connection.writeAndFlush( PULL_ALL, NO_OP_HANDLER, RESET, NO_OP_HANDLER ); channel.runPendingTasks(); // writeAndFlush is scheduled to execute in the event loop thread, trigger its execution @@ -234,7 +235,7 @@ void shouldWriteAndFlushMultipleMessage() void shouldNotWriteSingleMessageWhenReleased() { ResponseHandler handler = mock( ResponseHandler.class ); - DirectConnection connection = newConnection( newChannel() ); + NetworkConnection connection = newConnection( newChannel() ); connection.release(); connection.write( new RunMessage( "RETURN 1" ), handler ); @@ -249,7 +250,7 @@ void shouldNotWriteMultipleMessagesWhenReleased() { ResponseHandler runHandler = mock( ResponseHandler.class ); ResponseHandler pullAllHandler = mock( ResponseHandler.class ); - DirectConnection connection = newConnection( newChannel() ); + NetworkConnection connection = newConnection( newChannel() ); connection.release(); connection.write( new RunMessage( "RETURN 1" ), runHandler, PULL_ALL, pullAllHandler ); @@ -263,7 +264,7 @@ void shouldNotWriteMultipleMessagesWhenReleased() void shouldNotWriteAndFlushSingleMessageWhenReleased() { ResponseHandler handler = mock( ResponseHandler.class ); - DirectConnection connection = newConnection( newChannel() ); + NetworkConnection connection = newConnection( newChannel() ); connection.release(); connection.writeAndFlush( new RunMessage( "RETURN 1" ), handler ); @@ -278,7 +279,7 @@ void shouldNotWriteAndFlushMultipleMessagesWhenReleased() { ResponseHandler runHandler = mock( ResponseHandler.class ); ResponseHandler pullAllHandler = mock( ResponseHandler.class ); - DirectConnection connection = newConnection( newChannel() ); + NetworkConnection connection = newConnection( newChannel() ); connection.release(); connection.writeAndFlush( new RunMessage( "RETURN 1" ), runHandler, PULL_ALL, pullAllHandler ); @@ -292,7 +293,7 @@ void shouldNotWriteAndFlushMultipleMessagesWhenReleased() void shouldNotWriteSingleMessageWhenTerminated() { ResponseHandler handler = mock( ResponseHandler.class ); - DirectConnection connection = newConnection( newChannel() ); + NetworkConnection connection = newConnection( newChannel() ); connection.terminateAndRelease( "42" ); connection.write( new RunMessage( "RETURN 1" ), handler ); @@ -307,7 +308,7 @@ void shouldNotWriteMultipleMessagesWhenTerminated() { ResponseHandler runHandler = mock( ResponseHandler.class ); ResponseHandler pullAllHandler = mock( ResponseHandler.class ); - DirectConnection connection = newConnection( newChannel() ); + NetworkConnection connection = newConnection( newChannel() ); connection.terminateAndRelease( "42" ); connection.write( new RunMessage( "RETURN 1" ), runHandler, PULL_ALL, pullAllHandler ); @@ -321,7 +322,7 @@ void shouldNotWriteMultipleMessagesWhenTerminated() void shouldNotWriteAndFlushSingleMessageWhenTerminated() { ResponseHandler handler = mock( ResponseHandler.class ); - DirectConnection connection = newConnection( newChannel() ); + NetworkConnection connection = newConnection( newChannel() ); connection.terminateAndRelease( "42" ); connection.writeAndFlush( new RunMessage( "RETURN 1" ), handler ); @@ -336,7 +337,7 @@ void shouldNotWriteAndFlushMultipleMessagesWhenTerminated() { ResponseHandler runHandler = mock( ResponseHandler.class ); ResponseHandler pullAllHandler = mock( ResponseHandler.class ); - DirectConnection connection = newConnection( newChannel() ); + NetworkConnection connection = newConnection( newChannel() ); connection.terminateAndRelease( "42" ); connection.writeAndFlush( new RunMessage( "RETURN 1" ), runHandler, PULL_ALL, pullAllHandler ); @@ -353,7 +354,7 @@ void shouldReturnServerAddressWhenReleased() BoltServerAddress address = new BoltServerAddress( "host", 4242 ); ChannelAttributes.setServerAddress( channel, address ); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); connection.release(); assertEquals( address, connection.serverAddress() ); @@ -366,7 +367,7 @@ void shouldReturnServerVersionWhenReleased() ServerVersion version = ServerVersion.v3_2_0; ChannelAttributes.setServerVersion( channel, version ); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); connection.release(); assertEquals( version, connection.serverVersion() ); @@ -376,7 +377,7 @@ void shouldReturnServerVersionWhenReleased() void shouldReturnSameCompletionStageFromRelease() { EmbeddedChannel channel = newChannel(); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); CompletionStage releaseStage1 = connection.release(); CompletionStage releaseStage2 = connection.release(); @@ -398,7 +399,7 @@ void shouldEnableAutoRead() { EmbeddedChannel channel = newChannel(); channel.config().setAutoRead( false ); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); connection.enableAutoRead(); @@ -410,7 +411,7 @@ void shouldDisableAutoRead() { EmbeddedChannel channel = newChannel(); channel.config().setAutoRead( true ); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); connection.disableAutoRead(); @@ -421,7 +422,7 @@ void shouldDisableAutoRead() void shouldSetTerminationReasonOnChannelWhenTerminated() { EmbeddedChannel channel = newChannel(); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); String reason = "Something really bad has happened"; connection.terminateAndRelease( reason ); @@ -433,7 +434,7 @@ void shouldSetTerminationReasonOnChannelWhenTerminated() void shouldCloseChannelWhenTerminated() { EmbeddedChannel channel = newChannel(); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); assertTrue( channel.isActive() ); connection.terminateAndRelease( "test" ); @@ -446,7 +447,7 @@ void shouldReleaseChannelWhenTerminated() { EmbeddedChannel channel = newChannel(); ChannelPool pool = mock( ChannelPool.class ); - DirectConnection connection = newConnection( channel, pool ); + NetworkConnection connection = newConnection( channel, pool ); verify( pool, never() ).release( any() ); connection.terminateAndRelease( "test" ); @@ -459,7 +460,7 @@ void shouldNotReleaseChannelMultipleTimesWhenTerminatedMultipleTimes() { EmbeddedChannel channel = newChannel(); ChannelPool pool = mock( ChannelPool.class ); - DirectConnection connection = newConnection( channel, pool ); + NetworkConnection connection = newConnection( channel, pool ); verify( pool, never() ).release( any() ); connection.terminateAndRelease( "reason 1" ); @@ -477,7 +478,7 @@ void shouldNotReleaseAfterTermination() { EmbeddedChannel channel = newChannel(); ChannelPool pool = mock( ChannelPool.class ); - DirectConnection connection = newConnection( channel, pool ); + NetworkConnection connection = newConnection( channel, pool ); verify( pool, never() ).release( any() ); connection.terminateAndRelease( "test" ); @@ -493,7 +494,7 @@ void shouldNotReleaseAfterTermination() void shouldSendResetMessageWhenReset() { EmbeddedChannel channel = newChannel(); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); connection.reset(); channel.runPendingTasks(); @@ -506,7 +507,7 @@ void shouldSendResetMessageWhenReset() void shouldCompleteResetFutureWhenSuccessResponseArrives() { EmbeddedChannel channel = newChannel(); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); CompletableFuture resetFuture = connection.reset().toCompletableFuture(); channel.runPendingTasks(); @@ -521,7 +522,7 @@ void shouldCompleteResetFutureWhenSuccessResponseArrives() void shouldCompleteResetFutureWhenFailureResponseArrives() { EmbeddedChannel channel = newChannel(); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); CompletableFuture resetFuture = connection.reset().toCompletableFuture(); channel.runPendingTasks(); @@ -536,7 +537,7 @@ void shouldCompleteResetFutureWhenFailureResponseArrives() void shouldDoNothingInResetWhenClosed() { EmbeddedChannel channel = newChannel(); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); connection.release(); channel.runPendingTasks(); @@ -555,7 +556,7 @@ void shouldEnableAutoReadWhenDoingReset() { EmbeddedChannel channel = newChannel(); channel.config().setAutoRead( false ); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); connection.reset(); channel.runPendingTasks(); @@ -563,7 +564,7 @@ void shouldEnableAutoReadWhenDoingReset() assertTrue( channel.config().isAutoRead() ); } - private void testWriteInEventLoop( String threadName, Consumer action ) throws Exception + private void testWriteInEventLoop( String threadName, Consumer action ) throws Exception { EmbeddedChannel channel = spy( new EmbeddedChannel() ); initializeEventLoop( channel, threadName ); @@ -571,7 +572,7 @@ private void testWriteInEventLoop( String threadName, Consumer ChannelAttributes.setProtocolVersion( channel, DEFAULT_TEST_PROTOCOL_VERSION ); ChannelAttributes.setMessageDispatcher( channel, dispatcher ); - DirectConnection connection = newConnection( channel ); + NetworkConnection connection = newConnection( channel ); action.accept( connection ); shutdownEventLoop(); @@ -607,14 +608,14 @@ private static EmbeddedChannel newChannel() return channel; } - private static DirectConnection newConnection( Channel channel ) + private static NetworkConnection newConnection( Channel channel ) { return newConnection( channel, mock( ChannelPool.class ) ); } - private static DirectConnection newConnection( Channel channel, ChannelPool pool ) + private static NetworkConnection newConnection( Channel channel, ChannelPool pool ) { - return new DirectConnection( channel, pool, new FakeClock(), DEV_NULL_METRICS ); + return new NetworkConnection( channel, pool, new FakeClock(), DEV_NULL_METRICS ); } private static void assertConnectionReleasedError( IllegalStateException e ) diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/connection/DecoratedConnectionTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/connection/DecoratedConnectionTest.java index db0ae23915..97c0ffc7b4 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/async/connection/DecoratedConnectionTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/async/connection/DecoratedConnectionTest.java @@ -49,7 +49,7 @@ void shouldDelegateIsOpen( String open ) Connection mockConnection = mock( Connection.class ); when( mockConnection.isOpen() ).thenReturn( Boolean.valueOf( open ) ); - DecoratedConnection connection = newConnection( mockConnection ); + DirectConnection connection = newConnection( mockConnection ); assertEquals( Boolean.valueOf( open ).booleanValue(), connection.isOpen() ); verify( mockConnection ).isOpen(); @@ -59,7 +59,7 @@ void shouldDelegateIsOpen( String open ) void shouldDelegateEnableAutoRead() { Connection mockConnection = mock( Connection.class ); - DecoratedConnection connection = newConnection( mockConnection ); + DirectConnection connection = newConnection( mockConnection ); connection.enableAutoRead(); @@ -70,7 +70,7 @@ void shouldDelegateEnableAutoRead() void shouldDelegateDisableAutoRead() { Connection mockConnection = mock( Connection.class ); - DecoratedConnection connection = newConnection( mockConnection ); + DirectConnection connection = newConnection( mockConnection ); connection.disableAutoRead(); @@ -81,7 +81,7 @@ void shouldDelegateDisableAutoRead() void shouldDelegateWrite() { Connection mockConnection = mock( Connection.class ); - DecoratedConnection connection = newConnection( mockConnection ); + DirectConnection connection = newConnection( mockConnection ); Message message = mock( Message.class ); ResponseHandler handler = mock( ResponseHandler.class ); @@ -95,7 +95,7 @@ void shouldDelegateWrite() void shouldDelegateWriteTwoMessages() { Connection mockConnection = mock( Connection.class ); - DecoratedConnection connection = newConnection( mockConnection ); + DirectConnection connection = newConnection( mockConnection ); Message message1 = mock( Message.class ); ResponseHandler handler1 = mock( ResponseHandler.class ); @@ -111,7 +111,7 @@ void shouldDelegateWriteTwoMessages() void shouldDelegateWriteAndFlush() { Connection mockConnection = mock( Connection.class ); - DecoratedConnection connection = newConnection( mockConnection ); + DirectConnection connection = newConnection( mockConnection ); Message message = mock( Message.class ); ResponseHandler handler = mock( ResponseHandler.class ); @@ -125,7 +125,7 @@ void shouldDelegateWriteAndFlush() void shouldDelegateWriteAndFlush1() { Connection mockConnection = mock( Connection.class ); - DecoratedConnection connection = newConnection( mockConnection ); + DirectConnection connection = newConnection( mockConnection ); Message message1 = mock( Message.class ); ResponseHandler handler1 = mock( ResponseHandler.class ); @@ -141,7 +141,7 @@ void shouldDelegateWriteAndFlush1() void shouldDelegateReset() { Connection mockConnection = mock( Connection.class ); - DecoratedConnection connection = newConnection( mockConnection ); + DirectConnection connection = newConnection( mockConnection ); connection.reset(); @@ -152,7 +152,7 @@ void shouldDelegateReset() void shouldDelegateRelease() { Connection mockConnection = mock( Connection.class ); - DecoratedConnection connection = newConnection( mockConnection ); + DirectConnection connection = newConnection( mockConnection ); connection.release(); @@ -163,7 +163,7 @@ void shouldDelegateRelease() void shouldDelegateTerminateAndRelease() { Connection mockConnection = mock( Connection.class ); - DecoratedConnection connection = newConnection( mockConnection ); + DirectConnection connection = newConnection( mockConnection ); connection.terminateAndRelease( "a reason" ); @@ -176,7 +176,7 @@ void shouldDelegateServerAddress() BoltServerAddress address = BoltServerAddress.from( ServerAddress.of( "localhost", 9999 ) ); Connection mockConnection = mock( Connection.class ); when( mockConnection.serverAddress() ).thenReturn( address ); - DecoratedConnection connection = newConnection( mockConnection ); + DirectConnection connection = newConnection( mockConnection ); assertSame( address, connection.serverAddress() ); verify( mockConnection ).serverAddress(); @@ -188,7 +188,7 @@ void shouldDelegateServerVersion() ServerVersion version = ServerVersion.version( "Neo4j/3.5.3" ); Connection mockConnection = mock( Connection.class ); when( mockConnection.serverVersion() ).thenReturn( version ); - DecoratedConnection connection = newConnection( mockConnection ); + DirectConnection connection = newConnection( mockConnection ); assertSame( version, connection.serverVersion() ); verify( mockConnection ).serverVersion(); @@ -200,7 +200,7 @@ void shouldDelegateProtocol() BoltProtocol protocol = mock( BoltProtocol.class ); Connection mockConnection = mock( Connection.class ); when( mockConnection.protocol() ).thenReturn( protocol ); - DecoratedConnection connection = newConnection( mockConnection ); + DirectConnection connection = newConnection( mockConnection ); assertSame( protocol, connection.protocol() ); verify( mockConnection ).protocol(); @@ -210,7 +210,7 @@ void shouldDelegateProtocol() @EnumSource( AccessMode.class ) void shouldReturnModeFromConstructor( AccessMode mode ) { - DecoratedConnection connection = new DecoratedConnection( mock( Connection.class ), ABSENT_DB_NAME, mode ); + DirectConnection connection = new DirectConnection( mock( Connection.class ), ABSENT_DB_NAME, mode ); assertEquals( mode, connection.mode() ); } @@ -219,13 +219,13 @@ void shouldReturnModeFromConstructor( AccessMode mode ) void shouldReturnConnection() { Connection mockConnection = mock( Connection.class ); - DecoratedConnection connection = newConnection( mockConnection ); + DirectConnection connection = newConnection( mockConnection ); assertSame( mockConnection, connection.connection() ); } - private static DecoratedConnection newConnection( Connection connection ) + private static DirectConnection newConnection( Connection connection ) { - return new DecoratedConnection( connection, ABSENT_DB_NAME, READ ); + return new DirectConnection( connection, ABSENT_DB_NAME, READ ); } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/connection/RoutingConnectionTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/connection/RoutingConnectionTest.java index 1f728c5673..eac76030e0 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/async/connection/RoutingConnectionTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/async/connection/RoutingConnectionTest.java @@ -33,6 +33,7 @@ import static org.mockito.Mockito.verify; import static org.neo4j.driver.AccessMode.READ; import static org.neo4j.driver.internal.messaging.request.DiscardAllMessage.DISCARD_ALL; +import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.ABSENT_DB_NAME; import static org.neo4j.driver.internal.messaging.request.PullAllMessage.PULL_ALL; class RoutingConnectionTest @@ -65,7 +66,7 @@ private static void testHandlersWrappingWithSingleMessage( boolean flush ) { Connection connection = mock( Connection.class ); RoutingErrorHandler errorHandler = mock( RoutingErrorHandler.class ); - RoutingConnection routingConnection = new RoutingConnection( connection, READ, errorHandler ); + RoutingConnection routingConnection = new RoutingConnection( connection, ABSENT_DB_NAME, READ, errorHandler ); if ( flush ) { @@ -94,7 +95,7 @@ private static void testHandlersWrappingWithMultipleMessages( boolean flush ) { Connection connection = mock( Connection.class ); RoutingErrorHandler errorHandler = mock( RoutingErrorHandler.class ); - RoutingConnection routingConnection = new RoutingConnection( connection, READ, errorHandler ); + RoutingConnection routingConnection = new RoutingConnection( connection, ABSENT_DB_NAME, READ, errorHandler ); if ( flush ) { diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/pool/ConnectionPoolImplTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/pool/ConnectionPoolImplTest.java index a25a247de6..505043e442 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/async/pool/ConnectionPoolImplTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/async/pool/ConnectionPoolImplTest.java @@ -131,7 +131,7 @@ private static class TestConnectionPool extends ConnectionPoolImpl TestConnectionPool( NettyChannelTracker nettyChannelTracker ) { super( mock( ChannelConnector.class ), mock( Bootstrap.class ), nettyChannelTracker, newSettings(), DEV_NULL_METRICS, DEV_NULL_LOGGING, - new FakeClock(), true ); + new FakeClock(), true, mock( ConnectionFactory.class ) ); } ExtendedChannelPool getPool( BoltServerAddress address ) diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/pool/NettyChannelTrackerTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/pool/NettyChannelTrackerTest.java index e326836a0c..77f30c39ae 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/async/pool/NettyChannelTrackerTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/async/pool/NettyChannelTrackerTest.java @@ -49,15 +49,15 @@ class NettyChannelTrackerTest private final NettyChannelTracker tracker = new NettyChannelTracker( DEV_NULL_METRICS, mock( ChannelGroup.class ), DEV_NULL_LOGGING ); @Test - void shouldIncrementInUseCountWhenChannelCreated() + void shouldIncrementIdleCountWhenChannelCreated() { Channel channel = newChannel(); assertEquals( 0, tracker.inUseChannelCount( address ) ); assertEquals( 0, tracker.idleChannelCount( address ) ); tracker.channelCreated( channel, null ); - assertEquals( 1, tracker.inUseChannelCount( address ) ); - assertEquals( 0, tracker.idleChannelCount( address ) ); + assertEquals( 0, tracker.inUseChannelCount( address ) ); + assertEquals( 1, tracker.idleChannelCount( address ) ); } @Test @@ -68,16 +68,16 @@ void shouldIncrementInUseCountWhenChannelAcquired() assertEquals( 0, tracker.idleChannelCount( address ) ); tracker.channelCreated( channel, null ); - assertEquals( 1, tracker.inUseChannelCount( address ) ); - assertEquals( 0, tracker.idleChannelCount( address ) ); - - tracker.channelReleased( channel ); assertEquals( 0, tracker.inUseChannelCount( address ) ); assertEquals( 1, tracker.idleChannelCount( address ) ); tracker.channelAcquired( channel ); assertEquals( 1, tracker.inUseChannelCount( address ) ); assertEquals( 0, tracker.idleChannelCount( address ) ); + + tracker.channelReleased( channel ); + assertEquals( 0, tracker.inUseChannelCount( address ) ); + assertEquals( 1, tracker.idleChannelCount( address ) ); } @Test @@ -89,10 +89,13 @@ void shouldIncrementInuseCountForAddress() assertEquals( 0, tracker.inUseChannelCount( address ) ); tracker.channelCreated( channel1, null ); + tracker.channelAcquired( channel1 ); assertEquals( 1, tracker.inUseChannelCount( address ) ); tracker.channelCreated( channel2, null ); + tracker.channelAcquired( channel2 ); assertEquals( 2, tracker.inUseChannelCount( address ) ); tracker.channelCreated( channel3, null ); + tracker.channelAcquired( channel3 ); assertEquals( 3, tracker.inUseChannelCount( address ) ); assertEquals( 0, tracker.idleChannelCount( address ) ); } @@ -105,8 +108,11 @@ void shouldDecrementCountForAddress() Channel channel3 = newChannel(); tracker.channelCreated( channel1, null ); + tracker.channelAcquired( channel1 ); tracker.channelCreated( channel2, null ); + tracker.channelAcquired( channel2 ); tracker.channelCreated( channel3, null ); + tracker.channelAcquired( channel3 ); assertEquals( 3, tracker.inUseChannelCount( address ) ); assertEquals( 0, tracker.idleChannelCount( address ) ); @@ -127,6 +133,7 @@ void shouldDecreaseIdleWhenClosedOutsidePool() throws Throwable // Given Channel channel = newChannel(); tracker.channelCreated( channel, null ); + tracker.channelAcquired( channel ); assertEquals( 1, tracker.inUseChannelCount( address ) ); assertEquals( 0, tracker.idleChannelCount( address ) ); @@ -148,6 +155,7 @@ void shouldDecreaseIdleWhenClosedInsidePool() throws Throwable // Given Channel channel = newChannel(); tracker.channelCreated( channel, null ); + tracker.channelAcquired( channel ); assertEquals( 1, tracker.inUseChannelCount( address ) ); assertEquals( 0, tracker.idleChannelCount( address ) ); diff --git a/driver/src/test/java/org/neo4j/driver/internal/cluster/ClusterRoutingTableTest.java b/driver/src/test/java/org/neo4j/driver/internal/cluster/ClusterRoutingTableTest.java index 327afb4fa0..f5602f9203 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/cluster/ClusterRoutingTableTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/cluster/ClusterRoutingTableTest.java @@ -19,17 +19,25 @@ package org.neo4j.driver.internal.cluster; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import java.time.Duration; import java.util.List; import org.neo4j.driver.internal.BoltServerAddress; +import org.neo4j.driver.internal.util.Clock; import org.neo4j.driver.internal.util.FakeClock; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.neo4j.driver.AccessMode.READ; +import static org.neo4j.driver.AccessMode.WRITE; +import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.ABSENT_DB_NAME; import static org.neo4j.driver.internal.util.ClusterCompositionUtil.A; import static org.neo4j.driver.internal.util.ClusterCompositionUtil.B; import static org.neo4j.driver.internal.util.ClusterCompositionUtil.C; @@ -38,8 +46,6 @@ import static org.neo4j.driver.internal.util.ClusterCompositionUtil.EMPTY; import static org.neo4j.driver.internal.util.ClusterCompositionUtil.F; import static org.neo4j.driver.internal.util.ClusterCompositionUtil.createClusterComposition; -import static org.neo4j.driver.AccessMode.READ; -import static org.neo4j.driver.AccessMode.WRITE; class ClusterRoutingTableTest { @@ -48,7 +54,7 @@ void shouldReturnStaleIfTtlExpired() { // Given FakeClock clock = new FakeClock(); - RoutingTable routingTable = new ClusterRoutingTable( clock ); + RoutingTable routingTable = newRoutingTable( clock ); // When routingTable.update( createClusterComposition( 1000, @@ -64,8 +70,7 @@ void shouldReturnStaleIfTtlExpired() void shouldReturnStaleIfNoRouter() { // Given - FakeClock clock = new FakeClock(); - RoutingTable routingTable = new ClusterRoutingTable( clock ); + RoutingTable routingTable = newRoutingTable(); // When routingTable.update( createClusterComposition( EMPTY, asList( C ), asList( D, E ) ) ); @@ -79,8 +84,7 @@ void shouldReturnStaleIfNoRouter() void shouldBeStaleForReadsButNotWritesWhenNoReaders() { // Given - FakeClock clock = new FakeClock(); - RoutingTable routingTable = new ClusterRoutingTable( clock ); + RoutingTable routingTable = newRoutingTable(); // When routingTable.update( createClusterComposition( asList( A, B ), asList( C ), EMPTY ) ); @@ -94,8 +98,7 @@ void shouldBeStaleForReadsButNotWritesWhenNoReaders() void shouldBeStaleForWritesButNotReadsWhenNoWriters() { // Given - FakeClock clock = new FakeClock(); - RoutingTable routingTable = new ClusterRoutingTable( clock ); + RoutingTable routingTable = newRoutingTable(); // When routingTable.update( createClusterComposition( asList( A, B ), EMPTY, asList( D, E ) ) ); @@ -109,8 +112,7 @@ void shouldBeStaleForWritesButNotReadsWhenNoWriters() void shouldBeNotStaleWithReadersWritersAndRouters() { // Given - FakeClock clock = new FakeClock(); - RoutingTable routingTable = new ClusterRoutingTable( clock ); + RoutingTable routingTable = newRoutingTable(); // When routingTable.update( createClusterComposition( asList( A, B ), asList( C ), asList( D, E ) ) ); @@ -127,17 +129,46 @@ void shouldBeStaleForReadsAndWritesAfterCreation() FakeClock clock = new FakeClock(); // When - RoutingTable routingTable = new ClusterRoutingTable( clock, A ); + RoutingTable routingTable = new ClusterRoutingTable( ABSENT_DB_NAME, clock, A ); // Then assertTrue( routingTable.isStaleFor( READ ) ); assertTrue( routingTable.isStaleFor( WRITE ) ); } + @ParameterizedTest + @ValueSource( strings = {"Molly", ABSENT_DB_NAME, "I AM A NAME"} ) + void shouldReturnDatabaseNameCorrectly( String db ) + { + // Given + FakeClock clock = new FakeClock(); + + // When + RoutingTable routingTable = new ClusterRoutingTable( db, clock, A ); + + // Then + assertEquals( db, routingTable.database() ); + } + + @Test + void shouldContainInitialRouters() + { + // Given + FakeClock clock = new FakeClock(); + + // When + RoutingTable routingTable = new ClusterRoutingTable( ABSENT_DB_NAME, clock, A, B, C ); + + // Then + assertArrayEquals( new BoltServerAddress[]{A, B, C}, routingTable.routers().toArray() ); + assertArrayEquals( new BoltServerAddress[0], routingTable.readers().toArray() ); + assertArrayEquals( new BoltServerAddress[0], routingTable.writers().toArray() ); + } + @Test void shouldPreserveOrderingOfRouters() { - ClusterRoutingTable routingTable = new ClusterRoutingTable( new FakeClock() ); + ClusterRoutingTable routingTable = newRoutingTable(); List routers = asList( A, C, D, F, B, E ); routingTable.update( createClusterComposition( routers, EMPTY, EMPTY ) ); @@ -148,7 +179,7 @@ void shouldPreserveOrderingOfRouters() @Test void shouldPreserveOrderingOfWriters() { - ClusterRoutingTable routingTable = new ClusterRoutingTable( new FakeClock() ); + ClusterRoutingTable routingTable = newRoutingTable(); List writers = asList( D, F, A, C, E ); routingTable.update( createClusterComposition( EMPTY, writers, EMPTY ) ); @@ -159,7 +190,7 @@ void shouldPreserveOrderingOfWriters() @Test void shouldPreserveOrderingOfReaders() { - ClusterRoutingTable routingTable = new ClusterRoutingTable( new FakeClock() ); + ClusterRoutingTable routingTable = newRoutingTable(); List readers = asList( B, A, F, C, D ); routingTable.update( createClusterComposition( EMPTY, EMPTY, readers ) ); @@ -170,7 +201,7 @@ void shouldPreserveOrderingOfReaders() @Test void shouldTreatOneRouterAsValid() { - ClusterRoutingTable routingTable = new ClusterRoutingTable( new FakeClock() ); + ClusterRoutingTable routingTable = newRoutingTable(); List routers = singletonList( A ); List writers = asList( B, C ); @@ -181,4 +212,75 @@ void shouldTreatOneRouterAsValid() assertFalse( routingTable.isStaleFor( READ ) ); assertFalse( routingTable.isStaleFor( WRITE ) ); } + + @Test + void shouldHaveBeStaleForExpiredTime() throws Throwable + { + ClusterRoutingTable routingTable = newRoutingTable( Clock.SYSTEM ); + assertTrue( routingTable.hasBeenStaleFor( 0 ) ); + } + + @Test + void shouldNotHaveBeStaleForUnexpiredTime() throws Throwable + { + ClusterRoutingTable routingTable = newRoutingTable( Clock.SYSTEM ); + assertFalse( routingTable.hasBeenStaleFor( Duration.ofSeconds( 30 ).toMillis() ) ); + } + + @Test + void shouldDefaultToPreferInitialRouter() throws Throwable + { + ClusterRoutingTable routingTable = newRoutingTable(); + assertTrue( routingTable.preferInitialRouter() ); + } + + @Test + void shouldPreferInitialRouterIfNoWriter() throws Throwable + { + ClusterRoutingTable routingTable = newRoutingTable(); + routingTable.update( createClusterComposition( EMPTY, EMPTY, EMPTY ) ); + assertTrue( routingTable.preferInitialRouter() ); + + routingTable.update( createClusterComposition( singletonList( A ), EMPTY, singletonList( A ) ) ); + assertTrue( routingTable.preferInitialRouter() ); + + routingTable.update( createClusterComposition( asList( A, B ), EMPTY, asList( A, B ) ) ); + assertTrue( routingTable.preferInitialRouter() ); + + routingTable.update( createClusterComposition( EMPTY, EMPTY, singletonList( A ) ) ); + assertTrue( routingTable.preferInitialRouter() ); + + routingTable.update( createClusterComposition( singletonList( A ), EMPTY, EMPTY ) ); + assertTrue( routingTable.preferInitialRouter() ); + } + + @Test + void shouldNotPreferInitialRouterIfHasWriter() throws Throwable + { + ClusterRoutingTable routingTable = newRoutingTable(); + routingTable.update( createClusterComposition( EMPTY, singletonList( A ), EMPTY ) ); + assertFalse( routingTable.preferInitialRouter() ); + + routingTable.update( createClusterComposition( singletonList( A ), singletonList( A ), singletonList( A ) ) ); + assertFalse( routingTable.preferInitialRouter() ); + + routingTable.update( createClusterComposition( asList( A, B ), singletonList( A ), asList( A, B ) ) ); + assertFalse( routingTable.preferInitialRouter() ); + + routingTable.update( createClusterComposition( EMPTY, singletonList( A ), singletonList( A ) ) ); + assertFalse( routingTable.preferInitialRouter() ); + + routingTable.update( createClusterComposition( singletonList( A ), singletonList( A ), EMPTY ) ); + assertFalse( routingTable.preferInitialRouter() ); + } + + private ClusterRoutingTable newRoutingTable() + { + return new ClusterRoutingTable( ABSENT_DB_NAME, new FakeClock() ); + } + + private ClusterRoutingTable newRoutingTable( Clock clock ) + { + return new ClusterRoutingTable( ABSENT_DB_NAME, clock ); + } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/cluster/RediscoveryTest.java b/driver/src/test/java/org/neo4j/driver/internal/cluster/RediscoveryTest.java index f072d2ef5b..f13c4f55a7 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/cluster/RediscoveryTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/cluster/RediscoveryTest.java @@ -26,26 +26,26 @@ import java.util.Map; import java.util.concurrent.CompletionStage; -import org.neo4j.driver.internal.BoltServerAddress; -import org.neo4j.driver.internal.cluster.ClusterCompositionResponse.Failure; -import org.neo4j.driver.internal.cluster.ClusterCompositionResponse.Success; -import org.neo4j.driver.internal.spi.Connection; -import org.neo4j.driver.internal.spi.ConnectionPool; -import org.neo4j.driver.internal.util.ImmediateSchedulingEventExecutor; import org.neo4j.driver.Logger; import org.neo4j.driver.exceptions.AuthenticationException; import org.neo4j.driver.exceptions.ProtocolException; import org.neo4j.driver.exceptions.ServiceUnavailableException; import org.neo4j.driver.exceptions.SessionExpiredException; +import org.neo4j.driver.internal.BoltServerAddress; +import org.neo4j.driver.internal.spi.Connection; +import org.neo4j.driver.internal.spi.ConnectionPool; +import org.neo4j.driver.internal.util.FakeClock; +import org.neo4j.driver.internal.util.ImmediateSchedulingEventExecutor; import org.neo4j.driver.net.ServerAddressResolver; import static java.util.Arrays.asList; import static java.util.Collections.emptySet; import static java.util.Collections.singletonMap; import static java.util.concurrent.CompletableFuture.completedFuture; +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -54,12 +54,13 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.neo4j.driver.internal.logging.DevNullLogger.DEV_NULL_LOGGER; +import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.ABSENT_DB_NAME; import static org.neo4j.driver.internal.util.ClusterCompositionUtil.A; import static org.neo4j.driver.internal.util.ClusterCompositionUtil.B; import static org.neo4j.driver.internal.util.ClusterCompositionUtil.C; import static org.neo4j.driver.internal.util.ClusterCompositionUtil.D; import static org.neo4j.driver.internal.util.ClusterCompositionUtil.E; -import static org.neo4j.driver.internal.logging.DevNullLogger.DEV_NULL_LOGGER; import static org.neo4j.driver.internal.util.Futures.failedFuture; import static org.neo4j.driver.util.TestUtil.asOrderedSet; import static org.neo4j.driver.util.TestUtil.await; @@ -75,7 +76,7 @@ void shouldUseFirstRouterInTable() asOrderedSet( B, C ), asOrderedSet( C, D ), asOrderedSet( B ) ); Map responsesByAddress = new HashMap<>(); - responsesByAddress.put( B, new Success( expectedComposition ) ); // first -> valid cluster composition + responsesByAddress.put( B, expectedComposition ); // first -> valid cluster composition ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); Rediscovery rediscovery = newRediscovery( A, compositionProvider, mock( ServerAddressResolver.class ) ); @@ -96,7 +97,7 @@ void shouldSkipFailingRouters() Map responsesByAddress = new HashMap<>(); responsesByAddress.put( A, new RuntimeException( "Hi!" ) ); // first -> non-fatal failure responsesByAddress.put( B, new ServiceUnavailableException( "Hi!" ) ); // second -> non-fatal failure - responsesByAddress.put( C, new Success( expectedComposition ) ); // third -> valid cluster composition + responsesByAddress.put( C, expectedComposition ); // third -> valid cluster composition ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); Rediscovery rediscovery = newRediscovery( A, compositionProvider, mock( ServerAddressResolver.class ) ); @@ -139,7 +140,7 @@ void shouldFallbackToInitialRouterWhenKnownRoutersFail() Map responsesByAddress = new HashMap<>(); responsesByAddress.put( B, new ServiceUnavailableException( "Hi!" ) ); // first -> non-fatal failure responsesByAddress.put( C, new ServiceUnavailableException( "Hi!" ) ); // second -> non-fatal failure - responsesByAddress.put( initialRouter, new Success( expectedComposition ) ); // initial -> valid response + responsesByAddress.put( initialRouter, expectedComposition ); // initial -> valid response ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); ServerAddressResolver resolver = resolverMock( initialRouter, initialRouter ); @@ -161,8 +162,8 @@ void shouldFailImmediatelyWhenClusterCompositionProviderReturnsFailure() ProtocolException protocolError = new ProtocolException( "Wrong record!" ); Map responsesByAddress = new HashMap<>(); - responsesByAddress.put( B, new Failure( protocolError ) ); // first -> fatal failure - responsesByAddress.put( C, new Success( validComposition ) ); // second -> valid cluster composition + responsesByAddress.put( B, protocolError ); // first -> fatal failure + responsesByAddress.put( C, validComposition ); // second -> valid cluster composition Logger logger = mock( Logger.class ); @@ -174,7 +175,7 @@ void shouldFailImmediatelyWhenClusterCompositionProviderReturnsFailure() ClusterComposition composition = await( rediscovery.lookupClusterComposition( table, pool ) ); assertEquals( validComposition, composition ); - verify( logger ).warn( String.format( "Unable to process routing table received from '%s'.", B ), protocolError ); + verify( logger ).warn( String.format( "Failed to update routing table with server '%s'.", B ), protocolError ); } @Test @@ -188,7 +189,7 @@ void shouldResolveInitialRouterAddress() responsesByAddress.put( B, new ServiceUnavailableException( "Hi!" ) ); // first -> non-fatal failure responsesByAddress.put( C, new ServiceUnavailableException( "Hi!" ) ); // second -> non-fatal failure responsesByAddress.put( D, new IOException( "Hi!" ) ); // resolved first -> non-fatal failure - responsesByAddress.put( E, new Success( expectedComposition ) ); // resolved second -> valid response + responsesByAddress.put( E, expectedComposition ); // resolved second -> valid response ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); // initial router resolved to two other addresses @@ -219,7 +220,7 @@ void shouldResolveInitialRouterAddressUsingCustomResolver() Map responsesByAddress = new HashMap<>(); responsesByAddress.put( B, new ServiceUnavailableException( "Hi!" ) ); // first -> non-fatal failure responsesByAddress.put( C, new ServiceUnavailableException( "Hi!" ) ); // second -> non-fatal failure - responsesByAddress.put( E, new Success( expectedComposition ) ); // resolved second -> valid response + responsesByAddress.put( E, expectedComposition ); // resolved second -> valid response ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); Rediscovery rediscovery = newRediscovery( A, compositionProvider, resolver ); @@ -238,7 +239,7 @@ void shouldPropagateFailureWhenResolverFails() ClusterComposition expectedComposition = new ClusterComposition( 42, asOrderedSet( A, B ), asOrderedSet( A, B ), asOrderedSet( A, B ) ); - Map responsesByAddress = singletonMap( A, new Success( expectedComposition ) ); + Map responsesByAddress = singletonMap( A, expectedComposition ); ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); // failing server address resolver @@ -268,7 +269,7 @@ void shouldFailWhenNoRoutersRespond() RoutingTable table = routingTableMock( A, B, C ); ServiceUnavailableException e = assertThrows( ServiceUnavailableException.class, () -> await( rediscovery.lookupClusterComposition( table, pool ) ) ); - assertEquals( "Could not perform discovery. No routing servers available.", e.getMessage() ); + assertThat( e.getMessage(), containsString( "Could not perform discovery" ) ); } @Test @@ -281,16 +282,13 @@ void shouldUseInitialRouterAfterDiscoveryReturnsNoWriters() asOrderedSet( B, A ), asOrderedSet( B, A ), asOrderedSet( B, A ) ); Map responsesByAddress = new HashMap<>(); - responsesByAddress.put( B, new Success( noWritersComposition ) ); // first -> valid cluster composition - responsesByAddress.put( initialRouter, new Success( validComposition ) ); // initial -> valid composition + responsesByAddress.put( initialRouter, validComposition ); // initial -> valid composition ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); ServerAddressResolver resolver = resolverMock( initialRouter, initialRouter ); Rediscovery rediscovery = newRediscovery( initialRouter, compositionProvider, resolver ); - RoutingTable table = routingTableMock( B ); - - ClusterComposition composition1 = await( rediscovery.lookupClusterComposition( table, pool ) ); - assertEquals( noWritersComposition, composition1 ); + RoutingTable table = new ClusterRoutingTable( ABSENT_DB_NAME, new FakeClock() ); + table.update( noWritersComposition ); ClusterComposition composition2 = await( rediscovery.lookupClusterComposition( table, pool ) ); assertEquals( validComposition, composition2 ); @@ -304,12 +302,12 @@ void shouldUseInitialRouterToStartWith() asOrderedSet( A ), asOrderedSet( A ), asOrderedSet( A ) ); Map responsesByAddress = new HashMap<>(); - responsesByAddress.put( initialRouter, new Success( validComposition ) ); // initial -> valid composition + responsesByAddress.put( initialRouter, validComposition ); // initial -> valid composition ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); ServerAddressResolver resolver = resolverMock( initialRouter, initialRouter ); - Rediscovery rediscovery = newRediscovery( initialRouter, compositionProvider, resolver, mock( Logger.class ), true ); - RoutingTable table = routingTableMock( B, C, D ); + Rediscovery rediscovery = newRediscovery( initialRouter, compositionProvider, resolver ); + RoutingTable table = routingTableMock( true, B, C, D ); ClusterComposition composition = await( rediscovery.lookupClusterComposition( table, pool ) ); assertEquals( validComposition, composition ); @@ -325,12 +323,12 @@ void shouldUseKnownRoutersWhenInitialRouterFails() Map responsesByAddress = new HashMap<>(); responsesByAddress.put( initialRouter, new ServiceUnavailableException( "Hi" ) ); // initial -> non-fatal error responsesByAddress.put( D, new IOException( "Hi" ) ); // first known -> non-fatal failure - responsesByAddress.put( E, new Success( validComposition ) ); // second known -> valid composition + responsesByAddress.put( E, validComposition ); // second known -> valid composition ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); ServerAddressResolver resolver = resolverMock( initialRouter, initialRouter ); - Rediscovery rediscovery = newRediscovery( initialRouter, compositionProvider, resolver, mock( Logger.class ), true ); - RoutingTable table = routingTableMock( D, E ); + Rediscovery rediscovery = newRediscovery( initialRouter, compositionProvider, resolver ); + RoutingTable table = routingTableMock( true, D, E ); ClusterComposition composition = await( rediscovery.lookupClusterComposition( table, pool ) ); assertEquals( validComposition, composition ); @@ -349,7 +347,7 @@ void shouldRetryConfiguredNumberOfTimesWithDelay() Map responsesByAddress = new HashMap<>(); responsesByAddress.put( A, new ServiceUnavailableException( "Hi!" ) ); responsesByAddress.put( B, new ServiceUnavailableException( "Hi!" ) ); - responsesByAddress.put( E, new Success( expectedComposition ) ); + responsesByAddress.put( E, expectedComposition ); ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); ServerAddressResolver resolver = mock( ServerAddressResolver.class ); @@ -359,9 +357,8 @@ void shouldRetryConfiguredNumberOfTimesWithDelay() ImmediateSchedulingEventExecutor eventExecutor = new ImmediateSchedulingEventExecutor(); RoutingSettings settings = new RoutingSettings( maxRoutingFailures, retryTimeoutDelay ); - Rediscovery rediscovery = new Rediscovery( A, settings, compositionProvider, resolver, eventExecutor, - DEV_NULL_LOGGER, false ); - RoutingTable table = routingTableMock( A, B ); + Rediscovery rediscovery = new RediscoveryImpl( A, settings, compositionProvider, eventExecutor, resolver, DEV_NULL_LOGGER ); + RoutingTable table = routingTableMock(A, B ); ClusterComposition actualComposition = await( rediscovery.lookupClusterComposition( table, pool ) ); @@ -384,12 +381,11 @@ void shouldNotLogWhenSingleRetryAttemptFails() ImmediateSchedulingEventExecutor eventExecutor = new ImmediateSchedulingEventExecutor(); RoutingSettings settings = new RoutingSettings( maxRoutingFailures, retryTimeoutDelay ); Logger logger = mock( Logger.class ); - Rediscovery rediscovery = new Rediscovery( A, settings, compositionProvider, resolver, eventExecutor, - logger, false ); + Rediscovery rediscovery = new RediscoveryImpl( A, settings, compositionProvider, eventExecutor, resolver, logger ); RoutingTable table = routingTableMock( A ); ServiceUnavailableException e = assertThrows( ServiceUnavailableException.class, () -> await( rediscovery.lookupClusterComposition( table, pool ) ) ); - assertEquals( "Could not perform discovery. No routing servers available.", e.getMessage() ); + assertThat( e.getMessage(), containsString( "Could not perform discovery" ) ); // rediscovery should not log about retries and should not schedule any retries verify( logger, never() ).info( startsWith( "Unable to fetch new routing table, will try again in " ) ); @@ -404,16 +400,9 @@ private Rediscovery newRediscovery( BoltServerAddress initialRouter, ClusterComp private Rediscovery newRediscovery( BoltServerAddress initialRouter, ClusterCompositionProvider compositionProvider, ServerAddressResolver resolver, Logger logger ) - { - return newRediscovery( initialRouter, compositionProvider, resolver, logger, false ); - } - - private Rediscovery newRediscovery( BoltServerAddress initialRouter, ClusterCompositionProvider compositionProvider, - ServerAddressResolver resolver, Logger logger, boolean useInitialRouter ) { RoutingSettings settings = new RoutingSettings( 1, 0 ); - return new Rediscovery( initialRouter, settings, compositionProvider, resolver, - GlobalEventExecutor.INSTANCE, logger, useInitialRouter ); + return new RediscoveryImpl( initialRouter, settings, compositionProvider, GlobalEventExecutor.INSTANCE, resolver, logger ); } @SuppressWarnings( "unchecked" ) @@ -421,7 +410,7 @@ private static ClusterCompositionProvider compositionProviderMock( Map responsesByAddress ) { ClusterCompositionProvider provider = mock( ClusterCompositionProvider.class ); - when( provider.getClusterComposition( any( CompletionStage.class ) ) ).then( invocation -> + when( provider.getClusterComposition( any( CompletionStage.class ), any( String.class ) ) ).then( invocation -> { CompletionStage connectionStage = invocation.getArgument( 0 ); BoltServerAddress address = await( connectionStage ).serverAddress(); @@ -465,11 +454,18 @@ private static Connection asyncConnectionMock( BoltServerAddress address ) } private static RoutingTable routingTableMock( BoltServerAddress... routers ) + { + return routingTableMock( false, routers ); + } + + private static RoutingTable routingTableMock( boolean preferInitialRouter, BoltServerAddress... routers ) { RoutingTable routingTable = mock( RoutingTable.class ); AddressSet addressSet = new AddressSet(); addressSet.update( asOrderedSet( routers ) ); when( routingTable.routers() ).thenReturn( addressSet ); + when( routingTable.database() ).thenReturn( ABSENT_DB_NAME ); + when( routingTable.preferInitialRouter() ).thenReturn( preferInitialRouter ); return routingTable; } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/cluster/RoutingProcedureClusterCompositionProviderTest.java b/driver/src/test/java/org/neo4j/driver/internal/cluster/RoutingProcedureClusterCompositionProviderTest.java index 91dc2922bf..426fd528ec 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/cluster/RoutingProcedureClusterCompositionProviderTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/cluster/RoutingProcedureClusterCompositionProviderTest.java @@ -26,29 +26,30 @@ import java.util.Set; import java.util.concurrent.CompletionStage; -import org.neo4j.driver.internal.BoltServerAddress; -import org.neo4j.driver.internal.InternalRecord; -import org.neo4j.driver.internal.spi.Connection; -import org.neo4j.driver.internal.util.Clock; -import org.neo4j.driver.internal.value.StringValue; import org.neo4j.driver.Record; import org.neo4j.driver.Statement; import org.neo4j.driver.Value; import org.neo4j.driver.exceptions.ProtocolException; import org.neo4j.driver.exceptions.ServiceUnavailableException; +import org.neo4j.driver.internal.BoltServerAddress; +import org.neo4j.driver.internal.InternalRecord; +import org.neo4j.driver.internal.spi.Connection; +import org.neo4j.driver.internal.util.Clock; +import org.neo4j.driver.internal.value.StringValue; import static java.util.Arrays.asList; import static java.util.concurrent.CompletableFuture.completedFuture; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.instanceOf; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.neo4j.driver.internal.util.Futures.failedFuture; import static org.neo4j.driver.Values.value; +import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.ABSENT_DB_NAME; +import static org.neo4j.driver.internal.util.Futures.failedFuture; import static org.neo4j.driver.util.TestUtil.await; class RoutingProcedureClusterCompositionProviderTest @@ -63,14 +64,10 @@ void shouldProtocolErrorWhenNoRecord() CompletionStage connectionStage = completedFuture( mock( Connection.class ) ); RoutingProcedureResponse noRecordsResponse = newRoutingResponse(); - when( mockedRunner.run( connectionStage ) ).thenReturn( completedFuture( noRecordsResponse ) ); + when( mockedRunner.run( eq( connectionStage ), any( String.class ) ) ).thenReturn( completedFuture( noRecordsResponse ) ); - // When - ClusterCompositionResponse response = await( provider.getClusterComposition( connectionStage ) ); - - // Then - assertThat( response, instanceOf( ClusterCompositionResponse.Failure.class ) ); - ProtocolException error = assertThrows( ProtocolException.class, response::clusterComposition ); + // When & Then + ProtocolException error = assertThrows( ProtocolException.class, () -> await( provider.getClusterComposition( connectionStage, ABSENT_DB_NAME ) ) ); assertThat( error.getMessage(), containsString( "records received '0' is too few or too many." ) ); } @@ -85,14 +82,10 @@ void shouldProtocolErrorWhenMoreThanOneRecord() CompletionStage connectionStage = completedFuture( mock( Connection.class ) ); Record aRecord = new InternalRecord( asList( "key1", "key2" ), new Value[]{ new StringValue( "a value" ) } ); RoutingProcedureResponse routingResponse = newRoutingResponse( aRecord, aRecord ); - when( mockedRunner.run( connectionStage ) ).thenReturn( completedFuture( routingResponse ) ); + when( mockedRunner.run( eq( connectionStage ), any( String.class ) ) ).thenReturn( completedFuture( routingResponse ) ); // When - ClusterCompositionResponse response = await( provider.getClusterComposition( connectionStage ) ); - - // Then - assertThat( response, instanceOf( ClusterCompositionResponse.Failure.class ) ); - ProtocolException error = assertThrows( ProtocolException.class, response::clusterComposition ); + ProtocolException error = assertThrows( ProtocolException.class, () -> await( provider.getClusterComposition( connectionStage, ABSENT_DB_NAME ) ) ); assertThat( error.getMessage(), containsString( "records received '2' is too few or too many." ) ); } @@ -107,14 +100,10 @@ void shouldProtocolErrorWhenUnparsableRecord() CompletionStage connectionStage = completedFuture( mock( Connection.class ) ); Record aRecord = new InternalRecord( asList( "key1", "key2" ), new Value[]{ new StringValue( "a value" ) } ); RoutingProcedureResponse routingResponse = newRoutingResponse( aRecord ); - when( mockedRunner.run( connectionStage ) ).thenReturn( completedFuture( routingResponse ) ); + when( mockedRunner.run( eq( connectionStage ), any( String.class ) ) ).thenReturn( completedFuture( routingResponse ) ); // When - ClusterCompositionResponse response = await( provider.getClusterComposition( connectionStage ) ); - - // Then - assertThat( response, instanceOf( ClusterCompositionResponse.Failure.class ) ); - ProtocolException error = assertThrows( ProtocolException.class, response::clusterComposition ); + ProtocolException error = assertThrows( ProtocolException.class, () -> await( provider.getClusterComposition( connectionStage, ABSENT_DB_NAME ) ) ); assertThat( error.getMessage(), containsString( "unparsable record received." ) ); } @@ -134,15 +123,11 @@ void shouldProtocolErrorWhenNoRouters() serverInfo( "WRITE", "one:1337" ) ) ) } ); RoutingProcedureResponse routingResponse = newRoutingResponse( record ); - when( mockedRunner.run( connectionStage ) ).thenReturn( completedFuture( routingResponse ) ); + when( mockedRunner.run( eq( connectionStage ), any( String.class ) ) ).thenReturn( completedFuture( routingResponse ) ); when( mockedClock.millis() ).thenReturn( 12345L ); // When - ClusterCompositionResponse response = await( provider.getClusterComposition( connectionStage ) ); - - // Then - assertThat( response, instanceOf( ClusterCompositionResponse.Failure.class ) ); - ProtocolException error = assertThrows( ProtocolException.class, response::clusterComposition ); + ProtocolException error = assertThrows( ProtocolException.class, () -> await( provider.getClusterComposition( connectionStage, ABSENT_DB_NAME ) ) ); assertThat( error.getMessage(), containsString( "no router or reader found in response." ) ); } @@ -162,19 +147,14 @@ void shouldProtocolErrorWhenNoReaders() serverInfo( "ROUTE", "one:1337", "two:1337" ) ) ) } ); RoutingProcedureResponse routingResponse = newRoutingResponse( record ); - when( mockedRunner.run( connectionStage ) ).thenReturn( completedFuture( routingResponse ) ); + when( mockedRunner.run( eq( connectionStage ), any( String.class ) ) ).thenReturn( completedFuture( routingResponse ) ); when( mockedClock.millis() ).thenReturn( 12345L ); // When - ClusterCompositionResponse response = await( provider.getClusterComposition( connectionStage ) ); - - // Then - assertThat( response, instanceOf( ClusterCompositionResponse.Failure.class ) ); - ProtocolException error = assertThrows( ProtocolException.class, response::clusterComposition ); + ProtocolException error = assertThrows( ProtocolException.class, () -> await( provider.getClusterComposition( connectionStage, ABSENT_DB_NAME ) ) ); assertThat( error.getMessage(), containsString( "no router or reader found in response." ) ); } - @Test void shouldPropagateConnectionFailureExceptions() { @@ -184,11 +164,11 @@ void shouldPropagateConnectionFailureExceptions() new RoutingProcedureClusterCompositionProvider( mock( Clock.class ), mockedRunner ); CompletionStage connectionStage = completedFuture( mock( Connection.class ) ); - when( mockedRunner.run( connectionStage ) ).thenReturn( failedFuture( + when( mockedRunner.run( eq( connectionStage ), any( String.class ) ) ).thenReturn( failedFuture( new ServiceUnavailableException( "Connection breaks during cypher execution" ) ) ); // When & Then - ServiceUnavailableException e = assertThrows( ServiceUnavailableException.class, () -> await( provider.getClusterComposition( connectionStage ) ) ); + ServiceUnavailableException e = assertThrows( ServiceUnavailableException.class, () -> await( provider.getClusterComposition( connectionStage, ABSENT_DB_NAME ) ) ); assertThat( e.getMessage(), containsString( "Connection breaks during cypher execution" ) ); } @@ -209,15 +189,13 @@ void shouldReturnSuccessResultWhenNoError() serverInfo( "ROUTE", "one:1337", "two:1337" ) ) ) } ); RoutingProcedureResponse routingResponse = newRoutingResponse( record ); - when( mockedRunner.run( connectionStage ) ).thenReturn( completedFuture( routingResponse ) ); + when( mockedRunner.run( eq( connectionStage ), any( String.class ) ) ).thenReturn( completedFuture( routingResponse ) ); when( mockedClock.millis() ).thenReturn( 12345L ); // When - ClusterCompositionResponse response = await( provider.getClusterComposition( connectionStage ) ); + ClusterComposition cluster = await( provider.getClusterComposition( connectionStage, ABSENT_DB_NAME ) ); // Then - assertThat( response, instanceOf( ClusterCompositionResponse.Success.class ) ); - ClusterComposition cluster = response.clusterComposition(); assertEquals( 12345 + 100_000, cluster.expirationTimestamp() ); assertEquals( serverSet( "one:1337", "two:1337" ), cluster.readers() ); assertEquals( serverSet( "one:1337" ), cluster.writers() ); @@ -225,22 +203,21 @@ void shouldReturnSuccessResultWhenNoError() } @Test - @SuppressWarnings( "unchecked" ) void shouldReturnFailureWhenProcedureRunnerFails() { RoutingProcedureRunner procedureRunner = newProcedureRunnerMock(); + CompletionStage connectionStage = completedFuture( mock( Connection.class ) ); + RuntimeException error = new RuntimeException( "hi" ); - when( procedureRunner.run( any( CompletionStage.class ) ) ) + when( procedureRunner.run( eq( connectionStage ), any( String.class ) ) ) .thenReturn( completedFuture( newRoutingResponse( error ) ) ); RoutingProcedureClusterCompositionProvider provider = new RoutingProcedureClusterCompositionProvider( mock( Clock.class ), procedureRunner ); - CompletionStage connectionStage = completedFuture( mock( Connection.class ) ); - ClusterCompositionResponse response = await( provider.getClusterComposition( connectionStage ) ); - - ServiceUnavailableException e = assertThrows( ServiceUnavailableException.class, response::clusterComposition ); - assertEquals( error, e.getCause() ); + RuntimeException e = assertThrows( RuntimeException.class, + () -> await( provider.getClusterComposition( connectionStage, ABSENT_DB_NAME ) ) ); + assertEquals( error, e ); } private static Map serverInfo( String role, String... addresses ) diff --git a/driver/src/test/java/org/neo4j/driver/internal/cluster/RoutingProcedureRunnerTest.java b/driver/src/test/java/org/neo4j/driver/internal/cluster/RoutingProcedureRunnerTest.java index 6dd7370bd5..524dd4e7dd 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/cluster/RoutingProcedureRunnerTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/cluster/RoutingProcedureRunnerTest.java @@ -19,17 +19,21 @@ package org.neo4j.driver.internal.cluster; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import java.net.URI; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.CompletionStage; -import org.neo4j.driver.internal.BoltServerAddress; -import org.neo4j.driver.internal.spi.Connection; import org.neo4j.driver.Record; import org.neo4j.driver.Statement; import org.neo4j.driver.Value; import org.neo4j.driver.exceptions.ClientException; +import org.neo4j.driver.internal.BoltServerAddress; +import org.neo4j.driver.internal.spi.Connection; import static java.util.Arrays.asList; import static java.util.Collections.EMPTY_MAP; @@ -42,62 +46,83 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.neo4j.driver.Values.parameters; +import static org.neo4j.driver.internal.cluster.RoutingProcedureRunner.DATABASE_NAME; import static org.neo4j.driver.internal.cluster.RoutingProcedureRunner.GET_ROUTING_TABLE; -import static org.neo4j.driver.internal.cluster.RoutingProcedureRunner.GET_ROUTING_TABLE_PARAM; -import static org.neo4j.driver.internal.cluster.RoutingProcedureRunner.GET_SERVERS; +import static org.neo4j.driver.internal.cluster.RoutingProcedureRunner.MULTI_DB_GET_ROUTING_TABLE; +import static org.neo4j.driver.internal.cluster.RoutingProcedureRunner.ROUTING_CONTEXT; +import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.ABSENT_DB_NAME; +import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.SYSTEM_DB_NAME; import static org.neo4j.driver.internal.util.Futures.completedWithNull; import static org.neo4j.driver.internal.util.Futures.failedFuture; import static org.neo4j.driver.internal.util.ServerVersion.version; -import static org.neo4j.driver.Values.parameters; import static org.neo4j.driver.util.TestUtil.await; class RoutingProcedureRunnerTest { - @Test - void shouldCallGetRoutingTableWithEmptyMap() + @ParameterizedTest + @ValueSource( strings = {ABSENT_DB_NAME, SYSTEM_DB_NAME, " this is a db name "} ) + void shouldCallGetRoutingTableWithEmptyMapOnSystemDatabaseForDatabase( String db ) { RoutingProcedureRunner runner = new TestRoutingProcedureRunner( RoutingContext.EMPTY, - completedFuture( asList( mock( Record.class ), mock( Record.class ) ) ) ); + completedFuture( asList( mock( Record.class ), mock( Record.class ) ) ), SYSTEM_DB_NAME ); - RoutingProcedureResponse response = await( runner.run( connectionStage( "Neo4j/3.2.1" ) ) ); + RoutingProcedureResponse response = await( runner.run( connectionStage( "Neo4j/4.0.0" ), db ) ); assertTrue( response.isSuccess() ); assertEquals( 2, response.records().size() ); - assertEquals( new Statement( "CALL " + GET_ROUTING_TABLE, parameters( GET_ROUTING_TABLE_PARAM, EMPTY_MAP ) ), - response.procedure() ); + + Value parameters = generateMultiDatabaseRoutingParameters( EMPTY_MAP, db ); + assertEquals( new Statement( "CALL " + MULTI_DB_GET_ROUTING_TABLE, parameters ), response.procedure() ); } - @Test - void shouldCallGetRoutingTableWithParam() + @ParameterizedTest + @ValueSource( strings = {ABSENT_DB_NAME, SYSTEM_DB_NAME, " this is a db name "} ) + void shouldCallGetRoutingTableWithParamOnSystemDatabaseForDatabase( String db ) { URI uri = URI.create( "neo4j://localhost/?key1=value1&key2=value2" ); RoutingContext context = new RoutingContext( uri ); RoutingProcedureRunner runner = new TestRoutingProcedureRunner( context, - completedFuture( singletonList( mock( Record.class ) ) ) ); + completedFuture( singletonList( mock( Record.class ) ) ), SYSTEM_DB_NAME ); - RoutingProcedureResponse response = await( runner.run( connectionStage( "Neo4j/3.2.1" ) ) ); + RoutingProcedureResponse response = await( runner.run( connectionStage( "Neo4j/4.0.0" ), db ) ); assertTrue( response.isSuccess() ); assertEquals( 1, response.records().size() ); - Value expectedParams = parameters( GET_ROUTING_TABLE_PARAM, context.asMap() ); - assertEquals( new Statement( "CALL " + GET_ROUTING_TABLE, expectedParams ), response.procedure() ); + Value expectedParams = generateMultiDatabaseRoutingParameters( context.asMap(), db ); + assertEquals( new Statement( "CALL " + MULTI_DB_GET_ROUTING_TABLE, expectedParams ), response.procedure() ); } @Test - void shouldCallGetServers() + void shouldCallGetRoutingTableWithEmptyMap() + { + RoutingProcedureRunner runner = new TestRoutingProcedureRunner( RoutingContext.EMPTY, + completedFuture( asList( mock( Record.class ), mock( Record.class ) ) ) ); + + RoutingProcedureResponse response = await( runner.run( connectionStage( "Neo4j/3.2.1" ), ABSENT_DB_NAME ) ); + + assertTrue( response.isSuccess() ); + assertEquals( 2, response.records().size() ); + assertEquals( new Statement( "CALL " + GET_ROUTING_TABLE, parameters( ROUTING_CONTEXT, EMPTY_MAP ) ), + response.procedure() ); + } + + @Test + void shouldCallGetRoutingTableWithParam() { URI uri = URI.create( "neo4j://localhost/?key1=value1&key2=value2" ); RoutingContext context = new RoutingContext( uri ); RoutingProcedureRunner runner = new TestRoutingProcedureRunner( context, - completedFuture( asList( mock( Record.class ), mock( Record.class ) ) ) ); + completedFuture( singletonList( mock( Record.class ) ) ) ); - RoutingProcedureResponse response = await( runner.run( connectionStage( "Neo4j/3.1.8" ) ) ); + RoutingProcedureResponse response = await( runner.run( connectionStage( "Neo4j/3.2.1" ), ABSENT_DB_NAME ) ); assertTrue( response.isSuccess() ); - assertEquals( 2, response.records().size() ); - assertEquals( new Statement( "CALL " + GET_SERVERS ), response.procedure() ); + assertEquals( 1, response.records().size() ); + Value expectedParams = parameters( ROUTING_CONTEXT, context.asMap() ); + assertEquals( new Statement( "CALL " + GET_ROUTING_TABLE, expectedParams ), response.procedure() ); } @Test @@ -106,7 +131,7 @@ void shouldReturnFailedResponseOnClientException() ClientException error = new ClientException( "Hi" ); RoutingProcedureRunner runner = new TestRoutingProcedureRunner( RoutingContext.EMPTY, failedFuture( error ) ); - RoutingProcedureResponse response = await( runner.run( connectionStage( "Neo4j/3.2.2" ) ) ); + RoutingProcedureResponse response = await( runner.run( connectionStage( "Neo4j/3.2.2" ), ABSENT_DB_NAME ) ); assertFalse( response.isSuccess() ); assertEquals( error, response.error() ); @@ -118,7 +143,7 @@ void shouldReturnFailedStageOnError() Exception error = new Exception( "Hi" ); RoutingProcedureRunner runner = new TestRoutingProcedureRunner( RoutingContext.EMPTY, failedFuture( error ) ); - Exception e = assertThrows( Exception.class, () -> await( runner.run( connectionStage( "Neo4j/3.2.2" ) ) ) ); + Exception e = assertThrows( Exception.class, () -> await( runner.run( connectionStage( "Neo4j/3.2.2" ), ABSENT_DB_NAME ) ) ); assertEquals( error, e ); } @@ -128,7 +153,7 @@ void shouldPropagateErrorFromConnectionStage() RuntimeException error = new RuntimeException( "Hi" ); RoutingProcedureRunner runner = new TestRoutingProcedureRunner( RoutingContext.EMPTY ); - RuntimeException e = assertThrows( RuntimeException.class, () -> await( runner.run( failedFuture( error ) ) ) ); + RuntimeException e = assertThrows( RuntimeException.class, () -> await( runner.run( failedFuture( error ), ABSENT_DB_NAME ) ) ); assertEquals( error, e ); } @@ -140,7 +165,7 @@ void shouldReleaseConnectionOnSuccess() CompletionStage connectionStage = connectionStage( "Neo4j/3.2.2" ); Connection connection = await( connectionStage ); - RoutingProcedureResponse response = await( runner.run( connectionStage ) ); + RoutingProcedureResponse response = await( runner.run( connectionStage, ABSENT_DB_NAME ) ); assertTrue( response.isSuccess() ); verify( connection ).release(); @@ -156,11 +181,20 @@ void shouldPropagateReleaseError() CompletionStage connectionStage = connectionStage( "Neo4j/3.3.3", failedFuture( releaseError ) ); Connection connection = await( connectionStage ); - RuntimeException e = assertThrows( RuntimeException.class, () -> await( runner.run( connectionStage ) ) ); + RuntimeException e = assertThrows( RuntimeException.class, () -> await( runner.run( connectionStage, ABSENT_DB_NAME ) ) ); assertEquals( releaseError, e ); verify( connection ).release(); } + private static Value generateMultiDatabaseRoutingParameters( Map context, String db ) + { + if ( Objects.equals( ABSENT_DB_NAME, db ) ) + { + db = null; + } + return parameters( ROUTING_CONTEXT, context, DATABASE_NAME, db ); + } + private static CompletionStage connectionStage( String serverVersion ) { return connectionStage( serverVersion, completedWithNull() ); @@ -179,21 +213,29 @@ private static CompletionStage connectionStage( String serverVersion private static class TestRoutingProcedureRunner extends RoutingProcedureRunner { final CompletionStage> runProcedureResult; + final String executionDatabase; TestRoutingProcedureRunner( RoutingContext context ) { - this( context, null ); + this( context, null, ABSENT_DB_NAME ); } TestRoutingProcedureRunner( RoutingContext context, CompletionStage> runProcedureResult ) + { + this( context, runProcedureResult, ABSENT_DB_NAME ); + } + + TestRoutingProcedureRunner( RoutingContext context, CompletionStage> runProcedureResult, String executionDatabase ) { super( context ); this.runProcedureResult = runProcedureResult; + this.executionDatabase = executionDatabase; } @Override CompletionStage> runProcedure( Connection connection, Statement procedure ) { + assertEquals( executionDatabase, connection.databaseName() ); return runProcedureResult; } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/cluster/RoutingTableHandlerTest.java b/driver/src/test/java/org/neo4j/driver/internal/cluster/RoutingTableHandlerTest.java new file mode 100644 index 0000000000..183dcb346c --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/cluster/RoutingTableHandlerTest.java @@ -0,0 +1,298 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.cluster; + +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.CompletionStage; + +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.exceptions.ServiceUnavailableException; +import org.neo4j.driver.internal.BoltServerAddress; +import org.neo4j.driver.internal.spi.Connection; +import org.neo4j.driver.internal.spi.ConnectionPool; +import org.neo4j.driver.internal.util.FakeClock; +import org.neo4j.driver.internal.util.Futures; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptySet; +import static java.util.Collections.singletonList; +import static java.util.concurrent.CompletableFuture.completedFuture; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.neo4j.driver.AccessMode.READ; +import static org.neo4j.driver.AccessMode.WRITE; +import static org.neo4j.driver.internal.BoltServerAddress.LOCAL_DEFAULT; +import static org.neo4j.driver.internal.logging.DevNullLogger.DEV_NULL_LOGGER; +import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.ABSENT_DB_NAME; +import static org.neo4j.driver.internal.util.ClusterCompositionUtil.A; +import static org.neo4j.driver.internal.util.ClusterCompositionUtil.B; +import static org.neo4j.driver.internal.util.ClusterCompositionUtil.C; +import static org.neo4j.driver.internal.util.ClusterCompositionUtil.D; +import static org.neo4j.driver.internal.util.ClusterCompositionUtil.E; +import static org.neo4j.driver.internal.util.ClusterCompositionUtil.F; +import static org.neo4j.driver.util.TestUtil.asOrderedSet; +import static org.neo4j.driver.util.TestUtil.await; + +class RoutingTableHandlerTest +{ + @Test + void shouldRemoveAddressFromRoutingTableOnConnectionFailure() + { + RoutingTable routingTable = new ClusterRoutingTable( ABSENT_DB_NAME, new FakeClock() ); + routingTable.update( new ClusterComposition( + 42, asOrderedSet( A, B, C ), asOrderedSet( A, C, E ), asOrderedSet( B, D, F ) ) ); + + RoutingTableHandler handler = + new RoutingTableHandler( routingTable, newRediscoveryMock(), newConnectionPoolMock(), newRoutingTablesMock(), DEV_NULL_LOGGER ); + + + handler.onConnectionFailure( B ); + + assertArrayEquals( new BoltServerAddress[]{A, C}, routingTable.readers().toArray() ); + assertArrayEquals( new BoltServerAddress[]{A, C, E}, routingTable.writers().toArray() ); + assertArrayEquals( new BoltServerAddress[]{D, F}, routingTable.routers().toArray() ); + + handler.onConnectionFailure( A ); + + assertArrayEquals( new BoltServerAddress[]{C}, routingTable.readers().toArray() ); + assertArrayEquals( new BoltServerAddress[]{C, E}, routingTable.writers().toArray() ); + assertArrayEquals( new BoltServerAddress[]{D, F}, routingTable.routers().toArray() ); + } + + @Test + void acquireShouldUpdateRoutingTableWhenKnownRoutingTableIsStale() + { + BoltServerAddress initialRouter = new BoltServerAddress( "initialRouter", 1 ); + BoltServerAddress reader1 = new BoltServerAddress( "reader-1", 2 ); + BoltServerAddress reader2 = new BoltServerAddress( "reader-1", 3 ); + BoltServerAddress writer1 = new BoltServerAddress( "writer-1", 4 ); + BoltServerAddress router1 = new BoltServerAddress( "router-1", 5 ); + + ConnectionPool connectionPool = newConnectionPoolMock(); + ClusterRoutingTable routingTable = new ClusterRoutingTable( ABSENT_DB_NAME, new FakeClock(), initialRouter ); + + Set readers = new LinkedHashSet<>( asList( reader1, reader2 ) ); + Set writers = new LinkedHashSet<>( singletonList( writer1 ) ); + Set routers = new LinkedHashSet<>( singletonList( router1 ) ); + ClusterComposition clusterComposition = new ClusterComposition( 42, readers, writers, routers ); + Rediscovery rediscovery = mock( RediscoveryImpl.class ); + when( rediscovery.lookupClusterComposition( routingTable, connectionPool ) ) + .thenReturn( completedFuture( clusterComposition ) ); + + RoutingTableHandler handler = new RoutingTableHandler( routingTable, rediscovery, connectionPool, + newRoutingTablesMock(), DEV_NULL_LOGGER ); + + assertNotNull( await( handler.refreshRoutingTable( READ ) ) ); + + verify( rediscovery ).lookupClusterComposition( routingTable, connectionPool ); + assertArrayEquals( new BoltServerAddress[]{reader1, reader2}, routingTable.readers().toArray() ); + assertArrayEquals( new BoltServerAddress[]{writer1}, routingTable.writers().toArray() ); + assertArrayEquals( new BoltServerAddress[]{router1}, routingTable.routers().toArray() ); + } + + @Test + void shouldRediscoverOnReadWhenRoutingTableIsStaleForReads() + { + testRediscoveryWhenStale( READ ); + } + + @Test + void shouldRediscoverOnWriteWhenRoutingTableIsStaleForWrites() + { + testRediscoveryWhenStale( WRITE ); + } + + @Test + void shouldNotRediscoverOnReadWhenRoutingTableIsStaleForWritesButNotReads() + { + testNoRediscoveryWhenNotStale( WRITE, READ ); + } + + @Test + void shouldNotRediscoverOnWriteWhenRoutingTableIsStaleForReadsButNotWrites() + { + testNoRediscoveryWhenNotStale( READ, WRITE ); + } + + @Test + void shouldRetainAllFetchedAddressesInConnectionPoolAfterFetchingOfRoutingTable() + { + RoutingTable routingTable = new ClusterRoutingTable( ABSENT_DB_NAME, new FakeClock() ); + routingTable.update( new ClusterComposition( + 42, asOrderedSet(), asOrderedSet( B, C ), asOrderedSet( D, E ) ) ); + + ConnectionPool connectionPool = newConnectionPoolMock(); + + Rediscovery rediscovery = newRediscoveryMock(); + when( rediscovery.lookupClusterComposition( any(), any() ) ).thenReturn( completedFuture( + new ClusterComposition( 42, asOrderedSet( A, B ), asOrderedSet( B, C ), asOrderedSet( A, C ) ) ) ); + + RoutingTableRegistry routingTables = new RoutingTableRegistry() + { + @Override + public CompletionStage refreshRoutingTable( String databaseName, AccessMode mode ) + { + throw new UnsupportedOperationException(); + } + + @Override + public Set allServers() + { + return routingTable.servers(); + } + + @Override + public void remove( String databaseName ) + { + throw new UnsupportedOperationException(); + } + + @Override + public void purgeAged() + { + } + }; + + RoutingTableHandler handler = new RoutingTableHandler( routingTable, rediscovery, connectionPool, + routingTables, DEV_NULL_LOGGER ); + + RoutingTable actual = await( handler.refreshRoutingTable( READ ) ); + assertEquals( routingTable, actual ); + + verify( connectionPool ).retainAll( new HashSet<>( asList( A, B, C ) ) ); + } + + @Test + void shouldRemoveRoutingTableHandlerIfFailedToLookup() throws Throwable + { + // Given + RoutingTable routingTable = new ClusterRoutingTable( ABSENT_DB_NAME, new FakeClock() ); + + Rediscovery rediscovery = newRediscoveryMock(); + when( rediscovery.lookupClusterComposition( any(), any() ) ).thenReturn( Futures.failedFuture( new RuntimeException( "Bang!" ) ) ); + + ConnectionPool connectionPool = newConnectionPoolMock(); + RoutingTableRegistry routingTables = newRoutingTablesMock(); + // When + + RoutingTableHandler handler = new RoutingTableHandler( routingTable, rediscovery, connectionPool, routingTables, DEV_NULL_LOGGER ); + assertThrows( RuntimeException.class, () -> await( handler.refreshRoutingTable( READ ) ) ); + + // Then + verify( routingTables ).remove( ABSENT_DB_NAME ); + } + + private void testRediscoveryWhenStale( AccessMode mode ) + { + ConnectionPool connectionPool = mock( ConnectionPool.class ); + when( connectionPool.acquire( LOCAL_DEFAULT ) ) + .thenReturn( completedFuture( mock( Connection.class ) ) ); + + RoutingTable routingTable = newStaleRoutingTableMock( mode ); + Rediscovery rediscovery = newRediscoveryMock(); + + RoutingTableHandler handler = new RoutingTableHandler( routingTable, rediscovery, connectionPool, + newRoutingTablesMock(), DEV_NULL_LOGGER ); + RoutingTable actual = await( handler.refreshRoutingTable( mode ) ); + assertEquals( routingTable, actual ); + + verify( routingTable ).isStaleFor( mode ); + verify( rediscovery ).lookupClusterComposition( routingTable, connectionPool ); + } + + private void testNoRediscoveryWhenNotStale( AccessMode staleMode, AccessMode notStaleMode ) + { + ConnectionPool connectionPool = mock( ConnectionPool.class ); + when( connectionPool.acquire( LOCAL_DEFAULT ) ) + .thenReturn( completedFuture( mock( Connection.class ) ) ); + + RoutingTable routingTable = newStaleRoutingTableMock( staleMode ); + Rediscovery rediscovery = newRediscoveryMock(); + + RoutingTableHandler handler = new RoutingTableHandler( routingTable, rediscovery, connectionPool, + newRoutingTablesMock(), DEV_NULL_LOGGER ); + + assertNotNull( await( handler.refreshRoutingTable( notStaleMode ) ) ); + verify( routingTable ).isStaleFor( notStaleMode ); + verify( rediscovery, never() ).lookupClusterComposition( routingTable, connectionPool ); + } + + private static RoutingTable newStaleRoutingTableMock( AccessMode mode ) + { + RoutingTable routingTable = mock( RoutingTable.class ); + when( routingTable.isStaleFor( mode ) ).thenReturn( true ); + + AddressSet addresses = new AddressSet(); + addresses.update( new HashSet<>( singletonList( LOCAL_DEFAULT ) ) ); + when( routingTable.readers() ).thenReturn( addresses ); + when( routingTable.writers() ).thenReturn( addresses ); + + return routingTable; + } + + private static RoutingTableRegistry newRoutingTablesMock() + { + return mock( RoutingTableRegistry.class ); + } + + private static Rediscovery newRediscoveryMock() + { + Rediscovery rediscovery = mock( RediscoveryImpl.class ); + Set noServers = Collections.emptySet(); + ClusterComposition clusterComposition = new ClusterComposition( 1, noServers, noServers, noServers ); + when( rediscovery.lookupClusterComposition( any( RoutingTable.class ), any( ConnectionPool.class ) ) ) + .thenReturn( completedFuture( clusterComposition ) ); + return rediscovery; + } + + private static ConnectionPool newConnectionPoolMock() + { + return newConnectionPoolMockWithFailures( emptySet() ); + } + + private static ConnectionPool newConnectionPoolMockWithFailures( + Set unavailableAddresses ) + { + ConnectionPool pool = mock( ConnectionPool.class ); + when( pool.acquire( any( BoltServerAddress.class ) ) ).then( invocation -> + { + BoltServerAddress requestedAddress = invocation.getArgument( 0 ); + if ( unavailableAddresses.contains( requestedAddress ) ) + { + return Futures.failedFuture( new ServiceUnavailableException( requestedAddress + " is unavailable!" ) ); + } + Connection connection = mock( Connection.class ); + when( connection.serverAddress() ).thenReturn( requestedAddress ); + return completedFuture( connection ); + } ); + return pool; + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/cluster/RoutingTableRegistryImplTest.java b/driver/src/test/java/org/neo4j/driver/internal/cluster/RoutingTableRegistryImplTest.java new file mode 100644 index 0000000000..034d5413ff --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/cluster/RoutingTableRegistryImplTest.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.cluster; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.internal.BoltServerAddress; +import org.neo4j.driver.internal.cluster.RoutingTableRegistryImpl.RoutingTableHandlerFactory; +import org.neo4j.driver.internal.spi.ConnectionPool; +import org.neo4j.driver.internal.util.Clock; + +import static java.util.concurrent.CompletableFuture.completedFuture; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.neo4j.driver.internal.logging.DevNullLogger.DEV_NULL_LOGGER; +import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.ABSENT_DB_NAME; +import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.SYSTEM_DB_NAME; +import static org.neo4j.driver.internal.util.ClusterCompositionUtil.A; +import static org.neo4j.driver.internal.util.ClusterCompositionUtil.B; +import static org.neo4j.driver.internal.util.ClusterCompositionUtil.C; +import static org.neo4j.driver.internal.util.ClusterCompositionUtil.D; +import static org.neo4j.driver.internal.util.ClusterCompositionUtil.E; +import static org.neo4j.driver.internal.util.ClusterCompositionUtil.F; +import static org.neo4j.driver.util.TestUtil.await; + +class RoutingTableRegistryImplTest +{ + @Test + void factoryShouldCreateARoutingTableWithSameDatabaseName() throws Throwable + { + Clock clock = Clock.SYSTEM; + RoutingTableHandlerFactory factory = + new RoutingTableHandlerFactory( mock( ConnectionPool.class ), mock( RediscoveryImpl.class ), clock, DEV_NULL_LOGGER ); + + RoutingTableHandler handler = factory.newInstance( "Molly", null ); + RoutingTable table = handler.routingTable(); + + assertThat( table.database(), equalTo( "Molly" ) ); + + assertThat( table.routers().size(), equalTo( 0 ) ); + assertThat( table.readers().size(), equalTo( 0 ) ); + assertThat( table.writers().size(), equalTo( 0 ) ); + + assertTrue( table.isStaleFor( AccessMode.READ ) ); + assertTrue( table.isStaleFor( AccessMode.WRITE ) ); + } + + @ParameterizedTest + @ValueSource( strings = {ABSENT_DB_NAME, SYSTEM_DB_NAME, "", "database", " molly "} ) + void shouldCreateRoutingTableHandlerIfAbsentWhenFreshRoutingTable( String databaseName ) throws Throwable + { + // Given + ConcurrentMap map = new ConcurrentHashMap<>(); + RoutingTableHandlerFactory factory = mockedHandlerFactory(); + RoutingTableRegistryImpl routingTables = newRoutingTables( map, factory ); + + // When + routingTables.refreshRoutingTable( databaseName, AccessMode.READ ); + + // Then + assertTrue( map.containsKey( databaseName ) ); + verify( factory ).newInstance( eq( databaseName ), eq( routingTables ) ); + } + + @ParameterizedTest + @ValueSource( strings = {ABSENT_DB_NAME, SYSTEM_DB_NAME, "", "database", " molly "} ) + void shouldReturnExistingRoutingTableHandlerWhenFreshRoutingTable( String databaseName ) throws Throwable + { + // Given + ConcurrentMap map = new ConcurrentHashMap<>(); + RoutingTableHandler handler = mockedRoutingTableHandler(); + map.put( databaseName, handler ); + + RoutingTableHandlerFactory factory = mockedHandlerFactory(); + RoutingTableRegistryImpl routingTables = newRoutingTables( map, factory ); + + // When + RoutingTableHandler actual = await( routingTables.refreshRoutingTable( databaseName, AccessMode.READ ) ); + + // Then it is the one we put in map that is picked up. + verify( handler ).refreshRoutingTable( AccessMode.READ ); + // Then it is the one we put in map that is picked up. + assertEquals( handler, actual ); + } + + @ParameterizedTest + @EnumSource( AccessMode.class ) + void shouldReturnFreshRoutingTable( AccessMode mode ) throws Throwable + { + // Given + ConcurrentMap map = new ConcurrentHashMap<>(); + RoutingTableHandler handler = mockedRoutingTableHandler(); + RoutingTableHandlerFactory factory = mockedHandlerFactory( handler ); + RoutingTableRegistryImpl routingTables = new RoutingTableRegistryImpl( map, factory, DEV_NULL_LOGGER ); + + // When + routingTables.refreshRoutingTable( ABSENT_DB_NAME, mode ); + + // Then + verify( handler ).refreshRoutingTable( mode ); + } + + @Test + void shouldReturnServersInAllRoutingTables() throws Throwable + { + // Given + ConcurrentMap map = new ConcurrentHashMap<>(); + map.put( "Apple", mockedRoutingTableHandler( A, B, C ) ); + map.put( "Banana", mockedRoutingTableHandler( B, C, D ) ); + map.put( "Orange", mockedRoutingTableHandler( E, F, C ) ); + RoutingTableHandlerFactory factory = mockedHandlerFactory(); + RoutingTableRegistryImpl routingTables = new RoutingTableRegistryImpl( map, factory, DEV_NULL_LOGGER ); + + // When + Set servers = routingTables.allServers(); + + // Then + assertThat( servers, containsInAnyOrder( A, B, C, D, E, F ) ); + } + + @Test + void shouldRemoveRoutingTableHandler() throws Throwable + { + // Given + ConcurrentMap map = new ConcurrentHashMap<>(); + map.put( "Apple", mockedRoutingTableHandler( A ) ); + map.put( "Banana", mockedRoutingTableHandler( B ) ); + map.put( "Orange", mockedRoutingTableHandler( C ) ); + + RoutingTableHandlerFactory factory = mockedHandlerFactory(); + RoutingTableRegistryImpl routingTables = newRoutingTables( map, factory ); + + // When + routingTables.remove( "Apple" ); + routingTables.remove( "Banana" ); + // Then + assertThat( routingTables.allServers(), contains( C ) ); + } + + @Test + void shouldRemoveStatleRoutingTableHandlers() throws Throwable + { + ConcurrentMap map = new ConcurrentHashMap<>(); + map.put( "Apple", mockedRoutingTableHandler( A ) ); + map.put( "Banana", mockedRoutingTableHandler( B ) ); + map.put( "Orange", mockedRoutingTableHandler( C ) ); + + RoutingTableHandlerFactory factory = mockedHandlerFactory(); + RoutingTableRegistryImpl routingTables = newRoutingTables( map, factory ); + + // When + routingTables.purgeAged(); + // Then + assertThat( routingTables.allServers(), empty() ); + } + + private RoutingTableHandler mockedRoutingTableHandler( BoltServerAddress... servers ) + { + RoutingTableHandler handler = mock( RoutingTableHandler.class ); + when( handler.servers() ).thenReturn( new HashSet<>( Arrays.asList( servers ) ) ); + when( handler.isRoutingTableAged() ).thenReturn( true ); + return handler; + } + + private RoutingTableRegistryImpl newRoutingTables( ConcurrentMap handlers, RoutingTableHandlerFactory factory ) + { + return new RoutingTableRegistryImpl( handlers, factory, DEV_NULL_LOGGER ); + } + + private RoutingTableHandlerFactory mockedHandlerFactory( RoutingTableHandler handler ) + { + RoutingTableHandlerFactory factory = mock( RoutingTableHandlerFactory.class ); + when( factory.newInstance( any(), any() ) ).thenReturn( handler ); + return factory; + } + + private RoutingTableHandlerFactory mockedHandlerFactory() + { + return mockedHandlerFactory( mockedRoutingTableHandler() ); + } + + private RoutingTableHandler mockedRoutingTableHandler() + { + RoutingTableHandler handler = mock( RoutingTableHandler.class ); + when( handler.refreshRoutingTable( any() ) ).thenReturn( completedFuture( mock( RoutingTable.class ) ) ); + return handler; + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/cluster/loadbalancing/LoadBalancerTest.java b/driver/src/test/java/org/neo4j/driver/internal/cluster/loadbalancing/LoadBalancerTest.java index 38ca2bbc6c..7d170a7c5a 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/cluster/loadbalancing/LoadBalancerTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/cluster/loadbalancing/LoadBalancerTest.java @@ -24,29 +24,30 @@ import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; -import java.util.Collections; +import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.exceptions.ServiceUnavailableException; +import org.neo4j.driver.exceptions.SessionExpiredException; import org.neo4j.driver.internal.BoltServerAddress; -import org.neo4j.driver.internal.async.connection.DecoratedConnection; +import org.neo4j.driver.internal.async.connection.RoutingConnection; import org.neo4j.driver.internal.cluster.AddressSet; import org.neo4j.driver.internal.cluster.ClusterComposition; import org.neo4j.driver.internal.cluster.ClusterRoutingTable; -import org.neo4j.driver.internal.cluster.Rediscovery; import org.neo4j.driver.internal.cluster.RoutingTable; +import org.neo4j.driver.internal.cluster.RoutingTableHandler; +import org.neo4j.driver.internal.cluster.RoutingTableRegistry; import org.neo4j.driver.internal.spi.Connection; import org.neo4j.driver.internal.spi.ConnectionPool; import org.neo4j.driver.internal.util.FakeClock; import org.neo4j.driver.internal.util.Futures; -import org.neo4j.driver.AccessMode; -import org.neo4j.driver.exceptions.ServiceUnavailableException; -import org.neo4j.driver.exceptions.SessionExpiredException; import static java.util.Arrays.asList; import static java.util.Collections.emptySet; -import static java.util.Collections.singletonList; import static java.util.concurrent.CompletableFuture.completedFuture; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; @@ -59,20 +60,16 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.neo4j.driver.internal.BoltServerAddress.LOCAL_DEFAULT; +import static org.neo4j.driver.AccessMode.READ; +import static org.neo4j.driver.AccessMode.WRITE; +import static org.neo4j.driver.internal.logging.DevNullLogger.DEV_NULL_LOGGER; +import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.ABSENT_DB_NAME; import static org.neo4j.driver.internal.util.ClusterCompositionUtil.A; import static org.neo4j.driver.internal.util.ClusterCompositionUtil.B; import static org.neo4j.driver.internal.util.ClusterCompositionUtil.C; -import static org.neo4j.driver.internal.util.ClusterCompositionUtil.D; -import static org.neo4j.driver.internal.util.ClusterCompositionUtil.E; -import static org.neo4j.driver.internal.util.ClusterCompositionUtil.F; -import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; -import static org.neo4j.driver.AccessMode.READ; -import static org.neo4j.driver.AccessMode.WRITE; import static org.neo4j.driver.util.TestUtil.asOrderedSet; import static org.neo4j.driver.util.TestUtil.await; @@ -90,14 +87,12 @@ void returnsCorrectAccessMode( AccessMode mode ) when( writerAddresses.toArray() ).thenReturn( new BoltServerAddress[]{B} ); when( routingTable.readers() ).thenReturn( readerAddresses ); when( routingTable.writers() ).thenReturn( writerAddresses ); - Rediscovery rediscovery = mock( Rediscovery.class ); - LoadBalancer loadBalancer = new LoadBalancer( connectionPool, routingTable, rediscovery, - GlobalEventExecutor.INSTANCE, DEV_NULL_LOGGING ); + LoadBalancer loadBalancer = newLoadBalancer( connectionPool, routingTable ); Connection acquired = await( loadBalancer.acquireConnection( ABSENT_DB_NAME, mode ) ); - assertThat( acquired, instanceOf( DecoratedConnection.class ) ); + assertThat( acquired, instanceOf( RoutingConnection.class ) ); assertThat( acquired.mode(), equalTo( mode ) ); } @@ -110,87 +105,25 @@ void returnsCorrectDatabaseName( String databaseName ) AddressSet readerAddresses = mock( AddressSet.class ); when( readerAddresses.toArray() ).thenReturn( new BoltServerAddress[]{A} ); when( routingTable.readers() ).thenReturn( readerAddresses ); - Rediscovery rediscovery = mock( Rediscovery.class ); - LoadBalancer loadBalancer = new LoadBalancer( connectionPool, routingTable, rediscovery, GlobalEventExecutor.INSTANCE, DEV_NULL_LOGGING ); + LoadBalancer loadBalancer = newLoadBalancer( connectionPool, routingTable ); Connection acquired = await( loadBalancer.acquireConnection( databaseName, READ ) ); - assertThat( acquired, instanceOf( DecoratedConnection.class ) ); + assertThat( acquired, instanceOf( RoutingConnection.class ) ); assertThat( acquired.databaseName(), equalTo( databaseName ) ); verify( connectionPool ).acquire( A ); } - @Test - void acquireShouldUpdateRoutingTableWhenKnownRoutingTableIsStale() - { - BoltServerAddress initialRouter = new BoltServerAddress( "initialRouter", 1 ); - BoltServerAddress reader1 = new BoltServerAddress( "reader-1", 2 ); - BoltServerAddress reader2 = new BoltServerAddress( "reader-1", 3 ); - BoltServerAddress writer1 = new BoltServerAddress( "writer-1", 4 ); - BoltServerAddress router1 = new BoltServerAddress( "router-1", 5 ); - - ConnectionPool connectionPool = newConnectionPoolMock(); - ClusterRoutingTable routingTable = new ClusterRoutingTable( new FakeClock(), initialRouter ); - - Set readers = new LinkedHashSet<>( asList( reader1, reader2 ) ); - Set writers = new LinkedHashSet<>( singletonList( writer1 ) ); - Set routers = new LinkedHashSet<>( singletonList( router1 ) ); - ClusterComposition clusterComposition = new ClusterComposition( 42, readers, writers, routers ); - Rediscovery rediscovery = mock( Rediscovery.class ); - when( rediscovery.lookupClusterComposition( routingTable, connectionPool ) ) - .thenReturn( completedFuture( clusterComposition ) ); - - LoadBalancer loadBalancer = new LoadBalancer( connectionPool, routingTable, rediscovery, - GlobalEventExecutor.INSTANCE, DEV_NULL_LOGGING ); - - assertNotNull( await( loadBalancer.acquireConnection( ABSENT_DB_NAME, READ ) ) ); - - verify( rediscovery ).lookupClusterComposition( routingTable, connectionPool ); - assertArrayEquals( new BoltServerAddress[]{reader1, reader2}, routingTable.readers().toArray() ); - assertArrayEquals( new BoltServerAddress[]{writer1}, routingTable.writers().toArray() ); - assertArrayEquals( new BoltServerAddress[]{router1}, routingTable.routers().toArray() ); - } - - @Test - void shouldRediscoverOnReadWhenRoutingTableIsStaleForReads() - { - testRediscoveryWhenStale( READ ); - } - - @Test - void shouldRediscoverOnWriteWhenRoutingTableIsStaleForWrites() - { - testRediscoveryWhenStale( WRITE ); - } - - @Test - void shouldNotRediscoverOnReadWhenRoutingTableIsStaleForWritesButNotReads() - { - testNoRediscoveryWhenNotStale( WRITE, READ ); - } - - @Test - void shouldNotRediscoverOnWriteWhenRoutingTableIsStaleForReadsButNotWrites() - { - testNoRediscoveryWhenNotStale( READ, WRITE ); - } - @Test void shouldThrowWhenRediscoveryReturnsNoSuitableServers() { ConnectionPool connectionPool = newConnectionPoolMock(); RoutingTable routingTable = mock( RoutingTable.class ); - when( routingTable.isStaleFor( any( AccessMode.class ) ) ).thenReturn( true ); - Rediscovery rediscovery = mock( Rediscovery.class ); - ClusterComposition emptyClusterComposition = new ClusterComposition( 42, emptySet(), emptySet(), emptySet() ); - when( rediscovery.lookupClusterComposition( routingTable, connectionPool ) ) - .thenReturn( completedFuture( emptyClusterComposition ) ); when( routingTable.readers() ).thenReturn( new AddressSet() ); when( routingTable.writers() ).thenReturn( new AddressSet() ); - LoadBalancer loadBalancer = new LoadBalancer( connectionPool, routingTable, rediscovery, - GlobalEventExecutor.INSTANCE, DEV_NULL_LOGGING ); + LoadBalancer loadBalancer = newLoadBalancer( connectionPool, routingTable ); SessionExpiredException error1 = assertThrows( SessionExpiredException.class, () -> await( loadBalancer.acquireConnection( ABSENT_DB_NAME, READ ) ) ); assertThat( error1.getMessage(), startsWith( "Failed to obtain connection towards READ server" ) ); @@ -213,10 +146,8 @@ void shouldSelectLeastConnectedAddress() when( readerAddresses.toArray() ).thenReturn( new BoltServerAddress[]{A, B, C} ); when( routingTable.readers() ).thenReturn( readerAddresses ); - Rediscovery rediscovery = mock( Rediscovery.class ); - LoadBalancer loadBalancer = new LoadBalancer( connectionPool, routingTable, rediscovery, - GlobalEventExecutor.INSTANCE, DEV_NULL_LOGGING ); + LoadBalancer loadBalancer = newLoadBalancer( connectionPool, routingTable ); Set seenAddresses = new HashSet<>(); for ( int i = 0; i < 10; i++ ) @@ -240,10 +171,7 @@ void shouldRoundRobinWhenNoActiveConnections() when( readerAddresses.toArray() ).thenReturn( new BoltServerAddress[]{A, B, C} ); when( routingTable.readers() ).thenReturn( readerAddresses ); - Rediscovery rediscovery = mock( Rediscovery.class ); - - LoadBalancer loadBalancer = new LoadBalancer( connectionPool, routingTable, rediscovery, - GlobalEventExecutor.INSTANCE, DEV_NULL_LOGGING ); + LoadBalancer loadBalancer = newLoadBalancer( connectionPool, routingTable ); Set seenAddresses = new HashSet<>(); for ( int i = 0; i < 10; i++ ) @@ -262,15 +190,10 @@ void shouldTryMultipleServersAfterRediscovery() Set unavailableAddresses = asOrderedSet( A ); ConnectionPool connectionPool = newConnectionPoolMockWithFailures( unavailableAddresses ); - ClusterRoutingTable routingTable = new ClusterRoutingTable( new FakeClock(), A ); - Rediscovery rediscovery = mock( Rediscovery.class ); - ClusterComposition clusterComposition = new ClusterComposition( 42, - asOrderedSet( A, B ), asOrderedSet( A, B ), asOrderedSet( A, B ) ); - when( rediscovery.lookupClusterComposition( any(), any() ) ) - .thenReturn( completedFuture( clusterComposition ) ); + RoutingTable routingTable = new ClusterRoutingTable( ABSENT_DB_NAME, new FakeClock() ); + routingTable.update( new ClusterComposition( -1, new LinkedHashSet<>( Arrays.asList( A, B ) ), emptySet(), emptySet() ) ); - LoadBalancer loadBalancer = new LoadBalancer( connectionPool, routingTable, rediscovery, - GlobalEventExecutor.INSTANCE, DEV_NULL_LOGGING ); + LoadBalancer loadBalancer = newLoadBalancer( connectionPool, routingTable ); Connection connection = await( loadBalancer.acquireConnection( ABSENT_DB_NAME, READ ) ); @@ -280,109 +203,6 @@ void shouldTryMultipleServersAfterRediscovery() assertArrayEquals( new BoltServerAddress[]{B}, routingTable.readers().toArray() ); } - @Test - void shouldRemoveAddressFromRoutingTableOnConnectionFailure() - { - RoutingTable routingTable = new ClusterRoutingTable( new FakeClock() ); - routingTable.update( new ClusterComposition( - 42, asOrderedSet( A, B, C ), asOrderedSet( A, C, E ), asOrderedSet( B, D, F ) ) ); - - LoadBalancer loadBalancer = new LoadBalancer( newConnectionPoolMock(), routingTable, newRediscoveryMock(), - GlobalEventExecutor.INSTANCE, DEV_NULL_LOGGING ); - - loadBalancer.onConnectionFailure( B ); - - assertArrayEquals( new BoltServerAddress[]{A, C}, routingTable.readers().toArray() ); - assertArrayEquals( new BoltServerAddress[]{A, C, E}, routingTable.writers().toArray() ); - assertArrayEquals( new BoltServerAddress[]{D, F}, routingTable.routers().toArray() ); - - loadBalancer.onConnectionFailure( A ); - - assertArrayEquals( new BoltServerAddress[]{C}, routingTable.readers().toArray() ); - assertArrayEquals( new BoltServerAddress[]{C, E}, routingTable.writers().toArray() ); - assertArrayEquals( new BoltServerAddress[]{D, F}, routingTable.routers().toArray() ); - } - - @Test - void shouldRetainAllFetchedAddressesInConnectionPoolAfterFetchingOfRoutingTable() - { - RoutingTable routingTable = new ClusterRoutingTable( new FakeClock() ); - routingTable.update( new ClusterComposition( - 42, asOrderedSet(), asOrderedSet( B, C ), asOrderedSet( D, E ) ) ); - - ConnectionPool connectionPool = newConnectionPoolMock(); - - Rediscovery rediscovery = newRediscoveryMock(); - when( rediscovery.lookupClusterComposition( any(), any() ) ).thenReturn( completedFuture( - new ClusterComposition( 42, asOrderedSet( A, B ), asOrderedSet( B, C ), asOrderedSet( A, C ) ) ) ); - - LoadBalancer loadBalancer = new LoadBalancer( connectionPool, routingTable, rediscovery, - GlobalEventExecutor.INSTANCE, DEV_NULL_LOGGING ); - - Connection connection = await( loadBalancer.acquireConnection( ABSENT_DB_NAME, READ ) ); - assertNotNull( connection ); - - verify( connectionPool ).retainAll( new HashSet<>( asList( A, B, C ) ) ); - } - - private void testRediscoveryWhenStale( AccessMode mode ) - { - ConnectionPool connectionPool = mock( ConnectionPool.class ); - when( connectionPool.acquire( LOCAL_DEFAULT ) ) - .thenReturn( completedFuture( mock( Connection.class ) ) ); - - RoutingTable routingTable = newStaleRoutingTableMock( mode ); - Rediscovery rediscovery = newRediscoveryMock(); - - LoadBalancer loadBalancer = new LoadBalancer( connectionPool, routingTable, rediscovery, - GlobalEventExecutor.INSTANCE, DEV_NULL_LOGGING ); - Connection connection = await( loadBalancer.acquireConnection( ABSENT_DB_NAME, mode ) ); - assertNotNull( connection ); - - verify( routingTable ).isStaleFor( mode ); - verify( rediscovery ).lookupClusterComposition( routingTable, connectionPool ); - } - - private void testNoRediscoveryWhenNotStale( AccessMode staleMode, AccessMode notStaleMode ) - { - ConnectionPool connectionPool = mock( ConnectionPool.class ); - when( connectionPool.acquire( LOCAL_DEFAULT ) ) - .thenReturn( completedFuture( mock( Connection.class ) ) ); - - RoutingTable routingTable = newStaleRoutingTableMock( staleMode ); - Rediscovery rediscovery = newRediscoveryMock(); - - LoadBalancer loadBalancer = new LoadBalancer( connectionPool, routingTable, rediscovery, - GlobalEventExecutor.INSTANCE, DEV_NULL_LOGGING ); - - assertNotNull( await( loadBalancer.acquireConnection( ABSENT_DB_NAME, notStaleMode ) ) ); - verify( routingTable ).isStaleFor( notStaleMode ); - verify( rediscovery, never() ).lookupClusterComposition( routingTable, connectionPool ); - } - - private static RoutingTable newStaleRoutingTableMock( AccessMode mode ) - { - RoutingTable routingTable = mock( RoutingTable.class ); - when( routingTable.isStaleFor( mode ) ).thenReturn( true ); - - AddressSet addresses = new AddressSet(); - addresses.update( new HashSet<>( singletonList( LOCAL_DEFAULT ) ) ); - when( routingTable.readers() ).thenReturn( addresses ); - when( routingTable.writers() ).thenReturn( addresses ); - - return routingTable; - } - - private static Rediscovery newRediscoveryMock() - { - Rediscovery rediscovery = mock( Rediscovery.class ); - Set noServers = Collections.emptySet(); - ClusterComposition clusterComposition = new ClusterComposition( 1, noServers, noServers, noServers ); - when( rediscovery.lookupClusterComposition( any( RoutingTable.class ), any( ConnectionPool.class ) ) ) - .thenReturn( completedFuture( clusterComposition ) ); - return rediscovery; - } - private static ConnectionPool newConnectionPoolMock() { return newConnectionPoolMockWithFailures( emptySet() ); @@ -405,4 +225,16 @@ private static ConnectionPool newConnectionPoolMockWithFailures( } ); return pool; } + + private static LoadBalancer newLoadBalancer( ConnectionPool connectionPool, RoutingTable routingTable ) + { + // Used only in testing + RoutingTableRegistry routingTables = mock( RoutingTableRegistry.class ); + RoutingTableHandler handler = mock( RoutingTableHandler.class ); + when( handler.routingTable() ).thenReturn( routingTable ); + when( routingTables.refreshRoutingTable( any( String.class ), any( AccessMode.class ) ) ).thenReturn( CompletableFuture.completedFuture( handler ) ); + return new LoadBalancer( connectionPool, routingTables, DEV_NULL_LOGGER, new LeastConnectedLoadBalancingStrategy( connectionPool, DEV_NULL_LOGGING ), + GlobalEventExecutor.INSTANCE ); + } + } diff --git a/driver/src/test/java/org/neo4j/driver/internal/cluster/loadbalancing/RoutingTableAndConnectionPoolTest.java b/driver/src/test/java/org/neo4j/driver/internal/cluster/loadbalancing/RoutingTableAndConnectionPoolTest.java new file mode 100644 index 0000000000..7487ff4620 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/cluster/loadbalancing/RoutingTableAndConnectionPoolTest.java @@ -0,0 +1,498 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.cluster.loadbalancing; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelPromise; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.util.AttributeKey; +import io.netty.util.concurrent.GlobalEventExecutor; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.Logging; +import org.neo4j.driver.exceptions.FatalDiscoveryException; +import org.neo4j.driver.exceptions.ProtocolException; +import org.neo4j.driver.internal.BoltServerAddress; +import org.neo4j.driver.internal.async.connection.BootstrapFactory; +import org.neo4j.driver.internal.async.connection.ChannelConnector; +import org.neo4j.driver.internal.async.pool.ConnectionFactory; +import org.neo4j.driver.internal.async.pool.ConnectionPoolImpl; +import org.neo4j.driver.internal.async.pool.ExtendedChannelPool; +import org.neo4j.driver.internal.async.pool.NettyChannelTracker; +import org.neo4j.driver.internal.async.pool.PoolSettings; +import org.neo4j.driver.internal.cluster.ClusterComposition; +import org.neo4j.driver.internal.cluster.Rediscovery; +import org.neo4j.driver.internal.cluster.RoutingTable; +import org.neo4j.driver.internal.cluster.RoutingTableRegistry; +import org.neo4j.driver.internal.cluster.RoutingTableRegistryImpl; +import org.neo4j.driver.internal.messaging.BoltProtocol; +import org.neo4j.driver.internal.messaging.Message; +import org.neo4j.driver.internal.metrics.InternalAbstractMetrics; +import org.neo4j.driver.internal.spi.Connection; +import org.neo4j.driver.internal.spi.ConnectionPool; +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.internal.util.Clock; +import org.neo4j.driver.internal.util.Futures; +import org.neo4j.driver.internal.util.ServerVersion; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.junit.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.neo4j.driver.Logging.none; +import static org.neo4j.driver.internal.async.connection.ChannelAttributes.setServerAddress; +import static org.neo4j.driver.internal.cluster.RoutingTableHandler.STALE_ROUTING_TABLE_PURGE_TIMEOUT; +import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.ABSENT_DB_NAME; +import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.SYSTEM_DB_NAME; +import static org.neo4j.driver.internal.metrics.InternalAbstractMetrics.DEV_NULL_METRICS; +import static org.neo4j.driver.util.TestUtil.await; + +class RoutingTableAndConnectionPoolTest +{ + private static final BoltServerAddress A = new BoltServerAddress( "localhost:30000" ); + private static final BoltServerAddress B = new BoltServerAddress( "localhost:30001" ); + private static final BoltServerAddress C = new BoltServerAddress( "localhost:30002" ); + private static final BoltServerAddress D = new BoltServerAddress( "localhost:30003" ); + private static final BoltServerAddress E = new BoltServerAddress( "localhost:30004" ); + private static final BoltServerAddress F = new BoltServerAddress( "localhost:30005" ); + private static final List SERVERS = new LinkedList<>( Arrays.asList( null, A, B, C, D, E, F ) ); + + private static final String[] DATABASES = new String[]{"", ABSENT_DB_NAME, SYSTEM_DB_NAME, "my database"}; + + private final Random random = new Random(); + private final Clock clock = Clock.SYSTEM; + private final Logging logging = none(); + + @Test + void shouldAddServerToRoutingTableAndConnectionPool() throws Throwable + { + // Given + ConnectionPool connectionPool = newConnectionPool(); + Rediscovery rediscovery = mock( Rediscovery.class ); + when( rediscovery.lookupClusterComposition( any(), any() ) ).thenReturn( clusterComposition( A ) ); + RoutingTableRegistryImpl routingTables = newRoutingTables( connectionPool, rediscovery ); + LoadBalancer loadBalancer = newLoadBalancer( connectionPool, routingTables ); + + // When + await( loadBalancer.acquireConnection( "neo4j", AccessMode.WRITE ) ); + + // Then + assertThat( routingTables.allServers().size(), equalTo( 1 ) ); + assertTrue( routingTables.allServers().contains( A ) ); + assertTrue( routingTables.contains( "neo4j" ) ); + assertTrue( connectionPool.isOpen( A ) ); + } + + @Test + void shouldNotAddToRoutingTableWhenFailedWithRoutingError() throws Throwable + { + // Given + ConnectionPool connectionPool = newConnectionPool(); + Rediscovery rediscovery = mock( Rediscovery.class ); + when( rediscovery.lookupClusterComposition( any(), any() ) ).thenReturn( Futures.failedFuture( new FatalDiscoveryException( "No database found" ) ) ); + RoutingTableRegistryImpl routingTables = newRoutingTables( connectionPool, rediscovery ); + LoadBalancer loadBalancer = newLoadBalancer( connectionPool, routingTables ); + + // When + assertThrows( FatalDiscoveryException.class, () -> await( loadBalancer.acquireConnection( "neo4j", AccessMode.WRITE ) ) ); + + // Then + assertTrue( routingTables.allServers().isEmpty() ); + assertFalse( routingTables.contains( "neo4j" ) ); + assertFalse( connectionPool.isOpen( A ) ); + } + + @Test + void shouldNotAddToRoutingTableWhenFailedWithProtocolError() throws Throwable + { + // Given + ConnectionPool connectionPool = newConnectionPool(); + Rediscovery rediscovery = mock( Rediscovery.class ); + when( rediscovery.lookupClusterComposition( any(), any() ) ).thenReturn( Futures.failedFuture( new ProtocolException( "No database found" ) ) ); + RoutingTableRegistryImpl routingTables = newRoutingTables( connectionPool, rediscovery ); + LoadBalancer loadBalancer = newLoadBalancer( connectionPool, routingTables ); + + // When + assertThrows( ProtocolException.class, () -> await( loadBalancer.acquireConnection( "neo4j", AccessMode.WRITE ) ) ); + + // Then + assertTrue( routingTables.allServers().isEmpty() ); + assertFalse( routingTables.contains( "neo4j" ) ); + assertFalse( connectionPool.isOpen( A ) ); + } + + @Test + void shouldNotAddToRoutingTableWhenFailedWithSecurityError() throws Throwable + { + // Given + ConnectionPool connectionPool = newConnectionPool(); + Rediscovery rediscovery = mock( Rediscovery.class ); + when( rediscovery.lookupClusterComposition( any(), any() ) ).thenReturn( Futures.failedFuture( new SecurityException( "No database found" ) ) ); + RoutingTableRegistryImpl routingTables = newRoutingTables( connectionPool, rediscovery ); + LoadBalancer loadBalancer = newLoadBalancer( connectionPool, routingTables ); + + // When + assertThrows( SecurityException.class, () -> await( loadBalancer.acquireConnection( "neo4j", AccessMode.WRITE ) ) ); + + // Then + assertTrue( routingTables.allServers().isEmpty() ); + assertFalse( routingTables.contains( "neo4j" ) ); + assertFalse( connectionPool.isOpen( A ) ); + } + + @Test + void shouldNotRemoveNewlyAddedRoutingTableEvenIfItIsExpired() throws Throwable + { + // Given + ConnectionPool connectionPool = newConnectionPool(); + Rediscovery rediscovery = mock( Rediscovery.class ); + when( rediscovery.lookupClusterComposition( any(), any() ) ).thenReturn( expiredClusterComposition( A ) ); + RoutingTableRegistryImpl routingTables = newRoutingTables( connectionPool, rediscovery ); + LoadBalancer loadBalancer = newLoadBalancer( connectionPool, routingTables ); + + // When + Connection connection = await( loadBalancer.acquireConnection( "neo4j", AccessMode.WRITE ) ); + await( connection.release() ); + + // Then + assertTrue( routingTables.contains( "neo4j" ) ); + + assertThat( routingTables.allServers().size(), equalTo( 1 ) ); + assertTrue( routingTables.allServers().contains( A ) ); + + assertTrue( connectionPool.isOpen( A ) ); + } + + @Test + void shouldRemoveExpiredRoutingTableAndServers() throws Throwable + { + // Given + ConnectionPool connectionPool = newConnectionPool(); + Rediscovery rediscovery = mock( Rediscovery.class ); + when( rediscovery.lookupClusterComposition( any(), any() ) ).thenReturn( expiredClusterComposition( A ) ).thenReturn( clusterComposition( B ) ); + RoutingTableRegistryImpl routingTables = newRoutingTables( connectionPool, rediscovery ); + LoadBalancer loadBalancer = newLoadBalancer( connectionPool, routingTables ); + + // When + Connection connection = await( loadBalancer.acquireConnection( "neo4j", AccessMode.WRITE ) ); + await( connection.release() ); + await( loadBalancer.acquireConnection( "foo", AccessMode.WRITE ) ); + + // Then + assertFalse( routingTables.contains( "neo4j" ) ); + assertTrue( routingTables.contains( "foo" ) ); + + assertThat( routingTables.allServers().size(), equalTo( 1 ) ); + assertTrue( routingTables.allServers().contains( B ) ); + + assertTrue( connectionPool.isOpen( B ) ); + } + + @Test + void shouldRemoveExpiredRoutingTableButNotServer() throws Throwable + { + // Given + ConnectionPool connectionPool = newConnectionPool(); + Rediscovery rediscovery = mock( Rediscovery.class ); + when( rediscovery.lookupClusterComposition( any(), any() ) ).thenReturn( expiredClusterComposition( A ) ).thenReturn( clusterComposition( B ) ); + RoutingTableRegistryImpl routingTables = newRoutingTables( connectionPool, rediscovery ); + LoadBalancer loadBalancer = newLoadBalancer( connectionPool, routingTables ); + + // When + await( loadBalancer.acquireConnection( "neo4j", AccessMode.WRITE ) ); + await( loadBalancer.acquireConnection( "foo", AccessMode.WRITE ) ); + + // Then + assertThat( routingTables.allServers().size(), equalTo( 1 ) ); + assertTrue( routingTables.allServers().contains( B ) ); + assertTrue( connectionPool.isOpen( B ) ); + assertFalse( routingTables.contains( "neo4j" ) ); + assertTrue( routingTables.contains( "foo" ) ); + + // I still have A as A's connection is in use + assertTrue( connectionPool.isOpen( A ) ); + } + + @Test + void shouldHandleAddAndRemoveFromRoutingTableAndConnectionPool() throws Throwable + { + // Given + ConnectionPool connectionPool = newConnectionPool(); + Rediscovery rediscovery = new RandomizedRediscovery(); + RoutingTableRegistry routingTables = newRoutingTables( connectionPool, rediscovery ); + LoadBalancer loadBalancer = newLoadBalancer( connectionPool, routingTables ); + + // When + acquireAndReleaseConnections( loadBalancer ); + Set servers = routingTables.allServers(); + BoltServerAddress openServer = null; + for( BoltServerAddress server: servers ) + { + if ( connectionPool.isOpen( server ) ) + { + openServer = server; + break; + } + } + assertNotNull( servers ); + + // if we remove the open server from servers, then the connection pool should remove the server from the pool. + SERVERS.remove( openServer ); + acquireAndReleaseConnections( loadBalancer ); + + assertFalse( connectionPool.isOpen( openServer ) ); + } + + private void acquireAndReleaseConnections( LoadBalancer loadBalancer ) throws InterruptedException + { + ExecutorService executorService = Executors.newFixedThreadPool( 4 ); + int count = 100; + Future[] futures = new Future[count]; + + for ( int i = 0; i < count; i++ ) + { + Future future = executorService.submit( () -> { + int index = random.nextInt( DATABASES.length ); + CompletionStage task = loadBalancer.acquireConnection( DATABASES[index], AccessMode.WRITE ).thenCompose( Connection::release ); + await( task ); + } ); + futures[i] = future; + } + + executorService.shutdown(); + executorService.awaitTermination( 10, TimeUnit.SECONDS ); + + List errors = new ArrayList<>(); + for ( Future f : futures ) + { + try + { + f.get(); + } + catch ( ExecutionException e ) + { + errors.add( e.getCause() ); + } + } + + // Then + assertThat( errors.size(), equalTo( 0 ) ); + } + + private ChannelFuture newChannelFuture( BoltServerAddress address ) + { + EmbeddedChannel channel = new EmbeddedChannel(); + ChannelPromise channelPromise = channel.newPromise(); + channelPromise.setSuccess(); + setServerAddress( channel, address ); + return channelPromise; + } + + private ConnectionPool newConnectionPool() + { + InternalAbstractMetrics metrics = DEV_NULL_METRICS; + PoolSettings poolSettings = new PoolSettings( 10, 5000, -1, -1 ); + + ChannelConnector connector = ( address, bootstrap ) -> newChannelFuture( address ); + Bootstrap bootstrap = BootstrapFactory.newBootstrap(); + + NettyChannelTracker channelTracker = new NettyChannelTracker( metrics, bootstrap.config().group().next(), logging ); + + ConnectionFactory connectionFactory = PooledConnection::new; + return new ConnectionPoolImpl( connector, bootstrap, channelTracker, poolSettings, metrics, logging, clock, true, connectionFactory ); + } + + private RoutingTableRegistryImpl newRoutingTables( ConnectionPool connectionPool, Rediscovery rediscovery ) + { + return new RoutingTableRegistryImpl( connectionPool, rediscovery, clock, logging.getLog( "RT" ) ); + } + + private LoadBalancer newLoadBalancer( ConnectionPool connectionPool, RoutingTableRegistry routingTables ) + { + return new LoadBalancer( connectionPool, routingTables, logging.getLog( "LB" ), new LeastConnectedLoadBalancingStrategy( connectionPool, logging ), + GlobalEventExecutor.INSTANCE ); + } + + private CompletableFuture clusterComposition( BoltServerAddress... addresses ) + { + return clusterComposition( Duration.ofSeconds( 30 ).toMillis(), addresses ); + } + + private CompletableFuture expiredClusterComposition( BoltServerAddress... addresses ) + { + return clusterComposition( -STALE_ROUTING_TABLE_PURGE_TIMEOUT.toMillis() - 1, addresses ); + } + + private CompletableFuture clusterComposition( long expireAfterMs, BoltServerAddress... addresses ) + { + HashSet servers = new HashSet<>( Arrays.asList( addresses ) ); + ClusterComposition composition = new ClusterComposition( clock.millis() + expireAfterMs, servers, servers, servers ); + return CompletableFuture.completedFuture( composition ); + } + + private class RandomizedRediscovery implements Rediscovery + { + @Override + public CompletionStage lookupClusterComposition( RoutingTable routingTable, ConnectionPool connectionPool ) + { + // when looking up a new routing table, we return a valid random routing table back + Set servers = new HashSet<>(); + for ( int i = 0; i < 3; i++ ) + { + int index = random.nextInt( SERVERS.size() ); + BoltServerAddress server = SERVERS.get( index ); + if ( server != null ) + { + servers.add( server ); + } + } + if ( servers.size() == 0 ) + { + servers.add( A ); + } + ClusterComposition composition = new ClusterComposition( clock.millis() + 1, servers, servers, servers ); + return CompletableFuture.completedFuture( composition ); + } + } + + // This connection can be acquired from a connection pool and/or released back to it. + private static class PooledConnection implements Connection + { + private final Channel channel; + private final ExtendedChannelPool pool; + + PooledConnection( Channel channel, ExtendedChannelPool pool ) + { + this.channel = channel; + this.pool = pool; + + this.channel.attr( AttributeKey.valueOf( "channelPool" ) ).setIfAbsent( pool ); + } + + @Override + public boolean isOpen() + { + return false; + } + + @Override + public void enableAutoRead() + { + + } + + @Override + public void disableAutoRead() + { + + } + + @Override + public void write( Message message, ResponseHandler handler ) + { + + } + + @Override + public void write( Message message1, ResponseHandler handler1, Message message2, ResponseHandler handler2 ) + { + + } + + @Override + public void writeAndFlush( Message message, ResponseHandler handler ) + { + + } + + @Override + public void writeAndFlush( Message message1, ResponseHandler handler1, Message message2, ResponseHandler handler2 ) + { + + } + + @Override + public CompletionStage reset() + { + return Futures.completedWithNull(); + } + + @Override + public CompletionStage release() + { + CompletableFuture releaseFuture = new CompletableFuture<>(); + pool.release( channel ).addListener( ignore -> releaseFuture.complete( null ) ); + return releaseFuture; + } + + @Override + public void terminateAndRelease( String reason ) + { + + } + + @Override + public BoltServerAddress serverAddress() + { + return null; + } + + @Override + public ServerVersion serverVersion() + { + return null; + } + + @Override + public BoltProtocol protocol() + { + return null; + } + + @Override + public void flush() + { + + } + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/util/Neo4jFeature.java b/driver/src/test/java/org/neo4j/driver/internal/util/Neo4jFeature.java index b8ee261c14..e9f6e11b2a 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/util/Neo4jFeature.java +++ b/driver/src/test/java/org/neo4j/driver/internal/util/Neo4jFeature.java @@ -19,8 +19,6 @@ package org.neo4j.driver.internal.util; import static java.util.Objects.requireNonNull; -import static org.neo4j.driver.internal.util.ServerVersion.v3_1_0; -import static org.neo4j.driver.internal.util.ServerVersion.v3_2_0; import static org.neo4j.driver.internal.util.ServerVersion.v3_4_0; import static org.neo4j.driver.internal.util.ServerVersion.v3_5_0; import static org.neo4j.driver.internal.util.ServerVersion.v4_0_0; diff --git a/driver/src/test/java/org/neo4j/driver/stress/AbstractAsyncQuery.java b/driver/src/test/java/org/neo4j/driver/stress/AbstractAsyncQuery.java index 23f1588108..82cefa3cda 100644 --- a/driver/src/test/java/org/neo4j/driver/stress/AbstractAsyncQuery.java +++ b/driver/src/test/java/org/neo4j/driver/stress/AbstractAsyncQuery.java @@ -22,6 +22,8 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.async.AsyncSession; +import static org.neo4j.driver.internal.SessionConfig.builder; + public abstract class AbstractAsyncQuery implements AsyncCommand { protected final Driver driver; @@ -37,8 +39,8 @@ public AsyncSession newSession( AccessMode mode, C context ) { if ( useBookmark ) { - return driver.asyncSession( t -> t.withDefaultAccessMode( mode ).withBookmarks( context.getBookmark() ) ); + return driver.asyncSession( builder().withDefaultAccessMode( mode ).withBookmarks( context.getBookmark() ).build() ); } - return driver.asyncSession( t -> t.withDefaultAccessMode( mode ) ); + return driver.asyncSession( builder().withDefaultAccessMode( mode ).build() ); } } diff --git a/driver/src/test/java/org/neo4j/driver/stress/AbstractBlockingQuery.java b/driver/src/test/java/org/neo4j/driver/stress/AbstractBlockingQuery.java index fe981b3e5e..1e19b8f603 100644 --- a/driver/src/test/java/org/neo4j/driver/stress/AbstractBlockingQuery.java +++ b/driver/src/test/java/org/neo4j/driver/stress/AbstractBlockingQuery.java @@ -24,6 +24,8 @@ import org.neo4j.driver.Transaction; import org.neo4j.driver.exceptions.TransientException; +import static org.neo4j.driver.internal.SessionConfig.builder; + public abstract class AbstractBlockingQuery implements BlockingCommand { protected final Driver driver; @@ -39,9 +41,9 @@ public Session newSession( AccessMode mode, C context ) { if ( useBookmark ) { - return driver.session( t -> t.withDefaultAccessMode( mode ).withBookmarks( context.getBookmark() ) ); + return driver.session( builder().withDefaultAccessMode( mode ).withBookmarks( context.getBookmark() ).build() ); } - return driver.session( t -> t.withDefaultAccessMode( mode ) ); + return driver.session( builder().withDefaultAccessMode( mode ).build() ); } public Transaction beginTransaction( Session session, C context ) diff --git a/driver/src/test/java/org/neo4j/driver/stress/AbstractRxQuery.java b/driver/src/test/java/org/neo4j/driver/stress/AbstractRxQuery.java index 59f0d61ea2..13d4d080e7 100644 --- a/driver/src/test/java/org/neo4j/driver/stress/AbstractRxQuery.java +++ b/driver/src/test/java/org/neo4j/driver/stress/AbstractRxQuery.java @@ -22,6 +22,8 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.reactive.RxSession; +import static org.neo4j.driver.internal.SessionConfig.builder; + public abstract class AbstractRxQuery implements RxCommand { protected final Driver driver; @@ -37,8 +39,8 @@ public RxSession newSession( AccessMode mode, C context ) { if ( useBookmark ) { - return driver.rxSession( t -> t.withDefaultAccessMode( mode ).withBookmarks( context.getBookmark() ) ); + return driver.rxSession( builder().withDefaultAccessMode( mode ).withBookmarks( context.getBookmark() ).build() ); } - return driver.rxSession( t -> t.withDefaultAccessMode( mode ) ); + return driver.rxSession( builder().withDefaultAccessMode( mode ).build() ); } } diff --git a/driver/src/test/java/org/neo4j/driver/stress/AbstractStressTestBase.java b/driver/src/test/java/org/neo4j/driver/stress/AbstractStressTestBase.java index 70811c92b3..359f9bba68 100644 --- a/driver/src/test/java/org/neo4j/driver/stress/AbstractStressTestBase.java +++ b/driver/src/test/java/org/neo4j/driver/stress/AbstractStressTestBase.java @@ -86,6 +86,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.neo4j.driver.internal.SessionConfig.builder; abstract class AbstractStressTestBase { @@ -524,7 +525,7 @@ private static String createNodesBlocking( int batchCount, int batchSize, Driver private static void readNodesBlocking( Driver driver, String bookmark, int expectedNodeCount ) { long start = System.nanoTime(); - try ( Session session = driver.session( t -> t.withBookmarks( bookmark ) ) ) + try ( Session session = driver.session( builder().withBookmarks( bookmark ).build() ) ) { int nodesProcessed = session.readTransaction( tx -> { @@ -584,7 +585,7 @@ private static void readNodesAsync( Driver driver, String bookmark, int expected { long start = System.nanoTime(); - AsyncSession session = driver.asyncSession( t -> t.withBookmarks( bookmark ) ); + AsyncSession session = driver.asyncSession( builder().withBookmarks( bookmark ).build() ); AtomicInteger nodesSeen = new AtomicInteger(); CompletionStage readQuery = session.readTransactionAsync( tx -> @@ -645,7 +646,7 @@ private void readNodesRx( InternalDriver driver, String bookmark, int expectedNo { long start = System.nanoTime(); - RxSession session = driver.rxSession( t -> t.withBookmarks( bookmark ) ); + RxSession session = driver.rxSession( builder().withBookmarks( bookmark ).build() ); AtomicInteger nodesSeen = new AtomicInteger(); Publisher readQuery = session.readTransaction( tx -> Flux.from( tx.run( "MATCH (n:Node) RETURN n" ).records() ).doOnNext( record -> { diff --git a/driver/src/test/java/org/neo4j/driver/integration/CausalClusteringIT.java b/driver/src/test/java/org/neo4j/driver/stress/CausalClusteringIT.java similarity index 91% rename from driver/src/test/java/org/neo4j/driver/integration/CausalClusteringIT.java rename to driver/src/test/java/org/neo4j/driver/stress/CausalClusteringIT.java index e2efbb62da..39e627ae00 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/CausalClusteringIT.java +++ b/driver/src/test/java/org/neo4j/driver/stress/CausalClusteringIT.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.neo4j.driver.integration; +package org.neo4j.driver.stress; import io.netty.channel.Channel; import org.junit.jupiter.api.AfterEach; @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.CompletionStage; @@ -56,6 +57,8 @@ import org.neo4j.driver.exceptions.Neo4jException; import org.neo4j.driver.exceptions.ServiceUnavailableException; import org.neo4j.driver.exceptions.SessionExpiredException; +import org.neo4j.driver.integration.NestedQueries; +import org.neo4j.driver.internal.BoltServerAddress; import org.neo4j.driver.internal.cluster.RoutingSettings; import org.neo4j.driver.internal.retry.RetrySettings; import org.neo4j.driver.internal.util.DisabledOnNeo4jWith; @@ -69,6 +72,8 @@ import org.neo4j.driver.util.cc.ClusterExtension; import org.neo4j.driver.util.cc.ClusterMember; import org.neo4j.driver.util.cc.ClusterMemberRole; +import org.neo4j.driver.util.cc.ClusterMemberRoleDiscoveryFactory; +import org.neo4j.driver.util.cc.ClusterMemberRoleDiscoveryFactory.ClusterMemberRoleDiscovery; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MINUTES; @@ -84,6 +89,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.neo4j.driver.Values.parameters; +import static org.neo4j.driver.internal.SessionConfig.builder; import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; import static org.neo4j.driver.internal.util.Matchers.connectionAcquisitionTimeoutError; import static org.neo4j.driver.internal.util.Neo4jFeature.BOLT_V3; @@ -110,7 +116,7 @@ public Session newSession( AccessMode mode ) driver = createDriver( clusterRule.getCluster().leader().getRoutingUri() ); } - return driver.session( t -> t.withDefaultAccessMode( mode ) ); + return driver.session( builder().withDefaultAccessMode( mode ).build() ); } @AfterEach @@ -165,8 +171,9 @@ void sessionCreationShouldFailIfCallingDiscoveryProcedureOnEdgeServer() Cluster cluster = clusterRule.getCluster(); ClusterMember readReplica = cluster.anyReadReplica(); - ServiceUnavailableException e = assertThrows( ServiceUnavailableException.class, () -> createDriver( readReplica.getRoutingUri() ) ); - assertThat( e.getMessage(), containsString( "Could not perform discovery. No routing servers available." ) ); + final Driver driver = createDriver( readReplica.getRoutingUri() ); + ServiceUnavailableException e = assertThrows( ServiceUnavailableException.class, driver::verifyConnectivity ); + assertThat( e.getMessage(), containsString( "Unable to connect to database" ) ); } // Ensure that Bookmarks work with single instances using a driver created using a bolt[not+routing] URI. @@ -191,7 +198,7 @@ void bookmarksShouldWorkWithDriverPinnedToSingleServer() throws Exception assertNotNull( bookmark ); - try ( Session session = driver.session( t -> t.withBookmarks( bookmark ) ); + try ( Session session = driver.session( builder().withBookmarks( bookmark ).build() ); Transaction tx = session.beginTransaction() ) { Record record = tx.run( "MATCH (n:Person) RETURN COUNT(*) AS count" ).next(); @@ -216,7 +223,7 @@ void shouldUseBookmarkFromAReadSessionInAWriteSession() throws Exception } ); final String bookmark; - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ) ) + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) { try ( Transaction tx = session.beginTransaction() ) { @@ -286,7 +293,7 @@ void shouldDropBrokenOldConnections() throws Exception // now all idle channels should be considered too old and will be verified during acquisition // they will appear broken because they were closed and new valid connection will be created - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ) ) + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) ) { List records = session.run( "MATCH (n) RETURN count(n)" ).list(); assertEquals( 1, records.size() ); @@ -308,7 +315,7 @@ void beginTransactionThrowsForInvalidBookmark() ClusterMember leader = clusterRule.getCluster().leader(); try ( Driver driver = createDriver( leader.getBoltUri() ); - Session session = driver.session( t -> t.withBookmarks( invalidBookmark ) ) ) + Session session = driver.session( builder().withBookmarks( invalidBookmark ).build() ) ) { ClientException e = assertThrows( ClientException.class, session::beginTransaction ); assertThat( e.getMessage(), containsString( invalidBookmark ) ); @@ -347,7 +354,7 @@ void shouldHandleGracefulLeaderSwitch() throws Exception return session.lastBookmark(); } ); - try ( Session session2 = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ).withBookmarks( bookmark ) ); + try ( Session session2 = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).withBookmarks( bookmark ).build() ); Transaction tx2 = session2.beginTransaction() ) { Record record = tx2.run( "MATCH (n:Person) RETURN COUNT(*) AS count" ).next(); @@ -377,7 +384,7 @@ void shouldNotServeWritesWhenMajorityOfCoresAreDead() { assertThrows( SessionExpiredException.class, () -> { - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ) ) + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) ) { session.run( "CREATE (p:Person {name: 'Gamora'})" ).consume(); } @@ -419,7 +426,7 @@ void shouldServeReadsWhenMajorityOfCoresAreDead() // now we should be unable to write because majority of cores is down assertThrows( SessionExpiredException.class, () -> { - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ) ) + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) ) { session.run( "CREATE (p:Person {name: 'Gamora'})" ).consume(); } @@ -468,7 +475,7 @@ void shouldAcceptMultipleBookmarks() throws Exception executor.shutdown(); assertTrue( executor.awaitTermination( 5, SECONDS ) ); - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ).withBookmarks( bookmarks ) ) ) + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).withBookmarks( bookmarks ).build() ) ) { int count = countNodes( session, label, property, value ); assertEquals( count, threadCount ); @@ -484,7 +491,7 @@ void shouldNotReuseReadConnectionForWriteTransaction() try ( Driver driver = createDriver( leader.getRoutingUri() ) ) { - AsyncSession session = driver.asyncSession( t -> t.withDefaultAccessMode( AccessMode.READ ) ); + AsyncSession session = driver.asyncSession( builder().withDefaultAccessMode( AccessMode.READ ).build() ); CompletionStage> resultsStage = session.runAsync( "RETURN 42" ) .thenCompose( cursor1 -> @@ -528,20 +535,20 @@ void shouldRespectMaxConnectionPoolSizePerClusterMember() try ( Driver driver = createDriver( leader.getRoutingUri(), config ) ) { - Session writeSession1 = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ); + Session writeSession1 = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ); writeSession1.beginTransaction(); - Session writeSession2 = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ); + Session writeSession2 = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ); writeSession2.beginTransaction(); // should not be possible to acquire more connections towards leader because limit is 2 - Session writeSession3 = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ); + Session writeSession3 = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ); ClientException e = assertThrows( ClientException.class, writeSession3::beginTransaction ); assertThat( e, is( connectionAcquisitionTimeoutError( 42 ) ) ); // should be possible to acquire new connection towards read server // it's a different machine, not leader, so different max connection pool size limit applies - Session readSession = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ); + Session readSession = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ); Record record = readSession.readTransaction( tx -> tx.run( "RETURN 1" ).single() ); assertEquals( 1, record.get( 0 ).asInt() ); } @@ -573,7 +580,7 @@ void shouldAllowExistingTransactionToCompleteAfterDifferentConnectionBreaks() closeTx( tx2 ); closeTx( tx1 ); - try ( Session session3 = driver.session( t -> t.withBookmarks( session1.lastBookmark() ) ) ) + try ( Session session3 = driver.session( builder().withBookmarks( session1.lastBookmark() ).build() ) ) { // tx1 should not be terminated and should commit successfully assertEquals( 1, countNodes( session3, "Node1", "name", "Node1" ) ); @@ -583,7 +590,7 @@ void shouldAllowExistingTransactionToCompleteAfterDifferentConnectionBreaks() // rediscovery should happen for the new write query String session4Bookmark = createNodeAndGetBookmark( driver.session(), "Node3", "name", "Node3" ); - try ( Session session5 = driver.session( t -> t.withBookmarks( session4Bookmark ) ) ) + try ( Session session5 = driver.session( builder().withBookmarks( session4Bookmark ).build() ) ) { assertEquals( 1, countNodes( session5, "Node3", "name", "Node3" ) ); } @@ -616,7 +623,7 @@ void shouldRediscoverWhenConnectionsToAllCoresBreak() makeAllChannelsFailToRunQueries( driverFactory, ServerVersion.version( driver ) ); // observe that connection towards writer is broken - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ) ) + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) ) { SessionExpiredException e = assertThrows( SessionExpiredException.class, () -> runCreateNode( session, "Person", "name", "Vision" ).consume() ); @@ -627,7 +634,7 @@ void shouldRediscoverWhenConnectionsToAllCoresBreak() int readersCount = cluster.followers().size() + cluster.readReplicas().size(); for ( int i = 0; i < readersCount; i++ ) { - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ) ) + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) { runCountNodes( session, "Person", "name", "Vision" ); } @@ -745,7 +752,7 @@ private int executeWriteAndReadThroughBoltOnFirstAvailableAddress( ClusterMember private Function createWritableSession( final String bookmark ) { - return driver -> driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ).withBookmarks( bookmark ) ); + return driver -> driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).withBookmarks( bookmark ).build() ); } private Function executeWriteAndRead() @@ -793,7 +800,7 @@ private void ensureNodeVisible( Cluster cluster, String name, String bookmark ) private int countNodesUsingDirectDriver( ClusterMember member, final String name, String bookmark ) { Driver driver = clusterRule.getCluster().getDirectDriver( member ); - try ( Session session = driver.session( t -> t.withBookmarks( bookmark ) ) ) + try ( Session session = driver.session( builder().withBookmarks( bookmark ).build() ) ) { return session.readTransaction( tx -> { @@ -842,18 +849,20 @@ private Driver discoverDriver( List routingUris ) return GraphDatabase.routingDriver( routingUris, clusterRule.getDefaultAuthToken(), configWithoutLogging() ); } - private ClusterOverview fetchClusterOverview( ClusterMember member ) + private static ClusterOverview fetchClusterOverview( ClusterMember member ) { int leaderCount = 0; int followerCount = 0; int readReplicaCount = 0; Driver driver = clusterRule.getCluster().getDirectDriver( member ); - try ( Session session = driver.session() ) + try { - for ( Record record : session.run( "CALL dbms.cluster.overview()" ).list() ) + final ClusterMemberRoleDiscovery discovery = ClusterMemberRoleDiscoveryFactory.newInstance( ServerVersion.version( driver ) ); + final Map clusterOverview = discovery.findClusterOverview( driver ); + for ( BoltServerAddress address : clusterOverview.keySet() ) { - ClusterMemberRole role = ClusterMemberRole.valueOf( record.get( "role" ).asString() ); + ClusterMemberRole role = clusterOverview.get( address ); if ( role == ClusterMemberRole.LEADER ) { leaderCount++; @@ -890,7 +899,7 @@ private static void createNodesInDifferentThreads( int count, final Driver drive executor.submit( () -> { beforeRunLatch.countDown(); - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ) ) + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) ) { runQueryLatch.await(); session.run( "CREATE ()" ); @@ -912,7 +921,7 @@ private static Callable createNodesCallable( Driver driver, String label, { while ( !stop.get() ) { - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ) ) + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) ) { createNode( session, label, property, value ); } @@ -932,7 +941,7 @@ private static Callable readNodesCallable( Driver driver, String label, St { while ( !stop.get() ) { - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ) ) + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) { List ids = readNodeIds( session, label, property, value ); assertNotNull( ids ); diff --git a/driver/src/test/java/org/neo4j/driver/stress/CausalClusteringStressIT.java b/driver/src/test/java/org/neo4j/driver/stress/CausalClusteringStressIT.java index 2af6c21465..f522e96fcf 100644 --- a/driver/src/test/java/org/neo4j/driver/stress/CausalClusteringStressIT.java +++ b/driver/src/test/java/org/neo4j/driver/stress/CausalClusteringStressIT.java @@ -34,11 +34,12 @@ import org.neo4j.driver.AuthToken; import org.neo4j.driver.Driver; -import org.neo4j.driver.Record; -import org.neo4j.driver.Session; import org.neo4j.driver.exceptions.SessionExpiredException; +import org.neo4j.driver.internal.BoltServerAddress; +import org.neo4j.driver.internal.util.ServerVersion; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.util.cc.ClusterMemberRole; +import org.neo4j.driver.util.cc.ClusterMemberRoleDiscoveryFactory; import org.neo4j.driver.util.cc.LocalOrRemoteClusterExtension; import static org.hamcrest.Matchers.both; @@ -46,7 +47,6 @@ import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.junit.MatcherAssert.assertThat; -import static org.neo4j.driver.util.cc.ClusterMember.SIMPLE_SCHEME; class CausalClusteringStressIT extends AbstractStressTestBase { @@ -133,23 +133,21 @@ private static ClusterAddresses fetchClusterAddresses( Driver driver ) Set followers = new HashSet<>(); Set readReplicas = new HashSet<>(); - try ( Session session = driver.session() ) + final ClusterMemberRoleDiscoveryFactory.ClusterMemberRoleDiscovery discovery = + ClusterMemberRoleDiscoveryFactory.newInstance( ServerVersion.version( driver ) ); + final Map clusterOverview = discovery.findClusterOverview( driver ); + + for ( BoltServerAddress address : clusterOverview.keySet() ) { - List records = session.run( "CALL dbms.cluster.overview()" ).list(); - for ( Record record : records ) + String boltAddress = String.format( "%s:%s", address.host(), address.port() ); + ClusterMemberRole role = clusterOverview.get( address ); + if ( role == ClusterMemberRole.FOLLOWER ) { - List addresses = record.get( "addresses" ).asList(); - String boltAddress = ((String) addresses.get( 0 )).replace( SIMPLE_SCHEME, "" ); - - ClusterMemberRole role = ClusterMemberRole.valueOf( record.get( "role" ).asString() ); - if ( role == ClusterMemberRole.FOLLOWER ) - { - followers.add( boltAddress ); - } - else if ( role == ClusterMemberRole.READ_REPLICA ) - { - readReplicas.add( boltAddress ); - } + followers.add( boltAddress ); + } + else if ( role == ClusterMemberRole.READ_REPLICA ) + { + readReplicas.add( boltAddress ); } } diff --git a/driver/src/test/java/org/neo4j/driver/stress/FailedAuth.java b/driver/src/test/java/org/neo4j/driver/stress/FailedAuth.java index 692f815f2a..6f884716f3 100644 --- a/driver/src/test/java/org/neo4j/driver/stress/FailedAuth.java +++ b/driver/src/test/java/org/neo4j/driver/stress/FailedAuth.java @@ -21,6 +21,7 @@ import java.net.URI; import org.neo4j.driver.Config; +import org.neo4j.driver.Driver; import org.neo4j.driver.GraphDatabase; import org.neo4j.driver.Logging; import org.neo4j.driver.exceptions.SecurityException; @@ -46,8 +47,9 @@ public void execute( C context ) { Config config = Config.builder().withLogging( logging ).build(); - SecurityException e = assertThrows( SecurityException.class, - () -> GraphDatabase.driver( clusterUri, basic( "wrongUsername", "wrongPassword" ), config ) ); + final Driver driver = GraphDatabase.driver( clusterUri, basic( "wrongUsername", "wrongPassword" ), config ); + SecurityException e = assertThrows( SecurityException.class, driver::verifyConnectivity ); assertThat( e.getMessage(), containsString( "authentication failure" ) ); + driver.close(); } } diff --git a/driver/src/test/java/org/neo4j/driver/stress/RxWriteQuery.java b/driver/src/test/java/org/neo4j/driver/stress/RxWriteQuery.java index 193ee9571a..bda658dac5 100644 --- a/driver/src/test/java/org/neo4j/driver/stress/RxWriteQuery.java +++ b/driver/src/test/java/org/neo4j/driver/stress/RxWriteQuery.java @@ -44,7 +44,7 @@ public RxWriteQuery( AbstractStressTestBase stressTest, Driver driver, boolea public CompletionStage execute( C context ) { CompletableFuture queryFinished = new CompletableFuture<>(); - Flux.using( () -> newSession( AccessMode.READ, context ), + Flux.using( () -> newSession( AccessMode.WRITE, context ), session -> session.run( "CREATE ()" ).summary(), RxSession::close ) .subscribe( summary -> { queryFinished.complete( null ); diff --git a/driver/src/test/java/org/neo4j/driver/stress/RxWriteQueryInTx.java b/driver/src/test/java/org/neo4j/driver/stress/RxWriteQueryInTx.java index 7ad35a7854..5b930df03f 100644 --- a/driver/src/test/java/org/neo4j/driver/stress/RxWriteQueryInTx.java +++ b/driver/src/test/java/org/neo4j/driver/stress/RxWriteQueryInTx.java @@ -45,7 +45,7 @@ public RxWriteQueryInTx( AbstractStressTestBase stressTest, Driver driver, bo public CompletionStage execute( C context ) { CompletableFuture queryFinished = new CompletableFuture<>(); - RxSession session = newSession( AccessMode.READ, context ); + RxSession session = newSession( AccessMode.WRITE, context ); Flux.usingWhen( session.beginTransaction(), tx -> tx.run( "CREATE ()" ).summary(), RxTransaction::commit, RxTransaction::rollback ).subscribe( summary -> { assertEquals( 1, summary.counters().nodesCreated() ); diff --git a/driver/src/test/java/org/neo4j/driver/util/StubServer.java b/driver/src/test/java/org/neo4j/driver/util/StubServer.java index 6941b6fe3e..97dcf014c9 100644 --- a/driver/src/test/java/org/neo4j/driver/util/StubServer.java +++ b/driver/src/test/java/org/neo4j/driver/util/StubServer.java @@ -37,13 +37,14 @@ import static java.util.concurrent.Executors.newCachedThreadPool; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.neo4j.driver.Logging.none; import static org.neo4j.driver.util.DaemonThreadFactory.daemon; public class StubServer { private static final int SOCKET_CONNECT_ATTEMPTS = 20; - public static final Config INSECURE_CONFIG = Config.builder().withoutEncryption().build(); + public static final Config INSECURE_CONFIG = insecureBuilder().build(); private static final ExecutorService executor = newCachedThreadPool( daemon( "stub-server-output-reader-" ) ); @@ -86,6 +87,11 @@ public int exitStatus() throws InterruptedException, ForceKilled } } + public static Config.ConfigBuilder insecureBuilder() + { + return Config.builder().withoutEncryption().withLogging( none() ); + } + private void exit() throws InterruptedException { process.destroy(); diff --git a/driver/src/test/java/org/neo4j/driver/util/TestUtil.java b/driver/src/test/java/org/neo4j/driver/util/TestUtil.java index cfdd01bf0a..2c767fddd6 100644 --- a/driver/src/test/java/org/neo4j/driver/util/TestUtil.java +++ b/driver/src/test/java/org/neo4j/driver/util/TestUtil.java @@ -91,6 +91,7 @@ import static org.mockito.Mockito.when; import static org.neo4j.driver.AccessMode.WRITE; import static org.neo4j.driver.internal.Bookmarks.empty; +import static org.neo4j.driver.internal.SessionConfig.builder; import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.ABSENT_DB_NAME; import static org.neo4j.driver.internal.util.Futures.completedWithNull; @@ -220,7 +221,7 @@ public static Set asOrderedSet( T... elements ) public static long countNodes( Driver driver, String bookmark ) { - try ( Session session = driver.session( t -> t.withBookmarks( bookmark ) ) ) + try ( Session session = driver.session( builder().withBookmarks( bookmark ).build() ) ) { return session.readTransaction( tx -> tx.run( "MATCH (n) RETURN count(n)" ).single().get( 0 ).asLong() ); } diff --git a/driver/src/test/java/org/neo4j/driver/util/cc/Cluster.java b/driver/src/test/java/org/neo4j/driver/util/cc/Cluster.java index c978f7dc2c..cf16d83ed3 100644 --- a/driver/src/test/java/org/neo4j/driver/util/cc/Cluster.java +++ b/driver/src/test/java/org/neo4j/driver/util/cc/Cluster.java @@ -23,21 +23,19 @@ import java.nio.file.Path; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; -import org.neo4j.driver.internal.BoltServerAddress; -import org.neo4j.driver.AccessMode; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; -import org.neo4j.driver.Session; -import org.neo4j.driver.StatementResult; +import org.neo4j.driver.internal.BoltServerAddress; import org.neo4j.driver.util.TestUtil; +import org.neo4j.driver.util.cc.ClusterMemberRoleDiscoveryFactory.ClusterMemberRoleDiscovery; import static java.util.Collections.emptySet; import static java.util.Collections.unmodifiableSet; -import static org.neo4j.driver.internal.util.Iterables.single; import static org.neo4j.driver.util.TestUtil.sleep; public class Cluster implements AutoCloseable @@ -220,21 +218,18 @@ private Set membersWithRole( ClusterMemberRole role ) Set membersWithRole = new HashSet<>(); Driver driver = driverToAnyCore( members, clusterDrivers ); - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ) ) + final ClusterMemberRoleDiscovery discovery = clusterDrivers.getDiscovery(); + final Map clusterOverview = discovery.findClusterOverview( driver ); + for ( BoltServerAddress boltAddress : clusterOverview.keySet() ) { - List records = findClusterOverview( session ); - for ( Record record : records ) + if ( role == clusterOverview.get( boltAddress ) ) { - if ( role == extractRole( record ) ) + ClusterMember member = findByBoltAddress( boltAddress, members ); + if ( member == null ) { - BoltServerAddress boltAddress = extractBoltAddress( record ); - ClusterMember member = findByBoltAddress( boltAddress, members ); - if ( member == null ) - { - throw new IllegalStateException( "Unknown cluster member: '" + boltAddress + "'\n" + this ); - } - membersWithRole.add( member ); + throw new IllegalStateException( "Unknown cluster member: '" + boltAddress + "'\n" + this ); } + membersWithRole.add( member ); } } @@ -278,10 +273,15 @@ private static void waitForMembersToBeOnline( Set members, Cluste assertDeadlineNotReached( deadline, expectedOnlineAddresses, actualOnlineAddresses, error ); Driver driver = driverToAnyCore( members, clusterDrivers ); - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ) ) + final ClusterMemberRoleDiscovery discovery = clusterDrivers.getDiscovery(); + try { - List records = findClusterOverview( session ); - actualOnlineAddresses = extractBoltAddresses( records ); + final Map clusterOverview = discovery.findClusterOverview( driver ); + // we will wait until the leader is online + if ( clusterOverview.containsValue( ClusterMemberRole.LEADER ) ) + { + actualOnlineAddresses = clusterOverview.keySet(); + } } catch ( Throwable t ) { @@ -309,31 +309,16 @@ private static Driver driverToAnyCore( Set members, ClusterDriver for ( ClusterMember member : members ) { Driver driver = clusterDrivers.getDriver( member ); - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ) ) + final ClusterMemberRoleDiscovery discovery = clusterDrivers.getDiscovery(); + if ( discovery.isCoreMember( driver ) ) { - if ( isCoreMember( session ) ) - { - return driver; - } + return driver; } } throw new IllegalStateException( "No core members found among: " + members ); } - private static List findClusterOverview( Session session ) - { - StatementResult result = session.run( "CALL dbms.cluster.overview()" ); - return result.list(); - } - - private static boolean isCoreMember( Session session ) - { - Record record = single( session.run( "call dbms.cluster.role()" ).list() ); - ClusterMemberRole role = extractRole( record ); - return role != ClusterMemberRole.READ_REPLICA; - } - private static void assertDeadlineNotReached( long deadline, Set expectedAddresses, Set actualAddresses, Throwable error ) throws ClusterUnavailableException { @@ -366,17 +351,6 @@ private static Set extractBoltAddresses( Set m return addresses; } - private static Set extractBoltAddresses( List records ) - { - Set addresses = new HashSet<>(); - for ( Record record : records ) - { - BoltServerAddress boltAddress = extractBoltAddress( record ); - addresses.add( boltAddress ); - } - return addresses; - } - private static BoltServerAddress extractBoltAddress( Record record ) { List addresses = record.get( "addresses" ).asList(); diff --git a/driver/src/test/java/org/neo4j/driver/util/cc/ClusterDrivers.java b/driver/src/test/java/org/neo4j/driver/util/cc/ClusterDrivers.java index 66c1b1336a..3a4d91869f 100644 --- a/driver/src/test/java/org/neo4j/driver/util/cc/ClusterDrivers.java +++ b/driver/src/test/java/org/neo4j/driver/util/cc/ClusterDrivers.java @@ -22,18 +22,21 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; -import org.neo4j.driver.internal.util.DriverFactoryWithOneEventLoopThread; import org.neo4j.driver.AuthTokens; import org.neo4j.driver.Config; import org.neo4j.driver.Driver; +import org.neo4j.driver.internal.util.DriverFactoryWithOneEventLoopThread; +import org.neo4j.driver.util.cc.ClusterMemberRoleDiscoveryFactory.ClusterMemberRoleDiscovery; import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; +import static org.neo4j.driver.internal.util.ServerVersion.version; public class ClusterDrivers implements AutoCloseable { private final String user; private final String password; private final Map membersWithDrivers; + private ClusterMemberRoleDiscovery discovery; public ClusterDrivers( String user, String password ) { @@ -44,7 +47,17 @@ public ClusterDrivers( String user, String password ) public Driver getDriver( ClusterMember member ) { - return membersWithDrivers.computeIfAbsent( member, this::createDriver ); + final Driver driver = membersWithDrivers.computeIfAbsent( member, this::createDriver ); + if ( discovery == null ) + { + discovery = ClusterMemberRoleDiscoveryFactory.newInstance( version( driver ) ); + } + return driver; + } + + public ClusterMemberRoleDiscovery getDiscovery() + { + return discovery; } @Override diff --git a/driver/src/test/java/org/neo4j/driver/util/cc/ClusterMemberRole.java b/driver/src/test/java/org/neo4j/driver/util/cc/ClusterMemberRole.java index 9f1337bd5d..cb459cb25c 100644 --- a/driver/src/test/java/org/neo4j/driver/util/cc/ClusterMemberRole.java +++ b/driver/src/test/java/org/neo4j/driver/util/cc/ClusterMemberRole.java @@ -22,5 +22,6 @@ public enum ClusterMemberRole { LEADER, FOLLOWER, - READ_REPLICA + READ_REPLICA, + UNKNOWN } diff --git a/driver/src/test/java/org/neo4j/driver/util/cc/ClusterMemberRoleDiscoveryFactory.java b/driver/src/test/java/org/neo4j/driver/util/cc/ClusterMemberRoleDiscoveryFactory.java new file mode 100644 index 0000000000..804b09a795 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/util/cc/ClusterMemberRoleDiscoveryFactory.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.util.cc; + +import java.net.URI; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Record; +import org.neo4j.driver.Session; +import org.neo4j.driver.StatementResult; +import org.neo4j.driver.Values; +import org.neo4j.driver.internal.BoltServerAddress; +import org.neo4j.driver.internal.util.ServerVersion; + +import static org.neo4j.driver.Values.parameters; +import static org.neo4j.driver.internal.SessionConfig.builder; +import static org.neo4j.driver.internal.util.Iterables.single; + +public class ClusterMemberRoleDiscoveryFactory +{ + public static ClusterMemberRoleDiscovery newInstance( ServerVersion version ) + { + if ( version.greaterThanOrEqual( ServerVersion.v4_0_0 ) ) + { + return new SimpleClusterMemberRoleDiscovery(); + } + else + { + return new LegacyClusterMemberRoleDiscovery(); + } + } + + public interface ClusterMemberRoleDiscovery + { + boolean isCoreMember( Driver driver ); + + Map findClusterOverview( Driver driver ); + } + + public static class LegacyClusterMemberRoleDiscovery implements ClusterMemberRoleDiscovery + { + @Override + public boolean isCoreMember( Driver driver ) + { + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) + { + Record record = single( session.run( "CALL dbms.cluster.role()" ).list() ); + ClusterMemberRole role = extractRole( record ); + return role == ClusterMemberRole.LEADER || role == ClusterMemberRole.FOLLOWER; + } + } + + @Override + public Map findClusterOverview( Driver driver ) + { + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) + { + StatementResult result = session.run( "CALL dbms.cluster.overview()" ); + Map overview = new HashMap<>(); + for ( Record record : result.list() ) + { + final BoltServerAddress address = extractBoltAddress( record ); + final ClusterMemberRole role = extractRole( record ); + overview.put( address, role ); + } + return overview; + } + } + } + + public static class SimpleClusterMemberRoleDiscovery implements ClusterMemberRoleDiscovery + { + private static final String DEFAULT_DATABASE = "neo4j"; + + @Override + public boolean isCoreMember( Driver driver ) + { + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) + { + Record record = single( session.run( "CALL dbms.cluster.role($database)", + parameters( "database", DEFAULT_DATABASE ) ).list() ); + ClusterMemberRole role = extractRole( record ); + return role == ClusterMemberRole.LEADER || role == ClusterMemberRole.FOLLOWER; + } + } + + @Override + public Map findClusterOverview( Driver driver ) + { + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) + { + StatementResult result = session.run( "CALL dbms.cluster.overview()" ); + Map overview = new HashMap<>(); + for ( Record record : result.list() ) + { + final BoltServerAddress address = extractBoltAddress( record ); + final ClusterMemberRole role = extractRoleForDatabase( record, DEFAULT_DATABASE ); + if ( role != ClusterMemberRole.UNKNOWN ) // the unknown ones has not fully come online + { + overview.put( address, role ); + } + } + return overview; + } + } + } + + private static ClusterMemberRole extractRoleForDatabase( Record record, String database ) + { + final Map databases = record.get( "databases" ).asMap( Values.ofString() ); + final String roleString = databases.get( database ); + return ClusterMemberRole.valueOf( roleString.toUpperCase() ); + } + + private static BoltServerAddress extractBoltAddress( Record record ) + { + List addresses = record.get( "addresses" ).asList(); + String boltUriString = (String) addresses.get( 0 ); + URI boltUri = URI.create( boltUriString ); + return newBoltServerAddress( boltUri ); + } + + private static BoltServerAddress newBoltServerAddress( URI uri ) + { + try + { + return new BoltServerAddress( uri ).resolve(); + } + catch ( UnknownHostException e ) + { + throw new RuntimeException( "Unable to resolve host to IP in URI: '" + uri + "'" ); + } + } + + private static ClusterMemberRole extractRole( Record record ) + { + String roleString = record.get( "role" ).asString(); + return ClusterMemberRole.valueOf( roleString.toUpperCase() ); + } +} diff --git a/driver/src/test/resources/acquire_endpoints_v4.script b/driver/src/test/resources/acquire_endpoints_v4.script new file mode 100644 index 0000000000..cece16f684 --- /dev/null +++ b/driver/src/test/resources/acquire_endpoints_v4.script @@ -0,0 +1,10 @@ +!: BOLT 4 +!: AUTO RESET +!: AUTO HELLO +!: AUTO GOODBYE + +C: RUN "CALL dbms.routing.getRoutingTable($context, $database)" {"context": {}, "database": "myDatabase"} {"mode": "r", "db": "system"} + PULL {"n": -1} +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9007","127.0.0.1:9008"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9005","127.0.0.1:9006"], "role": "READ"},{"addresses": ["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"], "role": "ROUTE"}]] + SUCCESS {} diff --git a/driver/src/test/resources/acquire_endpoints_v4_database_not_found.script b/driver/src/test/resources/acquire_endpoints_v4_database_not_found.script new file mode 100644 index 0000000000..29e8e0ba5e --- /dev/null +++ b/driver/src/test/resources/acquire_endpoints_v4_database_not_found.script @@ -0,0 +1,9 @@ +!: BOLT 4 +!: AUTO RESET +!: AUTO HELLO +!: AUTO GOODBYE + +C: RUN "CALL dbms.routing.getRoutingTable($context, $database)" {"context": {}, "database": "myDatabase"} {"mode": "r", "db": "system"} + PULL {"n": -1} +S: FAILURE {"code": "Neo.ClientError.Database.DatabaseNotFound", "message": "wut!"} + IGNORED diff --git a/driver/src/test/resources/acquire_endpoints_v4_empty.script b/driver/src/test/resources/acquire_endpoints_v4_empty.script new file mode 100644 index 0000000000..3bafae3745 --- /dev/null +++ b/driver/src/test/resources/acquire_endpoints_v4_empty.script @@ -0,0 +1,10 @@ +!: BOLT 4 +!: AUTO RESET +!: AUTO HELLO +!: AUTO GOODBYE + +C: RUN "CALL dbms.routing.getRoutingTable($context, $database)" {"context": {}, "database": "myDatabase"} {"mode": "r", "db": "system"} + PULL {"n": -1} +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, []] + SUCCESS {} diff --git a/driver/src/test/resources/acquire_endpoints_v4_multi_db.script b/driver/src/test/resources/acquire_endpoints_v4_multi_db.script new file mode 100644 index 0000000000..5b29ec5d7b --- /dev/null +++ b/driver/src/test/resources/acquire_endpoints_v4_multi_db.script @@ -0,0 +1,15 @@ +!: BOLT 4 +!: AUTO RESET +!: AUTO HELLO +!: AUTO GOODBYE + +C: RUN "CALL dbms.routing.getRoutingTable($context, $database)" {"context": {}, "database": "Unreachable"} {"mode": "r", "db": "system"} + PULL {"n": -1} +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, []] + SUCCESS {} +C: RUN "CALL dbms.routing.getRoutingTable($context, $database)" {"context": {}, "database": "myDatabase"} {"mode": "r", "db": "system"} + PULL {"n": -1} +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9007","127.0.0.1:9008"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9005","127.0.0.1:9006"], "role": "READ"},{"addresses": ["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"], "role": "ROUTE"}]] + SUCCESS {} diff --git a/driver/src/test/resources/acquire_endpoints_v4_verify_connectivity.script b/driver/src/test/resources/acquire_endpoints_v4_verify_connectivity.script new file mode 100644 index 0000000000..f9fa055f14 --- /dev/null +++ b/driver/src/test/resources/acquire_endpoints_v4_verify_connectivity.script @@ -0,0 +1,15 @@ +!: BOLT 4 +!: AUTO RESET +!: AUTO HELLO +!: AUTO GOODBYE + +C: RUN "CALL dbms.routing.getRoutingTable($context, $database)" {"context": {}, "database": null} {"mode": "r", "db": "system"} + PULL {"n": -1} +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9007","127.0.0.1:9008"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9005","127.0.0.1:9006"], "role": "READ"},{"addresses": ["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"], "role": "ROUTE"}]] + SUCCESS {} +C: RUN "CALL dbms.routing.getRoutingTable($context, $database)" {"context": {}, "database": "myDatabase"} {"mode": "r", "db": "system"} + PULL {"n": -1} +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9007","127.0.0.1:9008"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9005","127.0.0.1:9006"], "role": "READ"},{"addresses": ["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"], "role": "ROUTE"}]] + SUCCESS {} diff --git a/driver/src/test/resources/database_shutdown.script b/driver/src/test/resources/database_shutdown.script index ec565687d3..9d315be683 100644 --- a/driver/src/test/resources/database_shutdown.script +++ b/driver/src/test/resources/database_shutdown.script @@ -3,8 +3,6 @@ !: AUTO HELLO !: AUTO GOODBYE -C: RESET -S: SUCCESS {} C: BEGIN {"bookmarks": ["neo4j:bookmark:v1:tx0"]} S: SUCCESS {} C: RUN "RETURN 1" {} {} diff --git a/driver/src/test/resources/dead_read_server.script b/driver/src/test/resources/dead_read_server.script index 0451d49776..186d3d81fb 100644 --- a/driver/src/test/resources/dead_read_server.script +++ b/driver/src/test/resources/dead_read_server.script @@ -2,8 +2,7 @@ !: AUTO RESET !: AUTO HELLO !: AUTO GOODBYE -!: AUTO BEGIN -C: RUN "MATCH (n) RETURN n.name" {} {} +C: RUN "MATCH (n) RETURN n.name" {} {"mode": "r"} C: PULL_ALL S: diff --git a/driver/src/test/resources/dead_read_server_tx.script b/driver/src/test/resources/dead_read_server_tx.script new file mode 100644 index 0000000000..0451d49776 --- /dev/null +++ b/driver/src/test/resources/dead_read_server_tx.script @@ -0,0 +1,9 @@ +!: BOLT 3 +!: AUTO RESET +!: AUTO HELLO +!: AUTO GOODBYE +!: AUTO BEGIN + +C: RUN "MATCH (n) RETURN n.name" {} {} +C: PULL_ALL +S: diff --git a/examples/src/main/java/org/neo4j/docs/driver/ConfigCustomResolverExample.java b/examples/src/main/java/org/neo4j/docs/driver/ConfigCustomResolverExample.java index 54a507ded0..037ccd4462 100644 --- a/examples/src/main/java/org/neo4j/docs/driver/ConfigCustomResolverExample.java +++ b/examples/src/main/java/org/neo4j/docs/driver/ConfigCustomResolverExample.java @@ -31,6 +31,7 @@ import org.neo4j.driver.net.ServerAddress; import static org.neo4j.driver.Values.parameters; +import static org.neo4j.driver.internal.SessionConfig.builder; public class ConfigCustomResolverExample implements AutoCloseable { @@ -64,7 +65,7 @@ private void addPerson( String name ) try ( Driver driver = createDriver( "neo4j://x.acme.com", username, password, ServerAddress.of( "a.acme.com", 7676 ), ServerAddress.of( "b.acme.com", 8787 ), ServerAddress.of( "c.acme.com", 9898 ) ) ) { - try ( Session session = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ) ) + try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) ) { session.run( "CREATE (a:Person {name: $name})", parameters( "name", name ) ); } @@ -80,7 +81,7 @@ public void close() throws Exception public boolean canConnect() { - StatementResult result = driver.session( t -> t.withDefaultAccessMode( AccessMode.READ ) ).run( "RETURN 1" ); + StatementResult result = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ).run( "RETURN 1" ); return result.single().get( 0 ).asInt() == 1; } } diff --git a/examples/src/main/java/org/neo4j/docs/driver/PassBookmarkExample.java b/examples/src/main/java/org/neo4j/docs/driver/PassBookmarkExample.java index 831ed01ff8..cf98207cfe 100644 --- a/examples/src/main/java/org/neo4j/docs/driver/PassBookmarkExample.java +++ b/examples/src/main/java/org/neo4j/docs/driver/PassBookmarkExample.java @@ -29,6 +29,7 @@ import org.neo4j.driver.Transaction; import static org.neo4j.driver.Values.parameters; +import static org.neo4j.driver.internal.SessionConfig.builder; // end::pass-bookmarks-import[] public class PassBookmarkExample extends BaseApplication @@ -89,7 +90,7 @@ public void addEmployAndMakeFriends() List savedBookmarks = new ArrayList<>(); // Create the first person and employment relationship. - try ( Session session1 = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ) ) + try ( Session session1 = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) ) { session1.writeTransaction( tx -> addCompany( tx, "Wayne Enterprises" ) ); session1.writeTransaction( tx -> addPerson( tx, "Alice" ) ); @@ -99,7 +100,7 @@ public void addEmployAndMakeFriends() } // Create the second person and employment relationship. - try ( Session session2 = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ) ) ) + try ( Session session2 = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) ) { session2.writeTransaction( tx -> addCompany( tx, "LexCorp" ) ); session2.writeTransaction( tx -> addPerson( tx, "Bob" ) ); @@ -109,7 +110,7 @@ public void addEmployAndMakeFriends() } // Create a friendship between the two people created above. - try ( Session session3 = driver.session( t -> t.withDefaultAccessMode( AccessMode.WRITE ).withBookmarks( savedBookmarks ) ) ) + try ( Session session3 = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).withBookmarks( savedBookmarks ).build() ) ) { session3.writeTransaction( tx -> makeFriends( tx, "Alice", "Bob" ) ); diff --git a/examples/src/test/java/org/neo4j/docs/driver/ExamplesIT.java b/examples/src/test/java/org/neo4j/docs/driver/ExamplesIT.java index 5f55630012..18a6d0ed9b 100644 --- a/examples/src/test/java/org/neo4j/docs/driver/ExamplesIT.java +++ b/examples/src/test/java/org/neo4j/docs/driver/ExamplesIT.java @@ -23,8 +23,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import java.nio.file.Files; import java.nio.file.Path; @@ -33,8 +31,6 @@ import java.util.List; import java.util.Set; import java.util.UUID; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; import org.neo4j.driver.Session; import org.neo4j.driver.Value; @@ -566,18 +562,16 @@ void testShouldRunRxTransactionFunctionExampleReactor() throws Exception } StdIOCapture stdIOCapture = new StdIOCapture(); - CountDownLatch latch = new CountDownLatch( 1 ); // print all 'Product' nodes to fake stdout try ( AutoCloseable ignore = stdIOCapture.capture() ) { - example.printAllProductsReactor() - .single() - .doAfterTerminate( () -> latch.countDown() ) - .subscribe( summary -> assertEquals( StatementType.READ_ONLY, summary.statementType() ) ); + final List summaryList = await( example.printAllProductsReactor() ); + assertThat( summaryList.size(), equalTo( 1 ) ); + ResultSummary summary = summaryList.get( 0 ); + assertEquals( StatementType.READ_ONLY, summary.statementType() ); } - latch.await( 1, TimeUnit.MINUTES ); Set capturedOutput = new HashSet<>( stdIOCapture.stdout() ); assertEquals( new HashSet<>( asList( "Infinity Gauntlet", "Mjölnir" ) ), capturedOutput ); } @@ -598,18 +592,16 @@ void testShouldRunRxTransactionFunctionExampleRxJava() throws Exception } StdIOCapture stdIOCapture = new StdIOCapture(); - CountDownLatch latch = new CountDownLatch( 1 ); // print all 'Product' nodes to fake stdout try ( AutoCloseable ignore = stdIOCapture.capture() ) { - Flux.from( example.printAllProductsRxJava() ) - .single() - .doAfterTerminate( () -> latch.countDown() ) - .subscribe( summary -> assertEquals( StatementType.READ_ONLY, summary.statementType() ) ); + final List summaryList = await( example.printAllProductsRxJava() ); + assertThat( summaryList.size(), equalTo( 1 ) ); + ResultSummary summary = summaryList.get( 0 ); + assertEquals( StatementType.READ_ONLY, summary.statementType() ); } - latch.await( 1, TimeUnit.MINUTES ); Set capturedOutput = new HashSet<>( stdIOCapture.stdout() ); assertEquals( new HashSet<>( asList( "Infinity Gauntlet", "Mjölnir" ) ), capturedOutput ); } diff --git a/pom.xml b/pom.xml index 41d5ffc06d..ccf04b726c 100644 --- a/pom.xml +++ b/pom.xml @@ -64,7 +64,7 @@ io.netty netty-handler - 4.1.22.Final + 4.1.36.Final io.projectreactor @@ -140,6 +140,13 @@ 3.2.6.RELEASE test + + com.oracle.substratevm + svm + 19.0.2 + + provided +