Skip to content

Make connectivity verification not part of driver creation. #609

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions driver/src/main/java/org/neo4j/driver/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,9 @@ RetrySettings retrySettings()
return retrySettings;
}

/**
* @return if the metrics is enabled or not on this driver.
*/
public boolean isMetricsEnabled()
{
return isMetricsEnabled;
Expand Down
23 changes: 23 additions & 0 deletions driver/src/main/java/org/neo4j/driver/Driver.java
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,27 @@ public interface Driver extends AutoCloseable
*/
@Experimental
TypeSystem defaultTypeSystem();

/**
* 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.
*
* 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.
*/
void verifyConnectivity();

/**
* 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.
*
* 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 a {@link CompletionStage completion stage} that represents the asynchronous verification.
*/
CompletionStage<Void> verifyConnectivityAsync();
}
23 changes: 22 additions & 1 deletion driver/src/main/java/org/neo4j/driver/GraphDatabase.java
Original file line number Diff line number Diff line change
Expand Up @@ -154,19 +154,40 @@ public static Driver routingDriver( Iterable<URI> 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<URI> uris )
{
for ( URI uri : uris )
Expand Down
54 changes: 9 additions & 45 deletions driver/src/main/java/org/neo4j/driver/internal/DriverFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,18 @@ public final TypeSystem defaultTypeSystem()
return InternalTypeSystem.TYPE_SYSTEM;
}

public CompletionStage<Void> verifyConnectivity()
@Override
public CompletionStage<Void> verifyConnectivityAsync()
{
return sessionFactory.verifyConnectivity();
}

@Override
public void verifyConnectivity()
{
Futures.blockingGet( verifyConnectivityAsync() );
}

/**
* Get the underlying session factory.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
*/
public class RediscoveryImpl implements Rediscovery
{
private static final String NO_ROUTERS_AVAILABLE = "Could not perform discovery for database '%s'. No routing servers available.";
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,19 @@ public CompletionStage<Connection> acquireConnection( String databaseName, Acces
@Override
public CompletionStage<Void> verifyConnectivity()
{
return routingTables.refreshRoutingTable( ABSENT_DB_NAME, AccessMode.READ ).thenApply( ignored -> null );
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
Expand Down
37 changes: 34 additions & 3 deletions driver/src/test/java/org/neo4j/driver/GraphDatabaseTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -60,6 +61,7 @@ void boltSchemeShouldInstantiateDirectDriver() throws Exception

// When
Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG );
driver.verifyConnectivity();

// Then
assertThat( driver, is( directDriver() ) );
Expand All @@ -78,6 +80,7 @@ void boltPlusDiscoverySchemeShouldInstantiateClusterDriver() throws Exception

// When
Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG );
driver.verifyConnectivity();

// Then
assertThat( driver, is( clusterDriver() ) );
Expand Down Expand Up @@ -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
{
Expand All @@ -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
{
Expand All @@ -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" );
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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 );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 )
Expand Down
Loading