diff --git a/driver/pom.xml b/driver/pom.xml index 6a13b29e60..c5be24348b 100644 --- a/driver/pom.xml +++ b/driver/pom.xml @@ -8,6 +8,7 @@ ${project.groupId}.${project.artifactId} 'v'yyyyMMdd-HHmm + 4.1.15.Final @@ -32,7 +33,13 @@ - + + io.netty + netty-all + ${netty.version} + + + org.hamcrest hamcrest-library diff --git a/driver/src/main/java/org/neo4j/driver/ResultResourcesHandler.java b/driver/src/main/java/org/neo4j/driver/ResultResourcesHandler.java new file mode 100644 index 0000000000..95e19ec5de --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/ResultResourcesHandler.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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; + +public interface ResultResourcesHandler +{ + void resultFetched(); + + void resultFailed( Throwable error ); + + ResultResourcesHandler NO_OP = new ResultResourcesHandler() + { + @Override + public void resultFetched() + { + } + + @Override + public void resultFailed( Throwable error ) + { + } + }; +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/BookmarkCollector.java b/driver/src/main/java/org/neo4j/driver/StatementKeys.java similarity index 64% rename from driver/src/main/java/org/neo4j/driver/internal/BookmarkCollector.java rename to driver/src/main/java/org/neo4j/driver/StatementKeys.java index ae8529f247..de2d237495 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/BookmarkCollector.java +++ b/driver/src/main/java/org/neo4j/driver/StatementKeys.java @@ -16,23 +16,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.neo4j.driver.internal; +package org.neo4j.driver; -import org.neo4j.driver.internal.spi.Collector.NoOperationCollector; +import java.util.Collections; +import java.util.List; -class BookmarkCollector extends NoOperationCollector +public class StatementKeys { - private final ExplicitTransaction transaction; + private List keys; - BookmarkCollector( ExplicitTransaction transaction ) + public boolean isPopulated() { - this.transaction = transaction; + return keys != null; } - @Override - public void bookmark( Bookmark bookmark ) + public void set( List keys ) { - transaction.setBookmark( bookmark ); + this.keys = keys; } + public List asList() + { + return keys == null ? Collections.emptyList() : keys; + } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/ConnectionSettings.java b/driver/src/main/java/org/neo4j/driver/internal/ConnectionSettings.java index 65da9161c4..4683a16e6b 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/ConnectionSettings.java +++ b/driver/src/main/java/org/neo4j/driver/internal/ConnectionSettings.java @@ -52,18 +52,18 @@ private static String driverVersion() private final AuthToken authToken; private final String userAgent; - private final int timeoutMillis; + private final int connectTimeoutMillis; - public ConnectionSettings( AuthToken authToken, String userAgent, int timeoutMillis ) + public ConnectionSettings( AuthToken authToken, String userAgent, int connectTimeoutMillis ) { this.authToken = authToken; this.userAgent = userAgent; - this.timeoutMillis = timeoutMillis; + this.connectTimeoutMillis = connectTimeoutMillis; } - public ConnectionSettings( AuthToken authToken, int timeoutMillis ) + public ConnectionSettings( AuthToken authToken, int connectTimeoutMillis ) { - this( authToken, DEFAULT_USER_AGENT, timeoutMillis ); + this( authToken, DEFAULT_USER_AGENT, connectTimeoutMillis ); } public AuthToken authToken() @@ -76,8 +76,8 @@ public String userAgent() return userAgent; } - public int timeoutMillis() + public int connectTimeoutMillis() { - return timeoutMillis; + return connectTimeoutMillis; } } 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 3b80a75eac..4bd40e7bd2 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/DirectConnectionProvider.java +++ b/driver/src/main/java/org/neo4j/driver/internal/DirectConnectionProvider.java @@ -18,6 +18,9 @@ */ package org.neo4j.driver.internal; +import org.neo4j.driver.internal.async.AsyncConnection; +import org.neo4j.driver.internal.async.InternalFuture; +import org.neo4j.driver.internal.async.pool.AsyncConnectionPool; import org.neo4j.driver.internal.net.BoltServerAddress; import org.neo4j.driver.internal.spi.ConnectionPool; import org.neo4j.driver.internal.spi.ConnectionProvider; @@ -32,11 +35,13 @@ public class DirectConnectionProvider implements ConnectionProvider { private final BoltServerAddress address; private final ConnectionPool pool; + private final AsyncConnectionPool asyncPool; - DirectConnectionProvider( BoltServerAddress address, ConnectionPool pool ) + DirectConnectionProvider( BoltServerAddress address, ConnectionPool pool, AsyncConnectionPool asyncPool ) { this.address = address; this.pool = pool; + this.asyncPool = asyncPool; verifyConnectivity(); } @@ -47,10 +52,17 @@ public PooledConnection acquireConnection( AccessMode mode ) return pool.acquire( address ); } + @Override + public InternalFuture acquireAsyncConnection( AccessMode mode ) + { + return asyncPool.acquire( address ); + } + @Override public void close() throws Exception { pool.close(); + asyncPool.closeAsync().syncUninterruptibly(); } public BoltServerAddress getAddress() 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 746fc4ed95..45ad5e0b8a 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/DriverFactory.java +++ b/driver/src/main/java/org/neo4j/driver/internal/DriverFactory.java @@ -18,10 +18,17 @@ */ package org.neo4j.driver.internal; +import io.netty.bootstrap.Bootstrap; + import java.io.IOException; import java.net.URI; import java.security.GeneralSecurityException; +import org.neo4j.driver.internal.async.AsyncConnectorImpl; +import org.neo4j.driver.internal.async.BootstrapFactory; +import org.neo4j.driver.internal.async.pool.ActiveChannelTracker; +import org.neo4j.driver.internal.async.pool.AsyncConnectionPool; +import org.neo4j.driver.internal.async.pool.AsyncConnectionPoolImpl; import org.neo4j.driver.internal.cluster.RoutingContext; import org.neo4j.driver.internal.cluster.RoutingSettings; import org.neo4j.driver.internal.cluster.loadbalancing.LeastConnectedLoadBalancingStrategy; @@ -59,15 +66,20 @@ public class DriverFactory public final Driver newInstance( URI uri, AuthToken authToken, RoutingSettings routingSettings, RetrySettings retrySettings, Config config ) { + authToken = authToken == null ? AuthTokens.none() : authToken; + BoltServerAddress address = new BoltServerAddress( uri ); RoutingSettings newRoutingSettings = routingSettings.withRoutingContext( new RoutingContext( uri ) ); SecurityPlan securityPlan = createSecurityPlan( address, config ); ConnectionPool connectionPool = createConnectionPool( authToken, securityPlan, config ); RetryLogic retryLogic = createRetryLogic( retrySettings, config.logging() ); + AsyncConnectionPool asyncConnectionPool = createAsyncConnectionPool( authToken, securityPlan, config ); + try { - return createDriver( uri, address, connectionPool, config, newRoutingSettings, securityPlan, retryLogic ); + return createDriver( uri, address, connectionPool, config, newRoutingSettings, securityPlan, retryLogic, + asyncConnectionPool ); } catch ( Throwable driverError ) { @@ -75,6 +87,7 @@ public final Driver newInstance( URI uri, AuthToken authToken, RoutingSettings r try { connectionPool.close(); + asyncConnectionPool.closeAsync().syncUninterruptibly(); } catch ( Throwable closeError ) { @@ -84,16 +97,33 @@ public final Driver newInstance( URI uri, AuthToken authToken, RoutingSettings r } } + private AsyncConnectionPool createAsyncConnectionPool( AuthToken authToken, SecurityPlan securityPlan, + Config config ) + { + Clock clock = createClock(); + ConnectionSettings connectionSettings = new ConnectionSettings( authToken, config.connectionTimeoutMillis() ); + ActiveChannelTracker activeChannelTracker = new ActiveChannelTracker( config.logging() ); + AsyncConnectorImpl connector = new AsyncConnectorImpl( connectionSettings, securityPlan, + activeChannelTracker, config.logging(), clock ); + Bootstrap bootstrap = BootstrapFactory.newBootstrap(); + PoolSettings poolSettings = new PoolSettings( config.maxIdleConnectionPoolSize(), + config.idleTimeBeforeConnectionTest(), config.maxConnectionLifetimeMillis(), + config.maxConnectionPoolSize(), + config.connectionAcquisitionTimeoutMillis() ); + return new AsyncConnectionPoolImpl( connector, bootstrap, activeChannelTracker, poolSettings, config.logging(), + clock ); + } + private Driver createDriver( URI uri, BoltServerAddress address, ConnectionPool connectionPool, Config config, RoutingSettings routingSettings, SecurityPlan securityPlan, - RetryLogic retryLogic ) + RetryLogic retryLogic, AsyncConnectionPool asyncConnectionPool ) { String scheme = uri.getScheme().toLowerCase(); switch ( scheme ) { case BOLT_URI_SCHEME: assertNoRoutingContext( uri, routingSettings ); - return createDirectDriver( address, connectionPool, config, securityPlan, retryLogic ); + return createDirectDriver( address, connectionPool, config, securityPlan, retryLogic, asyncConnectionPool ); case BOLT_ROUTING_URI_SCHEME: return createRoutingDriver( address, connectionPool, config, routingSettings, securityPlan, retryLogic ); default: @@ -107,9 +137,10 @@ private Driver createDriver( URI uri, BoltServerAddress address, ConnectionPool * This method is protected only for testing */ protected Driver createDirectDriver( BoltServerAddress address, ConnectionPool connectionPool, Config config, - SecurityPlan securityPlan, RetryLogic retryLogic ) + SecurityPlan securityPlan, RetryLogic retryLogic, AsyncConnectionPool asyncConnectionPool ) { - ConnectionProvider connectionProvider = new DirectConnectionProvider( address, connectionPool ); + ConnectionProvider connectionProvider = + new DirectConnectionProvider( address, connectionPool, asyncConnectionPool ); SessionFactory sessionFactory = createSessionFactory( connectionProvider, retryLogic, config ); return createDriver( config, securityPlan, sessionFactory ); } @@ -173,11 +204,10 @@ private static LoadBalancingStrategy createLoadBalancingStrategy( Config config, */ protected ConnectionPool createConnectionPool( AuthToken authToken, SecurityPlan securityPlan, Config config ) { - authToken = authToken == null ? AuthTokens.none() : authToken; - ConnectionSettings connectionSettings = new ConnectionSettings( authToken, config.connectionTimeoutMillis() ); PoolSettings poolSettings = new PoolSettings( config.maxIdleConnectionPoolSize(), - config.idleTimeBeforeConnectionTest(), config.maxConnectionLifetime() ); + config.idleTimeBeforeConnectionTest(), config.maxConnectionLifetimeMillis(), + config.maxConnectionPoolSize(), config.connectionAcquisitionTimeoutMillis() ); Connector connector = createConnector( connectionSettings, securityPlan, config.logging() ); return new SocketConnectionPool( poolSettings, connector, createClock(), config.logging() ); @@ -198,7 +228,7 @@ protected Clock createClock() *

* This method is protected only for testing */ - protected Connector createConnector( ConnectionSettings connectionSettings, SecurityPlan securityPlan, + protected Connector createConnector( final ConnectionSettings connectionSettings, SecurityPlan securityPlan, Logging logging ) { return new SocketConnector( connectionSettings, securityPlan, logging ); diff --git a/driver/src/main/java/org/neo4j/driver/internal/ExplicitTransaction.java b/driver/src/main/java/org/neo4j/driver/internal/ExplicitTransaction.java index 4701ec2314..8d0ff5a364 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/ExplicitTransaction.java +++ b/driver/src/main/java/org/neo4j/driver/internal/ExplicitTransaction.java @@ -21,24 +21,41 @@ import java.util.Collections; import java.util.Map; -import org.neo4j.driver.internal.spi.Collector; +import org.neo4j.driver.ResultResourcesHandler; +import org.neo4j.driver.internal.async.AsyncConnection; +import org.neo4j.driver.internal.async.InternalFuture; +import org.neo4j.driver.internal.async.InternalPromise; +import org.neo4j.driver.internal.async.QueryRunner; +import org.neo4j.driver.internal.handlers.BeginTxResponseHandler; +import org.neo4j.driver.internal.handlers.BookmarkResponseHandler; +import org.neo4j.driver.internal.handlers.CommitTxResponseHandler; +import org.neo4j.driver.internal.handlers.NoOpResponseHandler; +import org.neo4j.driver.internal.handlers.RollbackTxResponseHandler; import org.neo4j.driver.internal.spi.Connection; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.v1.Record; +import org.neo4j.driver.v1.Response; import org.neo4j.driver.v1.Statement; import org.neo4j.driver.v1.StatementResult; +import org.neo4j.driver.v1.StatementResultCursor; import org.neo4j.driver.v1.Transaction; import org.neo4j.driver.v1.Value; import org.neo4j.driver.v1.Values; import org.neo4j.driver.v1.exceptions.ClientException; import org.neo4j.driver.v1.exceptions.Neo4jException; import org.neo4j.driver.v1.types.TypeSystem; +import org.neo4j.driver.v1.util.Function; +import static org.neo4j.driver.internal.util.ErrorUtil.isRecoverable; import static org.neo4j.driver.v1.Values.ofValue; import static org.neo4j.driver.v1.Values.value; -public class ExplicitTransaction implements Transaction +public class ExplicitTransaction implements Transaction, ResultResourcesHandler { + private static final String BEGIN_QUERY = "BEGIN"; + private static final String COMMIT_QUERY = "COMMIT"; + private static final String ROLLBACK_QUERY = "ROLLBACK"; + private enum State { /** The transaction is running with no explicit success or failure marked */ @@ -57,28 +74,68 @@ private enum State FAILED, /** This transaction has successfully committed */ - SUCCEEDED, + COMMITTED, /** This transaction has been rolled back */ ROLLED_BACK } private final SessionResourcesHandler resourcesHandler; - private final Connection conn; + private final Connection connection; + private final AsyncConnection asyncConnection; + private final NetworkSession session; - private Bookmark bookmark = Bookmark.empty(); - private State state = State.ACTIVE; + private volatile Bookmark bookmark = Bookmark.empty(); + private volatile State state = State.ACTIVE; - public ExplicitTransaction( Connection conn, SessionResourcesHandler resourcesHandler ) + public ExplicitTransaction( Connection connection, SessionResourcesHandler resourcesHandler ) { - this( conn, resourcesHandler, Bookmark.empty() ); + this.connection = connection; + this.asyncConnection = null; + this.session = null; + this.resourcesHandler = resourcesHandler; } - ExplicitTransaction( Connection conn, SessionResourcesHandler resourcesHandler, Bookmark initialBookmark ) + public ExplicitTransaction( AsyncConnection asyncConnection, NetworkSession session ) { - this.conn = conn; - this.resourcesHandler = resourcesHandler; - runBeginStatement( conn, initialBookmark ); + this.connection = null; + this.asyncConnection = asyncConnection; + this.session = session; + this.resourcesHandler = SessionResourcesHandler.NO_OP; + } + + public void begin( Bookmark initialBookmark ) + { + Map parameters = initialBookmark.asBeginTransactionParameters(); + + connection.run( BEGIN_QUERY, parameters, NoOpResponseHandler.INSTANCE ); + connection.pullAll( NoOpResponseHandler.INSTANCE ); + + if ( !initialBookmark.isEmpty() ) + { + connection.sync(); + } + } + + public InternalFuture beginAsync( Bookmark initialBookmark ) + { + InternalPromise beginTxPromise = asyncConnection.newPromise(); + + Map parameters = initialBookmark.asBeginTransactionParameters(); + asyncConnection.run( BEGIN_QUERY, parameters, NoOpResponseHandler.INSTANCE ); + + if ( initialBookmark.isEmpty() ) + { + asyncConnection.pullAll( NoOpResponseHandler.INSTANCE ); + beginTxPromise.setSuccess( this ); + } + else + { + asyncConnection.pullAll( new BeginTxResponseHandler<>( beginTxPromise, this ) ); + asyncConnection.flush(); + } + + return beginTxPromise; } @Override @@ -104,25 +161,26 @@ public void close() { try { - if ( conn != null && conn.isOpen() ) + if ( connection != null && connection.isOpen() ) { if ( state == State.MARKED_SUCCESS ) { try { - conn.run( "COMMIT", Collections.emptyMap(), Collector.NO_OP ); - conn.pullAll( new BookmarkCollector( this ) ); - conn.sync(); - state = State.SUCCEEDED; + connection.run( COMMIT_QUERY, Collections.emptyMap(), + NoOpResponseHandler.INSTANCE ); + connection.pullAll( new BookmarkResponseHandler( this ) ); + connection.sync(); + state = State.COMMITTED; } - catch( Throwable e ) + catch ( Throwable e ) { // failed to commit try { rollbackTx(); } - catch( Throwable ignored ) + catch ( Throwable ignored ) { // best effort. } @@ -149,32 +207,146 @@ else if ( state == State.FAILED ) private void rollbackTx() { - conn.run( "ROLLBACK", Collections.emptyMap(), Collector.NO_OP ); - conn.pullAll( new BookmarkCollector( this ) ); - conn.sync(); + connection.run( ROLLBACK_QUERY, Collections.emptyMap(), NoOpResponseHandler.INSTANCE ); + connection.pullAll( new BookmarkResponseHandler( this ) ); + connection.sync(); state = State.ROLLED_BACK; } @Override - @SuppressWarnings( "unchecked" ) + public Response commitAsync() + { + return internalCommitAsync(); + } + + private InternalFuture internalCommitAsync() + { + if ( state == State.COMMITTED ) + { + return asyncConnection.newPromise().setSuccess( null ); + } + else if ( state == State.ROLLED_BACK ) + { + return asyncConnection.newPromise().setFailure( + new ClientException( "Can't commit, transaction has already been rolled back" ) ); + } + else + { + return doCommitAsync().whenComplete( releaseConnectionAndNotifySession() ); + } + } + + @Override + public Response rollbackAsync() + { + return internalRollbackAsync(); + } + + InternalFuture internalRollbackAsync() + { + if ( state == State.COMMITTED ) + { + return asyncConnection.newPromise() + .setFailure( new ClientException( "Can't rollback, transaction has already been committed" ) ); + } + else if ( state == State.ROLLED_BACK ) + { + return asyncConnection.newPromise().setSuccess( null ); + } + else + { + return doRollbackAsync().whenComplete( releaseConnectionAndNotifySession() ); + } + } + + private Runnable releaseConnectionAndNotifySession() + { + return new Runnable() + { + @Override + public void run() + { + asyncConnection.release(); + session.asyncTransactionClosed( ExplicitTransaction.this ); + } + }; + } + + private InternalFuture doCommitAsync() + { + InternalPromise commitTxPromise = asyncConnection.newPromise(); + + asyncConnection.run( COMMIT_QUERY, Collections.emptyMap(), NoOpResponseHandler.INSTANCE ); + asyncConnection.pullAll( new CommitTxResponseHandler( commitTxPromise, this ) ); + asyncConnection.flush(); + + return commitTxPromise.thenApply( new Function() + { + @Override + public Void apply( Void ignore ) + { + ExplicitTransaction.this.state = State.COMMITTED; + return null; + } + } ); + } + + private InternalFuture doRollbackAsync() + { + InternalPromise rollbackTxPromise = asyncConnection.newPromise(); + asyncConnection.run( ROLLBACK_QUERY, Collections.emptyMap(), NoOpResponseHandler.INSTANCE ); + asyncConnection.pullAll( new RollbackTxResponseHandler( rollbackTxPromise ) ); + asyncConnection.flush(); + + return rollbackTxPromise.thenApply( new Function() + { + @Override + public Void apply( Void ignore ) + { + ExplicitTransaction.this.state = State.ROLLED_BACK; + return null; + } + } ); + } + + @Override public StatementResult run( String statementText, Value statementParameters ) { return run( new Statement( statementText, statementParameters ) ); } + @Override + public Response runAsync( String statementText, Value parameters ) + { + return runAsync( new Statement( statementText, parameters ) ); + } + @Override public StatementResult run( String statementText ) { return run( statementText, Values.EmptyMap ); } + @Override + public Response runAsync( String statementTemplate ) + { + return runAsync( statementTemplate, Values.EmptyMap ); + } + @Override public StatementResult run( String statementText, Map statementParameters ) { - Value params = statementParameters == null ? Values.EmptyMap : value(statementParameters); + Value params = statementParameters == null ? Values.EmptyMap : value( statementParameters ); return run( statementText, params ); } + @Override + public Response runAsync( String statementTemplate, Map statementParameters ) + { + Value params = statementParameters == null ? Values.EmptyMap : value( statementParameters ); + return runAsync( statementTemplate, params ); + } + @Override public StatementResult run( String statementTemplate, Record statementParameters ) { @@ -183,19 +355,26 @@ public StatementResult run( String statementTemplate, Record statementParameters } @Override - public synchronized StatementResult run( Statement statement ) + public Response runAsync( String statementTemplate, Record statementParameters ) + { + Value params = statementParameters == null ? Values.EmptyMap : value( statementParameters.asMap() ); + return runAsync( statementTemplate, params ); + } + + @Override + public StatementResult run( Statement statement ) { ensureNotFailed(); try { InternalStatementResult result = - new InternalStatementResult( conn, SessionResourcesHandler.NO_OP, this, statement ); - conn.run( statement.text(), + new InternalStatementResult( statement, connection, ResultResourcesHandler.NO_OP ); + connection.run( statement.text(), statement.parameters().asMap( ofValue() ), - result.runResponseCollector() ); - conn.pullAll( result.pullAllResponseCollector() ); - conn.flush(); + result.runResponseHandler() ); + connection.pullAll( result.pullAllResponseHandler() ); + connection.flush(); return result; } catch ( Neo4jException e ) @@ -207,10 +386,17 @@ public synchronized StatementResult run( Statement statement ) } } + @Override + public Response runAsync( Statement statement ) + { + ensureNotFailed(); + return QueryRunner.runAsync( asyncConnection, statement, this ); + } + @Override public boolean isOpen() { - return state != State.SUCCEEDED && state != State.ROLLED_BACK; + return state != State.COMMITTED && state != State.ROLLED_BACK; } private void ensureNotFailed() @@ -218,9 +404,9 @@ private void ensureNotFailed() if ( state == State.FAILED || state == State.MARKED_FAILED || state == State.ROLLED_BACK ) { throw new ClientException( - "Cannot run more statements in this transaction, because previous statements in the " + - "transaction has failed and the transaction has been rolled back. Please start a new" + - " transaction to run another statement." + "Cannot run more statements in this transaction, because previous statements in the " + + "transaction has failed and the transaction has been rolled back. Please start a new " + + "transaction to run another statement." ); } } @@ -231,7 +417,27 @@ public TypeSystem typeSystem() return InternalTypeSystem.TYPE_SYSTEM; } - public synchronized void markToClose() + @Override + public void resultFetched() + { + // no resources to release when result is fully fetched + } + + @Override + public void resultFailed( Throwable error ) + { + // RUN failed, this transaction should not commit + if ( isRecoverable( error ) ) + { + failure(); + } + else + { + markToClose(); + } + } + + public void markToClose() { state = State.FAILED; } @@ -241,25 +447,11 @@ public Bookmark bookmark() return bookmark; } - void setBookmark( Bookmark bookmark ) + public void setBookmark( Bookmark bookmark ) { if ( bookmark != null && !bookmark.isEmpty() ) { this.bookmark = bookmark; } } - - private static void runBeginStatement( Connection connection, Bookmark bookmark ) - { - Bookmark initialBookmark = bookmark == null ? Bookmark.empty() : bookmark; - Map parameters = initialBookmark.asBeginTransactionParameters(); - - connection.run( "BEGIN", parameters, Collector.NO_OP ); - connection.pullAll( Collector.NO_OP ); - - if ( !initialBookmark.isEmpty() ) - { - connection.sync(); - } - } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/InternalStatementResult.java b/driver/src/main/java/org/neo4j/driver/internal/InternalStatementResult.java index 23f2c785d5..21833b86fd 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/InternalStatementResult.java +++ b/driver/src/main/java/org/neo4j/driver/internal/InternalStatementResult.java @@ -19,27 +19,20 @@ package org.neo4j.driver.internal; import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedList; import java.util.List; -import java.util.Queue; -import org.neo4j.driver.internal.spi.Collector; +import org.neo4j.driver.ResultResourcesHandler; +import org.neo4j.driver.internal.handlers.RecordsResponseHandler; +import org.neo4j.driver.internal.handlers.RunResponseHandler; import org.neo4j.driver.internal.spi.Connection; -import org.neo4j.driver.internal.summary.SummaryBuilder; +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.internal.summary.InternalResultSummary; import org.neo4j.driver.v1.Record; import org.neo4j.driver.v1.Statement; import org.neo4j.driver.v1.StatementResult; -import org.neo4j.driver.v1.Value; import org.neo4j.driver.v1.exceptions.ClientException; import org.neo4j.driver.v1.exceptions.NoSuchRecordException; -import org.neo4j.driver.v1.summary.Notification; -import org.neo4j.driver.v1.summary.Plan; -import org.neo4j.driver.v1.summary.ProfiledPlan; import org.neo4j.driver.v1.summary.ResultSummary; -import org.neo4j.driver.v1.summary.ServerInfo; -import org.neo4j.driver.v1.summary.StatementType; -import org.neo4j.driver.v1.summary.SummaryCounters; import org.neo4j.driver.v1.util.Function; import org.neo4j.driver.v1.util.Functions; @@ -47,144 +40,39 @@ public class InternalStatementResult implements StatementResult { + private final Statement statement; private final Connection connection; - private final SessionResourcesHandler resourcesHandler; - private final Collector runResponseCollector; - private final Collector pullAllResponseCollector; - private final Queue recordBuffer = new LinkedList<>(); + private final ResultResourcesHandler resourcesHandler; + private final RunResponseHandler runResponseHandler; + private final RecordsResponseHandler pullAllResponseHandler; - private List keys = null; - private ResultSummary summary = null; - - private boolean done = false; - - InternalStatementResult( Connection connection, SessionResourcesHandler resourcesHandler, - ExplicitTransaction transaction, Statement statement ) + InternalStatementResult( Statement statement, Connection connection, ResultResourcesHandler resourcesHandler ) { + this.statement = statement; this.connection = connection; - this.runResponseCollector = newRunResponseCollector(); - this.pullAllResponseCollector = newStreamResponseCollector( transaction, statement, connection.server() ); + this.runResponseHandler = new RunResponseHandler( null ); + this.pullAllResponseHandler = new RecordsResponseHandler( runResponseHandler ); this.resourcesHandler = resourcesHandler; } - private Collector newRunResponseCollector() + ResponseHandler runResponseHandler() { - return new Collector.NoOperationCollector() - { - @Override - public void keys( String[] names ) - { - keys = Arrays.asList( names ); - } - - @Override - public void done() - { - if ( keys == null ) - { - keys = new ArrayList<>(); - } - } - - @Override - public void resultAvailableAfter( long l ) - { - pullAllResponseCollector.resultAvailableAfter( l ); - } - }; + return runResponseHandler; } - private Collector newStreamResponseCollector( final ExplicitTransaction transaction, final Statement statement, - final ServerInfo serverInfo ) + ResponseHandler pullAllResponseHandler() { - final SummaryBuilder summaryBuilder = new SummaryBuilder( statement, serverInfo ); - - return new Collector.NoOperationCollector() - { - @Override - public void record( Value[] fields ) - { - recordBuffer.add( new InternalRecord( keys, fields ) ); - } - - @Override - public void statementType( StatementType type ) - { - summaryBuilder.statementType( type ); - } - - @Override - public void statementStatistics( SummaryCounters statistics ) - { - summaryBuilder.statementStatistics( statistics ); - } - - @Override - public void plan( Plan plan ) - { - summaryBuilder.plan( plan ); - } - - @Override - public void profile( ProfiledPlan plan ) - { - summaryBuilder.profile( plan ); - } - - @Override - public void notifications( List notifications ) - { - summaryBuilder.notifications( notifications ); - } - - @Override - public void bookmark( Bookmark bookmark ) - { - if ( transaction != null ) - { - transaction.setBookmark( bookmark ); - } - } - - @Override - public void done() - { - summary = summaryBuilder.build(); - done = true; - } - - @Override - public void resultAvailableAfter(long l) - { - summaryBuilder.resultAvailableAfter( l ); - } - - @Override - public void resultConsumedAfter(long l) - { - summaryBuilder.resultConsumedAfter( l ); - } - }; - } - - Collector runResponseCollector() - { - return runResponseCollector; - } - - Collector pullAllResponseCollector() - { - return pullAllResponseCollector; + return pullAllResponseHandler; } @Override public List keys() { - if ( keys == null ) + if ( runResponseHandler.statementKeys() == null ) { tryFetchNext(); } - return keys; + return runResponseHandler.statementKeys(); } @Override @@ -196,15 +84,9 @@ public boolean hasNext() @Override public Record next() { - // Implementation note: - // We've chosen to use Iterator over a cursor-based version, - // after tests show escape analysis will eliminate short-lived allocations - // in a way that makes the two equivalent in performance. - // To get the intended benefit, we need to allocate Record in this method, - // and have it copy out its fields from some lower level data structure. if ( tryFetchNext() ) { - return recordBuffer.poll(); + return pullAllResponseHandler.recordBuffer().poll(); } else { @@ -242,7 +124,7 @@ public Record peek() { if ( tryFetchNext() ) { - return recordBuffer.peek(); + return pullAllResponseHandler.recordBuffer().peek(); } else { @@ -280,32 +162,32 @@ public List list( Function mapFunction ) @Override public ResultSummary consume() { - if ( done ) + if ( pullAllResponseHandler.isCompleted() ) { - recordBuffer.clear(); + pullAllResponseHandler.recordBuffer().clear(); } else { do { receiveOne(); - recordBuffer.clear(); + pullAllResponseHandler.recordBuffer().clear(); } - while ( !done ); + while ( !pullAllResponseHandler.isCompleted() ); } - return summary; + return createResultSummary(); } @Override public ResultSummary summary() { - while( !done ) + while ( !pullAllResponseHandler.isCompleted() ) { receiveOne(); } - return summary; + return createResultSummary(); } @Override @@ -316,9 +198,9 @@ public void remove() private boolean tryFetchNext() { - while ( recordBuffer.isEmpty() ) + while ( pullAllResponseHandler.recordBuffer().isEmpty() ) { - if ( done ) + if ( pullAllResponseHandler.isCompleted() ) { return false; } @@ -336,12 +218,27 @@ private void receiveOne() } catch ( Throwable error ) { - resourcesHandler.onResultConsumed(); + resourcesHandler.resultFailed( error ); throw error; } - if ( done ) + if ( pullAllResponseHandler.isCompleted() ) { - resourcesHandler.onResultConsumed(); + resourcesHandler.resultFetched(); } } + + private ResultSummary createResultSummary() + { + return new InternalResultSummary( + statement, + connection.server(), + pullAllResponseHandler.statementType(), + pullAllResponseHandler.counters(), + pullAllResponseHandler.plan(), + pullAllResponseHandler.profile(), + pullAllResponseHandler.notifications(), + runResponseHandler.resultAvailableAfter(), + pullAllResponseHandler.resultConsumedAfter() + ); + } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/NetworkSession.java b/driver/src/main/java/org/neo4j/driver/internal/NetworkSession.java index 8f2c78fe7d..51881a2c2b 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/NetworkSession.java +++ b/driver/src/main/java/org/neo4j/driver/internal/NetworkSession.java @@ -18,9 +18,16 @@ */ package org.neo4j.driver.internal; +import io.netty.util.concurrent.GlobalEventExecutor; + import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import org.neo4j.driver.ResultResourcesHandler; +import org.neo4j.driver.internal.async.AsyncConnection; +import org.neo4j.driver.internal.async.InternalFuture; +import org.neo4j.driver.internal.async.InternalPromise; +import org.neo4j.driver.internal.async.QueryRunner; import org.neo4j.driver.internal.logging.DelegatingLogger; import org.neo4j.driver.internal.retry.RetryLogic; import org.neo4j.driver.internal.spi.Connection; @@ -32,19 +39,22 @@ import org.neo4j.driver.v1.Logger; import org.neo4j.driver.v1.Logging; import org.neo4j.driver.v1.Record; +import org.neo4j.driver.v1.Response; import org.neo4j.driver.v1.Session; import org.neo4j.driver.v1.Statement; import org.neo4j.driver.v1.StatementResult; +import org.neo4j.driver.v1.StatementResultCursor; import org.neo4j.driver.v1.Transaction; import org.neo4j.driver.v1.TransactionWork; import org.neo4j.driver.v1.Value; import org.neo4j.driver.v1.Values; import org.neo4j.driver.v1.exceptions.ClientException; import org.neo4j.driver.v1.types.TypeSystem; +import org.neo4j.driver.v1.util.Function; import static org.neo4j.driver.v1.Values.value; -public class NetworkSession implements Session, SessionResourcesHandler +public class NetworkSession implements Session, SessionResourcesHandler, ResultResourcesHandler { private static final String LOG_NAME = "Session"; @@ -53,9 +63,12 @@ public class NetworkSession implements Session, SessionResourcesHandler private final RetryLogic retryLogic; protected final Logger logger; - private Bookmark bookmark = Bookmark.empty(); + private volatile Bookmark bookmark = Bookmark.empty(); private PooledConnection currentConnection; private ExplicitTransaction currentTransaction; + private volatile InternalFuture currentAsyncTransactionFuture; + + private InternalFuture asyncConnectionFuture; private final AtomicBoolean isOpen = new AtomicBoolean( true ); @@ -74,6 +87,12 @@ public StatementResult run( String statementText ) return run( statementText, Values.EmptyMap ); } + @Override + public Response runAsync( String statementText ) + { + return runAsync( statementText, Values.EmptyMap ); + } + @Override public StatementResult run( String statementText, Map statementParameters ) { @@ -81,6 +100,13 @@ public StatementResult run( String statementText, Map statementPa return run( statementText, params ); } + @Override + public Response runAsync( String statementText, Map statementParameters ) + { + Value params = statementParameters == null ? Values.EmptyMap : value( statementParameters ); + return runAsync( statementText, params ); + } + @Override public StatementResult run( String statementTemplate, Record statementParameters ) { @@ -88,12 +114,25 @@ public StatementResult run( String statementTemplate, Record statementParameters return run( statementTemplate, params ); } + @Override + public Response runAsync( String statementTemplate, Record statementParameters ) + { + Value params = statementParameters == null ? Values.EmptyMap : value( statementParameters.asMap() ); + return runAsync( statementTemplate, params ); + } + @Override public StatementResult run( String statementText, Value statementParameters ) { return run( new Statement( statementText, statementParameters ) ); } + @Override + public Response runAsync( String statementText, Value parameters ) + { + return runAsync( new Statement( statementText, parameters ) ); + } + @Override public StatementResult run( Statement statement ) { @@ -106,13 +145,31 @@ public StatementResult run( Statement statement ) return run( currentConnection, statement, this ); } + @Override + public Response runAsync( final Statement statement ) + { + ensureSessionIsOpen(); + ensureNoOpenTransactionBeforeRunningSession(); + + InternalFuture connectionFuture = acquireAsyncConnection( mode ); + + return connectionFuture.thenCombine( new Function>() + { + @Override + public InternalFuture apply( AsyncConnection connection ) + { + return QueryRunner.runAsync( connection, statement ); + } + } ); + } + public static StatementResult run( Connection connection, Statement statement, - SessionResourcesHandler resourcesHandler ) + ResultResourcesHandler resourcesHandler ) { - InternalStatementResult result = new InternalStatementResult( connection, resourcesHandler, null, statement ); + InternalStatementResult result = new InternalStatementResult( statement, connection, resourcesHandler ); connection.run( statement.text(), statement.parameters().asMap( Values.ofValue() ), - result.runResponseCollector() ); - connection.pullAll( result.pullAllResponseCollector() ); + result.runResponseHandler() ); + connection.pullAll( result.pullAllResponseHandler() ); connection.flush(); return result; } @@ -167,6 +224,46 @@ public void close() } syncAndCloseCurrentConnection(); + + try + { + closeAsync().get(); + } + catch ( Exception e ) + { + throw new RuntimeException( e ); + } + } + + @Override + public Response closeAsync() + { + if ( asyncConnectionFuture != null ) + { + return asyncConnectionFuture.thenCombine( new Function>() + { + @Override + public InternalFuture apply( AsyncConnection connection ) + { + return connection.forceRelease(); + } + } ); + } + else if ( currentAsyncTransactionFuture != null ) + { + return currentAsyncTransactionFuture.thenCombine( new Function>() + { + @Override + public InternalFuture apply( ExplicitTransaction tx ) + { + return tx.internalRollbackAsync(); + } + } ); + } + else + { + return new InternalPromise( GlobalEventExecutor.INSTANCE ).setSuccess( null ); + } } @Override @@ -183,6 +280,12 @@ public synchronized Transaction beginTransaction( String bookmark ) return beginTransaction(); } + @Override + public Response beginTransactionAsync() + { + return beginTransactionAsync( mode ); + } + @Override public T readTransaction( TransactionWork work ) { @@ -221,6 +324,18 @@ public synchronized void onResultConsumed() closeCurrentConnection(); } + @Override + public void resultFetched() + { + closeCurrentConnection(); + } + + @Override + public void resultFailed( Throwable error ) + { + resultFetched(); + } + @Override public synchronized void onTransactionClosed( ExplicitTransaction tx ) { @@ -232,6 +347,12 @@ public synchronized void onTransactionClosed( ExplicitTransaction tx ) } } + public void asyncTransactionClosed( ExplicitTransaction tx ) + { + setBookmark( tx.bookmark() ); + currentAsyncTransactionFuture = null; + } + @Override public synchronized void onConnectionError( boolean recoverable ) { @@ -284,11 +405,35 @@ private synchronized Transaction beginTransaction( AccessMode mode ) syncAndCloseCurrentConnection(); currentConnection = acquireConnection( mode ); - currentTransaction = new ExplicitTransaction( currentConnection, this, bookmark); + ExplicitTransaction tx = new ExplicitTransaction( currentConnection, this ); + tx.begin( bookmark ); + currentTransaction = tx; currentConnection.setResourcesHandler( this ); return currentTransaction; } + private synchronized Response beginTransactionAsync( AccessMode mode ) + { + ensureSessionIsOpen(); + ensureNoOpenTransactionBeforeOpeningTransaction(); + + InternalFuture connectionFuture = acquireAsyncConnection( mode ); + + currentAsyncTransactionFuture = connectionFuture.thenCombine( + new Function>() + { + @Override + public InternalFuture apply( AsyncConnection connection ) + { + ExplicitTransaction tx = new ExplicitTransaction( connection, NetworkSession.this ); + return tx.beginAsync( bookmark ); + } + } ); + + //noinspection unchecked + return (Response) currentAsyncTransactionFuture; + } + private void ensureNoUnrecoverableError() { if ( currentConnection != null && currentConnection.hasUnrecoverableErrors() ) @@ -302,7 +447,7 @@ private void ensureNoUnrecoverableError() //should be called from a synchronized block private void ensureNoOpenTransactionBeforeRunningSession() { - if ( currentTransaction != null ) + if ( currentTransaction != null || currentAsyncTransactionFuture != null ) { throw new ClientException( "Statements cannot be run directly on a session with an open transaction;" + " either run from within the transaction or use a different session." ); @@ -312,7 +457,7 @@ private void ensureNoOpenTransactionBeforeRunningSession() //should be called from a synchronized block private void ensureNoOpenTransactionBeforeOpeningTransaction() { - if ( currentTransaction != null ) + if ( currentTransaction != null || currentAsyncTransactionFuture != null ) { throw new ClientException( "You cannot begin a transaction on a session with an open transaction;" + " either run from within the transaction or use a different session." ); @@ -339,6 +484,38 @@ private PooledConnection acquireConnection( AccessMode mode ) return connection; } + private InternalFuture acquireAsyncConnection( final AccessMode mode ) + { + if ( asyncConnectionFuture == null ) + { + asyncConnectionFuture = connectionProvider.acquireAsyncConnection( mode ); + } + else + { + // memorize in local so same instance is transformed and used in callbacks + final InternalFuture currentAsyncConnectionFuture = asyncConnectionFuture; + + asyncConnectionFuture = currentAsyncConnectionFuture.thenCombine( + new Function>() + { + @Override + public InternalFuture apply( AsyncConnection connection ) + { + if ( connection.tryMarkInUse() ) + { + return currentAsyncConnectionFuture; + } + else + { + return connectionProvider.acquireAsyncConnection( mode ); + } + } + } ); + } + + return asyncConnectionFuture; + } + boolean currentConnectionIsOpen() { return currentConnection != null && currentConnection.isOpen(); diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/AsyncConnection.java b/driver/src/main/java/org/neo4j/driver/internal/async/AsyncConnection.java new file mode 100644 index 0000000000..a5f75a59c0 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/AsyncConnection.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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 java.util.Map; + +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.v1.Value; +import org.neo4j.driver.v1.summary.ServerInfo; + +public interface AsyncConnection +{ + boolean tryMarkInUse(); + + void enableAutoRead(); + + void disableAutoRead(); + + void run( String statement, Map parameters, ResponseHandler handler ); + + void pullAll( ResponseHandler handler ); + + void flush(); + + InternalPromise newPromise(); + + void release(); + + InternalFuture forceRelease(); + + ServerInfo serverInfo(); +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/AsyncConnector.java b/driver/src/main/java/org/neo4j/driver/internal/async/AsyncConnector.java new file mode 100644 index 0000000000..aed2d9dabe --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/AsyncConnector.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; + +import org.neo4j.driver.internal.net.BoltServerAddress; + +public interface AsyncConnector +{ + ChannelFuture connect( BoltServerAddress address, Bootstrap bootstrap ); +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/AsyncConnectorImpl.java b/driver/src/main/java/org/neo4j/driver/internal/async/AsyncConnectorImpl.java new file mode 100644 index 0000000000..3b51abd726 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/AsyncConnectorImpl.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPromise; +import io.netty.channel.pool.ChannelPoolHandler; + +import java.util.Map; + +import org.neo4j.driver.internal.ConnectionSettings; +import org.neo4j.driver.internal.net.BoltServerAddress; +import org.neo4j.driver.internal.security.InternalAuthToken; +import org.neo4j.driver.internal.security.SecurityPlan; +import org.neo4j.driver.internal.util.Clock; +import org.neo4j.driver.v1.AuthToken; +import org.neo4j.driver.v1.AuthTokens; +import org.neo4j.driver.v1.Logging; +import org.neo4j.driver.v1.Value; +import org.neo4j.driver.v1.exceptions.ClientException; + +import static java.util.Objects.requireNonNull; + +public class AsyncConnectorImpl implements AsyncConnector +{ + private final String userAgent; + private final Map authToken; + private final SecurityPlan securityPlan; + private final int connectTimeoutMillis; + private final ChannelPoolHandler channelPoolHandler; + private final Logging logging; + private final Clock clock; + + public AsyncConnectorImpl( ConnectionSettings connectionSettings, SecurityPlan securityPlan, + ChannelPoolHandler channelPoolHandler, Logging logging, Clock clock ) + { + this.userAgent = connectionSettings.userAgent(); + this.authToken = tokenAsMap( connectionSettings.authToken() ); + this.connectTimeoutMillis = connectionSettings.connectTimeoutMillis(); + this.securityPlan = requireNonNull( securityPlan ); + this.channelPoolHandler = requireNonNull( channelPoolHandler ); + this.logging = requireNonNull( logging ); + this.clock = requireNonNull( clock ); + } + + @Override + public ChannelFuture connect( BoltServerAddress address, Bootstrap bootstrap ) + { + bootstrap.option( ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeoutMillis ); + bootstrap.handler( new NettyChannelInitializer( address, securityPlan, channelPoolHandler, clock ) ); + + ChannelFuture channelConnected = bootstrap.connect( address.toSocketAddress() ); + + Channel channel = channelConnected.channel(); + ChannelPromise handshakeCompleted = channel.newPromise(); + ChannelPromise connectionInitialized = channel.newPromise(); + + channelConnected.addListener( new ChannelConnectedListener( address, handshakeCompleted, logging ) ); + handshakeCompleted.addListener( new HandshakeCompletedListener( userAgent, authToken, connectionInitialized ) ); + + return connectionInitialized; + } + + private static Map tokenAsMap( AuthToken token ) + { + if ( token instanceof InternalAuthToken ) + { + return ((InternalAuthToken) token).toMap(); + } + else + { + throw new ClientException( + "Unknown authentication token, `" + token + "`. Please use one of the supported " + + "tokens from `" + AuthTokens.class.getSimpleName() + "`." ); + } + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/BootstrapFactory.java b/driver/src/main/java/org/neo4j/driver/internal/async/BootstrapFactory.java new file mode 100644 index 0000000000..40f5245e22 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/BootstrapFactory.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.bootstrap.Bootstrap; +import io.netty.channel.ChannelOption; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; + +public final class BootstrapFactory +{ + private BootstrapFactory() + { + } + + public static Bootstrap newBootstrap() + { + return newBootstrap( new NioEventLoopGroup() ); + } + + public static Bootstrap newBootstrap( int threadCount ) + { + return newBootstrap( new NioEventLoopGroup( threadCount ) ); + } + + private static Bootstrap newBootstrap( NioEventLoopGroup eventLoopGroup ) + { + Bootstrap bootstrap = new Bootstrap(); + bootstrap.group( eventLoopGroup ); + bootstrap.channel( NioSocketChannel.class ); + bootstrap.option( ChannelOption.SO_KEEPALIVE, true ); + bootstrap.option( ChannelOption.SO_REUSEADDR, true ); + return bootstrap; + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/ChannelAttributes.java b/driver/src/main/java/org/neo4j/driver/internal/async/ChannelAttributes.java new file mode 100644 index 0000000000..9654695118 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/ChannelAttributes.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.util.AttributeKey; + +import org.neo4j.driver.internal.async.inbound.InboundMessageDispatcher; +import org.neo4j.driver.internal.net.BoltServerAddress; + +import static io.netty.util.AttributeKey.newInstance; + +public final class ChannelAttributes +{ + private static final AttributeKey ADDRESS = newInstance( "address" ); + private static final AttributeKey CREATION_TIMESTAMP = newInstance( "creationTimestamp" ); + private static final AttributeKey LAST_USED_TIMESTAMP = newInstance( "lastUsedTimestamp" ); + private static final AttributeKey MESSAGE_DISPATCHER = newInstance( "messageDispatcher" ); + private static final AttributeKey SERVER_VERSION = newInstance( "serverVersion" ); + + private ChannelAttributes() + { + } + + public static BoltServerAddress address( Channel channel ) + { + return get( channel, ADDRESS ); + } + + public static void setAddress( Channel channel, BoltServerAddress address ) + { + setOnce( channel, ADDRESS, address ); + } + + public static long creationTimestamp( Channel channel ) + { + return get( channel, CREATION_TIMESTAMP ); + } + + public static void setCreationTimestamp( Channel channel, long creationTimestamp ) + { + setOnce( channel, CREATION_TIMESTAMP, creationTimestamp ); + } + + public static Long lastUsedTimestamp( Channel channel ) + { + return get( channel, LAST_USED_TIMESTAMP ); + } + + public static void setLastUsedTimestamp( Channel channel, long lastUsedTimestamp ) + { + set( channel, LAST_USED_TIMESTAMP, lastUsedTimestamp ); + } + + public static InboundMessageDispatcher messageDispatcher( Channel channel ) + { + return get( channel, MESSAGE_DISPATCHER ); + } + + public static void setMessageDispatcher( Channel channel, InboundMessageDispatcher messageDispatcher ) + { + setOnce( channel, MESSAGE_DISPATCHER, messageDispatcher ); + } + + public static String serverVersion( Channel channel ) + { + return get( channel, SERVER_VERSION ); + } + + public static void setServerVersion( Channel channel, String serverVersion ) + { + setOnce( channel, SERVER_VERSION, serverVersion ); + } + + private static T get( Channel channel, AttributeKey key ) + { + return channel.attr( key ).get(); + } + + private static void set( Channel channel, AttributeKey key, T value ) + { + channel.attr( key ).set( value ); + } + + private static void setOnce( Channel channel, AttributeKey key, T value ) + { + T existingValue = channel.attr( key ).setIfAbsent( value ); + if ( existingValue != null ) + { + throw new IllegalStateException( + "Unable to set " + key.name() + " because it is already set to " + existingValue ); + } + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/ChannelConnectedListener.java b/driver/src/main/java/org/neo4j/driver/internal/async/ChannelConnectedListener.java new file mode 100644 index 0000000000..2382cfb23d --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/ChannelConnectedListener.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelPromise; + +import org.neo4j.driver.internal.net.BoltServerAddress; +import org.neo4j.driver.v1.Logging; +import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static org.neo4j.driver.internal.async.ProtocolUtil.handshake; + +public class ChannelConnectedListener implements ChannelFutureListener +{ + private final BoltServerAddress address; + private final ChannelPromise handshakeCompletedPromise; + private final Logging logging; + + public ChannelConnectedListener( BoltServerAddress address, ChannelPromise handshakeCompletedPromise, + Logging logging ) + { + this.address = requireNonNull( address ); + this.handshakeCompletedPromise = requireNonNull( handshakeCompletedPromise ); + this.logging = requireNonNull( logging ); + } + + @Override + public void operationComplete( ChannelFuture future ) + { + Channel channel = future.channel(); + + if ( future.isSuccess() ) + { + channel.pipeline().addLast( new HandshakeResponseHandler( handshakeCompletedPromise, logging ) ); + ChannelFuture handshakeFuture = channel.writeAndFlush( handshake() ); + + handshakeFuture.addListener( new ChannelFutureListener() + { + @Override + public void operationComplete( ChannelFuture future ) throws Exception + { + if ( !future.isSuccess() ) + { + handshakeCompletedPromise.setFailure( future.cause() ); + } + } + } ); + } + else + { + handshakeCompletedPromise.setFailure( databaseUnavailableError( address, future.cause() ) ); + } + } + + private static Throwable databaseUnavailableError( BoltServerAddress address, Throwable cause ) + { + return new ServiceUnavailableException( format( + "Unable to connect to %s, ensure the database is running and that there " + + "is a working network connection to it.", address ), cause ); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/Futures.java b/driver/src/main/java/org/neo4j/driver/internal/async/Futures.java new file mode 100644 index 0000000000..8f14c7d182 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/Futures.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.bootstrap.Bootstrap; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import io.netty.util.concurrent.Promise; + +import org.neo4j.driver.v1.util.Function; + +public final class Futures +{ + private Futures() + { + } + + public static InternalFuture thenApply( InternalFuture future, Function fn ) + { + InternalPromise result = new InternalPromise<>( future.eventExecutor() ); + future.addListener( new ThenApplyListener<>( result, fn ) ); + return result; + } + + public static InternalFuture thenApply( Future future, Bootstrap bootstrap, Function fn ) + { + InternalPromise result = new InternalPromise<>( bootstrap ); + future.addListener( new ThenApplyListener<>( result, fn ) ); + return result; + } + + public static InternalFuture thenCombine( InternalFuture future, Function> fn ) + { + InternalPromise result = new InternalPromise<>( future.eventExecutor() ); + future.addListener( new ThenCombineListener<>( result, fn ) ); + return result; + } + + public static InternalFuture whenComplete( InternalFuture future, Runnable action ) + { + InternalPromise result = new InternalPromise<>( future.eventExecutor() ); + future.addListener( new CompletionListener<>( result, action ) ); + return result; + } + + private static class ThenApplyListener implements GenericFutureListener> + { + final Promise result; + final Function fn; + + ThenApplyListener( Promise result, Function fn ) + { + this.result = result; + this.fn = fn; + } + + @Override + public void operationComplete( Future future ) throws Exception + { + if ( future.isCancelled() ) + { + result.cancel( true ); + } + else if ( future.isSuccess() ) + { + try + { + T originalValue = future.getNow(); + U newValue = fn.apply( originalValue ); + result.setSuccess( newValue ); + } + catch ( Throwable t ) + { + result.setFailure( t ); + } + } + else + { + result.setFailure( future.cause() ); + } + } + } + + private static class ThenCombineListener implements GenericFutureListener> + { + final Promise result; + final Function> fn; + + ThenCombineListener( Promise result, Function> fn ) + { + this.result = result; + this.fn = fn; + } + + @Override + public void operationComplete( Future future ) throws Exception + { + if ( future.isCancelled() ) + { + result.cancel( true ); + } + else if ( future.isSuccess() ) + { + try + { + T value = future.getNow(); + InternalFuture newFuture = fn.apply( value ); + newFuture.addListener( new NestedThenCombineListener<>( result, newFuture ) ); + } + catch ( Throwable t ) + { + result.setFailure( t ); + } + } + else + { + result.setFailure( future.cause() ); + } + } + } + + private static class NestedThenCombineListener implements GenericFutureListener> + { + + final Promise result; + final Future future; + + NestedThenCombineListener( Promise result, Future future ) + { + this.result = result; + this.future = future; + } + + @Override + public void operationComplete( Future future ) throws Exception + { + if ( future.isCancelled() ) + { + result.cancel( true ); + } + else if ( future.isSuccess() ) + { + result.setSuccess( future.getNow() ); + } + else + { + result.setFailure( future.cause() ); + } + } + } + + private static class CompletionListener implements GenericFutureListener> + { + final Promise result; + final Runnable action; + + CompletionListener( Promise result, Runnable action ) + { + this.result = result; + this.action = action; + } + + @Override + public void operationComplete( Future future ) throws Exception + { + if ( future.isCancelled() ) + { + result.cancel( true ); + } + else if ( future.isSuccess() ) + { + try + { + action.run(); + result.setSuccess( future.getNow() ); + } + catch ( Throwable t ) + { + result.setFailure( t ); + } + } + else + { + Throwable error = future.cause(); + try + { + action.run(); + } + catch ( Throwable t ) + { + error.addSuppressed( t ); + } + result.setFailure( error ); + } + } + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/HandshakeCompletedListener.java b/driver/src/main/java/org/neo4j/driver/internal/async/HandshakeCompletedListener.java new file mode 100644 index 0000000000..3094b13c79 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/HandshakeCompletedListener.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelPromise; + +import java.util.Map; + +import org.neo4j.driver.internal.handlers.AsyncInitResponseHandler; +import org.neo4j.driver.internal.messaging.InitMessage; +import org.neo4j.driver.v1.Value; + +import static java.util.Objects.requireNonNull; +import static org.neo4j.driver.internal.async.ChannelAttributes.messageDispatcher; + +public class HandshakeCompletedListener implements ChannelFutureListener +{ + private final String userAgent; + private final Map authToken; + private final ChannelPromise connectionInitializedPromise; + + public HandshakeCompletedListener( String userAgent, Map authToken, + ChannelPromise connectionInitializedPromise ) + { + this.userAgent = requireNonNull( userAgent ); + this.authToken = requireNonNull( authToken ); + this.connectionInitializedPromise = requireNonNull( connectionInitializedPromise ); + } + + @Override + public void operationComplete( ChannelFuture future ) + { + if ( future.isSuccess() ) + { + Channel channel = future.channel(); + + InitMessage message = new InitMessage( userAgent, authToken ); + AsyncInitResponseHandler handler = new AsyncInitResponseHandler( connectionInitializedPromise ); + + messageDispatcher( channel ).queue( handler ); + channel.writeAndFlush( message ); + } + else + { + connectionInitializedPromise.setFailure( future.cause() ); + } + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/HandshakeResponseHandler.java b/driver/src/main/java/org/neo4j/driver/internal/async/HandshakeResponseHandler.java new file mode 100644 index 0000000000..c9e1f6d2d3 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/HandshakeResponseHandler.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.ReplayingDecoder; + +import java.util.List; + +import org.neo4j.driver.internal.async.inbound.ChunkDecoder; +import org.neo4j.driver.internal.async.inbound.InboundMessageHandler; +import org.neo4j.driver.internal.async.inbound.MessageDecoder; +import org.neo4j.driver.internal.async.outbound.OutboundMessageHandler; +import org.neo4j.driver.internal.messaging.MessageFormat; +import org.neo4j.driver.internal.messaging.PackStreamMessageFormatV1; +import org.neo4j.driver.v1.Logger; +import org.neo4j.driver.v1.Logging; +import org.neo4j.driver.v1.exceptions.ClientException; + +import static java.util.Objects.requireNonNull; +import static org.neo4j.driver.internal.async.ProtocolUtil.HTTP; +import static org.neo4j.driver.internal.async.ProtocolUtil.NO_PROTOCOL_VERSION; +import static org.neo4j.driver.internal.async.ProtocolUtil.PROTOCOL_VERSION_1; +import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; + +public class HandshakeResponseHandler extends ReplayingDecoder +{ + private final ChannelPromise handshakeCompletedPromise; + private final Logger log; + + public HandshakeResponseHandler( ChannelPromise handshakeCompletedPromise, Logging logging ) + { + this.handshakeCompletedPromise = requireNonNull( handshakeCompletedPromise ); + this.log = logging.getLog( getClass().getSimpleName() ); + } + + @Override + public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause ) throws Exception + { + fail( ctx, cause ); + } + + @Override + protected void decode( ChannelHandlerContext ctx, ByteBuf in, List out ) throws Exception + { + int serverSuggestedVersion = in.readInt(); + log.debug( "Server suggested protocol version: %s", serverSuggestedVersion ); + + switch ( serverSuggestedVersion ) + { + case PROTOCOL_VERSION_1: + + MessageFormat format = new PackStreamMessageFormatV1(); + ChannelPipeline pipeline = ctx.pipeline(); + + pipeline.remove( this ); + + // inbound handlers + pipeline.addLast( new ChunkDecoder() ); + pipeline.addLast( new MessageDecoder() ); + pipeline.addLast( new InboundMessageHandler( format, DEV_NULL_LOGGING ) ); + + // outbound handlers + pipeline.addLast( OutboundMessageHandler.NAME, new OutboundMessageHandler( format, DEV_NULL_LOGGING ) ); + + handshakeCompletedPromise.setSuccess(); + + break; + case NO_PROTOCOL_VERSION: + fail( ctx, protocolNoSupportedByServerError() ); + break; + case HTTP: + fail( ctx, httpEndpointError() ); + break; + default: + fail( ctx, protocolNoSupportedByDriverError( serverSuggestedVersion ) ); + break; + } + } + + private void fail( ChannelHandlerContext ctx, Throwable error ) + { + ctx.close(); + handshakeCompletedPromise.setFailure( error ); + } + + private static Throwable protocolNoSupportedByServerError() + { + return new ClientException( "The server does not support any of the protocol versions supported by " + + "this driver. Ensure that you are using driver and server versions that " + + "are compatible with one another." ); + } + + private static Throwable httpEndpointError() + { + return new ClientException( + "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)" ); + } + + private static Throwable protocolNoSupportedByDriverError( int suggestedProtocolVersion ) + { + return new ClientException( + "Protocol error, server suggested unexpected protocol version: " + suggestedProtocolVersion ); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/InternalFuture.java b/driver/src/main/java/org/neo4j/driver/internal/async/InternalFuture.java new file mode 100644 index 0000000000..54245824be --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/InternalFuture.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.util.concurrent.EventExecutor; +import io.netty.util.concurrent.Future; + +import org.neo4j.driver.v1.Response; +import org.neo4j.driver.v1.util.Function; + +public interface InternalFuture extends Future, Response +{ + EventExecutor eventExecutor(); + + InternalFuture thenApply( Function fn ); + + InternalFuture thenCombine( Function> fn ); + + InternalFuture whenComplete( Runnable action ); +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/InternalPromise.java b/driver/src/main/java/org/neo4j/driver/internal/async/InternalPromise.java new file mode 100644 index 0000000000..dd1db10afd --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/InternalPromise.java @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.bootstrap.Bootstrap; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; +import io.netty.util.concurrent.GenericFutureListener; +import io.netty.util.concurrent.Promise; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.neo4j.driver.v1.ResponseListener; +import org.neo4j.driver.v1.util.Function; + +public class InternalPromise implements InternalFuture, Promise +{ + private final EventExecutor eventExecutor; + private final Promise delegate; + + public InternalPromise( Bootstrap bootstrap ) + { + this( bootstrap.config().group().next() ); + } + + public InternalPromise( EventExecutor eventExecutor ) + { + this.eventExecutor = eventExecutor; + this.delegate = eventExecutor.newPromise(); + } + + @Override + public EventExecutor eventExecutor() + { + return eventExecutor; + } + + @Override + public InternalFuture thenApply( Function fn ) + { + return Futures.thenApply( this, fn ); + } + + @Override + public InternalFuture thenCombine( Function> fn ) + { + return Futures.thenCombine( this, fn ); + } + + @Override + public InternalFuture whenComplete( Runnable action ) + { + return Futures.whenComplete( this, action ); + } + + @Override + public void addListener( final ResponseListener listener ) + { + delegate.addListener( new FutureListener() + { + @Override + public void operationComplete( Future future ) + { + if ( future.isSuccess() ) + { + listener.operationCompleted( future.getNow(), null ); + } + else + { + listener.operationCompleted( null, future.cause() ); + } + } + } ); + } + + @Override + public InternalPromise setSuccess( T result ) + { + delegate.setSuccess( result ); + return this; + } + + @Override + public boolean trySuccess( T result ) + { + return delegate.trySuccess( result ); + } + + @Override + public InternalPromise setFailure( Throwable cause ) + { + delegate.setFailure( cause ); + return this; + } + + @Override + public boolean tryFailure( Throwable cause ) + { + return delegate.tryFailure( cause ); + } + + @Override + public boolean setUncancellable() + { + return delegate.setUncancellable(); + } + + @Override + public InternalPromise addListener( GenericFutureListener> listener ) + { + delegate.addListener( listener ); + return this; + } + + @Override + public InternalPromise addListeners( GenericFutureListener>... listeners ) + { + delegate.addListeners( listeners ); + return this; + } + + @Override + public InternalPromise removeListener( GenericFutureListener> listener ) + { + delegate.removeListener( listener ); + return this; + } + + @Override + public InternalPromise removeListeners( GenericFutureListener>... listeners ) + { + delegate.removeListeners( listeners ); + return this; + } + + @Override + public InternalPromise await() throws InterruptedException + { + delegate.await(); + return this; + } + + @Override + public InternalPromise awaitUninterruptibly() + { + delegate.awaitUninterruptibly(); + return this; + } + + @Override + public InternalPromise sync() throws InterruptedException + { + delegate.sync(); + return this; + } + + @Override + public InternalPromise syncUninterruptibly() + { + delegate.syncUninterruptibly(); + return this; + } + + @Override + public boolean isSuccess() + { + return delegate.isSuccess(); + } + + @Override + public boolean isCancellable() + { + return delegate.isCancellable(); + } + + @Override + public Throwable cause() + { + return delegate.cause(); + } + + @Override + public boolean await( long timeout, TimeUnit unit ) throws InterruptedException + { + return delegate.await( timeout, unit ); + } + + @Override + public boolean await( long timeoutMillis ) throws InterruptedException + { + return delegate.await( timeoutMillis ); + } + + @Override + public boolean awaitUninterruptibly( long timeout, TimeUnit unit ) + { + return delegate.awaitUninterruptibly( timeout, unit ); + } + + @Override + public boolean awaitUninterruptibly( long timeoutMillis ) + { + return delegate.awaitUninterruptibly( timeoutMillis ); + } + + @Override + public T getNow() + { + return delegate.getNow(); + } + + @Override + public boolean cancel( boolean mayInterruptIfRunning ) + { + return delegate.cancel( mayInterruptIfRunning ); + } + + @Override + public boolean isCancelled() + { + return delegate.isCancelled(); + } + + @Override + public boolean isDone() + { + return delegate.isDone(); + } + + @Override + public T get() throws InterruptedException, ExecutionException + { + return delegate.get(); + } + + @Override + public T get( long timeout, TimeUnit unit ) throws InterruptedException, ExecutionException, TimeoutException + { + return delegate.get( timeout, unit ); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java b/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java new file mode 100644 index 0000000000..ca09fd5c01 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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 java.util.Collections; +import java.util.List; + +import org.neo4j.driver.internal.handlers.PullAllResponseHandler; +import org.neo4j.driver.internal.handlers.RunResponseHandler; +import org.neo4j.driver.v1.Record; +import org.neo4j.driver.v1.Response; +import org.neo4j.driver.v1.StatementResultCursor; +import org.neo4j.driver.v1.summary.ResultSummary; + +public class InternalStatementResultCursor implements StatementResultCursor +{ + private final RunResponseHandler runResponseHandler; + private final PullAllResponseHandler pullAllHandler; + + public InternalStatementResultCursor( RunResponseHandler runResponseHandler, PullAllResponseHandler pullAllHandler ) + { + this.runResponseHandler = runResponseHandler; + this.pullAllHandler = pullAllHandler; + } + + @Override + public List keys() + { + List keys = runResponseHandler.statementKeys(); + return keys == null ? Collections.emptyList() : Collections.unmodifiableList( keys ); + } + + @Override + public Response summaryAsync() + { + return pullAllHandler.summaryAsync(); + } + + @Override + public Response fetchAsync() + { + return pullAllHandler.fetchRecordAsync(); + } + + @Override + public Record current() + { + return pullAllHandler.currentRecord(); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/Main.java b/driver/src/main/java/org/neo4j/driver/internal/async/Main.java new file mode 100644 index 0000000000..2abd234d4c --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/Main.java @@ -0,0 +1,328 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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 java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.neo4j.driver.v1.AuthToken; +import org.neo4j.driver.v1.AuthTokens; +import org.neo4j.driver.v1.Config; +import org.neo4j.driver.v1.Driver; +import org.neo4j.driver.v1.GraphDatabase; +import org.neo4j.driver.v1.Record; +import org.neo4j.driver.v1.Response; +import org.neo4j.driver.v1.Session; +import org.neo4j.driver.v1.StatementResult; +import org.neo4j.driver.v1.StatementResultCursor; +import org.neo4j.driver.v1.Transaction; + +public class Main +{ + private static final int ITERATIONS = 100; + + private static final String QUERY1 = "MATCH (n:ActiveItem) RETURN n LIMIT 10000"; + + private static final String QUERY = + "MATCH (s:Sku{sku_no: {skuNo}})-[:HAS_ITEM_SOURCE]->(i:ItemSource{itemsource: {itemSource}})\n" + + "//Get master sku for auxiliary item\n" + + "OPTIONAL MATCH (s)-[:AUXILIARY_FOR]->(master_sku:Sku) WHERE NOT s.display_auxiliary_content\n" + + "//Get New item for Used item\n" + + "OPTIONAL MATCH (s)-[:USED_VERSION_OF]->(new_sku:Sku)\n" + + "//Get other items like kit details and bundle includes\n" + + "OPTIONAL MATCH (s)-[r:RELATED_ITEM]->(ri:Sku)\n" + + "WITH i, r, coalesce(ri, master_sku, new_sku, s) as sku, coalesce(master_sku, new_sku, s) as final_sku\n" + + "OPTIONAL MATCH (sku)-[:DESCRIBED_AS]->(d:Desc)\n" + + "OPTIONAL MATCH (sku)-[:FEATURED_WITH]->(f:Feature)\n" + + "//Get itemsource of related item\n" + + "OPTIONAL MATCH (sku)-[:HAS_ITEM_SOURCE]->(relatedItemSource:ItemSource)" + + "<-[:KIT_CONTAINS|INCLUDES_ITEMSOURCE*1..2]-(i)\n" + + "WITH i, final_sku, sku, d, f, relatedItemSource, r\n" + + "\tORDER BY f.seqnum\n" + + "WITH final_sku, sku, r, d, i, relatedItemSource, CASE WHEN f IS NOT null THEN collect({\n" + + "\ttitle: f.title,\n" + + "\tbody: f.body,\n" + + "\tisHeader: f.is_header,\n" + + "\tnote: f.note\n" + + "}) END as featureList ORDER BY coalesce(r.seqnum,0)\n" + + "//Get description of kit header or bundle heder\n" + + "OPTIONAL MATCH (final_sku)-[:DESCRIBED_AS]->(mainDescription:Desc) WHERE r is not null\n" + + "RETURN\n" + + "collect(DISTINCT CASE WHEN mainDescription is not null THEN\n" + + "{\n" + + "\titemName: null,\n" + + "\tdescription: {\n" + + "\t\ttext: mainDescription.description,\n" + + "\t\tnote: mainDescription.description_note\n" + + "\t},\n" + + "\tfeatures: {\n" + + "\t \tnote: null,\n" + + "\t\tfeatureList: null\n" + + "\t},\n" + + "\tupc: i.upc,\n" + + "\thasThirdPartyContent: final_sku.has_third_party_content\n" + + "} END)\n" + + "+\n" + + "collect({\n" + + "\titemName: r.item_name,\n" + + "\tdescription: {\n" + + "\t\ttext: d.description,\n" + + "\t\tnote: d.description_note\n" + + "\t},\n" + + "\tfeatures: {\n" + + "\t\tnote: d.feature_note,\n" + + "\t\tfeatureList: featureList\n" + + "\t},\n" + + "\tupc: coalesce(relatedItemSource, i).upc,\n" + + "\thasThirdPartyContent: sku.has_third_party_content\n" + + "}) AS overview;\n"; + + private static final Map PARAMS_OBJ = new HashMap<>(); + + private static final String USER = "neo4j"; + private static final String PASSWORD = "test"; + private static final String HOST = "ec2-54-73-57-164.eu-west-1.compute.amazonaws.com"; + private static final int PORT = 7687; + private static final String URI = "bolt://" + HOST + ":" + PORT; + + static + { + PARAMS_OBJ.put( "skuNo", 366421 ); + PARAMS_OBJ.put( "itemSource", "REG" ); + PARAMS_OBJ.put( "catalogId", 2 ); + PARAMS_OBJ.put( "locale", "en" ); + Map tmpObj = new HashMap<>(); + tmpObj.put( "skuNo", 366421 ); + tmpObj.put( "itemSource", "REG" ); + PARAMS_OBJ.put( "itemList", Collections.singletonList( tmpObj ) ); + } + + public static void main( String[] args ) throws Throwable + { + testSessionRun(); + testSessionRunAsync(); + + testTxRun(); + testTxRunAsync(); + } + + private static void testSessionRun() throws Throwable + { + test( "Session#run()", new Action() + { + @Override + public void apply( Driver driver, MutableInt recordsRead ) + { + try ( Session session = driver.session() ) + { + StatementResult result = session.run( QUERY, PARAMS_OBJ ); + while ( result.hasNext() ) + { + Record record = result.next(); + useRecord( record ); + recordsRead.increment(); + } + } + } + } ); + } + + private static void testSessionRunAsync() throws Throwable + { + test( "Session#runAsync()", new Action() + { + @Override + public void apply( Driver driver, MutableInt recordsRead ) + { + Session session = driver.session(); + Response cursorResponse = session.runAsync( QUERY, PARAMS_OBJ ); + StatementResultCursor cursor = await( cursorResponse ); + while ( await( cursor.fetchAsync() ) ) + { + Record record = cursor.current(); + useRecord( record ); + recordsRead.increment(); + } + await( session.closeAsync() ); + } + } ); + } + + private static void testTxRun() throws Throwable + { + test( "Transaction#run()", new Action() + { + @Override + public void apply( Driver driver, MutableInt recordsRead ) + { + try ( Session session = driver.session(); + Transaction tx = session.beginTransaction() ) + { + StatementResult result = tx.run( QUERY, PARAMS_OBJ ); + while ( result.hasNext() ) + { + Record record = result.next(); + useRecord( record ); + recordsRead.increment(); + } + tx.success(); + } + } + } ); + } + + private static void testTxRunAsync() throws Throwable + { + test( "Transaction#runAsync()", new Action() + { + @Override + public void apply( Driver driver, MutableInt recordsRead ) + { + Session session = driver.session(); + Transaction tx = await( session.beginTransactionAsync() ); + StatementResultCursor cursor = await( tx.runAsync( QUERY, PARAMS_OBJ ) ); + while ( await( cursor.fetchAsync() ) ) + { + Record record = cursor.current(); + useRecord( record ); + recordsRead.increment(); + } + await( tx.commitAsync() ); + await( session.closeAsync() ); + } + } ); + } + + private static void test( String actionName, Action action ) throws Throwable + { + AuthToken authToken = AuthTokens.basic( USER, PASSWORD ); + Config config = Config.build().withoutEncryption().toConfig(); + + List timings = new ArrayList<>(); + MutableInt recordsRead = new MutableInt(); + + try ( Driver driver = GraphDatabase.driver( URI, authToken, config ) ) + { + for ( int i = 0; i < ITERATIONS; i++ ) + { + long start = System.nanoTime(); + + action.apply( driver, recordsRead ); + + long end = System.nanoTime(); + timings.add( TimeUnit.NANOSECONDS.toMillis( end - start ) ); + } + } + + timings = clean( timings ); + + System.out.println( "============================================================" ); + System.out.println( actionName + ": mean --> " + mean( timings ) + "ms, stdDev --> " + stdDev( timings ) ); + System.out.println( actionName + ": timings --> " + timings ); + System.out.println( actionName + ": recordsRead --> " + recordsRead ); + System.out.println( "============================================================" ); + } + + private static List clean( List timings ) + { + int warmup = timings.size() / 10; // remove first 10% of measurements, they are just a warmup :) + return timings.subList( warmup, timings.size() ); + } + + private static long mean( List timings ) + { + long sum = 0; + for ( Long timing : timings ) + { + sum += timing; + } + return sum / timings.size(); + } + + private static double stdDev( List timings ) + { + long mean = mean( timings ); + long sum = 0; + for ( Long timing : timings ) + { + sum += ((timing - mean) * (timing - mean)); + } + + double squaredDiffMean = sum / timings.size(); + return (Math.sqrt( squaredDiffMean )); + } + + private static > T await( U future ) + { + try + { + return future.get(); + } + catch ( Throwable t ) + { + throw new RuntimeException( t ); + } + } + + private static void useRecord( Record record ) + { + if ( record.keys().size() > 5 ) + { + System.out.println( "Hello" ); + } + + if ( record.get( 0 ).isNull() ) + { + System.out.println( " " ); + } + + if ( record.get( "A" ) == null ) + { + System.out.println( "World" ); + } + +// System.out.println( record ); + } + + private interface Action + { + void apply( Driver driver, MutableInt recordsRead ); + } + + private static class MutableInt + { + int value; + + void increment() + { + value++; + } + + @Override + public String toString() + { + return String.valueOf( value ); + } + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/NettyChannelInitializer.java b/driver/src/main/java/org/neo4j/driver/internal/async/NettyChannelInitializer.java new file mode 100644 index 0000000000..8f4b1c5a67 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/NettyChannelInitializer.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.ChannelInitializer; +import io.netty.channel.pool.ChannelPoolHandler; +import io.netty.handler.ssl.SslHandler; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; + +import org.neo4j.driver.internal.async.inbound.InboundMessageDispatcher; +import org.neo4j.driver.internal.net.BoltServerAddress; +import org.neo4j.driver.internal.security.SecurityPlan; +import org.neo4j.driver.internal.util.Clock; + +import static org.neo4j.driver.internal.async.ChannelAttributes.setAddress; +import static org.neo4j.driver.internal.async.ChannelAttributes.setCreationTimestamp; +import static org.neo4j.driver.internal.async.ChannelAttributes.setMessageDispatcher; +import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; + +public class NettyChannelInitializer extends ChannelInitializer +{ + private final BoltServerAddress address; + private final SecurityPlan securityPlan; + private final ChannelPoolHandler channelPoolHandler; + private final Clock clock; + + public NettyChannelInitializer( BoltServerAddress address, SecurityPlan securityPlan, + ChannelPoolHandler channelPoolHandler, Clock clock ) + { + this.address = address; + this.securityPlan = securityPlan; + this.channelPoolHandler = channelPoolHandler; + this.clock = clock; + } + + @Override + protected void initChannel( Channel channel ) throws Exception + { + if ( securityPlan.requiresEncryption() ) + { + SslHandler sslHandler = createSslHandler(); + channel.pipeline().addFirst( sslHandler ); + } + + updateChannelAttributes( channel ); + + channelPoolHandler.channelCreated( channel ); + } + + private SslHandler createSslHandler() throws SSLException + { + SSLEngine sslEngine = createSslEngine(); + return new SslHandler( sslEngine ); + } + + private SSLEngine createSslEngine() throws SSLException + { + SSLContext sslContext = securityPlan.sslContext(); + SSLEngine sslEngine = sslContext.createSSLEngine( address.host(), address.port() ); + sslEngine.setUseClientMode( true ); + return sslEngine; + } + + private void updateChannelAttributes( Channel channel ) + { + setAddress( channel, address ); + setCreationTimestamp( channel, clock.millis() ); + setMessageDispatcher( channel, new InboundMessageDispatcher( channel, DEV_NULL_LOGGING ) ); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/NettyConnection.java b/driver/src/main/java/org/neo4j/driver/internal/async/NettyConnection.java new file mode 100644 index 0000000000..ebe3ab5d7c --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/NettyConnection.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.neo4j.driver.internal.async.inbound.InboundMessageDispatcher; +import org.neo4j.driver.internal.messaging.Message; +import org.neo4j.driver.internal.messaging.PullAllMessage; +import org.neo4j.driver.internal.messaging.ResetMessage; +import org.neo4j.driver.internal.messaging.RunMessage; +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.internal.summary.InternalServerInfo; +import org.neo4j.driver.internal.util.Clock; +import org.neo4j.driver.v1.Value; +import org.neo4j.driver.v1.summary.ServerInfo; + +import static org.neo4j.driver.internal.async.ChannelAttributes.address; +import static org.neo4j.driver.internal.async.ChannelAttributes.messageDispatcher; +import static org.neo4j.driver.internal.async.ChannelAttributes.serverVersion; + +// todo: keep state flags to prohibit interaction with released connections +public class NettyConnection implements AsyncConnection +{ + private final Channel channel; + private final InboundMessageDispatcher messageDispatcher; + private final ChannelPool channelPool; + private final Clock clock; + + private final AtomicBoolean autoReadEnabled = new AtomicBoolean( true ); + + private final NettyConnectionState state = new NettyConnectionState(); + + public NettyConnection( Channel channel, ChannelPool channelPool, Clock clock ) + { + this.channel = channel; + this.messageDispatcher = messageDispatcher( channel ); + this.channelPool = channelPool; + this.clock = clock; + } + + @Override + public boolean tryMarkInUse() + { + return state.markInUse(); + } + + @Override + public void enableAutoRead() + { + if ( autoReadEnabled.compareAndSet( false, true ) ) + { + System.out.println( "=== enableAutoRead" ); + setAutoRead( true ); + } + } + + @Override + public void disableAutoRead() + { + if ( autoReadEnabled.compareAndSet( true, false ) ) + { + System.out.println( "=== disableAutoRead" ); + setAutoRead( false ); + } + } + + @Override + public void run( String statement, Map parameters, ResponseHandler handler ) + { + write( new RunMessage( statement, parameters ), handler, false ); + } + + @Override + public void pullAll( ResponseHandler handler ) + { + write( PullAllMessage.PULL_ALL, handler, false ); + } + + @Override + public void flush() + { + channel.flush(); + } + + @Override + public InternalPromise newPromise() + { + return new InternalPromise<>( channel.eventLoop() ); + } + + @Override + public void release() + { + if ( state.release() ) + { + write( ResetMessage.RESET, new ReleaseChannelHandler( channel, channelPool, clock ), true ); + } + } + + @Override + public InternalFuture forceRelease() + { + InternalPromise releasePromise = newPromise(); + + if ( state.forceRelease() ) + { + write( ResetMessage.RESET, new ReleaseChannelHandler( channel, channelPool, clock, releasePromise ), true ); + } + else + { + releasePromise.setSuccess( null ); + } + + return releasePromise; + } + + @Override + public ServerInfo serverInfo() + { + return new InternalServerInfo( address( channel ), serverVersion( channel ) ); + } + + private void write( Message message, ResponseHandler handler, boolean flush ) + { + messageDispatcher.queue( handler ); + if ( flush ) + { + channel.writeAndFlush( message ); + } + else + { + channel.write( message ); + } + } + + private void setAutoRead( boolean value ) + { + channel.config().setAutoRead( value ); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/NettyConnectionState.java b/driver/src/main/java/org/neo4j/driver/internal/async/NettyConnectionState.java new file mode 100644 index 0000000000..7b1ed5dbc9 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/NettyConnectionState.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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 java.util.concurrent.atomic.AtomicInteger; + +public class NettyConnectionState +{ + private final AtomicInteger usageCounter = new AtomicInteger(); + + public boolean markInUse() + { + int current; + do + { + current = usageCounter.get(); + if ( current == -1 ) + { + return false; + } + } + while ( !usageCounter.compareAndSet( current, current + 1 ) ); + return true; + } + + public boolean release() + { + int current; + int next; + do + { + current = usageCounter.get(); + if ( current == -1 ) + { + return false; + } + next = current - 1; + } + while ( !usageCounter.compareAndSet( current, next ) ); + return next == -1; + } + + public boolean forceRelease() + { + int previous = usageCounter.getAndSet( -1 ); + if ( previous == -1 ) + { + return false; + } + else + { + return true; + } + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/ProtocolUtil.java b/driver/src/main/java/org/neo4j/driver/internal/async/ProtocolUtil.java new file mode 100644 index 0000000000..d69016937c --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/ProtocolUtil.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.buffer.ByteBuf; + +import static io.netty.buffer.Unpooled.copyInt; +import static io.netty.buffer.Unpooled.copyShort; +import static io.netty.buffer.Unpooled.unreleasableBuffer; + +public final class ProtocolUtil +{ + public static final int HTTP = 1213486160; //== 0x48545450 == "HTTP" + + public static final int BOLT_MAGIC_PREAMBLE = 0x6060B017; + public static final int PROTOCOL_VERSION_1 = 1; + public static final int NO_PROTOCOL_VERSION = 0; + + public static final int CHUNK_HEADER_SIZE_BYTES = 2; + + public static final int DEFAULT_MAX_OUTBOUND_CHUNK_SIZE_BYTES = Short.MAX_VALUE / 2; + + private static final ByteBuf HANDSHAKE_BUF = unreleasableBuffer( copyInt( + BOLT_MAGIC_PREAMBLE, + PROTOCOL_VERSION_1, + NO_PROTOCOL_VERSION, + NO_PROTOCOL_VERSION, + NO_PROTOCOL_VERSION ) ).asReadOnly(); + + private static final ByteBuf MESSAGE_BOUNDARY_BUF = unreleasableBuffer( copyShort( 0 ) ).asReadOnly(); + + private ProtocolUtil() + { + } + + public static ByteBuf handshake() + { + return HANDSHAKE_BUF.duplicate(); + } + + public static ByteBuf messageBoundary() + { + return MESSAGE_BOUNDARY_BUF.duplicate(); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/QueryRunner.java b/driver/src/main/java/org/neo4j/driver/internal/async/QueryRunner.java new file mode 100644 index 0000000000..2afae41876 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/QueryRunner.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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 java.util.Map; + +import org.neo4j.driver.internal.ExplicitTransaction; +import org.neo4j.driver.internal.handlers.PullAllResponseHandler; +import org.neo4j.driver.internal.handlers.RunResponseHandler; +import org.neo4j.driver.internal.handlers.SessionPullAllResponseHandler; +import org.neo4j.driver.internal.handlers.TransactionPullAllResponseHandler; +import org.neo4j.driver.v1.Statement; +import org.neo4j.driver.v1.StatementResultCursor; +import org.neo4j.driver.v1.Value; +import org.neo4j.driver.v1.util.Function; + +import static org.neo4j.driver.v1.Values.ofValue; + +public final class QueryRunner +{ + private QueryRunner() + { + } + + public static InternalFuture runAsync( AsyncConnection connection, Statement statement ) + { + return runAsync( connection, statement, null ); + } + + public static InternalFuture runAsync( AsyncConnection connection, Statement statement, + ExplicitTransaction tx ) + { + String query = statement.text(); + Map params = statement.parameters().asMap( ofValue() ); + + InternalPromise runCompletedPromise = connection.newPromise(); + final RunResponseHandler runHandler = new RunResponseHandler( runCompletedPromise ); + final PullAllResponseHandler pullAllHandler = newPullAllHandler( statement, runHandler, connection, tx ); + + connection.run( query, params, runHandler ); + connection.pullAll( pullAllHandler ); + connection.flush(); + + return runCompletedPromise.thenApply( new Function() + { + @Override + public StatementResultCursor apply( Void ignore ) + { + return new InternalStatementResultCursor( runHandler, pullAllHandler ); + } + } ); + } + + private static PullAllResponseHandler newPullAllHandler( Statement statement, RunResponseHandler runHandler, + AsyncConnection connection, ExplicitTransaction tx ) + { + if ( tx != null ) + { + return new TransactionPullAllResponseHandler( statement, runHandler, connection, tx ); + } + return new SessionPullAllResponseHandler( statement, runHandler, connection ); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/ReleaseChannelHandler.java b/driver/src/main/java/org/neo4j/driver/internal/async/ReleaseChannelHandler.java new file mode 100644 index 0000000000..3cba57577a --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/ReleaseChannelHandler.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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 io.netty.util.concurrent.Promise; + +import java.util.Map; + +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.internal.util.Clock; +import org.neo4j.driver.v1.Value; + +import static java.util.Objects.requireNonNull; +import static org.neo4j.driver.internal.async.ChannelAttributes.setLastUsedTimestamp; + +public class ReleaseChannelHandler implements ResponseHandler +{ + private final Channel channel; + private final ChannelPool pool; + private final Clock clock; + private final Promise releasePromise; + + public ReleaseChannelHandler( Channel channel, ChannelPool pool, Clock clock ) + { + this( channel, pool, clock, null ); + } + + public ReleaseChannelHandler( Channel channel, ChannelPool pool, Clock clock, Promise releasePromise ) + { + this.channel = requireNonNull( channel ); + this.pool = requireNonNull( pool ); + this.clock = requireNonNull( clock ); + this.releasePromise = releasePromise; + } + + @Override + public void onSuccess( Map metadata ) + { + releaseChannel(); + } + + @Override + public void onFailure( Throwable error ) + { + releaseChannel(); + } + + @Override + public void onRecord( Value[] fields ) + { + throw new UnsupportedOperationException(); + } + + private void releaseChannel() + { + setLastUsedTimestamp( channel, clock.millis() ); + + if ( releasePromise == null ) + { + pool.release( channel ); + } + else + { + pool.release( channel, releasePromise ); + } + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/inbound/ByteBufInput.java b/driver/src/main/java/org/neo4j/driver/internal/async/inbound/ByteBufInput.java new file mode 100644 index 0000000000..77fe447e61 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/inbound/ByteBufInput.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.inbound; + +import io.netty.buffer.ByteBuf; + +import org.neo4j.driver.internal.packstream.PackInput; + +import static java.util.Objects.requireNonNull; + +public class ByteBufInput implements PackInput +{ + private ByteBuf buf; + + public void start( ByteBuf newBuf ) + { + assertNotStarted(); + buf = requireNonNull( newBuf ); + } + + public void stop() + { + buf = null; + } + + @Override + public boolean hasMoreData() + { + return buf.isReadable(); + } + + @Override + public byte readByte() + { + return buf.readByte(); + } + + @Override + public short readShort() + { + return buf.readShort(); + } + + @Override + public int readInt() + { + return buf.readInt(); + } + + @Override + public long readLong() + { + return buf.readLong(); + } + + @Override + public double readDouble() + { + return buf.readDouble(); + } + + @Override + public PackInput readBytes( byte[] into, int offset, int toRead ) + { + buf.readBytes( into, offset, toRead ); + return this; + } + + @Override + public byte peekByte() + { + return buf.getByte( buf.readerIndex() ); + } + + @Override + public Runnable messageBoundaryHook() + { + return new Runnable() + { + @Override + public void run() + { + } + }; + } + + private void assertNotStarted() + { + if ( buf != null ) + { + throw new IllegalStateException( "Already started" ); + } + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/inbound/ChunkDecoder.java b/driver/src/main/java/org/neo4j/driver/internal/async/inbound/ChunkDecoder.java new file mode 100644 index 0000000000..27c1464a0d --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/inbound/ChunkDecoder.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.inbound; + +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; + +public class ChunkDecoder extends LengthFieldBasedFrameDecoder +{ + private static final int MAX_FRAME_LENGTH = Short.MAX_VALUE; + private static final int LENGTH_FIELD_OFFSET = 0; + private static final int LENGTH_FIELD_LENGTH = 2; + private static final int LENGTH_ADJUSTMENT = 0; + private static final int INITIAL_BYTES_TO_STRIP = LENGTH_FIELD_LENGTH; + + public ChunkDecoder() + { + super( MAX_FRAME_LENGTH, LENGTH_FIELD_OFFSET, LENGTH_FIELD_LENGTH, LENGTH_ADJUSTMENT, INITIAL_BYTES_TO_STRIP ); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/inbound/InboundMessageDispatcher.java b/driver/src/main/java/org/neo4j/driver/internal/async/inbound/InboundMessageDispatcher.java new file mode 100644 index 0000000000..eacc7dd5f8 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/inbound/InboundMessageDispatcher.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.inbound; + +import io.netty.channel.Channel; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Map; +import java.util.Queue; + +import org.neo4j.driver.internal.handlers.AckFailureResponseHandler; +import org.neo4j.driver.internal.messaging.AckFailureMessage; +import org.neo4j.driver.internal.messaging.MessageHandler; +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.internal.util.ErrorUtil; +import org.neo4j.driver.v1.Logger; +import org.neo4j.driver.v1.Logging; +import org.neo4j.driver.v1.Value; + +import static java.util.Objects.requireNonNull; + +public class InboundMessageDispatcher implements MessageHandler +{ + private final Channel channel; + private final Queue handlers = new LinkedList<>(); + private final Logger log; + + private Throwable currentError; + private boolean fatalErrorOccurred; + + public InboundMessageDispatcher( Channel channel, Logging logging ) + { + this.channel = requireNonNull( channel ); + this.log = logging.getLog( getClass().getSimpleName() ); + } + + public void queue( ResponseHandler handler ) + { + if ( fatalErrorOccurred ) + { + handler.onFailure( currentError ); + } + else + { + handlers.add( handler ); + } + } + + public int queuedHandlersCount() + { + return handlers.size(); + } + + @Override + public void handleInitMessage( String clientNameAndVersion, Map authToken ) + { + throw new UnsupportedOperationException( "Driver is not supposed to receive INIT message. " + + "Received INIT with client: '" + clientNameAndVersion + "' " + + "and auth: " + authToken ); + } + + @Override + public void handleRunMessage( String statement, Map parameters ) + { + throw new UnsupportedOperationException( "Driver is not supposed to receive RUN message. " + + "Received RUN with statement: '" + statement + "' " + + "and params: " + parameters ); + } + + @Override + public void handlePullAllMessage() + { + throw new UnsupportedOperationException( "Driver is not supposed to receive PULL_ALL message." ); + } + + @Override + public void handleDiscardAllMessage() + { + throw new UnsupportedOperationException( "Driver is not supposed to receive DISCARD_ALL message." ); + } + + @Override + public void handleResetMessage() + { + throw new UnsupportedOperationException( "Driver is not supposed to receive RESET message." ); + } + + @Override + public void handleAckFailureMessage() + { + throw new UnsupportedOperationException( "Driver is not supposed to receive ACK_FAILURE message." ); + } + + @Override + public void handleSuccessMessage( Map meta ) + { + ResponseHandler handler = handlers.remove(); + log.debug( "Received SUCCESS message with metadata %s for handler %s", meta, handler ); + handler.onSuccess( meta ); + } + + @Override + public void handleRecordMessage( Value[] fields ) + { + if ( log.isDebugEnabled() ) + { + log.debug( "Received RECORD message with metadata %s", Arrays.toString( fields ) ); + } + ResponseHandler handler = handlers.peek(); + handler.onRecord( fields ); + } + + @Override + public void handleFailureMessage( String code, String message ) + { + log.debug( "Received FAILURE message with code '%s' and message '%s'", code, message ); + currentError = ErrorUtil.newNeo4jError( code, message ); + + // queue ACK_FAILURE before notifying the next response handler + queue( new AckFailureResponseHandler( this ) ); + channel.writeAndFlush( AckFailureMessage.ACK_FAILURE ); + + ResponseHandler handler = handlers.remove(); + handler.onFailure( currentError ); + } + + @Override + public void handleIgnoredMessage() + { + ResponseHandler handler = handlers.remove(); + log.debug( "Received IGNORED message for handler %s", handler ); + if ( currentError != null ) + { + handler.onFailure( currentError ); + } + else + { + log.warn( "Received IGNORED message for handler %s but error is missing", handler ); + } + } + + public void handleFatalError( Throwable error ) + { + log.warn( "Fatal error occurred", error ); + + currentError = error; + fatalErrorOccurred = true; + + while ( !handlers.isEmpty() ) + { + ResponseHandler handler = handlers.remove(); + handler.onFailure( currentError ); + } + } + + public void clearCurrentError() + { + currentError = null; + } + + Throwable currentError() + { + return currentError; + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/inbound/InboundMessageHandler.java b/driver/src/main/java/org/neo4j/driver/internal/async/inbound/InboundMessageHandler.java new file mode 100644 index 0000000000..0d0dfb8445 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/inbound/InboundMessageHandler.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.inbound; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; + +import java.io.IOException; + +import org.neo4j.driver.internal.messaging.MessageFormat; +import org.neo4j.driver.v1.Logger; +import org.neo4j.driver.v1.Logging; +import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; + +import static org.neo4j.driver.internal.async.ChannelAttributes.messageDispatcher; + +public class InboundMessageHandler extends SimpleChannelInboundHandler +{ + private final ByteBufInput input; + private final MessageFormat.Reader reader; + private final Logger log; + + private InboundMessageDispatcher messageDispatcher; + + public InboundMessageHandler( MessageFormat messageFormat, Logging logging ) + { + this.input = new ByteBufInput(); + this.reader = messageFormat.newReader( input ); + this.log = logging.getLog( getClass().getSimpleName() ); + } + + @Override + public void handlerAdded( ChannelHandlerContext ctx ) + { + messageDispatcher = messageDispatcher( ctx.channel() ); + } + + @Override + protected void channelRead0( ChannelHandlerContext ctx, ByteBuf msg ) throws IOException + { + input.start( msg ); + reader.read( messageDispatcher ); + input.stop(); + } + + @Override + public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause ) + { + log.warn( "Fatal error in pipeline for channel %s", ctx.channel() ); + + messageDispatcher.handleFatalError( cause ); + ctx.close(); + } + + @Override + public void channelInactive( ChannelHandlerContext ctx ) + { + log.debug( "Channel inactive: %s", ctx.channel() ); + + messageDispatcher.handleFatalError( new ServiceUnavailableException( + "Connection terminated while receiving data. This can happen due to network " + + "instabilities, or due to restarts of the database." ) ); + + ctx.close(); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/inbound/MessageDecoder.java b/driver/src/main/java/org/neo4j/driver/internal/async/inbound/MessageDecoder.java new file mode 100644 index 0000000000..4165a1c7c7 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/inbound/MessageDecoder.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.inbound; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.util.List; + +public class MessageDecoder extends ByteToMessageDecoder +{ + private boolean readMessageBoundary; + + @Override + public void channelRead( ChannelHandlerContext ctx, Object msg ) throws Exception + { + if ( msg instanceof ByteBuf ) + { + // on every read check if input buffer is empty or not + // if it is empty then it's a message boundary and full message is in the buffer + readMessageBoundary = ((ByteBuf) msg).readableBytes() == 0; + } + super.channelRead( ctx, msg ); + } + + @Override + protected void decode( ChannelHandlerContext ctx, ByteBuf in, List out ) throws Exception + { + if ( readMessageBoundary ) + { + // now we have a complete message in the input buffer + + // increment ref count of the buffer because we will pass it's duplicate through + in.retain(); + ByteBuf res = in.duplicate(); + + // signal that whole message was read by making input buffer seem like it was fully read/consumed + in.readerIndex( in.readableBytes() ); + + // pass the full message to the next handler in the pipeline + out.add( res ); + + readMessageBoundary = false; + } + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/outbound/ChunkAwareByteBufOutput.java b/driver/src/main/java/org/neo4j/driver/internal/async/outbound/ChunkAwareByteBufOutput.java new file mode 100644 index 0000000000..973072ebd3 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/outbound/ChunkAwareByteBufOutput.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.outbound; + +import io.netty.buffer.ByteBuf; + +import org.neo4j.driver.internal.packstream.PackOutput; + +import static java.util.Objects.requireNonNull; +import static org.neo4j.driver.internal.async.ProtocolUtil.CHUNK_HEADER_SIZE_BYTES; +import static org.neo4j.driver.internal.async.ProtocolUtil.DEFAULT_MAX_OUTBOUND_CHUNK_SIZE_BYTES; + +public class ChunkAwareByteBufOutput implements PackOutput +{ + private final int maxChunkSize; + + private ByteBuf buf; + private int currentChunkStartIndex; + private int currentChunkSize; + + public ChunkAwareByteBufOutput() + { + this( DEFAULT_MAX_OUTBOUND_CHUNK_SIZE_BYTES ); + } + + ChunkAwareByteBufOutput( int maxChunkSize ) + { + this.maxChunkSize = verifyMaxChunkSize( maxChunkSize ); + } + + public void start( ByteBuf newBuf ) + { + assertNotStarted(); + buf = requireNonNull( newBuf ); + startNewChunk( 0 ); + } + + public void stop() + { + writeChunkSizeHeader(); + buf = null; + currentChunkStartIndex = 0; + currentChunkSize = 0; + } + + @Override + public PackOutput flush() + { + throw new UnsupportedOperationException( "Flush not supported, this output only writes to a buffer" ); + } + + @Override + public PackOutput writeByte( byte value ) + { + ensureCanFitInCurrentChunk( 1 ); + buf.writeByte( value ); + currentChunkSize += 1; + return this; + } + + @Override + public PackOutput writeBytes( byte[] data ) + { + int offset = 0; + int length = data.length; + while ( offset < length ) + { + // Ensure there is an open chunk, and that it has at least one byte of space left + ensureCanFitInCurrentChunk( 1 ); + + // Write as much as we can into the current chunk + int amountToWrite = Math.min( availableBytesInCurrentChunk(), length - offset ); + + buf.writeBytes( data, offset, amountToWrite ); + currentChunkSize += amountToWrite; + offset += amountToWrite; + } + return this; + } + + @Override + public PackOutput writeShort( short value ) + { + ensureCanFitInCurrentChunk( 2 ); + buf.writeShort( value ); + currentChunkSize += 2; + return this; + } + + @Override + public PackOutput writeInt( int value ) + { + ensureCanFitInCurrentChunk( 4 ); + buf.writeInt( value ); + currentChunkSize += 4; + return this; + } + + @Override + public PackOutput writeLong( long value ) + { + ensureCanFitInCurrentChunk( 8 ); + buf.writeLong( value ); + currentChunkSize += 8; + return this; + } + + @Override + public PackOutput writeDouble( double value ) + { + ensureCanFitInCurrentChunk( 8 ); + buf.writeDouble( value ); + currentChunkSize += 8; + return this; + } + + @Override + public Runnable messageBoundaryHook() + { + return new Runnable() + { + @Override + public void run() + { + } + }; + } + + private void ensureCanFitInCurrentChunk( int numberOfBytes ) + { + int targetChunkSize = currentChunkSize + numberOfBytes; + if ( targetChunkSize > maxChunkSize ) + { + writeChunkSizeHeader(); + startNewChunk( buf.writerIndex() ); + } + } + + private void startNewChunk( int index ) + { + currentChunkStartIndex = index; + buf.writerIndex( currentChunkStartIndex + CHUNK_HEADER_SIZE_BYTES ); + currentChunkSize = CHUNK_HEADER_SIZE_BYTES; + } + + private void writeChunkSizeHeader() + { + // go to the beginning of the chunk and write 2 byte size header + int chunkBodySize = currentChunkSize - CHUNK_HEADER_SIZE_BYTES; + buf.setShort( currentChunkStartIndex, chunkBodySize ); + } + + private int availableBytesInCurrentChunk() + { + return maxChunkSize - currentChunkSize; + } + + private void assertNotStarted() + { + if ( buf != null ) + { + throw new IllegalStateException( "Already started" ); + } + } + + private static int verifyMaxChunkSize( int maxChunkSize ) + { + if ( maxChunkSize <= 0 ) + { + throw new IllegalArgumentException( "Max chunk size should be > 0, given: " + maxChunkSize ); + } + return maxChunkSize; + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/outbound/OutboundMessageHandler.java b/driver/src/main/java/org/neo4j/driver/internal/async/outbound/OutboundMessageHandler.java new file mode 100644 index 0000000000..16a3a49f62 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/outbound/OutboundMessageHandler.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.outbound; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageEncoder; + +import java.util.List; + +import org.neo4j.driver.internal.messaging.Message; +import org.neo4j.driver.internal.messaging.MessageFormat; +import org.neo4j.driver.v1.Logger; +import org.neo4j.driver.v1.Logging; + +import static org.neo4j.driver.internal.async.ProtocolUtil.messageBoundary; + +public class OutboundMessageHandler extends MessageToMessageEncoder +{ + public static final String NAME = OutboundMessageHandler.class.getSimpleName(); + + private final MessageFormat messageFormat; + private final ChunkAwareByteBufOutput output; + private final MessageFormat.Writer writer; + private final Logger log; + + public OutboundMessageHandler( MessageFormat messageFormat, Logging logging ) + { + this( messageFormat, true, logging.getLog( NAME ) ); + } + + private OutboundMessageHandler( MessageFormat messageFormat, boolean byteArraySupportEnabled, Logger log ) + { + this.messageFormat = messageFormat; + this.output = new ChunkAwareByteBufOutput(); + this.writer = messageFormat.newWriter( output, byteArraySupportEnabled ); + this.log = log; + } + + @Override + protected void encode( ChannelHandlerContext ctx, Message msg, List out ) throws Exception + { + log.debug( "Sending message %s", msg ); + + ByteBuf messageBuf = ctx.alloc().ioBuffer(); + output.start( messageBuf ); + writer.write( msg ); + output.stop(); + + out.add( messageBuf ); + out.add( messageBoundary() ); + } + + public OutboundMessageHandler withoutByteArraySupport() + { + return new OutboundMessageHandler( messageFormat, false, log ); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/pool/ActiveChannelTracker.java b/driver/src/main/java/org/neo4j/driver/internal/async/pool/ActiveChannelTracker.java new file mode 100644 index 0000000000..0f938ecbf8 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/pool/ActiveChannelTracker.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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 io.netty.channel.pool.ChannelPoolHandler; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.neo4j.driver.internal.net.BoltServerAddress; +import org.neo4j.driver.internal.util.ConcurrentSet; +import org.neo4j.driver.v1.Logger; +import org.neo4j.driver.v1.Logging; + +import static org.neo4j.driver.internal.async.ChannelAttributes.address; + +public class ActiveChannelTracker implements ChannelPoolHandler +{ + private final ConcurrentMap> addressToActiveChannelCount; + private final Logger log; + + public ActiveChannelTracker( Logging logging ) + { + this.addressToActiveChannelCount = new ConcurrentHashMap<>(); + this.log = logging.getLog( getClass().getSimpleName() ); + } + + @Override + public void channelReleased( Channel channel ) + { + log.debug( "Channel %s released back to the pool", channel ); + channelInactive( channel ); + } + + @Override + public void channelAcquired( Channel channel ) + { + log.debug( "Channel %s acquired from the pool", channel ); + channelActive( channel ); + } + + @Override + public void channelCreated( Channel channel ) + { + log.debug( "Channel %s created", channel ); + channelActive( channel ); + } + + public int activeChannelCount( BoltServerAddress address ) + { + ConcurrentSet activeChannels = addressToActiveChannelCount.get( address ); + return activeChannels == null ? 0 : activeChannels.size(); + } + + public void purge( BoltServerAddress address ) + { + ConcurrentSet activeChannels = addressToActiveChannelCount.remove( address ); + if ( activeChannels != null ) + { + for ( Channel channel : activeChannels ) + { + channel.close(); + } + } + } + + private void channelActive( Channel channel ) + { + BoltServerAddress address = address( channel ); + ConcurrentSet activeChannels = addressToActiveChannelCount.get( address ); + if ( activeChannels == null ) + { + ConcurrentSet newActiveChannels = new ConcurrentSet<>(); + ConcurrentSet existingActiveChannels = addressToActiveChannelCount.putIfAbsent( address, + newActiveChannels ); + if ( existingActiveChannels == null ) + { + activeChannels = newActiveChannels; + } + else + { + activeChannels = existingActiveChannels; + } + } + + activeChannels.add( channel ); + } + + private void channelInactive( Channel channel ) + { + BoltServerAddress address = address( channel ); + ConcurrentSet activeChannels = addressToActiveChannelCount.get( address ); + if ( activeChannels == null ) + { + throw new IllegalStateException( "No channels exist for address '" + address + "'" ); + } + activeChannels.remove( channel ); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/pool/AsyncConnectionPool.java b/driver/src/main/java/org/neo4j/driver/internal/async/pool/AsyncConnectionPool.java new file mode 100644 index 0000000000..d83fd30d97 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/pool/AsyncConnectionPool.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.util.concurrent.Future; + +import org.neo4j.driver.internal.async.AsyncConnection; +import org.neo4j.driver.internal.async.InternalFuture; +import org.neo4j.driver.internal.net.BoltServerAddress; + +public interface AsyncConnectionPool +{ + InternalFuture acquire( BoltServerAddress address ); + + void purge( BoltServerAddress address ); + + boolean hasAddress( BoltServerAddress address ); + + int activeConnections( BoltServerAddress address ); + + Future closeAsync(); +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/pool/AsyncConnectionPoolImpl.java b/driver/src/main/java/org/neo4j/driver/internal/async/pool/AsyncConnectionPoolImpl.java new file mode 100644 index 0000000000..19f1d7df59 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/pool/AsyncConnectionPoolImpl.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.pool.ChannelPool; +import io.netty.util.concurrent.Future; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.neo4j.driver.internal.async.AsyncConnection; +import org.neo4j.driver.internal.async.AsyncConnector; +import org.neo4j.driver.internal.async.Futures; +import org.neo4j.driver.internal.async.InternalFuture; +import org.neo4j.driver.internal.async.NettyConnection; +import org.neo4j.driver.internal.net.BoltServerAddress; +import org.neo4j.driver.internal.net.pooling.PoolSettings; +import org.neo4j.driver.internal.util.Clock; +import org.neo4j.driver.v1.Logger; +import org.neo4j.driver.v1.Logging; +import org.neo4j.driver.v1.util.Function; + +public class AsyncConnectionPoolImpl implements AsyncConnectionPool +{ + private final AsyncConnector connector; + private final Bootstrap bootstrap; + private final ActiveChannelTracker activeChannelTracker; + private final NettyChannelHealthChecker channelHealthChecker; + private final PoolSettings settings; + private final Clock clock; + private final Logger log; + + private final ConcurrentMap pools = new ConcurrentHashMap<>(); + private final AtomicBoolean closed = new AtomicBoolean(); + + public AsyncConnectionPoolImpl( AsyncConnector connector, Bootstrap bootstrap, + ActiveChannelTracker activeChannelTracker, PoolSettings settings, Logging logging, Clock clock ) + { + this.connector = connector; + this.bootstrap = bootstrap; + this.activeChannelTracker = activeChannelTracker; + this.channelHealthChecker = new NettyChannelHealthChecker( settings, clock ); + this.settings = settings; + this.clock = clock; + this.log = logging.getLog( getClass().getSimpleName() ); + } + + @Override + public InternalFuture acquire( final BoltServerAddress address ) + { + log.debug( "Acquiring connection from pool for address: %s", address ); + + assertNotClosed(); + final ChannelPool pool = getOrCreatePool( address ); + final Future connectionFuture = pool.acquire(); + + return Futures.thenApply( connectionFuture, bootstrap, new Function() + { + @Override + public AsyncConnection apply( Channel channel ) + { + assertNotClosed( address, channel, pool ); + return new NettyConnection( channel, pool, clock ); + } + } ); + } + + @Override + public void purge( BoltServerAddress address ) + { + log.info( "Purging connections for address: %s", address ); + + // purge active connections + activeChannelTracker.purge( address ); + + // purge idle connections in the pool and pool itself + ChannelPool pool = pools.remove( address ); + if ( pool != null ) + { + pool.close(); + } + } + + @Override + public boolean hasAddress( BoltServerAddress address ) + { + return pools.containsKey( address ); + } + + @Override + public int activeConnections( BoltServerAddress address ) + { + return activeChannelTracker.activeChannelCount( address ); + } + + @Override + public Future closeAsync() + { + if ( closed.compareAndSet( false, true ) ) + { + log.info( "Closing the connection pool" ); + try + { + for ( ChannelPool pool : pools.values() ) + { + pool.close(); + } + + pools.clear(); + } + finally + { + eventLoopGroup().shutdownGracefully(); + } + } + return eventLoopGroup().terminationFuture(); + } + + private ChannelPool getOrCreatePool( BoltServerAddress address ) + { + ChannelPool pool = pools.get( address ); + if ( pool == null ) + { + pool = newPool( address ); + + if ( pools.putIfAbsent( address, pool ) != null ) + { + // We lost a race to create the pool, dispose of the one we created, and recurse + pool.close(); + return getOrCreatePool( address ); + } + } + return pool; + } + + private NettyChannelPool newPool( BoltServerAddress address ) + { + return new NettyChannelPool( address, connector, bootstrap, activeChannelTracker, channelHealthChecker, + settings.connectionAcquisitionTimeout(), settings.maxConnectionPoolSize() ); + } + + private EventLoopGroup eventLoopGroup() + { + return bootstrap.config().group(); + } + + private void assertNotClosed() + { + if ( closed.get() ) + { + throw new IllegalStateException( "Pool closed" ); + } + } + + private void assertNotClosed( BoltServerAddress address, Channel channel, ChannelPool pool ) + { + if ( closed.get() ) + { + pool.release( channel ); + pool.close(); + pools.remove( address ); + assertNotClosed(); + } + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/pool/NettyChannelHealthChecker.java b/driver/src/main/java/org/neo4j/driver/internal/async/pool/NettyChannelHealthChecker.java new file mode 100644 index 0000000000..fa59e82424 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/pool/NettyChannelHealthChecker.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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 io.netty.channel.pool.ChannelHealthChecker; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; + +import org.neo4j.driver.internal.handlers.PingResponseHandler; +import org.neo4j.driver.internal.messaging.ResetMessage; +import org.neo4j.driver.internal.net.pooling.PoolSettings; +import org.neo4j.driver.internal.util.Clock; + +import static java.util.Objects.requireNonNull; +import static org.neo4j.driver.internal.async.ChannelAttributes.creationTimestamp; +import static org.neo4j.driver.internal.async.ChannelAttributes.lastUsedTimestamp; +import static org.neo4j.driver.internal.async.ChannelAttributes.messageDispatcher; + +public class NettyChannelHealthChecker implements ChannelHealthChecker +{ + private final PoolSettings poolSettings; + private final Clock clock; + + public NettyChannelHealthChecker( PoolSettings poolSettings, Clock clock ) + { + this.poolSettings = requireNonNull( poolSettings ); + this.clock = requireNonNull( clock ); + } + + @Override + public Future isHealthy( Channel channel ) + { + if ( isTooOld( channel ) ) + { + return channel.eventLoop().newSucceededFuture( Boolean.FALSE ); + } + if ( hasBeenIdleForTooLong( channel ) ) + { + return ping( channel ); + } + return ACTIVE.isHealthy( channel ); + } + + private boolean isTooOld( Channel channel ) + { + if ( poolSettings.maxConnectionLifetimeEnabled() ) + { + long creationTimestampMillis = creationTimestamp( channel ); + long currentTimestampMillis = clock.millis(); + long ageMillis = currentTimestampMillis - creationTimestampMillis; + + return ageMillis > poolSettings.maxConnectionLifetime(); + } + return false; + } + + private boolean hasBeenIdleForTooLong( Channel channel ) + { + if ( poolSettings.idleTimeBeforeConnectionTestEnabled() ) + { + Long lastUsedTimestamp = lastUsedTimestamp( channel ); + if ( lastUsedTimestamp != null ) + { + long idleTime = clock.millis() - lastUsedTimestamp; + return idleTime > poolSettings.idleTimeBeforeConnectionTest(); + } + } + return false; + } + + private Future ping( Channel channel ) + { + Promise result = channel.eventLoop().newPromise(); + messageDispatcher( channel ).queue( new PingResponseHandler( result ) ); + channel.writeAndFlush( ResetMessage.RESET ); + return result; + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/pool/NettyChannelPool.java b/driver/src/main/java/org/neo4j/driver/internal/async/pool/NettyChannelPool.java new file mode 100644 index 0000000000..0a06aff5df --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/async/pool/NettyChannelPool.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.pool.ChannelHealthChecker; +import io.netty.channel.pool.ChannelPoolHandler; +import io.netty.channel.pool.FixedChannelPool; + +import org.neo4j.driver.internal.async.AsyncConnector; +import org.neo4j.driver.internal.net.BoltServerAddress; + +import static java.util.Objects.requireNonNull; + +public class NettyChannelPool extends FixedChannelPool +{ + /** + * Unlimited amount of parties are allowed to request channels from the pool. + */ + private static final int MAX_PENDING_ACQUIRES = Integer.MAX_VALUE; + /** + * Check channels when they are returned to the pool. + */ + private static final boolean RELEASE_HEALTH_CHECK = true; + + private final BoltServerAddress address; + private final AsyncConnector connector; + + public NettyChannelPool( BoltServerAddress address, AsyncConnector connector, Bootstrap bootstrap, + ChannelPoolHandler handler, ChannelHealthChecker healthCheck, long acquireTimeoutMillis, + int maxConnections ) + { + super( bootstrap, handler, healthCheck, AcquireTimeoutAction.FAIL, acquireTimeoutMillis, maxConnections, + MAX_PENDING_ACQUIRES, RELEASE_HEALTH_CHECK ); + + this.address = requireNonNull( address ); + this.connector = requireNonNull( connector ); + } + + @Override + protected ChannelFuture connectChannel( Bootstrap bootstrap ) + { + return connector.connect( address, bootstrap ); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingPooledConnection.java b/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingPooledConnection.java index 58f2d8a4b9..9b5ecc3e9a 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingPooledConnection.java +++ b/driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingPooledConnection.java @@ -24,8 +24,8 @@ import org.neo4j.driver.internal.RoutingErrorHandler; import org.neo4j.driver.internal.SessionResourcesHandler; import org.neo4j.driver.internal.net.BoltServerAddress; -import org.neo4j.driver.internal.spi.Collector; import org.neo4j.driver.internal.spi.PooledConnection; +import org.neo4j.driver.internal.spi.ResponseHandler; import org.neo4j.driver.v1.AccessMode; import org.neo4j.driver.v1.Value; import org.neo4j.driver.v1.exceptions.ClientException; @@ -63,11 +63,11 @@ public void init( String clientName, Map authToken ) } @Override - public void run( String statement, Map parameters, Collector collector ) + public void run( String statement, Map parameters, ResponseHandler handler ) { try { - delegate.run( statement, parameters, collector ); + delegate.run( statement, parameters, handler ); } catch ( RuntimeException e ) { @@ -76,11 +76,11 @@ public void run( String statement, Map parameters, Collector colle } @Override - public void discardAll( Collector collector ) + public void discardAll( ResponseHandler handler ) { try { - delegate.discardAll( collector ); + delegate.discardAll( handler ); } catch ( RuntimeException e ) { @@ -89,11 +89,11 @@ public void discardAll( Collector collector ) } @Override - public void pullAll( Collector collector ) + public void pullAll( ResponseHandler handler ) { try { - delegate.pullAll( collector ); + delegate.pullAll( handler ); } catch ( RuntimeException e ) { 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 7b9d0da22c..d604a06d83 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 @@ -21,12 +21,12 @@ import java.util.List; +import org.neo4j.driver.ResultResourcesHandler; import org.neo4j.driver.internal.NetworkSession; import org.neo4j.driver.internal.spi.Connection; import org.neo4j.driver.v1.Record; import org.neo4j.driver.v1.Statement; -import static org.neo4j.driver.internal.SessionResourcesHandler.NO_OP; import static org.neo4j.driver.internal.util.ServerVersion.v3_2_0; import static org.neo4j.driver.internal.util.ServerVersion.version; import static org.neo4j.driver.v1.Values.parameters; @@ -62,7 +62,7 @@ public List run( Connection connection ) List runProcedure( Connection connection, Statement procedure ) { - return NetworkSession.run( connection, procedure, NO_OP ).list(); + return NetworkSession.run( connection, procedure, ResultResourcesHandler.NO_OP ).list(); } Statement invokedProcedure() 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 cc90958e0c..8b4f9549ac 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 @@ -21,6 +21,8 @@ import java.util.Set; import org.neo4j.driver.internal.RoutingErrorHandler; +import org.neo4j.driver.internal.async.AsyncConnection; +import org.neo4j.driver.internal.async.InternalFuture; import org.neo4j.driver.internal.cluster.AddressSet; import org.neo4j.driver.internal.cluster.ClusterComposition; import org.neo4j.driver.internal.cluster.ClusterCompositionProvider; @@ -88,6 +90,12 @@ public PooledConnection acquireConnection( AccessMode mode ) return new RoutingPooledConnection( connection, this, mode ); } + @Override + public InternalFuture acquireAsyncConnection( AccessMode mode ) + { + throw new UnsupportedOperationException(); + } + @Override public void onConnectionFailure( BoltServerAddress address ) { diff --git a/driver/src/main/java/org/neo4j/driver/internal/handlers/AckFailureResponseHandler.java b/driver/src/main/java/org/neo4j/driver/internal/handlers/AckFailureResponseHandler.java new file mode 100644 index 0000000000..f6e782d26b --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/handlers/AckFailureResponseHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.handlers; + +import java.util.Map; + +import org.neo4j.driver.internal.async.inbound.InboundMessageDispatcher; +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.v1.Value; + +public class AckFailureResponseHandler implements ResponseHandler +{ + private final InboundMessageDispatcher messageDispatcher; + + public AckFailureResponseHandler( InboundMessageDispatcher messageDispatcher ) + { + this.messageDispatcher = messageDispatcher; + } + + @Override + public void onSuccess( Map metadata ) + { + messageDispatcher.clearCurrentError(); + } + + @Override + public void onFailure( Throwable error ) + { + } + + @Override + public void onRecord( Value[] fields ) + { + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/handlers/AsyncInitResponseHandler.java b/driver/src/main/java/org/neo4j/driver/internal/handlers/AsyncInitResponseHandler.java new file mode 100644 index 0000000000..2d8310d926 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/handlers/AsyncInitResponseHandler.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.handlers; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; + +import java.util.Map; + +import org.neo4j.driver.internal.async.outbound.OutboundMessageHandler; +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.internal.util.ServerVersion; +import org.neo4j.driver.v1.Value; + +import static org.neo4j.driver.internal.async.ChannelAttributes.setServerVersion; + +public class AsyncInitResponseHandler implements ResponseHandler +{ + private final ChannelPromise connectionInitializedPromise; + private final Channel channel; + + public AsyncInitResponseHandler( ChannelPromise connectionInitializedPromise ) + { + this.connectionInitializedPromise = connectionInitializedPromise; + this.channel = connectionInitializedPromise.channel(); + } + + @Override + public void onSuccess( Map metadata ) + { + Value versionValue = metadata.get( "server" ); + if ( versionValue != null ) + { + String serverVersion = versionValue.asString(); + setServerVersion( channel, serverVersion ); + updatePipelineIfNeeded( serverVersion, channel.pipeline() ); + } + connectionInitializedPromise.setSuccess(); + } + + @Override + public void onFailure( final Throwable error ) + { + channel.close().addListener( new ChannelFutureListener() + { + @Override + public void operationComplete( ChannelFuture future ) throws Exception + { + connectionInitializedPromise.setFailure( error ); + } + } ); + } + + @Override + public void onRecord( Value[] fields ) + { + throw new UnsupportedOperationException(); + } + + private static void updatePipelineIfNeeded( String serverVersionString, ChannelPipeline pipeline ) + { + ServerVersion serverVersion = ServerVersion.version( serverVersionString ); + if ( serverVersion.lessThan( ServerVersion.v3_2_0 ) ) + { + OutboundMessageHandler outboundHandler = pipeline.get( OutboundMessageHandler.class ); + if ( outboundHandler == null ) + { + throw new IllegalStateException( "Can't find " + OutboundMessageHandler.NAME + " in the pipeline" ); + } + pipeline.replace( outboundHandler, OutboundMessageHandler.NAME, outboundHandler.withoutByteArraySupport() ); + } + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/handlers/BeginTxResponseHandler.java b/driver/src/main/java/org/neo4j/driver/internal/handlers/BeginTxResponseHandler.java new file mode 100644 index 0000000000..b13e12fda7 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/handlers/BeginTxResponseHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.handlers; + +import io.netty.util.concurrent.Promise; + +import java.util.Arrays; +import java.util.Map; + +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.v1.Transaction; +import org.neo4j.driver.v1.Value; + +import static java.util.Objects.requireNonNull; + +public class BeginTxResponseHandler implements ResponseHandler +{ + private final Promise beginTxPromise; + private final T tx; + + public BeginTxResponseHandler( Promise beginTxPromise, T tx ) + { + this.beginTxPromise = requireNonNull( beginTxPromise ); + this.tx = requireNonNull( tx ); + } + + @Override + public void onSuccess( Map metadata ) + { + beginTxPromise.setSuccess( tx ); + } + + @Override + public void onFailure( Throwable error ) + { + beginTxPromise.setFailure( error ); + } + + @Override + public void onRecord( Value[] fields ) + { + throw new UnsupportedOperationException( + "Transaction begin is not expected to receive records: " + Arrays.toString( fields ) ); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/handlers/BookmarkResponseHandler.java b/driver/src/main/java/org/neo4j/driver/internal/handlers/BookmarkResponseHandler.java new file mode 100644 index 0000000000..9a0143010c --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/handlers/BookmarkResponseHandler.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.handlers; + +import java.util.Map; + +import org.neo4j.driver.internal.Bookmark; +import org.neo4j.driver.internal.ExplicitTransaction; +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.v1.Value; + +public class BookmarkResponseHandler implements ResponseHandler +{ + private final ExplicitTransaction tx; + + public BookmarkResponseHandler( ExplicitTransaction tx ) + { + this.tx = tx; + } + + @Override + public void onSuccess( Map metadata ) + { + Value bookmarkValue = metadata.get( "bookmark" ); + if ( bookmarkValue != null ) + { + tx.setBookmark( Bookmark.from( bookmarkValue.asString() ) ); + } + } + + @Override + public void onFailure( Throwable error ) + { + } + + @Override + public void onRecord( Value[] fields ) + { + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/handlers/CommitTxResponseHandler.java b/driver/src/main/java/org/neo4j/driver/internal/handlers/CommitTxResponseHandler.java new file mode 100644 index 0000000000..6bb60dafac --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/handlers/CommitTxResponseHandler.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.handlers; + +import io.netty.util.concurrent.Promise; + +import java.util.Arrays; +import java.util.Map; + +import org.neo4j.driver.internal.Bookmark; +import org.neo4j.driver.internal.ExplicitTransaction; +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.v1.Value; + +import static java.util.Objects.requireNonNull; + +public class CommitTxResponseHandler implements ResponseHandler +{ + private final Promise commitTxPromise; + private final ExplicitTransaction tx; + + public CommitTxResponseHandler( Promise commitTxPromise, ExplicitTransaction tx ) + { + this.commitTxPromise = requireNonNull( commitTxPromise ); + this.tx = requireNonNull( tx ); + } + + @Override + public void onSuccess( Map metadata ) + { + Value bookmarkValue = metadata.get( "bookmark" ); + if ( bookmarkValue != null ) + { + if ( tx != null ) + { + tx.setBookmark( Bookmark.from( bookmarkValue.asString() ) ); + } + } + + commitTxPromise.setSuccess( null ); + } + + @Override + public void onFailure( Throwable error ) + { + commitTxPromise.setFailure( error ); + } + + @Override + public void onRecord( Value[] fields ) + { + throw new UnsupportedOperationException( + "Transaction commit is not expected to receive records: " + Arrays.toString( fields ) ); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/handlers/InitResponseHandler.java b/driver/src/main/java/org/neo4j/driver/internal/handlers/InitResponseHandler.java new file mode 100644 index 0000000000..c055d38638 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/handlers/InitResponseHandler.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.handlers; + +import java.util.Map; + +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.v1.Value; + +public class InitResponseHandler implements ResponseHandler +{ + private String serverVersion; + + @Override + public void onSuccess( Map metadata ) + { + Value versionValue = metadata.get( "server" ); + if ( versionValue != null ) + { + serverVersion = versionValue.asString(); + } + } + + @Override + public void onFailure( Throwable error ) + { + } + + @Override + public void onRecord( Value[] fields ) + { + } + + public String serverVersion() + { + return serverVersion; + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/handlers/NoOpResponseHandler.java b/driver/src/main/java/org/neo4j/driver/internal/handlers/NoOpResponseHandler.java new file mode 100644 index 0000000000..039193f046 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/handlers/NoOpResponseHandler.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.handlers; + +import java.util.Map; + +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.v1.Value; + +public class NoOpResponseHandler implements ResponseHandler +{ + public static final NoOpResponseHandler INSTANCE = new NoOpResponseHandler(); + + @Override + public void onSuccess( Map metadata ) + { + } + + @Override + public void onFailure( Throwable error ) + { + } + + @Override + public void onRecord( Value[] fields ) + { + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/handlers/PingResponseHandler.java b/driver/src/main/java/org/neo4j/driver/internal/handlers/PingResponseHandler.java new file mode 100644 index 0000000000..b98581c423 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/handlers/PingResponseHandler.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.handlers; + +import io.netty.util.concurrent.Promise; + +import java.util.Map; + +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.v1.Value; + +import static java.util.Objects.requireNonNull; + +public class PingResponseHandler implements ResponseHandler +{ + private final Promise result; + + public PingResponseHandler( Promise result ) + { + this.result = requireNonNull( result ); + } + + @Override + public void onSuccess( Map metadata ) + { + result.setSuccess( true ); + } + + @Override + public void onFailure( Throwable error ) + { + result.setSuccess( false ); + } + + @Override + public void onRecord( Value[] fields ) + { + throw new UnsupportedOperationException(); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/handlers/PullAllResponseHandler.java b/driver/src/main/java/org/neo4j/driver/internal/handlers/PullAllResponseHandler.java new file mode 100644 index 0000000000..3803bec368 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/handlers/PullAllResponseHandler.java @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.handlers; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; + +import org.neo4j.driver.internal.InternalRecord; +import org.neo4j.driver.internal.async.AsyncConnection; +import org.neo4j.driver.internal.async.InternalFuture; +import org.neo4j.driver.internal.async.InternalPromise; +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.internal.summary.InternalNotification; +import org.neo4j.driver.internal.summary.InternalPlan; +import org.neo4j.driver.internal.summary.InternalProfiledPlan; +import org.neo4j.driver.internal.summary.InternalResultSummary; +import org.neo4j.driver.internal.summary.InternalSummaryCounters; +import org.neo4j.driver.v1.Record; +import org.neo4j.driver.v1.Statement; +import org.neo4j.driver.v1.Value; +import org.neo4j.driver.v1.summary.Notification; +import org.neo4j.driver.v1.summary.Plan; +import org.neo4j.driver.v1.summary.ProfiledPlan; +import org.neo4j.driver.v1.summary.ResultSummary; +import org.neo4j.driver.v1.summary.StatementType; + +import static java.util.Objects.requireNonNull; + +public abstract class PullAllResponseHandler implements ResponseHandler +{ + private static final boolean TOUCH_AUTO_READ = false; + + private final Statement statement; + private final RunResponseHandler runResponseHandler; + protected final AsyncConnection connection; + + private final Queue records; + private boolean succeeded; + private Throwable failure; + + private ResultSummary summary; + private volatile Record current; + + private InternalPromise recordAvailablePromise; + private InternalPromise summaryAvailablePromise; + + public PullAllResponseHandler( Statement statement, RunResponseHandler runResponseHandler, + AsyncConnection connection ) + { + this.statement = requireNonNull( statement ); + this.runResponseHandler = requireNonNull( runResponseHandler ); + this.connection = requireNonNull( connection ); + this.records = new LinkedList<>(); + } + + @Override + public synchronized void onSuccess( Map metadata ) + { + summary = extractResultSummary( metadata ); + if ( summaryAvailablePromise != null ) + { + summaryAvailablePromise.setSuccess( summary ); + summaryAvailablePromise = null; + } + + succeeded = true; + afterSuccess(); + + if ( recordAvailablePromise != null ) + { + recordAvailablePromise.setSuccess( false ); + recordAvailablePromise = null; + } + } + + protected abstract void afterSuccess(); + + @Override + public synchronized void onFailure( Throwable error ) + { + failure = error; + afterFailure( error ); + + if ( recordAvailablePromise != null ) + { + recordAvailablePromise.setFailure( error ); + recordAvailablePromise = null; + } + } + + protected abstract void afterFailure( Throwable error ); + + @Override + public synchronized void onRecord( Value[] fields ) + { + Record record = new InternalRecord( runResponseHandler.statementKeys(), fields ); + + if ( recordAvailablePromise != null ) + { + current = record; + recordAvailablePromise.setSuccess( true ); + recordAvailablePromise = null; + } + else + { + queueRecord( record ); + } + } + + public synchronized InternalFuture fetchRecordAsync() + { + Record record = dequeueRecord(); + if ( record == null ) + { + if ( succeeded ) + { + return connection.newPromise().setSuccess( false ); + } + + if ( failure != null ) + { + return connection.newPromise().setFailure( failure ); + } + + if ( recordAvailablePromise == null ) + { + recordAvailablePromise = connection.newPromise(); + } + + return recordAvailablePromise; + } + else + { + current = record; + return connection.newPromise().setSuccess( true ); + } + } + + public Record currentRecord() + { + Record result = current; + current = null; + return result; + } + + public synchronized InternalFuture summaryAsync() + { + if ( summary != null ) + { + return connection.newPromise().setSuccess( summary ); + } + else + { + if ( summaryAvailablePromise == null ) + { + summaryAvailablePromise = connection.newPromise(); + } + return summaryAvailablePromise; + } + } + + private void queueRecord( Record record ) + { + records.add( record ); + if ( TOUCH_AUTO_READ ) + { + if ( records.size() > 10_000 ) + { + connection.disableAutoRead(); + } + } + } + + private Record dequeueRecord() + { + Record record = records.poll(); + if ( TOUCH_AUTO_READ ) + { + if ( record != null && records.size() < 100 ) + { + connection.enableAutoRead(); + } + } + return record; + } + + private ResultSummary extractResultSummary( Map metadata ) + { + return new InternalResultSummary( statement, connection.serverInfo(), extractStatementType( metadata ), + extractCounters( metadata ), extractPlan( metadata ), extractProfiledPlan( metadata ), + extractNotifications( metadata ), runResponseHandler.resultAvailableAfter(), + extractResultConsumedAfter( metadata ) ); + } + + private static StatementType extractStatementType( Map metadata ) + { + Value typeValue = metadata.get( "type" ); + if ( typeValue != null ) + { + return StatementType.fromCode( typeValue.asString() ); + } + return null; + } + + private static InternalSummaryCounters extractCounters( Map metadata ) + { + Value countersValue = metadata.get( "stats" ); + if ( countersValue != null ) + { + return new InternalSummaryCounters( + counterValue( countersValue, "nodes-created" ), + counterValue( countersValue, "nodes-deleted" ), + counterValue( countersValue, "relationships-created" ), + counterValue( countersValue, "relationships-deleted" ), + counterValue( countersValue, "properties-set" ), + counterValue( countersValue, "labels-added" ), + counterValue( countersValue, "labels-removed" ), + counterValue( countersValue, "indexes-added" ), + counterValue( countersValue, "indexes-removed" ), + counterValue( countersValue, "constraints-added" ), + counterValue( countersValue, "constraints-removed" ) + ); + } + return null; + } + + private static int counterValue( Value countersValue, String name ) + { + Value value = countersValue.get( name ); + return value.isNull() ? 0 : value.asInt(); + } + + private static Plan extractPlan( Map metadata ) + { + Value planValue = metadata.get( "plan" ); + if ( planValue != null ) + { + return InternalPlan.EXPLAIN_PLAN_FROM_VALUE.apply( planValue ); + } + return null; + } + + private static ProfiledPlan extractProfiledPlan( Map metadata ) + { + Value profiledPlanValue = metadata.get( "profile" ); + if ( profiledPlanValue != null ) + { + return InternalProfiledPlan.PROFILED_PLAN_FROM_VALUE.apply( profiledPlanValue ); + } + return null; + } + + private static List extractNotifications( Map metadata ) + { + Value notificationsValue = metadata.get( "notifications" ); + if ( notificationsValue != null ) + { + return notificationsValue.asList( InternalNotification.VALUE_TO_NOTIFICATION ); + } + return Collections.emptyList(); + } + + private static long extractResultConsumedAfter( Map metadata ) + { + Value resultConsumedAfterValue = metadata.get( "result_consumed_after" ); + if ( resultConsumedAfterValue != null ) + { + return resultConsumedAfterValue.asLong(); + } + return -1; + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/handlers/RecordsResponseHandler.java b/driver/src/main/java/org/neo4j/driver/internal/handlers/RecordsResponseHandler.java new file mode 100644 index 0000000000..5ac756344e --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/handlers/RecordsResponseHandler.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.handlers; + +import io.netty.util.concurrent.Promise; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.neo4j.driver.internal.InternalRecord; +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.internal.summary.InternalNotification; +import org.neo4j.driver.internal.summary.InternalPlan; +import org.neo4j.driver.internal.summary.InternalProfiledPlan; +import org.neo4j.driver.internal.summary.InternalSummaryCounters; +import org.neo4j.driver.v1.Record; +import org.neo4j.driver.v1.Value; +import org.neo4j.driver.v1.summary.Notification; +import org.neo4j.driver.v1.summary.Plan; +import org.neo4j.driver.v1.summary.ProfiledPlan; +import org.neo4j.driver.v1.summary.StatementType; +import org.neo4j.driver.v1.summary.SummaryCounters; + +public class RecordsResponseHandler implements ResponseHandler +{ + private final RunResponseHandler runResponseHandler; + + private final Queue recordBuffer; + private Promise recordAvailablePromise; + + private StatementType statementType; + private SummaryCounters counters; + private Plan plan; + private ProfiledPlan profile; + private List notifications; + private long resultConsumedAfter; + + private boolean completed; + + public RecordsResponseHandler( RunResponseHandler runResponseHandler ) + { + this.runResponseHandler = runResponseHandler; + this.recordBuffer = new ConcurrentLinkedQueue<>(); + } + + @Override + public void onSuccess( Map metadata ) + { + statementType = extractStatementType( metadata ); + counters = extractCounters( metadata ); + plan = extractPlan( metadata ); + profile = extractProfiledPlan( metadata ); + notifications = extractNotifications( metadata ); + resultConsumedAfter = extractResultConsumedAfter( metadata ); + + completed = true; + + if ( recordAvailablePromise != null ) + { + boolean hasMoreRecords = !recordBuffer.isEmpty(); + recordAvailablePromise.setSuccess( hasMoreRecords ); + recordAvailablePromise = null; + } + } + + @Override + public void onFailure( Throwable error ) + { + completed = true; + + if ( recordAvailablePromise != null ) + { + recordAvailablePromise.setFailure( error ); + recordAvailablePromise = null; + } + } + + @Override + public void onRecord( Value[] fields ) + { + recordBuffer.add( new InternalRecord( runResponseHandler.statementKeys(), fields ) ); + + if ( recordAvailablePromise != null ) + { + recordAvailablePromise.setSuccess( true ); + recordAvailablePromise = null; + } + } + + public Queue recordBuffer() + { + return recordBuffer; + } + + public StatementType statementType() + { + return statementType; + } + + public SummaryCounters counters() + { + return counters; + } + + public Plan plan() + { + return plan; + } + + public ProfiledPlan profile() + { + return profile; + } + + public List notifications() + { + return notifications; + } + + public long resultConsumedAfter() + { + return resultConsumedAfter; + } + + public boolean isCompleted() + { + return completed; + } + + private static StatementType extractStatementType( Map metadata ) + { + Value typeValue = metadata.get( "type" ); + if ( typeValue != null ) + { + return StatementType.fromCode( typeValue.asString() ); + } + return null; + } + + private static InternalSummaryCounters extractCounters( Map metadata ) + { + Value countersValue = metadata.get( "stats" ); + if ( countersValue != null ) + { + return new InternalSummaryCounters( + counterValue( countersValue, "nodes-created" ), + counterValue( countersValue, "nodes-deleted" ), + counterValue( countersValue, "relationships-created" ), + counterValue( countersValue, "relationships-deleted" ), + counterValue( countersValue, "properties-set" ), + counterValue( countersValue, "labels-added" ), + counterValue( countersValue, "labels-removed" ), + counterValue( countersValue, "indexes-added" ), + counterValue( countersValue, "indexes-removed" ), + counterValue( countersValue, "constraints-added" ), + counterValue( countersValue, "constraints-removed" ) + ); + } + return null; + } + + private static int counterValue( Value countersValue, String name ) + { + Value value = countersValue.get( name ); + return value.isNull() ? 0 : value.asInt(); + } + + private static Plan extractPlan( Map metadata ) + { + Value planValue = metadata.get( "plan" ); + if ( planValue != null ) + { + return InternalPlan.EXPLAIN_PLAN_FROM_VALUE.apply( planValue ); + } + return null; + } + + private static ProfiledPlan extractProfiledPlan( Map metadata ) + { + Value profiledPlanValue = metadata.get( "profile" ); + if ( profiledPlanValue != null ) + { + return InternalProfiledPlan.PROFILED_PLAN_FROM_VALUE.apply( profiledPlanValue ); + } + return null; + } + + private static List extractNotifications( Map metadata ) + { + Value notificationsValue = metadata.get( "notifications" ); + if ( notificationsValue != null ) + { + return notificationsValue.asList( InternalNotification.VALUE_TO_NOTIFICATION ); + } + return Collections.emptyList(); + } + + private static long extractResultConsumedAfter( Map metadata ) + { + Value resultConsumedAfterValue = metadata.get( "result_consumed_after" ); + if ( resultConsumedAfterValue != null ) + { + return resultConsumedAfterValue.asLong(); + } + return -1; + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/handlers/ResetAsyncResponseHandler.java b/driver/src/main/java/org/neo4j/driver/internal/handlers/ResetAsyncResponseHandler.java new file mode 100644 index 0000000000..484e08bc51 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/handlers/ResetAsyncResponseHandler.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.handlers; + +import java.util.Map; + +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.v1.Value; + +public class ResetAsyncResponseHandler implements ResponseHandler +{ + private final Runnable successCallback; + + public ResetAsyncResponseHandler( Runnable successCallback ) + { + this.successCallback = successCallback; + } + + @Override + public void onSuccess( Map metadata ) + { + successCallback.run(); + } + + @Override + public void onFailure( Throwable error ) + { + } + + @Override + public void onRecord( Value[] fields ) + { + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/handlers/RollbackTxResponseHandler.java b/driver/src/main/java/org/neo4j/driver/internal/handlers/RollbackTxResponseHandler.java new file mode 100644 index 0000000000..b6939424a3 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/handlers/RollbackTxResponseHandler.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.handlers; + +import io.netty.util.concurrent.Promise; + +import java.util.Arrays; +import java.util.Map; + +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.v1.Value; + +import static java.util.Objects.requireNonNull; + +public class RollbackTxResponseHandler implements ResponseHandler +{ + private final Promise rollbackTxPromise; + + public RollbackTxResponseHandler( Promise rollbackTxPromise ) + { + this.rollbackTxPromise = requireNonNull( rollbackTxPromise ); + } + + @Override + public void onSuccess( Map metadata ) + { + rollbackTxPromise.setSuccess( null ); + } + + @Override + public void onFailure( Throwable error ) + { + rollbackTxPromise.setFailure( error ); + } + + @Override + public void onRecord( Value[] fields ) + { + throw new UnsupportedOperationException( + "Transaction rollback is not expected to receive records: " + Arrays.toString( fields ) ); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/handlers/RunResponseHandler.java b/driver/src/main/java/org/neo4j/driver/internal/handlers/RunResponseHandler.java new file mode 100644 index 0000000000..434554e21e --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/handlers/RunResponseHandler.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.handlers; + +import io.netty.util.concurrent.Promise; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.v1.Value; + +public class RunResponseHandler implements ResponseHandler +{ + private final Promise runCompletedPromise; + + private List statementKeys; + private long resultAvailableAfter; + + public RunResponseHandler( Promise runCompletedPromise ) + { + this.runCompletedPromise = runCompletedPromise; + } + + @Override + public void onSuccess( Map metadata ) + { + statementKeys = extractKeys( metadata ); + resultAvailableAfter = extractResultAvailableAfter( metadata ); + + if ( runCompletedPromise != null ) + { + runCompletedPromise.setSuccess( null ); + } + } + + @Override + public void onFailure( Throwable error ) + { + if ( runCompletedPromise != null ) + { + runCompletedPromise.setFailure( error ); + } + } + + @Override + public void onRecord( Value[] fields ) + { + throw new UnsupportedOperationException(); + } + + public List statementKeys() + { + return statementKeys; + } + + public long resultAvailableAfter() + { + return resultAvailableAfter; + } + + private static List extractKeys( Map metadata ) + { + Value keysValue = metadata.get( "fields" ); + if ( keysValue != null ) + { + if ( !keysValue.isEmpty() ) + { + List keys = new ArrayList<>( keysValue.size() ); + for ( Value value : keysValue.values() ) + { + keys.add( value.asString() ); + } + + return keys; + } + } + return Collections.emptyList(); + } + + private static long extractResultAvailableAfter( Map metadata ) + { + Value resultAvailableAfterValue = metadata.get( "result_available_after" ); + if ( resultAvailableAfterValue != null ) + { + return resultAvailableAfterValue.asLong(); + } + return -1; + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/handlers/SessionPullAllResponseHandler.java b/driver/src/main/java/org/neo4j/driver/internal/handlers/SessionPullAllResponseHandler.java new file mode 100644 index 0000000000..aa1c9b155e --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/handlers/SessionPullAllResponseHandler.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.handlers; + +import org.neo4j.driver.internal.async.AsyncConnection; +import org.neo4j.driver.v1.Statement; + +public class SessionPullAllResponseHandler extends PullAllResponseHandler +{ + public SessionPullAllResponseHandler( Statement statement, RunResponseHandler runResponseHandler, + AsyncConnection connection ) + { + super( statement, runResponseHandler, connection ); + } + + @Override + protected void afterSuccess() + { + connection.release(); + } + + @Override + protected void afterFailure( Throwable error ) + { + connection.release(); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/handlers/TransactionPullAllResponseHandler.java b/driver/src/main/java/org/neo4j/driver/internal/handlers/TransactionPullAllResponseHandler.java new file mode 100644 index 0000000000..939f78479d --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/handlers/TransactionPullAllResponseHandler.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.handlers; + +import org.neo4j.driver.internal.ExplicitTransaction; +import org.neo4j.driver.internal.async.AsyncConnection; +import org.neo4j.driver.v1.Statement; + +import static java.util.Objects.requireNonNull; + +public class TransactionPullAllResponseHandler extends PullAllResponseHandler +{ + private final ExplicitTransaction tx; + + public TransactionPullAllResponseHandler( Statement statement, RunResponseHandler runResponseHandler, + AsyncConnection connection, ExplicitTransaction tx ) + { + super( statement, runResponseHandler, connection ); + this.tx = requireNonNull( tx ); + } + + @Override + protected void afterSuccess() + { + } + + @Override + protected void afterFailure( Throwable error ) + { + tx.resultFailed( error ); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/messaging/InitMessage.java b/driver/src/main/java/org/neo4j/driver/internal/messaging/InitMessage.java index 4d1ef3d5a6..d4ec0a706e 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/messaging/InitMessage.java +++ b/driver/src/main/java/org/neo4j/driver/internal/messaging/InitMessage.java @@ -47,6 +47,16 @@ public void dispatch( MessageHandler handler ) throws IOException handler.handleInitMessage( userAgent, authToken ); } + public String userAgent() + { + return userAgent; + } + + public Map authToken() + { + return authToken; + } + @Override public String toString() { diff --git a/driver/src/main/java/org/neo4j/driver/internal/messaging/MessageFormat.java b/driver/src/main/java/org/neo4j/driver/internal/messaging/MessageFormat.java index 419b75b0a6..04d2be6117 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/messaging/MessageFormat.java +++ b/driver/src/main/java/org/neo4j/driver/internal/messaging/MessageFormat.java @@ -19,8 +19,9 @@ package org.neo4j.driver.internal.messaging; import java.io.IOException; -import java.nio.channels.ReadableByteChannel; -import java.nio.channels.WritableByteChannel; + +import org.neo4j.driver.internal.packstream.PackInput; +import org.neo4j.driver.internal.packstream.PackOutput; public interface MessageFormat { @@ -42,9 +43,9 @@ interface Reader } - Writer newWriter( WritableByteChannel ch, boolean byteArraySupportEnabled ); + Writer newWriter( PackOutput output, boolean byteArraySupportEnabled ); - Reader newReader( ReadableByteChannel ch ); + Reader newReader( PackInput input ); int version(); } diff --git a/driver/src/main/java/org/neo4j/driver/internal/messaging/PackStreamMessageFormatV1.java b/driver/src/main/java/org/neo4j/driver/internal/messaging/PackStreamMessageFormatV1.java index 29df271b00..e1e52731a9 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/messaging/PackStreamMessageFormatV1.java +++ b/driver/src/main/java/org/neo4j/driver/internal/messaging/PackStreamMessageFormatV1.java @@ -19,8 +19,6 @@ package org.neo4j.driver.internal.messaging; import java.io.IOException; -import java.nio.channels.ReadableByteChannel; -import java.nio.channels.WritableByteChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -31,13 +29,11 @@ import org.neo4j.driver.internal.InternalNode; import org.neo4j.driver.internal.InternalPath; import org.neo4j.driver.internal.InternalRelationship; -import org.neo4j.driver.internal.net.BufferingChunkedInput; -import org.neo4j.driver.internal.net.ChunkedOutput; +import org.neo4j.driver.internal.packstream.ByteArrayIncompatiblePacker; import org.neo4j.driver.internal.packstream.PackInput; import org.neo4j.driver.internal.packstream.PackOutput; import org.neo4j.driver.internal.packstream.PackStream; import org.neo4j.driver.internal.packstream.PackType; -import org.neo4j.driver.internal.packstream.ByteArrayIncompatiblePacker; import org.neo4j.driver.internal.util.Iterables; import org.neo4j.driver.internal.value.InternalValue; import org.neo4j.driver.internal.value.ListValue; @@ -80,16 +76,14 @@ public class PackStreamMessageFormatV1 implements MessageFormat private static final Map EMPTY_STRING_VALUE_MAP = new HashMap<>( 0 ); @Override - public MessageFormat.Writer newWriter( WritableByteChannel ch, boolean byteArraySupportEnabled ) + public MessageFormat.Writer newWriter( PackOutput output, boolean byteArraySupportEnabled ) { - ChunkedOutput output = new ChunkedOutput( ch ); return new Writer( output, output.messageBoundaryHook(), byteArraySupportEnabled ); } @Override - public MessageFormat.Reader newReader( ReadableByteChannel ch ) + public MessageFormat.Reader newReader( PackInput input ) { - BufferingChunkedInput input = new BufferingChunkedInput( ch ); return new Reader( input, input.messageBoundaryHook() ); } diff --git a/driver/src/main/java/org/neo4j/driver/internal/net/BufferingChunkedInput.java b/driver/src/main/java/org/neo4j/driver/internal/net/BufferingChunkedInput.java index 7c62b8c898..ae7fe6f0eb 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/net/BufferingChunkedInput.java +++ b/driver/src/main/java/org/neo4j/driver/internal/net/BufferingChunkedInput.java @@ -196,6 +196,7 @@ public void run() } }; + @Override public Runnable messageBoundaryHook() { return this.onMessageComplete; diff --git a/driver/src/main/java/org/neo4j/driver/internal/net/ChunkedInput.java b/driver/src/main/java/org/neo4j/driver/internal/net/ChunkedInput.java index 68aceb3b4a..572ca44e45 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/net/ChunkedInput.java +++ b/driver/src/main/java/org/neo4j/driver/internal/net/ChunkedInput.java @@ -326,6 +326,7 @@ public void run() } }; + @Override public Runnable messageBoundaryHook() { return this.onMessageComplete; diff --git a/driver/src/main/java/org/neo4j/driver/internal/net/ChunkedOutput.java b/driver/src/main/java/org/neo4j/driver/internal/net/ChunkedOutput.java index 505b152c93..af2b066a07 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/net/ChunkedOutput.java +++ b/driver/src/main/java/org/neo4j/driver/internal/net/ChunkedOutput.java @@ -105,8 +105,10 @@ public PackOutput writeDouble( double value ) throws IOException } @Override - public PackOutput writeBytes( byte[] data, int offset, int length ) throws IOException + public PackOutput writeBytes( byte[] data ) throws IOException { + int offset = 0; + int length = data.length; while ( offset < length ) { // Ensure there is an open chunk, and that it has at least one byte of space left @@ -178,6 +180,7 @@ public void run() } }; + @Override public Runnable messageBoundaryHook() { return onMessageComplete; diff --git a/driver/src/main/java/org/neo4j/driver/internal/net/ConcurrencyGuardingConnection.java b/driver/src/main/java/org/neo4j/driver/internal/net/ConcurrencyGuardingConnection.java index 13dcc21a16..2bd03dff30 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/net/ConcurrencyGuardingConnection.java +++ b/driver/src/main/java/org/neo4j/driver/internal/net/ConcurrencyGuardingConnection.java @@ -21,8 +21,8 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; -import org.neo4j.driver.internal.spi.Collector; import org.neo4j.driver.internal.spi.Connection; +import org.neo4j.driver.internal.spi.ResponseHandler; import org.neo4j.driver.v1.Value; import org.neo4j.driver.v1.exceptions.ClientException; import org.neo4j.driver.v1.summary.ServerInfo; @@ -58,13 +58,12 @@ public void init( String clientName, Map authToken ) } @Override - public void run( String statement, Map parameters, - Collector collector ) + public void run( String statement, Map parameters, ResponseHandler handler ) { try { markAsInUse(); - delegate.run(statement, parameters, collector); + delegate.run( statement, parameters, handler ); } finally { @@ -73,12 +72,12 @@ public void run( String statement, Map parameters, } @Override - public void discardAll( Collector collector ) + public void discardAll( ResponseHandler handler ) { try { markAsInUse(); - delegate.discardAll( collector ); + delegate.discardAll( handler ); } finally { @@ -87,12 +86,12 @@ public void discardAll( Collector collector ) } @Override - public void pullAll( Collector collector ) + public void pullAll( ResponseHandler handler ) { try { markAsInUse(); - delegate.pullAll(collector); + delegate.pullAll( handler ); } finally { diff --git a/driver/src/main/java/org/neo4j/driver/internal/net/SocketClient.java b/driver/src/main/java/org/neo4j/driver/internal/net/SocketClient.java index aff448a556..5370e5c62a 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/net/SocketClient.java +++ b/driver/src/main/java/org/neo4j/driver/internal/net/SocketClient.java @@ -180,7 +180,7 @@ public void send( Queue messages ) throws IOException public void receiveAll( SocketResponseHandler handler ) throws IOException { // Wait until all pending requests have been replied to - while ( handler.collectorsWaiting() > 0 ) + while ( handler.handlersWaiting() > 0 ) { receiveOne( handler ); } diff --git a/driver/src/main/java/org/neo4j/driver/internal/net/SocketConnection.java b/driver/src/main/java/org/neo4j/driver/internal/net/SocketConnection.java index 64d5356053..15b1c64b95 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/net/SocketConnection.java +++ b/driver/src/main/java/org/neo4j/driver/internal/net/SocketConnection.java @@ -25,13 +25,16 @@ import java.util.Queue; import java.util.concurrent.atomic.AtomicBoolean; +import org.neo4j.driver.internal.handlers.InitResponseHandler; +import org.neo4j.driver.internal.handlers.NoOpResponseHandler; +import org.neo4j.driver.internal.handlers.ResetAsyncResponseHandler; import org.neo4j.driver.internal.logging.DelegatingLogger; import org.neo4j.driver.internal.messaging.InitMessage; import org.neo4j.driver.internal.messaging.Message; import org.neo4j.driver.internal.messaging.RunMessage; import org.neo4j.driver.internal.security.SecurityPlan; -import org.neo4j.driver.internal.spi.Collector; import org.neo4j.driver.internal.spi.Connection; +import org.neo4j.driver.internal.spi.ResponseHandler; import org.neo4j.driver.internal.summary.InternalServerInfo; import org.neo4j.driver.v1.Logger; import org.neo4j.driver.v1.Logging; @@ -113,41 +116,58 @@ private SocketResponseHandler createResponseHandler( Logger logger ) @Override public void init( String clientName, Map authToken ) { - Collector.InitCollector initCollector = new Collector.InitCollector(); - queueMessage( new InitMessage( clientName, authToken ), initCollector ); + InitResponseHandler initHandler = new InitResponseHandler(); + queueMessage( new InitMessage( clientName, authToken ), initHandler ); sync(); - this.serverInfo = new InternalServerInfo( socket.address(), initCollector.serverVersion() ); + this.serverInfo = new InternalServerInfo( socket.address(), initHandler.serverVersion() ); socket.updateProtocol( serverInfo.version() ); } @Override - public void run( String statement, Map parameters, Collector collector ) + public void run( String statement, Map parameters, ResponseHandler handler ) { - queueMessage( new RunMessage( statement, parameters ), collector ); + queueMessage( new RunMessage( statement, parameters ), handler ); } @Override - public void discardAll( Collector collector ) + public void discardAll( ResponseHandler handler ) { - queueMessage( DISCARD_ALL, collector ); + queueMessage( DISCARD_ALL, handler ); } @Override - public void pullAll( Collector collector ) + public void pullAll( ResponseHandler handler ) { - queueMessage( PULL_ALL, collector ); + queueMessage( PULL_ALL, handler ); } @Override public void reset() { - queueMessage( RESET, Collector.RESET ); + queueMessage( RESET, NoOpResponseHandler.INSTANCE ); } @Override public void ackFailure() { - queueMessage( ACK_FAILURE, Collector.ACK_FAILURE ); + queueMessage( ACK_FAILURE, new ResponseHandler() + { + @Override + public void onSuccess( Map metadata ) + { + responseHandler.clearError(); + } + + @Override + public void onFailure( Throwable error ) + { + } + + @Override + public void onRecord( Value[] fields ) + { + } + } ); } @Override @@ -180,7 +200,7 @@ private void ensureNotInterrupted() if( isInterrupted.get() ) { // receive each of it and throw error immediately - while ( responseHandler.collectorsWaiting() > 0 ) + while ( responseHandler.handlersWaiting() > 0 ) { receiveOne(); } @@ -251,12 +271,12 @@ else if ( e instanceof SocketTimeoutException ) } } - private synchronized void queueMessage( Message msg, Collector collector ) + private synchronized void queueMessage( Message msg, ResponseHandler handler ) { ensureNotInterrupted(); pendingMessages.add( msg ); - responseHandler.appendResultCollector( collector ); + responseHandler.appendResponseHandler( handler ); } @Override @@ -274,7 +294,7 @@ public boolean isOpen() @Override public synchronized void resetAsync() { - queueMessage( RESET, new Collector.ResetCollector( new Runnable() + queueMessage( RESET, new ResetAsyncResponseHandler( new Runnable() { @Override public void run() diff --git a/driver/src/main/java/org/neo4j/driver/internal/net/SocketConnector.java b/driver/src/main/java/org/neo4j/driver/internal/net/SocketConnector.java index 8e657adc34..0c946c4e7e 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/net/SocketConnector.java +++ b/driver/src/main/java/org/neo4j/driver/internal/net/SocketConnector.java @@ -47,7 +47,8 @@ public SocketConnector( ConnectionSettings connectionSettings, SecurityPlan secu @Override public final Connection connect( BoltServerAddress address ) { - Connection connection = createConnection( address, securityPlan, connectionSettings.timeoutMillis(), logging ); + Connection connection = + createConnection( address, securityPlan, connectionSettings.connectTimeoutMillis(), logging ); // Because SocketConnection is not thread safe, wrap it in this guard // to ensure concurrent access leads causes application errors diff --git a/driver/src/main/java/org/neo4j/driver/internal/net/SocketProtocolV1.java b/driver/src/main/java/org/neo4j/driver/internal/net/SocketProtocolV1.java index 1b104e9b05..5175f75427 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/net/SocketProtocolV1.java +++ b/driver/src/main/java/org/neo4j/driver/internal/net/SocketProtocolV1.java @@ -45,8 +45,8 @@ public static SocketProtocol createWithoutByteArraySupport( ByteChannel channel private SocketProtocolV1( ByteChannel channel, boolean byteArraySupportEnabled ) { messageFormat = new PackStreamMessageFormatV1(); - this.writer = messageFormat.newWriter( channel, byteArraySupportEnabled ); - this.reader = messageFormat.newReader( channel ); + this.writer = messageFormat.newWriter( new ChunkedOutput( channel ), byteArraySupportEnabled ); + this.reader = messageFormat.newReader( new BufferingChunkedInput( channel ) ); } @Override diff --git a/driver/src/main/java/org/neo4j/driver/internal/net/SocketResponseHandler.java b/driver/src/main/java/org/neo4j/driver/internal/net/SocketResponseHandler.java index b7dda67019..ca35e1969b 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/net/SocketResponseHandler.java +++ b/driver/src/main/java/org/neo4j/driver/internal/net/SocketResponseHandler.java @@ -19,255 +19,94 @@ package org.neo4j.driver.internal.net; import java.util.Map; +import java.util.Objects; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; -import org.neo4j.driver.internal.Bookmark; import org.neo4j.driver.internal.messaging.MessageHandler; -import org.neo4j.driver.internal.spi.Collector; -import org.neo4j.driver.internal.summary.InternalNotification; -import org.neo4j.driver.internal.summary.InternalPlan; -import org.neo4j.driver.internal.summary.InternalProfiledPlan; -import org.neo4j.driver.internal.summary.InternalSummaryCounters; +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.internal.util.ErrorUtil; import org.neo4j.driver.v1.Value; -import org.neo4j.driver.v1.exceptions.AuthenticationException; -import org.neo4j.driver.v1.exceptions.ClientException; -import org.neo4j.driver.v1.exceptions.DatabaseException; import org.neo4j.driver.v1.exceptions.Neo4jException; -import org.neo4j.driver.v1.exceptions.TransientException; -import org.neo4j.driver.v1.summary.Notification; -import org.neo4j.driver.v1.summary.StatementType; -import org.neo4j.driver.v1.util.Function; public class SocketResponseHandler implements MessageHandler { - private final Queue collectors = new ConcurrentLinkedQueue<>(); + private final Queue handlers = new ConcurrentLinkedQueue<>(); /** If a failure occurs, the error gets stored here */ private Neo4jException error; - public int collectorsWaiting() - { - return collectors.size(); - } - @Override public void handleRecordMessage( Value[] fields ) { - Collector collector = collectors.element(); - collector.record( fields ); + ResponseHandler handler = handlers.element(); + handler.onRecord( fields ); } @Override public void handleFailureMessage( String code, String message ) { - Collector collector = collectors.remove(); - String[] parts = code.split( "\\." ); - String classification = parts[1]; - switch ( classification ) - { - case "ClientError": - if( code.equalsIgnoreCase( "Neo.ClientError.Security.Unauthorized" ) ) - { - error = new AuthenticationException( code, message ); - } - else - { - error = new ClientException( code, message ); - } - break; - case "TransientError": - error = new TransientException( code, message ); - break; - default: - error = new DatabaseException( code, message ); - break; - } - if ( collector != null ) + ResponseHandler handler = handlers.remove(); + error = ErrorUtil.newNeo4jError( code, message ); + if ( handler != null ) { - collector.doneFailure( error ); + handler.onFailure( error ); } } @Override public void handleSuccessMessage( Map meta ) { - Collector collector = collectors.remove(); - collectServerVersion( collector, meta.get( "server" ) ); - collectFields( collector, meta.get( "fields" ) ); - collectType( collector, meta.get( "type" ) ); - collectStatistics( collector, meta.get( "stats" ) ); - collectPlan( collector, meta.get( "plan" ) ); - collectProfile( collector, meta.get( "profile" ) ); - collectNotifications( collector, meta.get( "notifications" ) ); - collectResultAvailableAfter( collector, meta.get("result_available_after") ); - collectResultConsumedAfter( collector, meta.get("result_consumed_after") ); - collectBookmark( collector, meta.get( "bookmark" ) ); - collector.doneSuccess(); - } - - private void collectServerVersion( Collector collector, Value serverVersion ) - { - if ( serverVersion != null ) - { - collector.serverVersion( serverVersion.asString() ); - } - } - - private void collectResultAvailableAfter( Collector collector, Value resultAvailableAfter ) - { - if (resultAvailableAfter != null) - { - collector.resultAvailableAfter(resultAvailableAfter.asLong()); - } - } - - private void collectResultConsumedAfter( Collector collector, Value resultConsumedAfter ) - { - if (resultConsumedAfter != null) - { - collector.resultConsumedAfter(resultConsumedAfter.asLong()); - } - } - - private void collectNotifications( Collector collector, Value notifications ) - { - if ( notifications != null ) - { - Function notification = InternalNotification - .VALUE_TO_NOTIFICATION; - collector.notifications( notifications.asList( notification ) ); - } - } - - private void collectPlan( Collector collector, Value plan ) - { - if ( plan != null ) - { - collector.plan( InternalPlan.EXPLAIN_PLAN_FROM_VALUE.apply( plan ) ); - } - } - - private void collectProfile( Collector collector, Value plan ) - { - if ( plan != null ) - { - collector.profile( InternalProfiledPlan.PROFILED_PLAN_FROM_VALUE.apply( plan ) ); - } - } - - private void collectFields( Collector collector, Value fieldValue ) - { - if ( fieldValue != null ) - { - if ( !fieldValue.isEmpty() ) - { - String[] fields = new String[fieldValue.size()]; - int idx = 0; - for ( Value value : fieldValue.values() ) - { - fields[idx++] = value.asString(); - } - collector.keys( fields ); - } - } - } - - private void collectType( Collector collector, Value type ) - { - if ( type != null ) - { - collector.statementType( StatementType.fromCode( type.asString() ) ); - } - } - - private void collectStatistics( Collector collector, Value stats ) - { - if ( stats != null ) - { - collector.statementStatistics( - new InternalSummaryCounters( - statsValue( stats, "nodes-created" ), - statsValue( stats, "nodes-deleted" ), - statsValue( stats, "relationships-created" ), - statsValue( stats, "relationships-deleted" ), - statsValue( stats, "properties-set" ), - statsValue( stats, "labels-added" ), - statsValue( stats, "labels-removed" ), - statsValue( stats, "indexes-added" ), - statsValue( stats, "indexes-removed" ), - statsValue( stats, "constraints-added" ), - statsValue( stats, "constraints-removed" ) - ) - ); - } - } - - private void collectBookmark( Collector collector, Value bookmark ) - { - if ( bookmark != null ) - { - collector.bookmark( Bookmark.from( bookmark.asString() ) ); - } - } - - private int statsValue( Value stats, String name ) - { - Value value = stats.get( name ); - return value.isNull() ? 0 : value.asInt(); + ResponseHandler handler = handlers.remove(); + handler.onSuccess( meta ); } @Override public void handleIgnoredMessage() { - Collector collector = collectors.remove(); - if (collector != null) - { - collector.doneIgnored(); - } + ResponseHandler handler = handlers.remove(); + handler.onFailure( error ); } @Override public void handleDiscardAllMessage() { - } @Override public void handleResetMessage() { - } @Override public void handleAckFailureMessage() { - } @Override public void handlePullAllMessage() { - } @Override public void handleInitMessage( String clientNameAndVersion, Map authToken ) { - } @Override public void handleRunMessage( String statement, Map parameters ) { - } - public void appendResultCollector( Collector collector ) + public void appendResponseHandler( ResponseHandler handler ) { - assert collector != null; + Objects.requireNonNull( handler ); + handlers.add( handler ); + } - collectors.add( collector ); + public int handlersWaiting() + { + return handlers.size(); } public boolean protocolViolationErrorOccurred() diff --git a/driver/src/main/java/org/neo4j/driver/internal/net/pooling/PoolSettings.java b/driver/src/main/java/org/neo4j/driver/internal/net/pooling/PoolSettings.java index 199f3235e7..a5f0a275a3 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/net/pooling/PoolSettings.java +++ b/driver/src/main/java/org/neo4j/driver/internal/net/pooling/PoolSettings.java @@ -22,20 +22,28 @@ public class PoolSettings { public static final int NO_IDLE_CONNECTION_TEST = -1; public static final int INFINITE_CONNECTION_LIFETIME = -1; + public static final int NOT_CONFIGURED = -1; public static final int DEFAULT_MAX_IDLE_CONNECTION_POOL_SIZE = 10; - public static final int DEFAULT_IDLE_TIME_BEFORE_CONNECTION_TEST = NO_IDLE_CONNECTION_TEST; - public static final int DEFAULT_MAX_CONNECTION_LIFETIME = INFINITE_CONNECTION_LIFETIME; + public static final int DEFAULT_MAX_CONNECTION_POOL_SIZE = Integer.MAX_VALUE; + public static final long DEFAULT_IDLE_TIME_BEFORE_CONNECTION_TEST = NOT_CONFIGURED; + public static final long DEFAULT_MAX_CONNECTION_LIFETIME = NOT_CONFIGURED; + public static final long DEFAULT_CONNECTION_ACQUISITION_TIMEOUT = Long.MAX_VALUE; private final int maxIdleConnectionPoolSize; private final long idleTimeBeforeConnectionTest; private final long maxConnectionLifetime; + private final int maxConnectionPoolSize; + private final long connectionAcquisitionTimeout; - public PoolSettings( int maxIdleConnectionPoolSize, long idleTimeBeforeConnectionTest, long maxConnectionLifetime ) + public PoolSettings( int maxIdleConnectionPoolSize, long idleTimeBeforeConnectionTest, long maxConnectionLifetime, + int maxConnectionPoolSize, long connectionAcquisitionTimeout ) { this.maxIdleConnectionPoolSize = maxIdleConnectionPoolSize; this.idleTimeBeforeConnectionTest = idleTimeBeforeConnectionTest; this.maxConnectionLifetime = maxConnectionLifetime; + this.maxConnectionPoolSize = maxConnectionPoolSize; + this.connectionAcquisitionTimeout = connectionAcquisitionTimeout; } public int maxIdleConnectionPoolSize() @@ -62,4 +70,14 @@ public boolean maxConnectionLifetimeEnabled() { return maxConnectionLifetime > 0; } + + public int maxConnectionPoolSize() + { + return maxConnectionPoolSize; + } + + public long connectionAcquisitionTimeout() + { + return connectionAcquisitionTimeout; + } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/net/pooling/PooledSocketConnection.java b/driver/src/main/java/org/neo4j/driver/internal/net/pooling/PooledSocketConnection.java index 0b74bff029..0f17d9ce9d 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/net/pooling/PooledSocketConnection.java +++ b/driver/src/main/java/org/neo4j/driver/internal/net/pooling/PooledSocketConnection.java @@ -22,15 +22,16 @@ import org.neo4j.driver.internal.SessionResourcesHandler; import org.neo4j.driver.internal.net.BoltServerAddress; -import org.neo4j.driver.internal.spi.Collector; import org.neo4j.driver.internal.spi.Connection; import org.neo4j.driver.internal.spi.PooledConnection; +import org.neo4j.driver.internal.spi.ResponseHandler; import org.neo4j.driver.internal.util.Clock; import org.neo4j.driver.internal.util.Consumer; import org.neo4j.driver.v1.Value; -import org.neo4j.driver.v1.exceptions.Neo4jException; import org.neo4j.driver.v1.summary.ServerInfo; +import static org.neo4j.driver.internal.util.ErrorUtil.isRecoverable; + /** * The state of a pooledConnection from a pool point of view could be one of the following: * Created, @@ -87,12 +88,11 @@ public void init( String clientName, Map authToken ) } @Override - public void run( String statement, Map parameters, - Collector collector ) + public void run( String statement, Map parameters, ResponseHandler handler ) { try { - delegate.run( statement, parameters, collector ); + delegate.run( statement, parameters, handler ); } catch(RuntimeException e) { @@ -101,11 +101,11 @@ public void run( String statement, Map parameters, } @Override - public void discardAll( Collector collector ) + public void discardAll( ResponseHandler handler ) { try { - delegate.discardAll( collector ); + delegate.discardAll( handler ); } catch ( RuntimeException e ) { @@ -114,11 +114,11 @@ public void discardAll( Collector collector ) } @Override - public void pullAll( Collector collector ) + public void pullAll( ResponseHandler handler ) { try { - delegate.pullAll( collector ); + delegate.pullAll( handler ); } catch ( RuntimeException e ) { @@ -262,7 +262,7 @@ public void dispose() */ private void onDelegateException( RuntimeException e ) { - if ( !isClientOrTransientError( e ) || isProtocolViolationError( e ) ) + if ( !isRecoverable( e ) ) { unrecoverableErrorsOccurred = true; } @@ -295,33 +295,6 @@ public long lastUsedTimestamp() return lastUsedTimestamp; } - private boolean isProtocolViolationError( RuntimeException e ) - { - if ( e instanceof Neo4jException ) - { - String errorCode = ((Neo4jException) e).code(); - if ( errorCode != null ) - { - return errorCode.startsWith( "Neo.ClientError.Request" ); - } - } - return false; - } - - private boolean isClientOrTransientError( RuntimeException e ) - { - // Eg: DatabaseErrors and unknown (no status code or not neo4j exception) cause session to be discarded - if ( e instanceof Neo4jException ) - { - String errorCode = ((Neo4jException) e).code(); - if ( errorCode != null ) - { - return errorCode.contains( "ClientError" ) || errorCode.contains( "TransientError" ); - } - } - return false; - } - private void updateLastUsedTimestamp() { lastUsedTimestamp = clock.millis(); diff --git a/driver/src/main/java/org/neo4j/driver/internal/packstream/PackInput.java b/driver/src/main/java/org/neo4j/driver/internal/packstream/PackInput.java index 0cff68f9b7..675be97d36 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/packstream/PackInput.java +++ b/driver/src/main/java/org/neo4j/driver/internal/packstream/PackInput.java @@ -49,4 +49,7 @@ public interface PackInput /** Get the next byte without forwarding the internal pointer */ byte peekByte() throws IOException; + + // todo: remove this method! it is temporary! + Runnable messageBoundaryHook(); } diff --git a/driver/src/main/java/org/neo4j/driver/internal/packstream/PackOutput.java b/driver/src/main/java/org/neo4j/driver/internal/packstream/PackOutput.java index fe988d7fe3..7db7d4fbe0 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/packstream/PackOutput.java +++ b/driver/src/main/java/org/neo4j/driver/internal/packstream/PackOutput.java @@ -32,7 +32,7 @@ public interface PackOutput PackOutput writeByte( byte value ) throws IOException; /** Produce binary data */ - PackOutput writeBytes( byte[] data, int offset, int amountToWrite ) throws IOException; + PackOutput writeBytes( byte[] data ) throws IOException; /** Produce a 4-byte signed integer */ PackOutput writeShort( short value ) throws IOException; @@ -45,4 +45,7 @@ public interface PackOutput /** Produce an 8-byte IEEE 754 "double format" floating-point number */ PackOutput writeDouble( double value ) throws IOException; + + // todo: remove this method! it is temporary! + Runnable messageBoundaryHook(); } diff --git a/driver/src/main/java/org/neo4j/driver/internal/packstream/PackStream.java b/driver/src/main/java/org/neo4j/driver/internal/packstream/PackStream.java index e3f69fb591..e5d67dbed6 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/packstream/PackStream.java +++ b/driver/src/main/java/org/neo4j/driver/internal/packstream/PackStream.java @@ -164,7 +164,7 @@ public void flush() throws IOException public void packRaw( byte[] data ) throws IOException { - out.writeBytes( data, 0, data.length ); + out.writeBytes( data ); } public void packNull() throws IOException diff --git a/driver/src/main/java/org/neo4j/driver/internal/spi/Collector.java b/driver/src/main/java/org/neo4j/driver/internal/spi/Collector.java deleted file mode 100644 index dd056f4d05..0000000000 --- a/driver/src/main/java/org/neo4j/driver/internal/spi/Collector.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright (c) 2002-2017 "Neo Technology," - * Network Engine for Objects in Lund AB [http://neotechnology.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.spi; - -import java.util.List; - -import org.neo4j.driver.internal.Bookmark; -import org.neo4j.driver.v1.Value; -import org.neo4j.driver.v1.exceptions.ClientException; -import org.neo4j.driver.v1.exceptions.Neo4jException; -import org.neo4j.driver.v1.summary.Notification; -import org.neo4j.driver.v1.summary.Plan; -import org.neo4j.driver.v1.summary.ProfiledPlan; -import org.neo4j.driver.v1.summary.StatementType; -import org.neo4j.driver.v1.summary.SummaryCounters; - -public interface Collector -{ - Collector NO_OP = new NoOperationCollector(); - - Collector ACK_FAILURE = new NoOperationCollector() - { - @Override - public void doneFailure( Neo4jException error ) - { - throw new ClientException( - "Invalid server response message `FAILURE` received for client message `ACK_FAILURE`.", error ); - } - }; - - class InitCollector extends NoOperationCollector - { - private String serverVersion; - @Override - public void doneIgnored() - { - throw new ClientException( - "Invalid server response message `IGNORED` received for client message `INIT`." ); - } - - @Override - public void serverVersion( String serverVersion ) - { - this.serverVersion = serverVersion; - } - - public String serverVersion() - { - return serverVersion; - } - } - - Collector RESET = new ResetCollector(); - - class ResetCollector extends NoOperationCollector - { - private final Runnable doneSuccessCallBack; - - public ResetCollector() - { - this( null ); - } - - public ResetCollector( Runnable doneSuccessCallBack ) - { - this.doneSuccessCallBack = doneSuccessCallBack; - } - - @Override - public void doneFailure( Neo4jException error ) - { - throw new ClientException( - "Invalid server response message `FAILURE` received for client message `RESET`.", error ); - } - - @Override - public void doneIgnored() - { - throw new ClientException( - "Invalid server response message `IGNORED` received for client message `RESET`." ); - } - - @Override - public void doneSuccess() - { - if( doneSuccessCallBack != null ) - { - doneSuccessCallBack.run(); - } - } - } - - - class NoOperationCollector implements Collector - { - @Override - public void keys( String[] names ) {} - - @Override - public void record( Value[] fields ) {} - - @Override - public void statementType( StatementType type ) {} - - @Override - public void statementStatistics( SummaryCounters statistics ) {} - - @Override - public void plan( Plan plan ) {} - - @Override - public void profile( ProfiledPlan plan ) {} - - @Override - public void notifications( List notifications ) {} - - @Override - public void bookmark( Bookmark bookmark ) - { - } - - @Override - public void done() {} - - @Override - public void doneSuccess() - { - done(); - } - - @Override - public void doneFailure( Neo4jException error ) - { - done(); - } - - @Override - public void doneIgnored() - { - done(); - } - - @Override - public void resultAvailableAfter( long l ) {} - - @Override - public void resultConsumedAfter( long l ) {} - - @Override - public void serverVersion( String server ){} - } - - // TODO: This should be modified to simply have head/record/tail methods - - void keys( String[] names ); - - void record( Value[] fields ); - - void statementType( StatementType type); - - void statementStatistics( SummaryCounters statistics ); - - void plan( Plan plan ); - - void profile( ProfiledPlan plan ); - - void notifications( List notifications ); - - void bookmark( Bookmark bookmark ); - - void done(); - - void doneSuccess(); - - void doneFailure( Neo4jException error ); - - void doneIgnored(); - - void resultAvailableAfter( long l ); - - void resultConsumedAfter( long l ); - - void serverVersion( String server ); -} - diff --git a/driver/src/main/java/org/neo4j/driver/internal/spi/Connection.java b/driver/src/main/java/org/neo4j/driver/internal/spi/Connection.java index 0901059631..d8f025a5f8 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/spi/Connection.java +++ b/driver/src/main/java/org/neo4j/driver/internal/spi/Connection.java @@ -38,34 +38,34 @@ public interface Connection extends AutoCloseable void init( String clientName, Map authToken ); /** - * Queue up a run action. The collector will value called with metadata about the stream that will become available - * for retrieval. + * Queue up a run action. The result handler will be called with metadata about the stream when that becomes + * available for retrieval. * @param parameters a map value of parameters */ - void run( String statement, Map parameters, Collector collector ); + void run( String statement, Map parameters, ResponseHandler handler ); /** * Queue a discard all action, consuming any items left in the current stream.This will - * close the stream once its completed, allowing another {@link #run(String, java.util.Map, Collector) run} + * close the stream once its completed, allowing another {@link #run(String, java.util.Map, ResponseHandler) run} */ - void discardAll( Collector collector ); + void discardAll( ResponseHandler handler ); /** - * Queue a pull-all action, output will be handed to the collector once the pull starts. This will - * close the stream once its completed, allowing another {@link #run(String, java.util.Map, Collector) run} + * Queue a pull-all action, output will be handed to the response handler once the pull starts. This will + * close the stream once its completed, allowing another {@link #run(String, java.util.Map, ResponseHandler) run} */ - void pullAll( Collector collector ); + void pullAll( ResponseHandler handler ); /** - * Queue a reset action, throw {@link org.neo4j.driver.v1.exceptions.ClientException} if an ignored message is received. This will - * close the stream once its completed, allowing another {@link #run(String, java.util.Map, Collector) run} + * Queue a reset action, throw {@link org.neo4j.driver.v1.exceptions.ClientException} if an ignored message is + * received. This will close the stream once its completed, allowing another + * {@link #run(String, java.util.Map, ResponseHandler) run}. */ void reset(); /** - * Queue a ack_failure action, valid output could only be success. Throw {@link org.neo4j.driver.v1.exceptions.ClientException} if - * a failure or ignored message is received. This will close the stream once it is completed, allowing another - * {@link #run(String, java.util.Map, Collector) run} + * Queue a ack_failure action, valid output could only be success. This will close the stream once it is completed, + * allowing another {@link #run(String, java.util.Map, ResponseHandler) run}. */ void ackFailure(); diff --git a/driver/src/main/java/org/neo4j/driver/internal/spi/ConnectionProvider.java b/driver/src/main/java/org/neo4j/driver/internal/spi/ConnectionProvider.java index 0b474b5ab3..b662f57a45 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/spi/ConnectionProvider.java +++ b/driver/src/main/java/org/neo4j/driver/internal/spi/ConnectionProvider.java @@ -18,6 +18,8 @@ */ package org.neo4j.driver.internal.spi; +import org.neo4j.driver.internal.async.AsyncConnection; +import org.neo4j.driver.internal.async.InternalFuture; import org.neo4j.driver.v1.AccessMode; /** @@ -33,4 +35,6 @@ public interface ConnectionProvider extends AutoCloseable * @return free or new pooled connection. */ PooledConnection acquireConnection( AccessMode mode ); + + InternalFuture acquireAsyncConnection( AccessMode mode ); } diff --git a/driver/src/test/java/org/neo4j/driver/v1/util/Lists.java b/driver/src/main/java/org/neo4j/driver/internal/spi/ResponseHandler.java similarity index 68% rename from driver/src/test/java/org/neo4j/driver/v1/util/Lists.java rename to driver/src/main/java/org/neo4j/driver/internal/spi/ResponseHandler.java index e43a5c851b..bbdac6174f 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/util/Lists.java +++ b/driver/src/main/java/org/neo4j/driver/internal/spi/ResponseHandler.java @@ -16,22 +16,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.neo4j.driver.v1.util; +package org.neo4j.driver.internal.spi; -import java.util.ArrayList; -import java.util.List; +import java.util.Map; -public class Lists +import org.neo4j.driver.v1.Value; + +public interface ResponseHandler { + void onSuccess( Map metadata ); - public static List asList( Iterable iterable ) - { - List list = new ArrayList<>(); - for ( T item : iterable ) - { - list.add( item ); - } - return list; - } + void onFailure( Throwable error ); + void onRecord( Value[] fields ); } diff --git a/driver/src/main/java/org/neo4j/driver/internal/summary/InternalResultSummary.java b/driver/src/main/java/org/neo4j/driver/internal/summary/InternalResultSummary.java new file mode 100644 index 0000000000..ed299e6bd4 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/summary/InternalResultSummary.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.summary; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import org.neo4j.driver.v1.Statement; +import org.neo4j.driver.v1.summary.Notification; +import org.neo4j.driver.v1.summary.Plan; +import org.neo4j.driver.v1.summary.ProfiledPlan; +import org.neo4j.driver.v1.summary.ResultSummary; +import org.neo4j.driver.v1.summary.ServerInfo; +import org.neo4j.driver.v1.summary.StatementType; +import org.neo4j.driver.v1.summary.SummaryCounters; + +public class InternalResultSummary implements ResultSummary +{ + private final Statement statement; + private final ServerInfo serverInfo; + private final StatementType statementType; + private final SummaryCounters counters; + private final Plan plan; + private final ProfiledPlan profile; + private final List notifications; + private final long resultAvailableAfter; + private final long resultConsumedAfter; + + public InternalResultSummary( Statement statement, ServerInfo serverInfo, StatementType statementType, + SummaryCounters counters, Plan plan, ProfiledPlan profile, List notifications, + long resultAvailableAfter, long resultConsumedAfter ) + { + this.statement = statement; + this.serverInfo = serverInfo; + this.statementType = statementType; + this.counters = counters; + this.plan = resolvePlan( plan, profile ); + this.profile = profile; + this.notifications = notifications; + this.resultAvailableAfter = resultAvailableAfter; + this.resultConsumedAfter = resultConsumedAfter; + } + + @Override + public Statement statement() + { + return statement; + } + + @Override + public SummaryCounters counters() + { + return counters == null ? InternalSummaryCounters.EMPTY_STATS : counters; + } + + @Override + public StatementType statementType() + { + return statementType; + } + + @Override + public boolean hasPlan() + { + return plan != null; + } + + @Override + public boolean hasProfile() + { + return profile != null; + } + + @Override + public Plan plan() + { + return plan; + } + + @Override + public ProfiledPlan profile() + { + return profile; + } + + @Override + public List notifications() + { + return notifications == null ? Collections.emptyList() : notifications; + } + + @Override + public long resultAvailableAfter( TimeUnit unit ) + { + return unit.convert( resultAvailableAfter, TimeUnit.MILLISECONDS ); + } + + @Override + public long resultConsumedAfter( TimeUnit unit ) + { + return unit.convert( resultConsumedAfter, TimeUnit.MILLISECONDS ); + } + + @Override + public ServerInfo server() + { + return serverInfo; + } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) + { + return true; + } + if ( o == null || getClass() != o.getClass() ) + { + return false; + } + InternalResultSummary that = (InternalResultSummary) o; + return resultAvailableAfter == that.resultAvailableAfter && + resultConsumedAfter == that.resultConsumedAfter && + Objects.equals( statement, that.statement ) && + Objects.equals( serverInfo, that.serverInfo ) && + statementType == that.statementType && + Objects.equals( counters, that.counters ) && + Objects.equals( plan, that.plan ) && + Objects.equals( profile, that.profile ) && + Objects.equals( notifications, that.notifications ); + } + + @Override + public int hashCode() + { + return Objects.hash( statement, serverInfo, statementType, counters, plan, profile, notifications, + resultAvailableAfter, resultConsumedAfter ); + } + + @Override + public String toString() + { + return "InternalResultSummary{" + + "statement=" + statement + + ", serverInfo=" + serverInfo + + ", statementType=" + statementType + + ", counters=" + counters + + ", plan=" + plan + + ", profile=" + profile + + ", notifications=" + notifications + + ", resultAvailableAfter=" + resultAvailableAfter + + ", resultConsumedAfter=" + resultConsumedAfter + + '}'; + } + + /** + * Profiled plan is a superset of plan. This method returns profiled plan if plan is {@code null}. + * + * @param plan the given plan, possibly {@code null}. + * @param profiledPlan the given profiled plan, possibly {@code null}. + * @return available plan. + */ + private static Plan resolvePlan( Plan plan, ProfiledPlan profiledPlan ) + { + return plan == null ? profiledPlan : plan; + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/summary/SummaryBuilder.java b/driver/src/main/java/org/neo4j/driver/internal/summary/SummaryBuilder.java deleted file mode 100644 index cd1b4dc577..0000000000 --- a/driver/src/main/java/org/neo4j/driver/internal/summary/SummaryBuilder.java +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright (c) 2002-2017 "Neo Technology," - * Network Engine for Objects in Lund AB [http://neotechnology.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.summary; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import org.neo4j.driver.internal.Bookmark; -import org.neo4j.driver.internal.spi.Collector; -import org.neo4j.driver.v1.Statement; -import org.neo4j.driver.v1.Value; -import org.neo4j.driver.v1.exceptions.ClientException; -import org.neo4j.driver.v1.exceptions.Neo4jException; -import org.neo4j.driver.v1.summary.Notification; -import org.neo4j.driver.v1.summary.Plan; -import org.neo4j.driver.v1.summary.ProfiledPlan; -import org.neo4j.driver.v1.summary.ResultSummary; -import org.neo4j.driver.v1.summary.ServerInfo; -import org.neo4j.driver.v1.summary.StatementType; -import org.neo4j.driver.v1.summary.SummaryCounters; - -public class SummaryBuilder implements Collector -{ - private final Statement statement; - private final ServerInfo serverInfo; - - private StatementType type = null; - private SummaryCounters statistics = null; - private Plan plan = null; - private ProfiledPlan profile; - private List notifications = null; - private long resultAvailableAfter = -1L; - private long resultConsumedAfter = -1L; - - public SummaryBuilder( Statement statement, ServerInfo serverInfo ) - { - this.statement = statement; - this.serverInfo = serverInfo; - } - - @Override - public void keys( String[] names ) - { - // intentionally empty - } - - @Override - public void record( Value[] fields ) - { - // intentionally empty - } - - public void statementType( StatementType type ) - { - if ( this.type == null ) - { - this.type = type; - } - else - { - throw new ClientException( "Received statement type twice" ); - } - } - - public void statementStatistics( SummaryCounters statistics ) - { - if ( this.statistics == null ) - { - this.statistics = statistics; - } - else - { - throw new ClientException( "Received statement statistics twice" ); - } - } - - @Override - public void plan( Plan plan ) - { - if ( this.plan == null ) - { - this.plan = plan; - } - else - { - throw new ClientException( "Received plan twice" ); - } - } - - @Override - public void profile( ProfiledPlan plan ) - { - if ( this.plan == null && this.profile == null ) - { - this.profile = plan; - this.plan = plan; - } - else - { - throw new ClientException( "Received plan twice" ); - } - } - - @Override - public void notifications( List notifications ) - { - if( this.notifications == null ) - { - this.notifications = notifications; - } - else - { - throw new ClientException( "Received notifications twice" ); - } - } - - @Override - public void bookmark( Bookmark bookmark ) - { - // intentionally empty - } - - @Override - public void done() - { - // intentionally empty - } - - @Override - public void doneSuccess() - { - // intentionally empty - } - - @Override - public void doneFailure( Neo4jException erro ) - { - // intentionally empty - } - - @Override - public void doneIgnored() - { - // intentionally empty - } - - @Override - public void resultAvailableAfter( long l ) - { - this.resultAvailableAfter = l; - } - - @Override - public void resultConsumedAfter( long l ) - { - this.resultConsumedAfter = l; - } - - @Override - public void serverVersion( String server ) - { - // intentionally empty - // collected in initCollector - } - - public ResultSummary build() - { - return new ResultSummary() - { - @Override - public Statement statement() - { - return statement; - } - - @Override - public SummaryCounters counters() - { - return statistics == null ? InternalSummaryCounters.EMPTY_STATS : statistics; - } - - @Override - public StatementType statementType() - { - return type; - } - - @Override - public boolean hasProfile() - { - return profile != null; - } - - @Override - public boolean hasPlan() - { - return plan != null; - } - - @Override - public Plan plan() - { - return plan; - } - - @Override - public ProfiledPlan profile() - { - return profile; - } - - @Override - public List notifications() - { - return notifications == null ? new ArrayList() : notifications; - } - - @Override - public long resultAvailableAfter( TimeUnit timeUnit ) - { - return timeUnit.convert( resultAvailableAfter, TimeUnit.MILLISECONDS ); - } - - @Override - public long resultConsumedAfter( TimeUnit timeUnit ) - { - return timeUnit.convert( resultConsumedAfter, TimeUnit.MILLISECONDS ); - } - - @Override - public ServerInfo server() - { - return serverInfo; - } - }; - } -} diff --git a/driver/src/main/java/org/neo4j/driver/internal/util/ConcurrentSet.java b/driver/src/main/java/org/neo4j/driver/internal/util/ConcurrentSet.java new file mode 100644 index 0000000000..f19b5e40df --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/util/ConcurrentSet.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.util; + +import java.util.AbstractSet; +import java.util.Iterator; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public final class ConcurrentSet extends AbstractSet +{ + private final ConcurrentMap map = new ConcurrentHashMap<>(); + + @Override + public int size() + { + return map.size(); + } + + @Override + public boolean contains( Object o ) + { + return map.containsKey( o ); + } + + @Override + public boolean add( E o ) + { + return map.putIfAbsent( o, Boolean.TRUE ) == null; + } + + @Override + public boolean remove( Object o ) + { + return map.remove( o ) != null; + } + + @Override + public void clear() + { + map.clear(); + } + + @Override + public Iterator iterator() + { + return map.keySet().iterator(); + } + + @Override + public boolean isEmpty() + { + return map.isEmpty(); + } +} 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 new file mode 100644 index 0000000000..c19c92525a --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/util/ErrorUtil.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.util; + +import org.neo4j.driver.v1.exceptions.AuthenticationException; +import org.neo4j.driver.v1.exceptions.ClientException; +import org.neo4j.driver.v1.exceptions.DatabaseException; +import org.neo4j.driver.v1.exceptions.Neo4jException; +import org.neo4j.driver.v1.exceptions.TransientException; + +public final class ErrorUtil +{ + private ErrorUtil() + { + } + + public static Neo4jException newNeo4jError( String code, String message ) + { + String classification = extractClassification( code ); + switch ( classification ) + { + case "ClientError": + if ( code.equalsIgnoreCase( "Neo.ClientError.Security.Unauthorized" ) ) + { + return new AuthenticationException( code, message ); + } + else + { + return new ClientException( code, message ); + } + case "TransientError": + return new TransientException( code, message ); + default: + return new DatabaseException( code, message ); + } + } + + public static boolean isRecoverable( Throwable error ) + { + if ( error instanceof Neo4jException ) + { + if ( isProtocolViolationError( ((Neo4jException) error) ) ) + { + return false; + } + + if ( isClientOrTransientError( ((Neo4jException) error) ) ) + { + return true; + } + } + return false; + } + + private static boolean isProtocolViolationError( Neo4jException error ) + { + String errorCode = error.code(); + return errorCode != null && errorCode.startsWith( "Neo.ClientError.Request" ); + } + + private static boolean isClientOrTransientError( Neo4jException error ) + { + String errorCode = error.code(); + return errorCode != null && (errorCode.contains( "ClientError" ) || errorCode.contains( "TransientError" )); + } + + private static String extractClassification( String code ) + { + String[] parts = code.split( "\\." ); + return parts[1]; + } +} diff --git a/driver/src/main/java/org/neo4j/driver/v1/Config.java b/driver/src/main/java/org/neo4j/driver/v1/Config.java index b638d51cff..6e267a4f9c 100644 --- a/driver/src/main/java/org/neo4j/driver/v1/Config.java +++ b/driver/src/main/java/org/neo4j/driver/v1/Config.java @@ -57,9 +57,11 @@ public class Config private final boolean logLeakedSessions; private final int maxIdleConnectionPoolSize; + private final int maxConnectionPoolSize; private final long idleTimeBeforeConnectionTest; - private final long maxConnectionLifetime; + private final long maxConnectionLifetimeMillis; + private final long connectionAcquisitionTimeoutMillis; /** Indicator for encrypted traffic */ private final boolean encrypted; @@ -80,8 +82,10 @@ private Config( ConfigBuilder builder) this.logLeakedSessions = builder.logLeakedSessions; this.idleTimeBeforeConnectionTest = builder.idleTimeBeforeConnectionTest; - this.maxConnectionLifetime = builder.maxConnectionLifetime; + this.maxConnectionLifetimeMillis = builder.maxConnectionLifetimeMillis; this.maxIdleConnectionPoolSize = builder.maxIdleConnectionPoolSize; + this.maxConnectionPoolSize = builder.maxConnectionPoolSize; + this.connectionAcquisitionTimeoutMillis = builder.connectionAcquisitionTimeoutMillis; this.encrypted = builder.encrypted; this.trustStrategy = builder.trustStrategy; @@ -146,9 +150,9 @@ public long idleTimeBeforeConnectionTest() * * @return maximum lifetime in milliseconds */ - public long maxConnectionLifetime() + public long maxConnectionLifetimeMillis() { - return maxConnectionLifetime; + return maxConnectionLifetimeMillis; } /** @@ -159,6 +163,16 @@ public int connectionTimeoutMillis() return connectionTimeoutMillis; } + public int maxConnectionPoolSize() + { + return maxConnectionPoolSize; + } + + public long connectionAcquisitionTimeoutMillis() + { + return connectionAcquisitionTimeoutMillis; + } + /** * @return the level of encryption required for all connections. */ @@ -230,8 +244,10 @@ public static class ConfigBuilder private Logging logging = new JULogging( Level.INFO ); private boolean logLeakedSessions; private int maxIdleConnectionPoolSize = PoolSettings.DEFAULT_MAX_IDLE_CONNECTION_POOL_SIZE; + private int maxConnectionPoolSize = PoolSettings.DEFAULT_MAX_CONNECTION_POOL_SIZE; private long idleTimeBeforeConnectionTest = PoolSettings.DEFAULT_IDLE_TIME_BEFORE_CONNECTION_TEST; - private long maxConnectionLifetime = PoolSettings.DEFAULT_MAX_CONNECTION_LIFETIME; + private long maxConnectionLifetimeMillis = PoolSettings.DEFAULT_MAX_CONNECTION_LIFETIME; + private long connectionAcquisitionTimeoutMillis = PoolSettings.DEFAULT_CONNECTION_ACQUISITION_TIMEOUT; private boolean encrypted = true; private TrustStrategy trustStrategy = trustAllCertificates(); private LoadBalancingStrategy loadBalancingStrategy = LoadBalancingStrategy.LEAST_CONNECTED; @@ -389,7 +405,32 @@ public ConfigBuilder withConnectionLivenessCheckTimeout( long value, TimeUnit un */ public ConfigBuilder withMaxConnectionLifetime( long value, TimeUnit unit ) { - this.maxConnectionLifetime = unit.toMillis( value ); + this.maxConnectionLifetimeMillis = unit.toMillis( value ); + return this; + } + + /** + * Todo: doc and validation + * + * @param value + * @return + */ + public ConfigBuilder withMaxConnectionPoolSize( int value ) + { + this.maxConnectionPoolSize = value; + return this; + } + + /** + * Todo: doc and validation + * + * @param value + * @param unit + * @return + */ + public ConfigBuilder withConnectionAcquisitionTimeout( long value, TimeUnit unit ) + { + this.connectionAcquisitionTimeoutMillis = unit.toMillis( value ); return this; } diff --git a/driver/src/main/java/org/neo4j/driver/v1/Response.java b/driver/src/main/java/org/neo4j/driver/v1/Response.java new file mode 100644 index 0000000000..af8a659b06 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/v1/Response.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.v1; + +import java.util.concurrent.Future; + +public interface Response extends Future +{ + void addListener( ResponseListener listener ); +} diff --git a/driver/src/main/java/org/neo4j/driver/v1/ResponseListener.java b/driver/src/main/java/org/neo4j/driver/v1/ResponseListener.java new file mode 100644 index 0000000000..dbc0652220 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/v1/ResponseListener.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.v1; + +public interface ResponseListener +{ + void operationCompleted( T result, Throwable error ); +} diff --git a/driver/src/main/java/org/neo4j/driver/v1/Session.java b/driver/src/main/java/org/neo4j/driver/v1/Session.java index 529cbeee23..a9674128cf 100644 --- a/driver/src/main/java/org/neo4j/driver/v1/Session.java +++ b/driver/src/main/java/org/neo4j/driver/v1/Session.java @@ -82,6 +82,8 @@ public interface Session extends Resource, StatementRunner @Deprecated Transaction beginTransaction( String bookmark ); + Response beginTransactionAsync(); + /** * Execute given unit of work in a {@link AccessMode#READ read} transaction. *

@@ -112,7 +114,7 @@ public interface Session extends Resource, StatementRunner * or if this transaction was rolled back, the bookmark value will * be null. * - * @return a reference to a previous transac'tion + * @return a reference to a previous transaction */ String lastBookmark(); @@ -142,4 +144,6 @@ public interface Session extends Resource, StatementRunner */ @Override void close(); + + Response closeAsync(); } diff --git a/driver/src/main/java/org/neo4j/driver/v1/StatementResultCursor.java b/driver/src/main/java/org/neo4j/driver/v1/StatementResultCursor.java new file mode 100644 index 0000000000..d44232fff7 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/v1/StatementResultCursor.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.v1; + +import java.util.List; + +import org.neo4j.driver.v1.summary.ResultSummary; + +public interface StatementResultCursor +{ + /** + * Retrieve the keys of the records this result cursor contains. + * + * @return list of all keys. + */ + List keys(); + + Response summaryAsync(); + + Response fetchAsync(); + + Record current(); +} diff --git a/driver/src/main/java/org/neo4j/driver/v1/StatementRunner.java b/driver/src/main/java/org/neo4j/driver/v1/StatementRunner.java index e0d7526418..1d7ea37622 100644 --- a/driver/src/main/java/org/neo4j/driver/v1/StatementRunner.java +++ b/driver/src/main/java/org/neo4j/driver/v1/StatementRunner.java @@ -20,8 +20,8 @@ import java.util.Map; -import org.neo4j.driver.v1.util.Experimental; import org.neo4j.driver.v1.types.TypeSystem; +import org.neo4j.driver.v1.util.Experimental; /** * Common interface for components that can execute Neo4j statements. @@ -93,6 +93,8 @@ public interface StatementRunner */ StatementResult run( String statementTemplate, Value parameters ); + Response runAsync( String statementText, Value parameters ); + /** * Run a statement and return a result stream. * @@ -122,6 +124,8 @@ public interface StatementRunner */ StatementResult run( String statementTemplate, Map statementParameters ); + Response runAsync( String statementTemplate, Map statementParameters ); + /** * Run a statement and return a result stream. * @@ -139,6 +143,8 @@ public interface StatementRunner */ StatementResult run( String statementTemplate, Record statementParameters ); + Response runAsync( String statementTemplate, Record statementParameters ); + /** * Run a statement and return a result stream. * @@ -147,6 +153,8 @@ public interface StatementRunner */ StatementResult run( String statementTemplate ); + Response runAsync( String statementTemplate ); + /** * Run a statement and return a result stream. *

Example

@@ -162,6 +170,8 @@ public interface StatementRunner */ StatementResult run( Statement statement ); + Response runAsync( Statement statement ); + /** * @return type system used by this statement runner for classifying values */ diff --git a/driver/src/main/java/org/neo4j/driver/v1/Transaction.java b/driver/src/main/java/org/neo4j/driver/v1/Transaction.java index a9863169df..d52b0cdaeb 100644 --- a/driver/src/main/java/org/neo4j/driver/v1/Transaction.java +++ b/driver/src/main/java/org/neo4j/driver/v1/Transaction.java @@ -78,4 +78,8 @@ public interface Transaction extends Resource, StatementRunner */ @Override void close(); + + Response commitAsync(); + + Response rollbackAsync(); } 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 406f07f08b..9923f5dac0 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/DirectConnectionProviderTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/DirectConnectionProviderTest.java @@ -20,6 +20,7 @@ import org.junit.Test; +import org.neo4j.driver.internal.async.pool.AsyncConnectionPool; import org.neo4j.driver.internal.net.BoltServerAddress; import org.neo4j.driver.internal.spi.ConnectionPool; import org.neo4j.driver.internal.spi.PooledConnection; @@ -109,11 +110,16 @@ public void throwsWhenTestConnectionThrows() private static DirectConnectionProvider newConnectionProvider( BoltServerAddress address ) { - return new DirectConnectionProvider( address, mock( ConnectionPool.class, RETURNS_MOCKS ) ); + return new DirectConnectionProvider( address, mock( ConnectionPool.class, RETURNS_MOCKS ), asyncPoolMock() ); } private static DirectConnectionProvider newConnectionProvider( ConnectionPool pool ) { - return new DirectConnectionProvider( BoltServerAddress.LOCAL_DEFAULT, pool ); + return new DirectConnectionProvider( BoltServerAddress.LOCAL_DEFAULT, pool, asyncPoolMock() ); + } + + private static AsyncConnectionPool asyncPoolMock() + { + return mock( AsyncConnectionPool.class, RETURNS_MOCKS ); } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/ExplicitTransactionTest.java b/driver/src/test/java/org/neo4j/driver/internal/ExplicitTransactionTest.java index ba41de32c5..35e5725987 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/ExplicitTransactionTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/ExplicitTransactionTest.java @@ -24,8 +24,10 @@ import java.util.Collections; import java.util.Map; -import org.neo4j.driver.internal.spi.Collector; +import org.neo4j.driver.internal.handlers.BookmarkResponseHandler; +import org.neo4j.driver.internal.handlers.NoOpResponseHandler; import org.neo4j.driver.internal.spi.Connection; +import org.neo4j.driver.internal.spi.ResponseHandler; import org.neo4j.driver.v1.Transaction; import org.neo4j.driver.v1.Value; @@ -50,18 +52,18 @@ public void shouldRollbackOnImplicitFailure() throws Throwable Connection conn = mock( Connection.class ); when( conn.isOpen() ).thenReturn( true ); SessionResourcesHandler resourcesHandler = mock( SessionResourcesHandler.class ); - ExplicitTransaction tx = new ExplicitTransaction( conn, resourcesHandler ); + ExplicitTransaction tx = beginTx( conn, resourcesHandler ); // When tx.close(); // Then InOrder order = inOrder( conn ); - order.verify( conn ).run( "BEGIN", Collections.emptyMap(),Collector.NO_OP ); - order.verify( conn ).pullAll( any( Collector.class ) ); + order.verify( conn ).run( "BEGIN", Collections.emptyMap(), NoOpResponseHandler.INSTANCE ); + order.verify( conn ).pullAll( any( ResponseHandler.class ) ); order.verify( conn ).isOpen(); - order.verify( conn ).run( "ROLLBACK", Collections.emptyMap(), Collector.NO_OP ); - order.verify( conn ).pullAll( any( Collector.class ) ); + order.verify( conn ).run( "ROLLBACK", Collections.emptyMap(), NoOpResponseHandler.INSTANCE ); + order.verify( conn ).pullAll( any( ResponseHandler.class ) ); order.verify( conn ).sync(); verify( resourcesHandler, only() ).onTransactionClosed( tx ); verifyNoMoreInteractions( conn, resourcesHandler ); @@ -74,7 +76,7 @@ public void shouldRollbackOnExplicitFailure() throws Throwable Connection conn = mock( Connection.class ); when( conn.isOpen() ).thenReturn( true ); SessionResourcesHandler resourcesHandler = mock( SessionResourcesHandler.class ); - ExplicitTransaction tx = new ExplicitTransaction( conn, resourcesHandler ); + ExplicitTransaction tx = beginTx( conn, resourcesHandler ); // When tx.failure(); @@ -83,11 +85,11 @@ public void shouldRollbackOnExplicitFailure() throws Throwable // Then InOrder order = inOrder( conn ); - order.verify( conn ).run( "BEGIN", Collections.emptyMap(), Collector.NO_OP ); - order.verify( conn ).pullAll( any( BookmarkCollector.class ) ); + order.verify( conn ).run( "BEGIN", Collections.emptyMap(), NoOpResponseHandler.INSTANCE ); + order.verify( conn ).pullAll( any( BookmarkResponseHandler.class ) ); order.verify( conn ).isOpen(); - order.verify( conn ).run( "ROLLBACK", Collections.emptyMap(), Collector.NO_OP ); - order.verify( conn ).pullAll( any( BookmarkCollector.class ) ); + order.verify( conn ).run( "ROLLBACK", Collections.emptyMap(), NoOpResponseHandler.INSTANCE ); + order.verify( conn ).pullAll( any( BookmarkResponseHandler.class ) ); order.verify( conn ).sync(); verify( resourcesHandler, only() ).onTransactionClosed( tx ); verifyNoMoreInteractions( conn, resourcesHandler ); @@ -100,7 +102,7 @@ public void shouldCommitOnSuccess() throws Throwable Connection conn = mock( Connection.class ); when( conn.isOpen() ).thenReturn( true ); SessionResourcesHandler resourcesHandler = mock( SessionResourcesHandler.class ); - ExplicitTransaction tx = new ExplicitTransaction( conn, resourcesHandler ); + ExplicitTransaction tx = beginTx( conn, resourcesHandler ); // When tx.success(); @@ -109,11 +111,11 @@ public void shouldCommitOnSuccess() throws Throwable // Then InOrder order = inOrder( conn ); - order.verify( conn ).run( "BEGIN", Collections.emptyMap(), Collector.NO_OP ); - order.verify( conn ).pullAll( any( BookmarkCollector.class ) ); + order.verify( conn ).run( "BEGIN", Collections.emptyMap(), NoOpResponseHandler.INSTANCE ); + order.verify( conn ).pullAll( any( BookmarkResponseHandler.class ) ); order.verify( conn ).isOpen(); - order.verify( conn ).run( "COMMIT", Collections.emptyMap(), Collector.NO_OP ); - order.verify( conn ).pullAll( any( BookmarkCollector.class ) ); + order.verify( conn ).run( "COMMIT", Collections.emptyMap(), NoOpResponseHandler.INSTANCE ); + order.verify( conn ).pullAll( any( BookmarkResponseHandler.class ) ); order.verify( conn ).sync(); verify( resourcesHandler, only() ).onTransactionClosed( tx ); verifyNoMoreInteractions( conn, resourcesHandler ); @@ -124,11 +126,11 @@ public void shouldOnlyQueueMessagesWhenNoBookmarkGiven() { Connection connection = mock( Connection.class ); - new ExplicitTransaction( connection, mock( SessionResourcesHandler.class ), null ); + beginTx( connection, mock( SessionResourcesHandler.class ), Bookmark.empty() ); InOrder inOrder = inOrder( connection ); - inOrder.verify( connection ).run( "BEGIN", Collections.emptyMap(), Collector.NO_OP ); - inOrder.verify( connection ).pullAll( Collector.NO_OP ); + inOrder.verify( connection ).run( "BEGIN", Collections.emptyMap(), NoOpResponseHandler.INSTANCE ); + inOrder.verify( connection ).pullAll( NoOpResponseHandler.INSTANCE ); inOrder.verify( connection, never() ).sync(); } @@ -138,20 +140,20 @@ public void shouldSyncWhenBookmarkGiven() Bookmark bookmark = Bookmark.from( "hi, I'm bookmark" ); Connection connection = mock( Connection.class ); - new ExplicitTransaction( connection, mock( SessionResourcesHandler.class ), bookmark ); + beginTx( connection, mock( SessionResourcesHandler.class ), bookmark ); Map expectedParams = bookmark.asBeginTransactionParameters(); InOrder inOrder = inOrder( connection ); - inOrder.verify( connection ).run( "BEGIN", expectedParams, Collector.NO_OP ); - inOrder.verify( connection ).pullAll( Collector.NO_OP ); + inOrder.verify( connection ).run( "BEGIN", expectedParams, NoOpResponseHandler.INSTANCE ); + inOrder.verify( connection ).pullAll( NoOpResponseHandler.INSTANCE ); inOrder.verify( connection ).sync(); } @Test public void shouldBeOpenAfterConstruction() { - Transaction tx = new ExplicitTransaction( openConnectionMock(), mock( SessionResourcesHandler.class ) ); + Transaction tx = beginTx( openConnectionMock(), mock( SessionResourcesHandler.class ) ); assertTrue( tx.isOpen() ); } @@ -159,7 +161,7 @@ public void shouldBeOpenAfterConstruction() @Test public void shouldBeOpenWhenMarkedForSuccess() { - Transaction tx = new ExplicitTransaction( openConnectionMock(), mock( SessionResourcesHandler.class ) ); + Transaction tx = beginTx( openConnectionMock(), mock( SessionResourcesHandler.class ) ); tx.success(); @@ -169,7 +171,7 @@ public void shouldBeOpenWhenMarkedForSuccess() @Test public void shouldBeOpenWhenMarkedForFailure() { - Transaction tx = new ExplicitTransaction( openConnectionMock(), mock( SessionResourcesHandler.class ) ); + Transaction tx = beginTx( openConnectionMock(), mock( SessionResourcesHandler.class ) ); tx.failure(); @@ -179,7 +181,7 @@ public void shouldBeOpenWhenMarkedForFailure() @Test public void shouldBeOpenWhenMarkedToClose() { - ExplicitTransaction tx = new ExplicitTransaction( openConnectionMock(), mock( SessionResourcesHandler.class ) ); + ExplicitTransaction tx = beginTx( openConnectionMock(), mock( SessionResourcesHandler.class ) ); tx.markToClose(); @@ -189,7 +191,7 @@ public void shouldBeOpenWhenMarkedToClose() @Test public void shouldBeClosedAfterCommit() { - Transaction tx = new ExplicitTransaction( openConnectionMock(), mock( SessionResourcesHandler.class ) ); + Transaction tx = beginTx( openConnectionMock(), mock( SessionResourcesHandler.class ) ); tx.success(); tx.close(); @@ -200,7 +202,7 @@ public void shouldBeClosedAfterCommit() @Test public void shouldBeClosedAfterRollback() { - Transaction tx = new ExplicitTransaction( openConnectionMock(), mock( SessionResourcesHandler.class ) ); + Transaction tx = beginTx( openConnectionMock(), mock( SessionResourcesHandler.class ) ); tx.failure(); tx.close(); @@ -211,7 +213,7 @@ public void shouldBeClosedAfterRollback() @Test public void shouldBeClosedWhenMarkedToCloseAndClosed() { - ExplicitTransaction tx = new ExplicitTransaction( openConnectionMock(), mock( SessionResourcesHandler.class ) ); + ExplicitTransaction tx = beginTx( openConnectionMock(), mock( SessionResourcesHandler.class ) ); tx.markToClose(); tx.close(); @@ -222,14 +224,14 @@ public void shouldBeClosedWhenMarkedToCloseAndClosed() @Test public void shouldHaveEmptyBookmarkInitially() { - ExplicitTransaction tx = new ExplicitTransaction( openConnectionMock(), mock( SessionResourcesHandler.class ) ); + ExplicitTransaction tx = beginTx( openConnectionMock(), mock( SessionResourcesHandler.class ) ); assertTrue( tx.bookmark().isEmpty() ); } @Test public void shouldNotKeepInitialBookmark() { - ExplicitTransaction tx = new ExplicitTransaction( openConnectionMock(), mock( SessionResourcesHandler.class ), + ExplicitTransaction tx = beginTx( openConnectionMock(), mock( SessionResourcesHandler.class ), Bookmark.from( "Dog" ) ); assertTrue( tx.bookmark().isEmpty() ); } @@ -237,7 +239,7 @@ public void shouldNotKeepInitialBookmark() @Test public void shouldNotOverwriteBookmarkWithNull() { - ExplicitTransaction tx = new ExplicitTransaction( openConnectionMock(), mock( SessionResourcesHandler.class ) ); + ExplicitTransaction tx = beginTx( openConnectionMock(), mock( SessionResourcesHandler.class ) ); tx.setBookmark( Bookmark.from( "Cat" ) ); assertEquals( "Cat", tx.bookmark().maxBookmarkAsString() ); tx.setBookmark( null ); @@ -247,7 +249,7 @@ public void shouldNotOverwriteBookmarkWithNull() @Test public void shouldNotOverwriteBookmarkWithEmptyBookmark() { - ExplicitTransaction tx = new ExplicitTransaction( openConnectionMock(), mock( SessionResourcesHandler.class ) ); + ExplicitTransaction tx = beginTx( openConnectionMock(), mock( SessionResourcesHandler.class ) ); tx.setBookmark( Bookmark.from( "Cat" ) ); assertEquals( "Cat", tx.bookmark().maxBookmarkAsString() ); tx.setBookmark( Bookmark.empty() ); @@ -260,4 +262,17 @@ private static Connection openConnectionMock() when( connection.isOpen() ).thenReturn( true ); return connection; } + + private static ExplicitTransaction beginTx( Connection connection, SessionResourcesHandler resourcesHandler ) + { + return beginTx( connection, resourcesHandler, Bookmark.empty() ); + } + + private static ExplicitTransaction beginTx( Connection connection, SessionResourcesHandler resourcesHandler, + Bookmark initialBookmark ) + { + ExplicitTransaction tx = new ExplicitTransaction( connection, resourcesHandler ); + tx.begin( initialBookmark ); + return tx; + } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/ExtractTest.java b/driver/src/test/java/org/neo4j/driver/internal/ExtractTest.java index 4c99f888fe..67338172fa 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/ExtractTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/ExtractTest.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Map; +import org.neo4j.driver.ResultResourcesHandler; import org.neo4j.driver.internal.spi.Connection; import org.neo4j.driver.internal.util.Extract; import org.neo4j.driver.v1.Statement; @@ -41,6 +42,7 @@ import static java.util.Arrays.asList; import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -170,15 +172,14 @@ public void testFields() throws Exception Connection connection = mock( Connection.class ); String statement = ""; - InternalStatementResult result = new InternalStatementResult( connection, SessionResourcesHandler.NO_OP, null, - new Statement( statement ) ); - result.runResponseCollector().keys( new String[]{"k1"} ); - result.runResponseCollector().done(); - result.pullAllResponseCollector().record( new Value[]{value( 42 )} ); - result.pullAllResponseCollector().done(); + Statement stmt = new Statement( statement ); + InternalStatementResult result = new InternalStatementResult( stmt, connection, ResultResourcesHandler.NO_OP ); + result.runResponseHandler().onSuccess( singletonMap( "fields", value( singletonList( "k1" ) ) ) ); + result.pullAllResponseHandler().onRecord( new Value[]{value( 42 )} ); + result.pullAllResponseHandler().onSuccess( Collections.emptyMap() ); - connection.run( statement, Values.EmptyMap.asMap( ofValue() ), result.runResponseCollector() ); - connection.pullAll( result.pullAllResponseCollector() ); + connection.run( statement, Values.EmptyMap.asMap( ofValue() ), result.runResponseHandler() ); + connection.pullAll( result.pullAllResponseHandler() ); connection.flush(); // WHEN diff --git a/driver/src/test/java/org/neo4j/driver/internal/InternalBuilderTest.java b/driver/src/test/java/org/neo4j/driver/internal/InternalBuilderTest.java deleted file mode 100644 index 514b81c2e5..0000000000 --- a/driver/src/test/java/org/neo4j/driver/internal/InternalBuilderTest.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) 2002-2017 "Neo Technology," - * Network Engine for Objects in Lund AB [http://neotechnology.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 org.junit.Test; - -import org.neo4j.driver.internal.net.BoltServerAddress; -import org.neo4j.driver.internal.summary.InternalServerInfo; -import org.neo4j.driver.internal.summary.InternalSummaryCounters; -import org.neo4j.driver.internal.summary.SummaryBuilder; -import org.neo4j.driver.v1.Statement; -import org.neo4j.driver.v1.summary.ResultSummary; -import org.neo4j.driver.v1.summary.ServerInfo; -import org.neo4j.driver.v1.summary.SummaryCounters; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.mock; - -public class InternalBuilderTest -{ - @Test - public void shouldReturnEmptyStatisticsIfNotProvided() throws Throwable - { - // Given - SummaryBuilder builder = new SummaryBuilder( mock( Statement.class ), mock( ServerInfo.class ) ); - - // When - ResultSummary summary = builder.build(); - SummaryCounters stats = summary.counters(); - - // Then - assertEquals( stats, new InternalSummaryCounters( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ) ); - } - - - @Test - public void shouldReturnNullIfNoPlanProfileProvided() throws Throwable - { - // Given - SummaryBuilder builder = new SummaryBuilder( mock( Statement.class ), mock( ServerInfo.class ) ); - - // When - ResultSummary summary = builder.build(); - - // Then - assertThat( summary.hasPlan(), equalTo( false ) ); - assertThat( summary.hasProfile(), equalTo( false ) ); - assertNull( summary.plan() ); - assertNull( summary.profile() ); - } - - - @Test - public void shouldReturnNullIfNoStatementTypeProvided() throws Throwable - { - // Given - SummaryBuilder builder = new SummaryBuilder( mock( Statement.class ), mock( ServerInfo.class ) ); - - // When - ResultSummary summary = builder.build(); - - // Then - assertNull( summary.statementType() ); - } - - - @Test - public void shouldObtainStatementAndServerInfoFromSummaryBuilder() throws Throwable - { - // Given - SummaryBuilder builder = new SummaryBuilder( new Statement( "This is a test statement" ), - new InternalServerInfo( new BoltServerAddress( "neo4j.com" ), "super-awesome" ) ); - - // When - ResultSummary summary = builder.build(); - - // Then - assertThat( summary.statement().text(), equalTo( "This is a test statement" ) ); - assertThat( summary.server().address(), equalTo( "neo4j.com:7687" ) ); - assertThat( summary.server().version(), equalTo( "super-awesome" ) ); - } -} diff --git a/driver/src/test/java/org/neo4j/driver/internal/InternalPathTest.java b/driver/src/test/java/org/neo4j/driver/internal/InternalPathTest.java index 52ac70a739..0b2547018b 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/InternalPathTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/InternalPathTest.java @@ -18,18 +18,18 @@ */ package org.neo4j.driver.internal; -import java.util.Arrays; -import java.util.List; - import org.hamcrest.MatcherAssert; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import java.util.Arrays; +import java.util.List; + +import org.neo4j.driver.internal.util.Iterables; import org.neo4j.driver.v1.types.Node; import org.neo4j.driver.v1.types.Path; import org.neo4j.driver.v1.types.Relationship; -import org.neo4j.driver.v1.util.Lists; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; @@ -80,7 +80,7 @@ public void shouldBeAbleToIterateOverPathAsSegments() throws Exception InternalPath path = testPath(); // When - List segments = Lists.asList( path ); + List segments = Iterables.asList( path ); // Then MatcherAssert.assertThat( segments, equalTo( Arrays.asList( (Path.Segment) @@ -110,7 +110,7 @@ public void shouldBeAbleToIterateOverPathNodes() throws Exception InternalPath path = testPath(); // When - List segments = Lists.asList( path.nodes() ); + List segments = Iterables.asList( path.nodes() ); // Then assertThat( segments, equalTo( Arrays.asList( (Node) @@ -127,7 +127,7 @@ public void shouldBeAbleToIterateOverPathRelationships() throws Exception InternalPath path = testPath(); // When - List segments = Lists.asList( path.relationships() ); + List segments = Iterables.asList( path.relationships() ); // Then assertThat( segments, equalTo( Arrays.asList( (Relationship) diff --git a/driver/src/test/java/org/neo4j/driver/internal/InternalStatementResultTest.java b/driver/src/test/java/org/neo4j/driver/internal/InternalStatementResultTest.java index d8d9e9eff5..9a09d97395 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/InternalStatementResultTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/InternalStatementResultTest.java @@ -18,7 +18,6 @@ */ package org.neo4j.driver.internal; - import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -26,9 +25,12 @@ import org.mockito.stubbing.Answer; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.LinkedList; import java.util.List; +import org.neo4j.driver.ResultResourcesHandler; import org.neo4j.driver.internal.spi.Connection; import org.neo4j.driver.internal.value.NullValue; import org.neo4j.driver.v1.Record; @@ -39,6 +41,7 @@ import org.neo4j.driver.v1.util.Pair; import static java.util.Arrays.asList; +import static java.util.Collections.singletonMap; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.junit.Assert.assertEquals; @@ -389,31 +392,31 @@ public void shouldNotPeekIntoTheFutureWhenResultIsEmpty() @Test public void shouldNotifyResourcesHandlerWhenFetchedViaList() { - SessionResourcesHandler resourcesHandler = mock( SessionResourcesHandler.class ); + ResultResourcesHandler resourcesHandler = mock( ResultResourcesHandler.class ); StatementResult result = createResult( 10, resourcesHandler ); List records = result.list(); assertEquals( 10, records.size() ); - verify( resourcesHandler ).onResultConsumed(); + verify( resourcesHandler ).resultFetched(); } @Test public void shouldNotifyResourcesHandlerWhenFetchedViaSingle() { - SessionResourcesHandler resourcesHandler = mock( SessionResourcesHandler.class ); + ResultResourcesHandler resourcesHandler = mock( ResultResourcesHandler.class ); StatementResult result = createResult( 1, resourcesHandler ); Record record = result.single(); assertEquals( "v1-1", record.get( "k1" ).asString() ); - verify( resourcesHandler ).onResultConsumed(); + verify( resourcesHandler ).resultFetched(); } @Test public void shouldNotifyResourcesHandlerWhenFetchedViaIterator() { - SessionResourcesHandler resourcesHandler = mock( SessionResourcesHandler.class ); + ResultResourcesHandler resourcesHandler = mock( ResultResourcesHandler.class ); StatementResult result = createResult( 1, resourcesHandler ); while ( result.hasNext() ) @@ -421,35 +424,35 @@ public void shouldNotifyResourcesHandlerWhenFetchedViaIterator() assertNotNull( result.next() ); } - verify( resourcesHandler ).onResultConsumed(); + verify( resourcesHandler ).resultFetched(); } @Test public void shouldNotifyResourcesHandlerWhenSummary() { - SessionResourcesHandler resourcesHandler = mock( SessionResourcesHandler.class ); + ResultResourcesHandler resourcesHandler = mock( ResultResourcesHandler.class ); StatementResult result = createResult( 10, resourcesHandler ); assertNotNull( result.summary() ); - verify( resourcesHandler ).onResultConsumed(); + verify( resourcesHandler ).resultFetched(); } @Test public void shouldNotifyResourcesHandlerWhenConsumed() { - SessionResourcesHandler resourcesHandler = mock( SessionResourcesHandler.class ); + ResultResourcesHandler resourcesHandler = mock( ResultResourcesHandler.class ); StatementResult result = createResult( 5, resourcesHandler ); result.consume(); - verify( resourcesHandler ).onResultConsumed(); + verify( resourcesHandler ).resultFetched(); } @Test public void shouldNotifyResourcesHandlerOnlyOnceWhenConsumed() { - SessionResourcesHandler resourcesHandler = mock( SessionResourcesHandler.class ); + ResultResourcesHandler resourcesHandler = mock( ResultResourcesHandler.class ); StatementResult result = createResult( 8, resourcesHandler ); assertEquals( 8, result.list().size() ); @@ -457,21 +460,21 @@ public void shouldNotifyResourcesHandlerOnlyOnceWhenConsumed() assertNotNull( result.consume() ); assertNotNull( result.summary() ); - verify( resourcesHandler ).onResultConsumed(); + verify( resourcesHandler ).resultFetched(); } private StatementResult createResult( int numberOfRecords ) { - return createResult( numberOfRecords, SessionResourcesHandler.NO_OP ); + return createResult( numberOfRecords, ResultResourcesHandler.NO_OP ); } - private StatementResult createResult( int numberOfRecords, SessionResourcesHandler resourcesHandler ) + private StatementResult createResult( int numberOfRecords, ResultResourcesHandler resourcesHandler ) { Connection connection = mock( Connection.class ); String statement = ""; - final InternalStatementResult result = new InternalStatementResult( connection, resourcesHandler, null, - new Statement( statement ) ); + Statement stmt = new Statement( statement ); + InternalStatementResult result = new InternalStatementResult( stmt, connection, resourcesHandler ); // Each time the cursor calls `recieveOne`, we'll run one of these, // to emulate how messages are handed over to the cursor @@ -504,7 +507,7 @@ private Runnable streamTailMessage( final InternalStatementResult cursor ) @Override public void run() { - cursor.pullAllResponseCollector().done(); + cursor.pullAllResponseHandler().onSuccess( Collections.emptyMap() ); } }; } @@ -516,7 +519,7 @@ private Runnable recordMessage( final InternalStatementResult cursor, final int @Override public void run() { - cursor.pullAllResponseCollector().record( new Value[]{value( "v1-" + val ), value( "v2-" + val )} ); + cursor.pullAllResponseHandler().onRecord( new Value[]{value( "v1-" + val ), value( "v2-" + val )} ); } }; } @@ -528,8 +531,7 @@ private Runnable streamHeadMessage( final InternalStatementResult cursor ) @Override public void run() { - cursor.runResponseCollector().keys( new String[]{"k1", "k2"} ); - cursor.runResponseCollector().done(); + cursor.runResponseHandler().onSuccess( singletonMap( "fields", value( Arrays.asList( "k1", "k2" ) ) ) ); } }; } diff --git a/driver/src/test/java/org/neo4j/driver/internal/NetworkSessionTest.java b/driver/src/test/java/org/neo4j/driver/internal/NetworkSessionTest.java index 2cc85b6579..132a4a826f 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/NetworkSessionTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/NetworkSessionTest.java @@ -29,11 +29,12 @@ import java.util.Map; +import org.neo4j.driver.internal.handlers.NoOpResponseHandler; import org.neo4j.driver.internal.retry.FixedRetryLogic; import org.neo4j.driver.internal.retry.RetryLogic; -import org.neo4j.driver.internal.spi.Collector; import org.neo4j.driver.internal.spi.ConnectionProvider; import org.neo4j.driver.internal.spi.PooledConnection; +import org.neo4j.driver.internal.spi.ResponseHandler; import org.neo4j.driver.internal.util.Supplier; import org.neo4j.driver.v1.AccessMode; import org.neo4j.driver.v1.Session; @@ -68,7 +69,6 @@ import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; -import static org.neo4j.driver.internal.spi.Collector.NO_OP; import static org.neo4j.driver.v1.AccessMode.READ; import static org.neo4j.driver.v1.AccessMode.WRITE; @@ -216,7 +216,7 @@ public void acquiresNewConnectionForRun() session.run( "RETURN 1" ); verify( connectionProvider ).acquireConnection( READ ); - verify( connection ).run( eq( "RETURN 1" ), anyParams(), any( Collector.class ) ); + verify( connection ).run( eq( "RETURN 1" ), anyParams(), any( ResponseHandler.class ) ); } @Test @@ -230,11 +230,11 @@ public void syncsAndClosesPreviousConnectionForRun() session.run( "RETURN 1" ); verify( connectionProvider ).acquireConnection( READ ); - verify( connection1 ).run( eq( "RETURN 1" ), anyParams(), any( Collector.class ) ); + verify( connection1 ).run( eq( "RETURN 1" ), anyParams(), any( ResponseHandler.class ) ); session.run( "RETURN 2" ); verify( connectionProvider, times( 2 ) ).acquireConnection( READ ); - verify( connection2 ).run( eq( "RETURN 2" ), anyParams(), any( Collector.class ) ); + verify( connection2 ).run( eq( "RETURN 2" ), anyParams(), any( ResponseHandler.class ) ); InOrder inOrder = inOrder( connection1 ); inOrder.verify( connection1 ).sync(); @@ -252,11 +252,11 @@ public void closesPreviousBrokenConnectionForRun() session.run( "RETURN 1" ); verify( connectionProvider ).acquireConnection( READ ); - verify( connection1 ).run( eq( "RETURN 1" ), anyParams(), any( Collector.class ) ); + verify( connection1 ).run( eq( "RETURN 1" ), anyParams(), any( ResponseHandler.class ) ); session.run( "RETURN 2" ); verify( connectionProvider, times( 2 ) ).acquireConnection( READ ); - verify( connection2 ).run( eq( "RETURN 2" ), anyParams(), any( Collector.class ) ); + verify( connection2 ).run( eq( "RETURN 2" ), anyParams(), any( ResponseHandler.class ) ); verify( connection1, never() ).sync(); verify( connection1 ).close(); @@ -272,7 +272,7 @@ public void closesAndSyncOpenConnectionUsedForRunWhenSessionIsClosed() session.run( "RETURN 1" ); verify( connectionProvider ).acquireConnection( READ ); - verify( connection ).run( eq( "RETURN 1" ), anyParams(), any( Collector.class ) ); + verify( connection ).run( eq( "RETURN 1" ), anyParams(), any( ResponseHandler.class ) ); session.close(); @@ -290,7 +290,7 @@ public void closesClosedConnectionUsedForRunWhenSessionIsClosed() session.run( "RETURN 1" ); verify( connectionProvider ).acquireConnection( READ ); - verify( connection ).run( eq( "RETURN 1" ), anyParams(), any( Collector.class ) ); + verify( connection ).run( eq( "RETURN 1" ), anyParams(), any( ResponseHandler.class ) ); session.close(); @@ -344,7 +344,7 @@ public void closesPreviousConnectionForBeginTx() session.run( "RETURN 1" ); verify( connectionProvider ).acquireConnection( READ ); - verify( connection1 ).run( eq( "RETURN 1" ), anyParams(), any( Collector.class ) ); + verify( connection1 ).run( eq( "RETURN 1" ), anyParams(), any( ResponseHandler.class ) ); session.beginTransaction(); verify( connection1 ).close(); @@ -380,7 +380,7 @@ public void closesConnectionWhenTxIsClosed() tx.run( "RETURN 1" ); verify( connectionProvider ).acquireConnection( READ ); - verify( connection ).run( eq( "RETURN 1" ), anyParams(), any( Collector.class ) ); + verify( connection ).run( eq( "RETURN 1" ), anyParams(), any( ResponseHandler.class ) ); tx.close(); verify( connection ).sync(); @@ -506,7 +506,7 @@ public void closesConnectionWhenResultIsBuffered() session.run( "RETURN 1" ); verify( connectionProvider ).acquireConnection( READ ); - verify( connection ).run( eq( "RETURN 1" ), anyParams(), any( Collector.class ) ); + verify( connection ).run( eq( "RETURN 1" ), anyParams(), any( ResponseHandler.class ) ); session.onResultConsumed(); @@ -1039,7 +1039,7 @@ public Void answer( InvocationOnMock invocation ) throws Throwable } return null; } - } ).when( connection ).run( eq( "COMMIT" ), anyParams(), any( Collector.class ) ); + } ).when( connection ).run( eq( "COMMIT" ), anyParams(), any( ResponseHandler.class ) ); return connection; } @@ -1056,7 +1056,7 @@ private static void verifyBeginTx( PooledConnection connectionMock, Verification private static void verifyBeginTx( PooledConnection connectionMock, Bookmark bookmark ) { - verify( connectionMock ).run( "BEGIN", bookmark.asBeginTransactionParameters(), NO_OP ); + verify( connectionMock ).run( "BEGIN", bookmark.asBeginTransactionParameters(), NoOpResponseHandler.INSTANCE ); } private static void verifyCommitTx( PooledConnection connectionMock, VerificationMode mode ) @@ -1071,7 +1071,7 @@ private static void verifyRollbackTx( PooledConnection connectionMock, Verificat private static void verifyRun( PooledConnection connectionMock, String statement, VerificationMode mode ) { - verify( connectionMock, mode ).run( eq( statement ), anyParams(), any( Collector.class ) ); + verify( connectionMock, mode ).run( eq( statement ), anyParams(), any( ResponseHandler.class ) ); } private static Map anyParams() diff --git a/driver/src/test/java/org/neo4j/driver/internal/RoutingDriverTest.java b/driver/src/test/java/org/neo4j/driver/internal/RoutingDriverTest.java index 0303f55f83..6212951034 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/RoutingDriverTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/RoutingDriverTest.java @@ -26,6 +26,7 @@ import org.mockito.stubbing.Answer; import java.io.File; +import java.util.Arrays; import java.util.Collections; import java.util.Map; @@ -35,10 +36,10 @@ import org.neo4j.driver.internal.net.BoltServerAddress; import org.neo4j.driver.internal.retry.FixedRetryLogic; import org.neo4j.driver.internal.retry.RetryLogic; -import org.neo4j.driver.internal.spi.Collector; import org.neo4j.driver.internal.spi.ConnectionPool; import org.neo4j.driver.internal.spi.ConnectionProvider; import org.neo4j.driver.internal.spi.PooledConnection; +import org.neo4j.driver.internal.spi.ResponseHandler; import org.neo4j.driver.internal.summary.InternalServerInfo; import org.neo4j.driver.internal.util.FakeClock; import org.neo4j.driver.v1.AccessMode; @@ -390,14 +391,14 @@ public PooledConnection answer( InvocationOnMock invocationOnMock ) throws Throw doAnswer( withKeys( "ttl", "servers" ) ).when( connection ).run( eq( GET_SERVERS ), eq( Collections.emptyMap() ), - any( Collector.class ) ); + any( ResponseHandler.class ) ); if ( answer > furtherGetServers.length ) { answer = furtherGetServers.length; } int offset = answer++; doAnswer( offset == 0 ? toGetServers : furtherGetServers[offset - 1] ) - .when( connection ).pullAll( any( Collector.class ) ); + .when( connection ).pullAll( any( ResponseHandler.class ) ); return connection; } @@ -406,29 +407,30 @@ public PooledConnection answer( InvocationOnMock invocationOnMock ) throws Throw return pool; } - private static CollectorAnswer withKeys( final String... keys ) + private static ResponseHandlerAnswer withKeys( final String... keys ) { - return new CollectorAnswer() + return new ResponseHandlerAnswer() { @Override - void collect( Collector collector ) + void setUp( ResponseHandler handler ) { - collector.keys( keys ); + handler.onSuccess( Collections.singletonMap( "fields", value( Arrays.asList( keys ) ) ) ); } }; } - private static CollectorAnswer withServerList( final Value[]... records ) + private static ResponseHandlerAnswer withServerList( final Value[]... records ) { - return new CollectorAnswer() + return new ResponseHandlerAnswer() { @Override - void collect( Collector collector ) + void setUp( ResponseHandler handler ) { for ( Value[] fields : records ) { - collector.record( fields ); + handler.onRecord( fields ); } + handler.onSuccess( Collections.emptyMap() ); } }; } @@ -462,27 +464,26 @@ private static class NetworkSessionWithAddress extends NetworkSession } } - private static abstract class CollectorAnswer implements Answer + private static abstract class ResponseHandlerAnswer implements Answer { - abstract void collect( Collector collector ); + abstract void setUp( ResponseHandler handler ); @Override - public final Object answer( InvocationOnMock invocation ) throws Throwable + public Void answer( InvocationOnMock invocation ) throws Throwable { - Collector collector = collector( invocation ); - collect( collector ); - collector.done(); + ResponseHandler handler = handlerFrom( invocation ); + setUp( handler ); return null; } - private Collector collector( InvocationOnMock invocation ) + private ResponseHandler handlerFrom( InvocationOnMock invocation ) { switch ( invocation.getMethod().getName() ) { case "pullAll": - return invocation.getArgumentAt( 0, Collector.class ); + return invocation.getArgumentAt( 0, ResponseHandler.class ); case "run": - return invocation.getArgumentAt( 2, Collector.class ); + return invocation.getArgumentAt( 2, ResponseHandler.class ); default: throw new UnsupportedOperationException( invocation.getMethod().getName() ); } diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/AsyncConnectorImplTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/AsyncConnectorImplTest.java new file mode 100644 index 0000000000..f6c11275de --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/async/AsyncConnectorImplTest.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.pool.ChannelPoolHandler; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.net.ConnectException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.neo4j.driver.internal.ConnectionSettings; +import org.neo4j.driver.internal.net.BoltServerAddress; +import org.neo4j.driver.internal.security.SecurityPlan; +import org.neo4j.driver.internal.util.FakeClock; +import org.neo4j.driver.v1.AuthToken; +import org.neo4j.driver.v1.AuthTokens; +import org.neo4j.driver.v1.exceptions.AuthenticationException; +import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; +import org.neo4j.driver.v1.util.TestNeo4j; + +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; +import static org.neo4j.driver.v1.util.TestUtil.await; + +public class AsyncConnectorImplTest +{ + @Rule + public final TestNeo4j neo4j = new TestNeo4j(); + + private Bootstrap bootstrap; + + @Before + public void setUp() throws Exception + { + bootstrap = BootstrapFactory.newBootstrap( 1 ); + } + + @After + public void tearDown() throws Exception + { + if ( bootstrap != null ) + { + bootstrap.config().group().shutdownGracefully(); + } + } + + @Test + public void shouldConnect() throws Exception + { + AsyncConnectorImpl connector = newConnector( neo4j.authToken() ); + + ChannelFuture channelFuture = connector.connect( neo4j.address(), bootstrap ); + assertTrue( channelFuture.await( 10, TimeUnit.SECONDS ) ); + Channel channel = channelFuture.channel(); + + assertNull( channelFuture.get() ); + assertTrue( channel.isActive() ); + } + + @Test + public void shouldFailToConnectToWrongAddress() throws Exception + { + AsyncConnectorImpl connector = newConnector( neo4j.authToken() ); + + ChannelFuture channelFuture = connector.connect( new BoltServerAddress( "wrong-localhost" ), bootstrap ); + assertTrue( channelFuture.await( 10, TimeUnit.SECONDS ) ); + Channel channel = channelFuture.channel(); + + try + { + channelFuture.get(); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( ExecutionException.class ) ); + assertThat( e.getCause(), instanceOf( ServiceUnavailableException.class ) ); + assertThat( e.getCause().getMessage(), startsWith( "Unable to connect" ) ); + } + assertFalse( channel.isActive() ); + } + + @Test + public void shouldFailToConnectWithWrongCredentials() throws Exception + { + AuthToken authToken = AuthTokens.basic( "neo4j", "wrong-password" ); + AsyncConnectorImpl connector = newConnector( authToken ); + + ChannelFuture channelFuture = connector.connect( neo4j.address(), bootstrap ); + assertTrue( channelFuture.await( 10, TimeUnit.SECONDS ) ); + Channel channel = channelFuture.channel(); + + try + { + channelFuture.get(); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( ExecutionException.class ) ); + assertThat( e.getCause(), instanceOf( AuthenticationException.class ) ); + } + assertFalse( channel.isActive() ); + } + + @Test( timeout = 10000 ) + public void shouldEnforceConnectTimeout() throws Exception + { + AsyncConnectorImpl connector = newConnector( neo4j.authToken(), 1000 ); + + // try connect to a non-routable ip address 10.0.0.0, it will never respond + ChannelFuture channelFuture = connector.connect( new BoltServerAddress( "10.0.0.0" ), bootstrap ); + + try + { + await( channelFuture ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( ServiceUnavailableException.class ) ); + assertThat( e.getCause(), instanceOf( ConnectException.class ) ); + } + } + + private AsyncConnectorImpl newConnector( AuthToken authToken ) throws Exception + { + return newConnector( authToken, Integer.MAX_VALUE ); + } + + private AsyncConnectorImpl newConnector( AuthToken authToken, int connectTimeoutMillis ) throws Exception + { + ConnectionSettings settings = new ConnectionSettings( authToken, 1000 ); + return new AsyncConnectorImpl( settings, SecurityPlan.forAllCertificates(), + mock( ChannelPoolHandler.class ), DEV_NULL_LOGGING, new FakeClock() ); + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/ChannelAttributesTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/ChannelAttributesTest.java new file mode 100644 index 0000000000..2c9f6073c9 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/async/ChannelAttributesTest.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.embedded.EmbeddedChannel; +import org.junit.After; +import org.junit.Test; + +import org.neo4j.driver.internal.async.inbound.InboundMessageDispatcher; +import org.neo4j.driver.internal.net.BoltServerAddress; + +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.neo4j.driver.internal.async.ChannelAttributes.address; +import static org.neo4j.driver.internal.async.ChannelAttributes.creationTimestamp; +import static org.neo4j.driver.internal.async.ChannelAttributes.lastUsedTimestamp; +import static org.neo4j.driver.internal.async.ChannelAttributes.messageDispatcher; +import static org.neo4j.driver.internal.async.ChannelAttributes.serverVersion; +import static org.neo4j.driver.internal.async.ChannelAttributes.setAddress; +import static org.neo4j.driver.internal.async.ChannelAttributes.setCreationTimestamp; +import static org.neo4j.driver.internal.async.ChannelAttributes.setLastUsedTimestamp; +import static org.neo4j.driver.internal.async.ChannelAttributes.setMessageDispatcher; +import static org.neo4j.driver.internal.async.ChannelAttributes.setServerVersion; + +public class ChannelAttributesTest +{ + private final EmbeddedChannel channel = new EmbeddedChannel(); + + @After + public void tearDown() throws Exception + { + channel.close(); + } + + @Test + public void shouldSetAndGetAddress() + { + BoltServerAddress address = new BoltServerAddress( "local:42" ); + setAddress( channel, address ); + assertEquals( address, address( channel ) ); + } + + @Test + public void shouldFailToSetAddressTwice() + { + setAddress( channel, BoltServerAddress.LOCAL_DEFAULT ); + + try + { + setAddress( channel, BoltServerAddress.LOCAL_DEFAULT ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( IllegalStateException.class ) ); + } + } + + @Test + public void shouldSetAndGetCreationTimestamp() + { + setCreationTimestamp( channel, 42L ); + assertEquals( 42L, creationTimestamp( channel ) ); + } + + @Test + public void shouldFailToSetCreationTimestampTwice() + { + setCreationTimestamp( channel, 42L ); + + try + { + setCreationTimestamp( channel, 42L ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( IllegalStateException.class ) ); + } + } + + @Test + public void shouldSetAndGetLastUsedTimestamp() + { + assertNull( lastUsedTimestamp( channel ) ); + setLastUsedTimestamp( channel, 42L ); + assertEquals( 42L, lastUsedTimestamp( channel ).longValue() ); + } + + @Test + public void shouldAllowSettingLastUsedTimestampMultipleTimes() + { + setLastUsedTimestamp( channel, 42L ); + setLastUsedTimestamp( channel, 4242L ); + setLastUsedTimestamp( channel, 424242L ); + + assertEquals( 424242L, lastUsedTimestamp( channel ).longValue() ); + } + + @Test + public void shouldSetAndGetMessageDispatcher() + { + InboundMessageDispatcher dispatcher = mock( InboundMessageDispatcher.class ); + setMessageDispatcher( channel, dispatcher ); + assertEquals( dispatcher, messageDispatcher( channel ) ); + } + + @Test + public void shouldFailToSetMessageDispatcherTwice() + { + setMessageDispatcher( channel, mock( InboundMessageDispatcher.class ) ); + + try + { + setMessageDispatcher( channel, mock( InboundMessageDispatcher.class ) ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( IllegalStateException.class ) ); + } + } + + @Test + public void shouldSetAndGetServerVersion() + { + setServerVersion( channel, "3.2.1" ); + assertEquals( "3.2.1", serverVersion( channel ) ); + } + + @Test + public void shouldFailToSetServerVersionTwice() + { + setServerVersion( channel, "3.2.2" ); + + try + { + setServerVersion( channel, "3.2.3" ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( IllegalStateException.class ) ); + } + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/ChannelConnectedListenerTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/ChannelConnectedListenerTest.java new file mode 100644 index 0000000000..a84e4cc726 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/async/ChannelConnectedListenerTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.ChannelPromise; +import io.netty.channel.embedded.EmbeddedChannel; +import org.junit.After; +import org.junit.Test; + +import java.io.IOException; + +import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; + +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.neo4j.driver.internal.async.ProtocolUtil.handshake; +import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; +import static org.neo4j.driver.internal.net.BoltServerAddress.LOCAL_DEFAULT; +import static org.neo4j.driver.v1.util.TestUtil.await; + +public class ChannelConnectedListenerTest +{ + private final EmbeddedChannel channel = new EmbeddedChannel(); + + @After + public void tearDown() throws Exception + { + channel.close(); + } + + @Test + public void shouldFailPromiseWhenChannelConnectionFails() + { + ChannelPromise handshakeCompletedPromise = channel.newPromise(); + ChannelConnectedListener listener = newListener( handshakeCompletedPromise ); + + ChannelPromise channelConnectedPromise = channel.newPromise(); + IOException cause = new IOException( "Unable to connect!" ); + channelConnectedPromise.setFailure( cause ); + + listener.operationComplete( channelConnectedPromise ); + + try + { + await( handshakeCompletedPromise ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( ServiceUnavailableException.class ) ); + assertEquals( cause, e.getCause() ); + } + } + + @Test + public void shouldWriteHandshakeWhenChannelConnected() + { + ChannelPromise handshakeCompletedPromise = channel.newPromise(); + ChannelConnectedListener listener = newListener( handshakeCompletedPromise ); + + ChannelPromise channelConnectedPromise = channel.newPromise(); + channelConnectedPromise.setSuccess(); + + listener.operationComplete( channelConnectedPromise ); + + assertNotNull( channel.pipeline().get( HandshakeResponseHandler.class ) ); + assertTrue( channel.finish() ); + assertEquals( handshake(), channel.readOutbound() ); + } + + private static ChannelConnectedListener newListener( ChannelPromise handshakeCompletedPromise ) + { + return new ChannelConnectedListener( LOCAL_DEFAULT, handshakeCompletedPromise, DEV_NULL_LOGGING ); + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/HandshakeCompletedListenerTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/HandshakeCompletedListenerTest.java new file mode 100644 index 0000000000..f6c54c1756 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/async/HandshakeCompletedListenerTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.ChannelPromise; +import io.netty.channel.embedded.EmbeddedChannel; +import org.junit.After; +import org.junit.Test; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.neo4j.driver.internal.async.inbound.InboundMessageDispatcher; +import org.neo4j.driver.internal.handlers.AsyncInitResponseHandler; +import org.neo4j.driver.internal.messaging.InitMessage; +import org.neo4j.driver.v1.Value; + +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.neo4j.driver.internal.async.ChannelAttributes.setMessageDispatcher; +import static org.neo4j.driver.v1.Values.value; +import static org.neo4j.driver.v1.util.TestUtil.await; + +public class HandshakeCompletedListenerTest +{ + private final EmbeddedChannel channel = new EmbeddedChannel(); + + @After + public void tearDown() throws Exception + { + channel.close(); + } + + @Test + public void shouldFailConnectionInitializedPromiseWhenHandshakeFails() + { + ChannelPromise channelInitializedPromise = channel.newPromise(); + HandshakeCompletedListener listener = new HandshakeCompletedListener( "user-agent", authToken(), + channelInitializedPromise ); + + ChannelPromise handshakeCompletedPromise = channel.newPromise(); + IOException cause = new IOException( "Bad handshake" ); + handshakeCompletedPromise.setFailure( cause ); + + listener.operationComplete( handshakeCompletedPromise ); + + try + { + await( channelInitializedPromise ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertEquals( cause, e ); + } + } + + @Test + public void shouldWriteInitMessageWhenHandshakeCompleted() + { + InboundMessageDispatcher messageDispatcher = mock( InboundMessageDispatcher.class ); + setMessageDispatcher( channel, messageDispatcher ); + + ChannelPromise channelInitializedPromise = channel.newPromise(); + HandshakeCompletedListener listener = new HandshakeCompletedListener( "user-agent", authToken(), + channelInitializedPromise ); + + ChannelPromise handshakeCompletedPromise = channel.newPromise(); + handshakeCompletedPromise.setSuccess(); + + listener.operationComplete( handshakeCompletedPromise ); + assertTrue( channel.finish() ); + + verify( messageDispatcher ).queue( any( AsyncInitResponseHandler.class ) ); + Object outboundMessage = channel.readOutbound(); + assertThat( outboundMessage, instanceOf( InitMessage.class ) ); + InitMessage initMessage = (InitMessage) outboundMessage; + assertEquals( "user-agent", initMessage.userAgent() ); + assertEquals( authToken(), initMessage.authToken() ); + } + + private static Map authToken() + { + Map authToken = new HashMap<>(); + authToken.put( "username", value( "neo4j" ) ); + authToken.put( "username", value( "secret" ) ); + return authToken; + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/HandshakeResponseHandlerTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/HandshakeResponseHandlerTest.java new file mode 100644 index 0000000000..200ae8d89f --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/async/HandshakeResponseHandlerTest.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.ChannelPromise; +import io.netty.channel.embedded.EmbeddedChannel; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.neo4j.driver.internal.async.inbound.ChunkDecoder; +import org.neo4j.driver.internal.async.inbound.InboundMessageDispatcher; +import org.neo4j.driver.internal.async.inbound.InboundMessageHandler; +import org.neo4j.driver.internal.async.inbound.MessageDecoder; +import org.neo4j.driver.internal.async.outbound.OutboundMessageHandler; +import org.neo4j.driver.v1.exceptions.ClientException; + +import static io.netty.buffer.Unpooled.copyInt; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.neo4j.driver.internal.async.ChannelAttributes.setMessageDispatcher; +import static org.neo4j.driver.internal.async.ProtocolUtil.HTTP; +import static org.neo4j.driver.internal.async.ProtocolUtil.NO_PROTOCOL_VERSION; +import static org.neo4j.driver.internal.async.ProtocolUtil.PROTOCOL_VERSION_1; +import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; +import static org.neo4j.driver.v1.util.TestUtil.await; + +public class HandshakeResponseHandlerTest +{ + private final EmbeddedChannel channel = new EmbeddedChannel(); + + @Before + public void setUp() throws Exception + { + setMessageDispatcher( channel, new InboundMessageDispatcher( channel, DEV_NULL_LOGGING ) ); + } + + @After + public void tearDown() throws Exception + { + channel.close(); + } + + @Test + public void shouldFailGivenPromiseWhenExceptionCaught() + { + ChannelPromise handshakeCompletedPromise = channel.newPromise(); + HandshakeResponseHandler handler = newHandler( handshakeCompletedPromise ); + channel.pipeline().addLast( handler ); + + RuntimeException cause = new RuntimeException( "Error!" ); + channel.pipeline().fireExceptionCaught( cause ); + + try + { + // promise should fail + await( handshakeCompletedPromise ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertEquals( cause, e ); + } + + // channel should be closed + assertNull( await( channel.closeFuture() ) ); + } + + @Test + public void shouldSelectProtocolV1WhenServerSuggests() + { + ChannelPromise handshakeCompletedPromise = channel.newPromise(); + HandshakeResponseHandler handler = newHandler( handshakeCompletedPromise ); + channel.pipeline().addLast( handler ); + + channel.pipeline().fireChannelRead( copyInt( PROTOCOL_VERSION_1 ) ); + + // all inbound handlers should be set + assertNotNull( channel.pipeline().get( ChunkDecoder.class ) ); + assertNotNull( channel.pipeline().get( MessageDecoder.class ) ); + assertNotNull( channel.pipeline().get( InboundMessageHandler.class ) ); + + // all outbound handlers should be set + assertNotNull( channel.pipeline().get( OutboundMessageHandler.class ) ); + + // promise should be successful + assertNull( await( handshakeCompletedPromise ) ); + } + + @Test + public void shouldFailGivenPromiseWhenServerSuggestsNoProtocol() + { + testFailure( NO_PROTOCOL_VERSION, "The server does not support any of the protocol versions" ); + } + + @Test + public void shouldFailGivenPromiseWhenServerSuggestsHttp() + { + testFailure( HTTP, "Server responded HTTP" ); + } + + @Test + public void shouldFailGivenPromiseWhenServerSuggestsUnknownProtocol() + { + testFailure( 42, "Protocol error" ); + } + + private void testFailure( int serverSuggestedVersion, String expectedMessagePrefix ) + { + ChannelPromise handshakeCompletedPromise = channel.newPromise(); + HandshakeResponseHandler handler = newHandler( handshakeCompletedPromise ); + channel.pipeline().addLast( handler ); + + channel.pipeline().fireChannelRead( copyInt( serverSuggestedVersion ) ); + + try + { + // promise should fail + await( handshakeCompletedPromise ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( ClientException.class ) ); + assertThat( e.getMessage(), startsWith( expectedMessagePrefix ) ); + } + + // channel should be closed + assertNull( await( channel.closeFuture() ) ); + } + + private static HandshakeResponseHandler newHandler( ChannelPromise handshakeCompletedPromise ) + { + return new HandshakeResponseHandler( handshakeCompletedPromise, DEV_NULL_LOGGING ); + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/ReleaseChannelHandlerTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/ReleaseChannelHandlerTest.java new file mode 100644 index 0000000000..9812fdd2d0 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/async/ReleaseChannelHandlerTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.embedded.EmbeddedChannel; +import io.netty.channel.pool.ChannelPool; +import io.netty.util.concurrent.Promise; +import org.junit.After; +import org.junit.Test; + +import java.util.Collections; + +import org.neo4j.driver.internal.util.FakeClock; +import org.neo4j.driver.v1.Value; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.neo4j.driver.internal.async.ChannelAttributes.lastUsedTimestamp; + +public class ReleaseChannelHandlerTest +{ + private final EmbeddedChannel channel = new EmbeddedChannel(); + + @After + public void tearDown() throws Exception + { + channel.close(); + } + + @Test + public void shouldReleaseChannelOnSuccess() + { + ChannelPool pool = mock( ChannelPool.class ); + FakeClock clock = new FakeClock(); + clock.progress( 5 ); + ReleaseChannelHandler handler = new ReleaseChannelHandler( channel, pool, clock ); + + handler.onSuccess( Collections.emptyMap() ); + + verifyLastUsedTimestamp( 5 ); + verify( pool ).release( channel ); + } + + @Test + public void shouldReleaseChannelWithPromiseOnSuccess() + { + ChannelPool pool = mock( ChannelPool.class ); + FakeClock clock = new FakeClock(); + clock.progress( 42 ); + Promise promise = channel.newPromise(); + ReleaseChannelHandler handler = new ReleaseChannelHandler( channel, pool, clock, promise ); + + handler.onSuccess( Collections.emptyMap() ); + + verifyLastUsedTimestamp( 42 ); + verify( pool ).release( channel, promise ); + } + + @Test + public void shouldReleaseChannelOnFailure() + { + ChannelPool pool = mock( ChannelPool.class ); + FakeClock clock = new FakeClock(); + clock.progress( 100 ); + ReleaseChannelHandler handler = new ReleaseChannelHandler( channel, pool, clock ); + + handler.onFailure( new RuntimeException() ); + + verifyLastUsedTimestamp( 100 ); + verify( pool ).release( channel ); + } + + @Test + public void shouldReleaseChannelWithPromiseOnFailure() + { + ChannelPool pool = mock( ChannelPool.class ); + FakeClock clock = new FakeClock(); + clock.progress( 99 ); + Promise promise = channel.newPromise(); + ReleaseChannelHandler handler = new ReleaseChannelHandler( channel, pool, clock, promise ); + + handler.onFailure( new RuntimeException() ); + + verifyLastUsedTimestamp( 99 ); + verify( pool ).release( channel, promise ); + } + + private void verifyLastUsedTimestamp( int expectedValue ) + { + assertEquals( expectedValue, lastUsedTimestamp( channel ).intValue() ); + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/inbound/ByteBufInputTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/inbound/ByteBufInputTest.java new file mode 100644 index 0000000000..6b57de58de --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/async/inbound/ByteBufInputTest.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.inbound; + +import io.netty.buffer.ByteBuf; +import org.junit.Test; + +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ByteBufInputTest +{ + @Test + public void shouldThrowWhenStartedWithNullBuf() + { + ByteBufInput input = new ByteBufInput(); + + try + { + input.start( null ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( NullPointerException.class ) ); + } + } + + @Test + public void shouldThrowWhenStartedTwice() + { + ByteBufInput input = new ByteBufInput(); + input.start( mock( ByteBuf.class ) ); + + try + { + input.start( mock( ByteBuf.class ) ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( IllegalStateException.class ) ); + } + } + + @Test + public void shouldDelegateHasMoreData() + { + ByteBufInput input = new ByteBufInput(); + ByteBuf buf = mock( ByteBuf.class ); + when( buf.isReadable() ).thenReturn( true ); + input.start( buf ); + + assertTrue( input.hasMoreData() ); + } + + @Test + public void shouldDelegateReadByte() + { + ByteBufInput input = new ByteBufInput(); + ByteBuf buf = mock( ByteBuf.class ); + when( buf.readByte() ).thenReturn( (byte) 42 ); + input.start( buf ); + + assertEquals( (byte) 42, input.readByte() ); + } + + @Test + public void shouldDelegateReadShort() + { + ByteBufInput input = new ByteBufInput(); + ByteBuf buf = mock( ByteBuf.class ); + when( buf.readShort() ).thenReturn( (short) -42 ); + input.start( buf ); + + assertEquals( (short) -42, input.readShort() ); + } + + @Test + public void shouldDelegateReadInt() + { + ByteBufInput input = new ByteBufInput(); + ByteBuf buf = mock( ByteBuf.class ); + when( buf.readInt() ).thenReturn( 15 ); + input.start( buf ); + + assertEquals( 15, input.readInt() ); + } + + @Test + public void shouldDelegateReadLong() + { + ByteBufInput input = new ByteBufInput(); + ByteBuf buf = mock( ByteBuf.class ); + when( buf.readLong() ).thenReturn( 4242L ); + input.start( buf ); + + assertEquals( 4242L, input.readLong() ); + } + + @Test + public void shouldDelegateReadDouble() + { + ByteBufInput input = new ByteBufInput(); + ByteBuf buf = mock( ByteBuf.class ); + when( buf.readDouble() ).thenReturn( 42.42D ); + input.start( buf ); + + assertEquals( 42.42D, input.readDouble(), 0.00001 ); + } + + @Test + public void shouldDelegateReadBytes() + { + ByteBufInput input = new ByteBufInput(); + ByteBuf buf = mock( ByteBuf.class ); + input.start( buf ); + + input.readBytes( new byte[10], 3, 5 ); + + verify( buf ).readBytes( new byte[10], 3, 5 ); + } + + @Test + public void shouldDelegatePeekByte() + { + ByteBufInput input = new ByteBufInput(); + ByteBuf buf = mock( ByteBuf.class ); + when( buf.getByte( anyInt() ) ).thenReturn( (byte) 42 ); + input.start( buf ); + + assertEquals( (byte) 42, input.peekByte() ); + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/inbound/ChunkDecoderTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/inbound/ChunkDecoderTest.java new file mode 100644 index 0000000000..20bf245ec7 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/async/inbound/ChunkDecoderTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.inbound; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.embedded.EmbeddedChannel; +import org.junit.Test; + +import static io.netty.buffer.Unpooled.buffer; +import static io.netty.buffer.Unpooled.copyShort; +import static io.netty.buffer.Unpooled.wrappedBuffer; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ChunkDecoderTest +{ + @Test + public void shouldDecodeFullChunk() + { + EmbeddedChannel channel = new EmbeddedChannel( new ChunkDecoder() ); + + // whole chunk with header and body arrives at once + ByteBuf input = buffer(); + input.writeShort( 7 ); + input.writeByte( 1 ); + input.writeByte( 11 ); + input.writeByte( 2 ); + input.writeByte( 22 ); + input.writeByte( 3 ); + input.writeByte( 33 ); + input.writeByte( 4 ); + + // after buffer is written there should be something to read on the other side + assertTrue( channel.writeInbound( input ) ); + assertTrue( channel.finish() ); + + // there should only be a single chunk available for reading + assertEquals( 1, channel.inboundMessages().size() ); + // it should have no size header and expected body + assertEquals( input.slice( 2, 7 ), channel.readInbound() ); + } + + @Test + public void shouldDecodeSplitChunk() + { + EmbeddedChannel channel = new EmbeddedChannel( new ChunkDecoder() ); + + // first part of the chunk contains size header and some bytes + ByteBuf input1 = buffer(); + input1.writeShort( 9 ); + input1.writeByte( 1 ); + input1.writeByte( 11 ); + input1.writeByte( 2 ); + // nothing should be available for reading + assertFalse( channel.writeInbound( input1 ) ); + + // second part contains just a single byte + ByteBuf input2 = buffer(); + input2.writeByte( 22 ); + // nothing should be available for reading + assertFalse( channel.writeInbound( input2 ) ); + + // third part contains couple more bytes + ByteBuf input3 = buffer(); + input3.writeByte( 3 ); + input3.writeByte( 33 ); + input3.writeByte( 4 ); + // nothing should be available for reading + assertFalse( channel.writeInbound( input3 ) ); + + // fourth part contains couple more bytes, and the chunk is now complete + ByteBuf input4 = buffer(); + input4.writeByte( 44 ); + input4.writeByte( 5 ); + // there should be something to read now + assertTrue( channel.writeInbound( input4 ) ); + + assertTrue( channel.finish() ); + + // there should only be a single chunk available for reading + assertEquals( 1, channel.inboundMessages().size() ); + // it should have no size header and expected body + assertEquals( wrappedBuffer( new byte[]{1, 11, 2, 22, 3, 33, 4, 44, 5} ), channel.readInbound() ); + } + + @Test + public void shouldDecodeEmptyChunk() + { + EmbeddedChannel channel = new EmbeddedChannel( new ChunkDecoder() ); + + // chunk contains just the size header which is zero + ByteBuf input = copyShort( 0 ); + assertTrue( channel.writeInbound( input ) ); + assertTrue( channel.finish() ); + + // there should only be a single chunk available for reading + assertEquals( 1, channel.inboundMessages().size() ); + // it should have no size header and empty body + assertEquals( wrappedBuffer( new byte[0] ), channel.readInbound() ); + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/inbound/InboundMessageDispatcherTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/inbound/InboundMessageDispatcherTest.java new file mode 100644 index 0000000000..7fb2df2cbd --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/async/inbound/InboundMessageDispatcherTest.java @@ -0,0 +1,348 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.inbound; + +import io.netty.channel.Channel; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.internal.value.IntegerValue; +import org.neo4j.driver.v1.Value; +import org.neo4j.driver.v1.exceptions.Neo4jException; + +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; +import static org.neo4j.driver.internal.messaging.AckFailureMessage.ACK_FAILURE; +import static org.neo4j.driver.v1.Values.value; + +public class InboundMessageDispatcherTest +{ + private static final String FAILURE_CODE = "Neo.ClientError.Security.Unauthorized"; + private static final String FAILURE_MESSAGE = "Error Message"; + + @Test + public void shouldFailWhenCreatedWithNullChannel() + { + try + { + new InboundMessageDispatcher( null, DEV_NULL_LOGGING ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( NullPointerException.class ) ); + } + } + + @Test + public void shouldFailWhenCreatedWithNullLogging() + { + try + { + new InboundMessageDispatcher( mock( Channel.class ), null ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( NullPointerException.class ) ); + } + } + + @Test + public void shouldDequeHandlerOnSuccess() + { + InboundMessageDispatcher dispatcher = newDispatcher(); + + ResponseHandler handler = mock( ResponseHandler.class ); + dispatcher.queue( handler ); + assertEquals( 1, dispatcher.queuedHandlersCount() ); + + Map metadata = new HashMap<>(); + metadata.put( "key1", value( 1 ) ); + metadata.put( "key2", value( "2" ) ); + dispatcher.handleSuccessMessage( metadata ); + + assertEquals( 0, dispatcher.queuedHandlersCount() ); + verify( handler ).onSuccess( metadata ); + } + + @Test + public void shouldDequeHandlerOnFailure() + { + InboundMessageDispatcher dispatcher = newDispatcher(); + + ResponseHandler handler = mock( ResponseHandler.class ); + dispatcher.queue( handler ); + assertEquals( 1, dispatcher.queuedHandlersCount() ); + + dispatcher.handleFailureMessage( FAILURE_CODE, FAILURE_MESSAGE ); + + // ACK_FAILURE handler should remain queued + assertEquals( 1, dispatcher.queuedHandlersCount() ); + verifyFailure( handler ); + assertEquals( FAILURE_CODE, ((Neo4jException) dispatcher.currentError()).code() ); + assertEquals( FAILURE_MESSAGE, dispatcher.currentError().getMessage() ); + } + + @Test + public void shouldSendAckFailureOnFailure() + { + Channel channel = mock( Channel.class ); + InboundMessageDispatcher dispatcher = newDispatcher( channel ); + + dispatcher.queue( mock( ResponseHandler.class ) ); + assertEquals( 1, dispatcher.queuedHandlersCount() ); + + dispatcher.handleFailureMessage( FAILURE_CODE, FAILURE_MESSAGE ); + + verify( channel ).writeAndFlush( ACK_FAILURE ); + } + + @Test + public void shouldClearFailureOnAckFailureSuccess() + { + InboundMessageDispatcher dispatcher = newDispatcher(); + + dispatcher.queue( mock( ResponseHandler.class ) ); + assertEquals( 1, dispatcher.queuedHandlersCount() ); + + dispatcher.handleFailureMessage( FAILURE_CODE, FAILURE_MESSAGE ); + dispatcher.handleSuccessMessage( Collections.emptyMap() ); + + assertNull( dispatcher.currentError() ); + } + + @Test + public void shouldPeekHandlerOnRecord() + { + InboundMessageDispatcher dispatcher = newDispatcher(); + + ResponseHandler handler = mock( ResponseHandler.class ); + dispatcher.queue( handler ); + assertEquals( 1, dispatcher.queuedHandlersCount() ); + + Value[] fields1 = {new IntegerValue( 1 )}; + Value[] fields2 = {new IntegerValue( 2 )}; + Value[] fields3 = {new IntegerValue( 3 )}; + + dispatcher.handleRecordMessage( fields1 ); + dispatcher.handleRecordMessage( fields2 ); + dispatcher.handleRecordMessage( fields3 ); + + verify( handler ).onRecord( fields1 ); + verify( handler ).onRecord( fields2 ); + verify( handler ).onRecord( fields3 ); + assertEquals( 1, dispatcher.queuedHandlersCount() ); + } + + @Test + public void shouldFailAllHandlersOnFatalError() + { + InboundMessageDispatcher dispatcher = newDispatcher(); + + ResponseHandler handler1 = mock( ResponseHandler.class ); + ResponseHandler handler2 = mock( ResponseHandler.class ); + ResponseHandler handler3 = mock( ResponseHandler.class ); + + dispatcher.queue( handler1 ); + dispatcher.queue( handler2 ); + dispatcher.queue( handler3 ); + + RuntimeException fatalError = new RuntimeException( "Fatal!" ); + dispatcher.handleFatalError( fatalError ); + + InOrder inOrder = inOrder( handler1, handler2, handler3 ); + inOrder.verify( handler1 ).onFailure( fatalError ); + inOrder.verify( handler2 ).onFailure( fatalError ); + inOrder.verify( handler3 ).onFailure( fatalError ); + } + + @Test + public void shouldFailNewHandlerAfterFatalError() + { + InboundMessageDispatcher dispatcher = newDispatcher(); + + RuntimeException fatalError = new RuntimeException( "Fatal!" ); + dispatcher.handleFatalError( fatalError ); + + ResponseHandler handler = mock( ResponseHandler.class ); + dispatcher.queue( handler ); + + verify( handler ).onFailure( fatalError ); + } + + @Test + public void shouldDequeHandlerOnIgnored() + { + InboundMessageDispatcher dispatcher = newDispatcher(); + ResponseHandler handler = mock( ResponseHandler.class ); + + dispatcher.queue( handler ); + dispatcher.handleIgnoredMessage(); + + assertEquals( 0, dispatcher.queuedHandlersCount() ); + verifyZeroInteractions( handler ); + } + + @Test + public void shouldDequeAndFailHandlerOnIgnoredWhenErrorHappened() + { + InboundMessageDispatcher dispatcher = newDispatcher(); + ResponseHandler handler1 = mock( ResponseHandler.class ); + ResponseHandler handler2 = mock( ResponseHandler.class ); + + dispatcher.queue( handler1 ); + dispatcher.queue( handler2 ); + dispatcher.handleFailureMessage( FAILURE_CODE, FAILURE_MESSAGE ); + dispatcher.handleIgnoredMessage(); + + // ACK_FAILURE handler should remain queued + assertEquals( 1, dispatcher.queuedHandlersCount() ); + verifyFailure( handler1 ); + verifyFailure( handler2 ); + } + + @Test + public void shouldNotSupportInitMessage() + { + InboundMessageDispatcher dispatcher = newDispatcher(); + + try + { + dispatcher.handleInitMessage( "Client", Collections.emptyMap() ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( UnsupportedOperationException.class ) ); + } + } + + @Test + public void shouldNotSupportRunMessage() + { + InboundMessageDispatcher dispatcher = newDispatcher(); + + try + { + dispatcher.handleRunMessage( "RETURN 1", Collections.emptyMap() ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( UnsupportedOperationException.class ) ); + } + } + + @Test + public void shouldNotSupportPullAllMessage() + { + InboundMessageDispatcher dispatcher = newDispatcher(); + + try + { + dispatcher.handlePullAllMessage(); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( UnsupportedOperationException.class ) ); + } + } + + @Test + public void shouldNotSupportDiscardAllMessage() + { + InboundMessageDispatcher dispatcher = newDispatcher(); + + try + { + dispatcher.handleDiscardAllMessage(); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( UnsupportedOperationException.class ) ); + } + } + + @Test + public void shouldNotSupportResetMessage() + { + InboundMessageDispatcher dispatcher = newDispatcher(); + + try + { + dispatcher.handleResetMessage(); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( UnsupportedOperationException.class ) ); + } + } + + @Test + public void shouldNotSupportAckFailureMessage() + { + InboundMessageDispatcher dispatcher = newDispatcher(); + + try + { + dispatcher.handleAckFailureMessage(); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( UnsupportedOperationException.class ) ); + } + } + + private static void verifyFailure( ResponseHandler handler ) + { + ArgumentCaptor captor = ArgumentCaptor.forClass( Neo4jException.class ); + verify( handler ).onFailure( captor.capture() ); + assertEquals( FAILURE_CODE, captor.getValue().code() ); + assertEquals( FAILURE_MESSAGE, captor.getValue().getMessage() ); + } + + private static InboundMessageDispatcher newDispatcher() + { + return newDispatcher( mock( Channel.class ) ); + } + + private static InboundMessageDispatcher newDispatcher( Channel channel ) + { + return new InboundMessageDispatcher( channel, DEV_NULL_LOGGING ); + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/inbound/InboundMessageHandlerTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/inbound/InboundMessageHandlerTest.java new file mode 100644 index 0000000000..04c06fd338 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/async/inbound/InboundMessageHandlerTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.inbound; + +import io.netty.channel.embedded.EmbeddedChannel; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.util.HashMap; +import java.util.Map; + +import org.neo4j.driver.internal.async.ChannelAttributes; +import org.neo4j.driver.internal.messaging.FailureMessage; +import org.neo4j.driver.internal.messaging.IgnoredMessage; +import org.neo4j.driver.internal.messaging.PackStreamMessageFormatV1; +import org.neo4j.driver.internal.messaging.RecordMessage; +import org.neo4j.driver.internal.messaging.SuccessMessage; +import org.neo4j.driver.internal.spi.ResponseHandler; +import org.neo4j.driver.v1.Value; +import org.neo4j.driver.v1.exceptions.Neo4jException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; +import static org.neo4j.driver.internal.util.MessageToByteBufWriter.asByteBuf; +import static org.neo4j.driver.v1.Values.value; + +public class InboundMessageHandlerTest +{ + private EmbeddedChannel channel; + private InboundMessageDispatcher dispatcher; + + @Before + public void setUp() throws Exception + { + channel = new EmbeddedChannel(); + dispatcher = new InboundMessageDispatcher( channel, DEV_NULL_LOGGING ); + ChannelAttributes.setMessageDispatcher( channel, dispatcher ); + + InboundMessageHandler handler = new InboundMessageHandler( new PackStreamMessageFormatV1(), DEV_NULL_LOGGING ); + channel.pipeline().addFirst( handler ); + } + + @Test + public void shouldReadSuccessMessage() + { + ResponseHandler responseHandler = mock( ResponseHandler.class ); + dispatcher.queue( responseHandler ); + + Map metadata = new HashMap<>(); + metadata.put( "key1", value( 1 ) ); + metadata.put( "key2", value( 2 ) ); + channel.writeInbound( asByteBuf( new SuccessMessage( metadata ) ) ); + + verify( responseHandler ).onSuccess( metadata ); + } + + @Test + public void shouldReadFailureMessage() + { + ResponseHandler responseHandler = mock( ResponseHandler.class ); + dispatcher.queue( responseHandler ); + + channel.writeInbound( asByteBuf( new FailureMessage( "Neo.TransientError.General.ReadOnly", "Hi!" ) ) ); + + ArgumentCaptor captor = ArgumentCaptor.forClass( Neo4jException.class ); + verify( responseHandler ).onFailure( captor.capture() ); + assertEquals( "Neo.TransientError.General.ReadOnly", captor.getValue().code() ); + assertEquals( "Hi!", captor.getValue().getMessage() ); + } + + @Test + public void shouldReadRecordMessage() + { + ResponseHandler responseHandler = mock( ResponseHandler.class ); + dispatcher.queue( responseHandler ); + + Value[] fields = {value( 1 ), value( 2 ), value( 3 )}; + channel.writeInbound( asByteBuf( new RecordMessage( fields ) ) ); + + verify( responseHandler ).onRecord( fields ); + } + + @Test + public void shouldReadIgnoredMessage() + { + ResponseHandler responseHandler = mock( ResponseHandler.class ); + dispatcher.queue( responseHandler ); + + channel.writeInbound( asByteBuf( new IgnoredMessage() ) ); + assertEquals( 0, dispatcher.queuedHandlersCount() ); + } + + @Test + public void shouldCloseContextWhenChannelInactive() throws Exception + { + assertTrue( channel.isOpen() ); + + channel.pipeline().fireChannelInactive(); + + assertFalse( channel.isOpen() ); + } + + @Test + public void shouldCloseContextWhenExceptionCaught() + { + assertTrue( channel.isOpen() ); + + channel.pipeline().fireExceptionCaught( new RuntimeException( "Hi!" ) ); + + assertFalse( channel.isOpen() ); + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/inbound/MessageDecoderTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/inbound/MessageDecoderTest.java new file mode 100644 index 0000000000..4ff26a7789 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/async/inbound/MessageDecoderTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.inbound; + +import io.netty.channel.embedded.EmbeddedChannel; +import org.junit.Test; + +import static io.netty.buffer.Unpooled.wrappedBuffer; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class MessageDecoderTest +{ + @Test + public void shouldDecodeMessageWithSingleChunk() + { + EmbeddedChannel channel = new EmbeddedChannel( new MessageDecoder() ); + + assertFalse( channel.writeInbound( wrappedBuffer( new byte[]{1, 2, 3, 4, 5} ) ) ); + assertTrue( channel.writeInbound( wrappedBuffer( new byte[0] ) ) ); + assertTrue( channel.finish() ); + + assertEquals( 1, channel.inboundMessages().size() ); + assertEquals( wrappedBuffer( new byte[]{1, 2, 3, 4, 5} ), channel.readInbound() ); + } + + @Test + public void shouldDecodeMessageWithMultipleChunks() + { + EmbeddedChannel channel = new EmbeddedChannel( new MessageDecoder() ); + + assertFalse( channel.writeInbound( wrappedBuffer( new byte[]{1, 2, 3} ) ) ); + assertFalse( channel.writeInbound( wrappedBuffer( new byte[]{4, 5} ) ) ); + assertFalse( channel.writeInbound( wrappedBuffer( new byte[]{6, 7, 8} ) ) ); + assertTrue( channel.writeInbound( wrappedBuffer( new byte[0] ) ) ); + assertTrue( channel.finish() ); + + assertEquals( 1, channel.inboundMessages().size() ); + assertEquals( wrappedBuffer( new byte[]{1, 2, 3, 4, 5, 6, 7, 8} ), channel.readInbound() ); + } + + @Test + public void shouldDecodeMultipleConsecutiveMessages() + { + EmbeddedChannel channel = new EmbeddedChannel( new MessageDecoder() ); + + channel.writeInbound( wrappedBuffer( new byte[]{1, 2, 3} ) ); + channel.writeInbound( wrappedBuffer( new byte[0] ) ); + + channel.writeInbound( wrappedBuffer( new byte[]{4, 5} ) ); + channel.writeInbound( wrappedBuffer( new byte[]{6} ) ); + channel.writeInbound( wrappedBuffer( new byte[0] ) ); + + channel.writeInbound( wrappedBuffer( new byte[]{7, 8} ) ); + channel.writeInbound( wrappedBuffer( new byte[]{9, 10} ) ); + channel.writeInbound( wrappedBuffer( new byte[0] ) ); + + assertEquals( 3, channel.inboundMessages().size() ); + assertEquals( wrappedBuffer( new byte[]{1, 2, 3} ), channel.readInbound() ); + assertEquals( wrappedBuffer( new byte[]{4, 5, 6} ), channel.readInbound() ); + assertEquals( wrappedBuffer( new byte[]{7, 8, 9, 10} ), channel.readInbound() ); + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/outbound/ChunkAwareByteBufOutputTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/outbound/ChunkAwareByteBufOutputTest.java new file mode 100644 index 0000000000..06956ae3f3 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/async/outbound/ChunkAwareByteBufOutputTest.java @@ -0,0 +1,431 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.outbound; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.junit.Test; + +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.neo4j.driver.v1.util.TestUtil.assertByteBufContains; + +public class ChunkAwareByteBufOutputTest +{ + @Test + public void shouldThrowForIllegalMaxChunkSize() + { + try + { + new ChunkAwareByteBufOutput( -42 ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( IllegalArgumentException.class ) ); + } + } + + @Test + public void shouldThrowWhenStartedWithNullBuf() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 16 ); + + try + { + output.start( null ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( NullPointerException.class ) ); + } + } + + @Test + public void shouldThrowWhenStartedTwice() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 16 ); + output.start( mock( ByteBuf.class ) ); + + try + { + output.start( mock( ByteBuf.class ) ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( IllegalStateException.class ) ); + } + } + + @Test + public void shouldWriteByteAtTheBeginningOfChunk() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 16 ); + ByteBuf buf = Unpooled.buffer(); + + output.start( buf ); + output.writeByte( (byte) 42 ); + output.stop(); + + assertByteBufContains( buf, (short) 1, (byte) 42 ); + } + + @Test + public void shouldWriteByteWhenCurrentChunkContainsSpace() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 16 ); + ByteBuf buf = Unpooled.buffer(); + + output.start( buf ); + output.writeByte( (byte) 1 ); + output.writeByte( (byte) 2 ); + output.writeByte( (byte) -24 ); + + output.writeByte( (byte) 42 ); + output.stop(); + + assertByteBufContains( buf, (short) 4, (byte) 1, (byte) 2, (byte) -24, (byte) 42 ); + } + + @Test + public void shouldWriteByteWhenCurrentChunkIsFull() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 5 ); + ByteBuf buf = Unpooled.buffer(); + + output.start( buf ); + output.writeByte( (byte) 5 ); + output.writeByte( (byte) 3 ); + output.writeByte( (byte) -5 ); + + output.writeByte( (byte) 42 ); + output.stop(); + + assertByteBufContains( buf, + (short) 3, (byte) 5, (byte) 3, (byte) -5, // chunk 1 + (short) 1, (byte) 42 // chunk 2 + ); + } + + @Test + public void shouldWriteShortAtTheBeginningOfChunk() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 10 ); + ByteBuf buf = Unpooled.buffer(); + + output.start( buf ); + output.writeShort( Short.MAX_VALUE ); + output.stop(); + + assertByteBufContains( buf, (short) 2, Short.MAX_VALUE ); + } + + @Test + public void shouldWriteShortWhenCurrentChunkContainsSpace() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 12 ); + ByteBuf buf = Unpooled.buffer(); + + output.start( buf ); + output.writeShort( (short) 1 ); + output.writeShort( (short) 42 ); + output.writeShort( (short) 4242 ); + output.writeShort( (short) 4242 ); + + output.writeShort( (short) -30 ); + output.stop(); + + assertByteBufContains( buf, (short) 10, (short) 1, (short) 42, (short) 4242, (short) 4242, (short) -30 ); + } + + @Test + public void shouldWriteShortWhenCurrentChunkIsFull() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 8 ); + ByteBuf buf = Unpooled.buffer(); + + output.start( buf ); + output.writeShort( (short) 14 ); + output.writeShort( (short) -99 ); + output.writeShort( (short) 202 ); + + output.writeShort( Short.MIN_VALUE ); + output.stop(); + + assertByteBufContains( buf, + (short) 6, (short) 14, (short) -99, (short) 202, // chunk 1 + (short) 2, Short.MIN_VALUE // chunk 2 + ); + } + + @Test + public void shouldWriteIntAtTheBeginningOfChunk() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 18 ); + ByteBuf buf = Unpooled.buffer(); + + output.start( buf ); + output.writeInt( 73649 ); + output.stop(); + + assertByteBufContains( buf, (short) 4, 73649 ); + } + + @Test + public void shouldWriteIntWhenCurrentChunkContainsSpace() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 40 ); + ByteBuf buf = Unpooled.buffer(); + + output.start( buf ); + output.writeInt( Integer.MAX_VALUE ); + output.writeInt( 20 ); + output.writeInt( -173 ); + + output.writeInt( Integer.MIN_VALUE ); + output.stop(); + + assertByteBufContains( buf, (short) 16, Integer.MAX_VALUE, 20, -173, Integer.MIN_VALUE ); + } + + @Test + public void shouldWriteIntWhenCurrentChunkIsFull() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 27 ); + ByteBuf buf = Unpooled.buffer(); + + output.start( buf ); + output.writeInt( 42 ); + output.writeInt( -73467193 ); + output.writeInt( 373 ); + output.writeInt( -93 ); + output.writeInt( 1312345 ); + output.writeInt( 785 ); + + output.writeInt( 42 ); + output.stop(); + + assertByteBufContains( buf, + (short) 24, 42, -73467193, 373, -93, 1312345, 785, // chunk 1 + (short) 4, 42 // chunk 2 + ); + } + + @Test + public void shouldWriteLongAtTheBeginningOfChunk() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 12 ); + ByteBuf buf = Unpooled.buffer(); + + output.start( buf ); + output.writeLong( 15 ); + output.stop(); + + assertByteBufContains( buf, (short) 8, 15L ); + } + + @Test + public void shouldWriteLongWhenCurrentChunkContainsSpace() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 34 ); + ByteBuf buf = Unpooled.buffer(); + + output.start( buf ); + output.writeLong( Long.MAX_VALUE ); + output.writeLong( -1 ); + output.writeLong( -100 ); + + output.writeLong( Long.MIN_VALUE / 2 ); + output.stop(); + + assertByteBufContains( buf, (short) 32, Long.MAX_VALUE, -1L, -100L, Long.MIN_VALUE / 2 ); + } + + @Test + public void shouldWriteLongWhenCurrentChunkIsFull() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 38 ); + ByteBuf buf = Unpooled.buffer(); + + output.start( buf ); + output.writeLong( 12 ); + output.writeLong( 8741 ); + output.writeLong( 2314 ); + output.writeLong( -85793 ); + + output.writeLong( -57999999 ); + output.stop(); + + assertByteBufContains( buf, + (short) 32, 12L, 8741L, 2314L, -85793L, // chunk 1 + (short) 8, -57999999L // chunk 2 + ); + } + + @Test + public void shouldWriteDoubleAtTheBeginningOfChunk() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 10 ); + ByteBuf buf = Unpooled.buffer(); + + output.start( buf ); + output.writeDouble( 12.99937 ); + output.stop(); + + assertByteBufContains( buf, (short) 8, 12.99937D ); + } + + @Test + public void shouldWriteDoubleWhenCurrentChunkContainsSpace() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 18 ); + ByteBuf buf = Unpooled.buffer(); + + output.start( buf ); + output.writeDouble( -5 ); + + output.writeDouble( 991.3333 ); + output.stop(); + + assertByteBufContains( buf, (short) 16, -5D, 991.3333D ); + } + + @Test + public void shouldWriteDoubleWhenCurrentChunkIsFull() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 20 ); + ByteBuf buf = Unpooled.buffer(); + + output.start( buf ); + output.writeDouble( 1839 ); + output.writeDouble( 5710923.34873 ); + + output.writeDouble( -47389.333399 ); + output.stop(); + + assertByteBufContains( buf, + (short) 16, 1839D, 5710923.34873D, // chunk 1 + (short) 8, -47389.333399D // chunk 2 + ); + } + + @Test + public void shouldWriteBytesAtTheBeginningOfChunk() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 10 ); + ByteBuf buf = Unpooled.buffer(); + + output.start( buf ); + output.writeBytes( new byte[]{1, 2, 3, -1, -2, -3, 127} ); + output.stop(); + + assertByteBufContains( buf, + (short) 7, (byte) 1, (byte) 2, (byte) 3, (byte) -1, (byte) -2, (byte) -3, (byte) 127 ); + } + + @Test + public void shouldWriteBytesWhenCurrentChunkContainsSpace() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 13 ); + ByteBuf buf = Unpooled.buffer(); + + output.start( buf ); + output.writeBytes( new byte[]{9, 8, -10} ); + output.writeBytes( new byte[]{127, 126, -128, -126} ); + output.writeBytes( new byte[]{0, 99} ); + + output.writeBytes( new byte[]{-42, 42} ); + output.stop(); + + assertByteBufContains( buf, (short) 11, (byte) 9, (byte) 8, (byte) -10, (byte) 127, (byte) 126, (byte) -128, + (byte) -126, (byte) 0, (byte) 99, (byte) -42, (byte) 42 ); + } + + @Test + public void shouldWriteBytesWhenCurrentChunkIsFull() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 9 ); + ByteBuf buf = Unpooled.buffer(); + + output.start( buf ); + output.writeBytes( new byte[]{1, 2} ); + output.writeBytes( new byte[]{3, 4, 5} ); + output.writeBytes( new byte[]{10} ); + + output.writeBytes( new byte[]{-1, -42, -43} ); + output.stop(); + + assertByteBufContains( buf, + (short) 7, (byte) 1, (byte) 2, (byte) 3, (byte) 4, (byte) 5, (byte) 10, (byte) -1, // chunk 1 + (short) 2, (byte) -42, (byte) -43 // chunk 2 + ); + } + + @Test + public void shouldWriteBytesThatSpanMultipleChunks() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 7 ); + ByteBuf buf = Unpooled.buffer(); + + output.start( buf ); + output.writeBytes( new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18} ); + output.stop(); + + assertByteBufContains( buf, + (short) 5, (byte) 1, (byte) 2, (byte) 3, (byte) 4, (byte) 5, // chunk 1 + (short) 5, (byte) 6, (byte) 7, (byte) 8, (byte) 9, (byte) 10, // chunk 2 + (short) 5, (byte) 11, (byte) 12, (byte) 13, (byte) 14, (byte) 15, // chunk 3 + (short) 3, (byte) 16, (byte) 17, (byte) 18 // chunk 4 + ); + } + + @Test + public void shouldWriteDataToMultipleChunks() + { + ChunkAwareByteBufOutput output = new ChunkAwareByteBufOutput( 13 ); + ByteBuf buf = Unpooled.buffer(); + + output.start( buf ); + output.writeDouble( 12.3 ); + output.writeByte( (byte) 42 ); + output.writeInt( -10 ); + output.writeInt( 99 ); + output.writeLong( 99 ); + output.writeBytes( new byte[]{9, 8, 7, 6} ); + output.writeDouble( 0.333 ); + output.writeShort( (short) 0 ); + output.writeShort( (short) 1 ); + output.writeInt( 12345 ); + output.writeBytes( new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} ); + output.stop(); + + assertByteBufContains( buf, + (short) 9, 12.3D, (byte) 42, // chunk 1 + (short) 8, -10, 99, // chunk 2 + (short) 11, 99L, (byte) 9, (byte) 8, (byte) 7, // chunk 3 + (short) 11, (byte) 6, 0.333D, (short) 0, // chunk 4 + (short) 11, (short) 1, 12345, (byte) 1, (byte) 2, (byte) 3, (byte) 4, (byte) 5, // chunk 5 + (short) 5, (byte) 6, (byte) 7, (byte) 8, (byte) 9, (byte) 10 // chunk 6 + ); + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/outbound/OutboundMessageHandlerTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/outbound/OutboundMessageHandlerTest.java new file mode 100644 index 0000000000..63578020cb --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/async/outbound/OutboundMessageHandlerTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.outbound; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.embedded.EmbeddedChannel; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.neo4j.driver.internal.messaging.Message; +import org.neo4j.driver.internal.messaging.MessageFormat; +import org.neo4j.driver.internal.messaging.PackStreamMessageFormatV1; +import org.neo4j.driver.internal.messaging.RunMessage; +import org.neo4j.driver.internal.packstream.PackOutput; +import org.neo4j.driver.internal.packstream.PackStream; +import org.neo4j.driver.v1.Value; + +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.neo4j.driver.internal.async.ProtocolUtil.messageBoundary; +import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; +import static org.neo4j.driver.internal.messaging.MessageFormat.Writer; +import static org.neo4j.driver.internal.messaging.PullAllMessage.PULL_ALL; +import static org.neo4j.driver.v1.Values.value; +import static org.neo4j.driver.v1.util.TestUtil.assertByteBufContains; +import static org.neo4j.driver.v1.util.TestUtil.assertByteBufEquals; + +public class OutboundMessageHandlerTest +{ + @Test + public void shouldOutputByteBufAsWrittenByWriterAndMessageBoundary() throws IOException + { + MessageFormat messageFormat = mockMessageFormatWithWriter( 1, 2, 3, 4, 5 ); + OutboundMessageHandler handler = newHandler( messageFormat ); + EmbeddedChannel channel = new EmbeddedChannel( handler ); + + // do not care which message, writer will return predefined bytes anyway + assertTrue( channel.writeOutbound( PULL_ALL ) ); + assertTrue( channel.finish() ); + + assertEquals( 2, channel.outboundMessages().size() ); + + ByteBuf buf1 = channel.readOutbound(); + assertByteBufContains( buf1, (short) 5, (byte) 1, (byte) 2, (byte) 3, (byte) 4, (byte) 5 ); + + ByteBuf buf2 = channel.readOutbound(); + assertByteBufEquals( messageBoundary(), buf2 ); + } + + @Test + public void shouldSupportByteArraysByDefault() + { + OutboundMessageHandler handler = newHandler( new PackStreamMessageFormatV1() ); + EmbeddedChannel channel = new EmbeddedChannel( handler ); + + Map params = new HashMap<>(); + params.put( "array", value( new byte[]{1, 2, 3} ) ); + + assertTrue( channel.writeOutbound( new RunMessage( "RETURN 1", params ) ) ); + assertTrue( channel.finish() ); + } + + @Test + public void shouldFailToWriteByteArrayWhenNotSupported() + { + OutboundMessageHandler handler = newHandler( new PackStreamMessageFormatV1() ).withoutByteArraySupport(); + EmbeddedChannel channel = new EmbeddedChannel( handler ); + + Map params = new HashMap<>(); + params.put( "array", value( new byte[]{1, 2, 3} ) ); + + try + { + channel.writeOutbound( new RunMessage( "RETURN 1", params ) ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e.getCause(), instanceOf( PackStream.UnPackable.class ) ); + assertThat( e.getCause().getMessage(), startsWith( "Packing bytes is not supported" ) ); + } + } + + private static MessageFormat mockMessageFormatWithWriter( final int... bytesToWrite ) + { + MessageFormat messageFormat = mock( MessageFormat.class ); + + when( messageFormat.newWriter( any( PackOutput.class ), anyBoolean() ) ).then( new Answer() + { + @Override + public Writer answer( InvocationOnMock invocation ) throws Throwable + { + PackOutput output = invocation.getArgumentAt( 0, PackOutput.class ); + return mockWriter( output, bytesToWrite ); + } + } ); + + return messageFormat; + } + + private static Writer mockWriter( final PackOutput output, final int... bytesToWrite ) throws IOException + { + final Writer writer = mock( Writer.class ); + + doAnswer( new Answer() + { + @Override + public Writer answer( InvocationOnMock invocation ) throws Throwable + { + for ( int b : bytesToWrite ) + { + output.writeByte( (byte) b ); + } + return writer; + } + } ).when( writer ).write( any( Message.class ) ); + + return writer; + } + + private static OutboundMessageHandler newHandler( MessageFormat messageFormat ) + { + return new OutboundMessageHandler( messageFormat, DEV_NULL_LOGGING ); + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/pool/ActiveChannelTrackerTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/pool/ActiveChannelTrackerTest.java new file mode 100644 index 0000000000..57c0fe39de --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/async/pool/ActiveChannelTrackerTest.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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 io.netty.channel.embedded.EmbeddedChannel; +import org.junit.Test; + +import org.neo4j.driver.internal.net.BoltServerAddress; + +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.neo4j.driver.internal.async.ChannelAttributes.setAddress; +import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; +import static org.neo4j.driver.v1.util.TestUtil.await; + +public class ActiveChannelTrackerTest +{ + private final BoltServerAddress address = BoltServerAddress.LOCAL_DEFAULT; + private final ActiveChannelTracker tracker = new ActiveChannelTracker( DEV_NULL_LOGGING ); + + @Test + public void shouldIncrementCountWhenChannelCreated() + { + Channel channel = newChannel(); + assertEquals( 0, tracker.activeChannelCount( address ) ); + + tracker.channelCreated( channel ); + assertEquals( 1, tracker.activeChannelCount( address ) ); + } + + @Test + public void shouldIncrementCountForAddress() + { + Channel channel1 = newChannel(); + Channel channel2 = newChannel(); + Channel channel3 = newChannel(); + + assertEquals( 0, tracker.activeChannelCount( address ) ); + tracker.channelAcquired( channel1 ); + assertEquals( 1, tracker.activeChannelCount( address ) ); + tracker.channelAcquired( channel2 ); + assertEquals( 2, tracker.activeChannelCount( address ) ); + tracker.channelAcquired( channel3 ); + assertEquals( 3, tracker.activeChannelCount( address ) ); + } + + @Test + public void shouldDecrementCountForAddress() + { + Channel channel1 = newChannel(); + Channel channel2 = newChannel(); + Channel channel3 = newChannel(); + + tracker.channelAcquired( channel1 ); + tracker.channelAcquired( channel2 ); + tracker.channelAcquired( channel3 ); + assertEquals( 3, tracker.activeChannelCount( address ) ); + + tracker.channelReleased( channel1 ); + assertEquals( 2, tracker.activeChannelCount( address ) ); + tracker.channelReleased( channel2 ); + assertEquals( 1, tracker.activeChannelCount( address ) ); + tracker.channelReleased( channel3 ); + assertEquals( 0, tracker.activeChannelCount( address ) ); + } + + @Test + public void shouldThrowWhenDecrementingForUnknownAddress() + { + Channel channel = newChannel(); + + try + { + tracker.channelReleased( channel ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( IllegalStateException.class ) ); + } + } + + @Test + public void shouldReturnZeroActiveCountForUnknownAddress() + { + assertEquals( 0, tracker.activeChannelCount( address ) ); + } + + @Test + public void shouldPruneForMissingAddress() + { + assertEquals( 0, tracker.activeChannelCount( address ) ); + tracker.purge( address ); + assertEquals( 0, tracker.activeChannelCount( address ) ); + } + + @Test + public void shouldPruneForExistingAddress() + { + Channel channel1 = newChannel(); + Channel channel2 = newChannel(); + Channel channel3 = newChannel(); + + tracker.channelAcquired( channel1 ); + tracker.channelAcquired( channel2 ); + tracker.channelAcquired( channel3 ); + + assertEquals( 3, tracker.activeChannelCount( address ) ); + + tracker.purge( address ); + + assertEquals( 0, tracker.activeChannelCount( address ) ); + assertNull( await( channel1.closeFuture() ) ); + assertNull( await( channel2.closeFuture() ) ); + assertNull( await( channel3.closeFuture() ) ); + } + + private Channel newChannel() + { + EmbeddedChannel channel = new EmbeddedChannel(); + setAddress( channel, address ); + return channel; + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/pool/AsyncConnectionPoolImplTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/pool/AsyncConnectionPoolImplTest.java new file mode 100644 index 0000000000..ec64e68c22 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/async/pool/AsyncConnectionPoolImplTest.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.bootstrap.Bootstrap; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import org.neo4j.driver.internal.ConnectionSettings; +import org.neo4j.driver.internal.async.AsyncConnection; +import org.neo4j.driver.internal.async.AsyncConnectorImpl; +import org.neo4j.driver.internal.async.BootstrapFactory; +import org.neo4j.driver.internal.net.BoltServerAddress; +import org.neo4j.driver.internal.net.pooling.PoolSettings; +import org.neo4j.driver.internal.security.SecurityPlan; +import org.neo4j.driver.internal.util.FakeClock; +import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; +import org.neo4j.driver.v1.util.TestNeo4j; + +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; +import static org.neo4j.driver.v1.util.TestUtil.await; + +public class AsyncConnectionPoolImplTest +{ + @Rule + public final TestNeo4j neo4j = new TestNeo4j(); + + private AsyncConnectionPoolImpl pool; + + @Before + public void setUp() throws Exception + { + pool = newPool(); + } + + @After + public void tearDown() throws Exception + { + pool.closeAsync(); + } + + @Test + public void shouldAcquireConnectionWhenPoolIsEmpty() throws Exception + { + AsyncConnection connection = await( pool.acquire( neo4j.address() ) ); + + assertNotNull( connection ); + } + + @Test + public void shouldAcquireIdleConnection() throws Exception + { + AsyncConnection connection1 = await( pool.acquire( neo4j.address() ) ); + await( connection1.forceRelease() ); + + AsyncConnection connection2 = await( pool.acquire( neo4j.address() ) ); + assertNotNull( connection2 ); + } + + @Test + public void shouldFailToAcquireConnectionToWrongAddress() throws Exception + { + try + { + await( pool.acquire( new BoltServerAddress( "wrong-localhost" ) ) ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( ServiceUnavailableException.class ) ); + assertThat( e.getMessage(), startsWith( "Unable to connect" ) ); + } + } + + @Test + public void shouldFailToAcquireWhenPoolClosed() throws Exception + { + AsyncConnection connection = await( pool.acquire( neo4j.address() ) ); + await( connection.forceRelease() ); + await( pool.closeAsync() ); + + try + { + pool.acquire( neo4j.address() ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( IllegalStateException.class ) ); + assertThat( e.getMessage(), startsWith( "Pool closed" ) ); + } + } + + @Test + public void shouldPurgeAddressWithConnections() + { + AsyncConnection connection1 = await( pool.acquire( neo4j.address() ) ); + AsyncConnection connection2 = await( pool.acquire( neo4j.address() ) ); + AsyncConnection connection3 = await( pool.acquire( neo4j.address() ) ); + + assertNotNull( connection1 ); + assertNotNull( connection2 ); + assertNotNull( connection3 ); + + assertEquals( 3, pool.activeConnections( neo4j.address() ) ); + + pool.purge( neo4j.address() ); + + assertEquals( 0, pool.activeConnections( neo4j.address() ) ); + } + + @Test + public void shouldPurgeAddressWithoutConnections() + { + assertEquals( 0, pool.activeConnections( neo4j.address() ) ); + + pool.purge( neo4j.address() ); + + assertEquals( 0, pool.activeConnections( neo4j.address() ) ); + } + + @Test + public void shouldCheckIfPoolHasAddress() + { + assertFalse( pool.hasAddress( neo4j.address() ) ); + + await( pool.acquire( neo4j.address() ) ); + + assertTrue( pool.hasAddress( neo4j.address() ) ); + } + + @Test + public void shouldNotCloseWhenClosed() + { + assertNull( await( pool.closeAsync() ) ); + assertTrue( pool.closeAsync().isDone() ); + } + + private AsyncConnectionPoolImpl newPool() throws Exception + { + FakeClock clock = new FakeClock(); + ConnectionSettings connectionSettings = new ConnectionSettings( neo4j.authToken(), 5000 ); + ActiveChannelTracker poolHandler = new ActiveChannelTracker( DEV_NULL_LOGGING ); + AsyncConnectorImpl connector = new AsyncConnectorImpl( connectionSettings, SecurityPlan.forAllCertificates(), + poolHandler, DEV_NULL_LOGGING, clock ); + PoolSettings poolSettings = new PoolSettings( 5, -1, -1, 10, 5000 ); + Bootstrap bootstrap = BootstrapFactory.newBootstrap( 1 ); + return new AsyncConnectionPoolImpl( connector, bootstrap, poolHandler, poolSettings, DEV_NULL_LOGGING, clock ); + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/pool/NettyChannelHealthCheckerTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/pool/NettyChannelHealthCheckerTest.java new file mode 100644 index 0000000000..f84c1034a1 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/async/pool/NettyChannelHealthCheckerTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.embedded.EmbeddedChannel; +import io.netty.util.concurrent.Future; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collections; + +import org.neo4j.driver.internal.async.inbound.InboundMessageDispatcher; +import org.neo4j.driver.internal.messaging.ResetMessage; +import org.neo4j.driver.internal.net.pooling.PoolSettings; +import org.neo4j.driver.internal.util.Clock; +import org.neo4j.driver.v1.Value; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.neo4j.driver.internal.async.ChannelAttributes.setCreationTimestamp; +import static org.neo4j.driver.internal.async.ChannelAttributes.setLastUsedTimestamp; +import static org.neo4j.driver.internal.async.ChannelAttributes.setMessageDispatcher; +import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; +import static org.neo4j.driver.internal.net.pooling.PoolSettings.DEFAULT_CONNECTION_ACQUISITION_TIMEOUT; +import static org.neo4j.driver.internal.net.pooling.PoolSettings.DEFAULT_IDLE_TIME_BEFORE_CONNECTION_TEST; +import static org.neo4j.driver.internal.net.pooling.PoolSettings.DEFAULT_MAX_CONNECTION_POOL_SIZE; +import static org.neo4j.driver.internal.net.pooling.PoolSettings.DEFAULT_MAX_IDLE_CONNECTION_POOL_SIZE; +import static org.neo4j.driver.internal.net.pooling.PoolSettings.NOT_CONFIGURED; +import static org.neo4j.driver.internal.util.Iterables.single; +import static org.neo4j.driver.v1.util.TestUtil.await; + +public class NettyChannelHealthCheckerTest +{ + private final EmbeddedChannel channel = new EmbeddedChannel(); + private final InboundMessageDispatcher dispatcher = new InboundMessageDispatcher( channel, DEV_NULL_LOGGING ); + + @Before + public void setUp() throws Exception + { + setMessageDispatcher( channel, dispatcher ); + } + + @After + public void tearDown() throws Exception + { + channel.close(); + } + + @Test + public void shouldDropTooOldChannelsWhenMaxLifetimeEnabled() + { + int maxConnectionLifetime = 1000; + PoolSettings settings = new PoolSettings( DEFAULT_MAX_IDLE_CONNECTION_POOL_SIZE, + DEFAULT_IDLE_TIME_BEFORE_CONNECTION_TEST, maxConnectionLifetime, DEFAULT_MAX_CONNECTION_POOL_SIZE, + DEFAULT_CONNECTION_ACQUISITION_TIMEOUT ); + Clock clock = Clock.SYSTEM; + NettyChannelHealthChecker healthChecker = new NettyChannelHealthChecker( settings, clock ); + + setCreationTimestamp( channel, clock.millis() - maxConnectionLifetime * 2 ); + Future healthy = healthChecker.isHealthy( channel ); + + assertThat( await( healthy ), is( false ) ); + } + + @Test + public void shouldAllowVeryOldChannelsWhenMaxLifetimeDisabled() + { + PoolSettings settings = new PoolSettings( DEFAULT_MAX_IDLE_CONNECTION_POOL_SIZE, + DEFAULT_IDLE_TIME_BEFORE_CONNECTION_TEST, NOT_CONFIGURED, DEFAULT_MAX_CONNECTION_POOL_SIZE, + DEFAULT_CONNECTION_ACQUISITION_TIMEOUT ); + NettyChannelHealthChecker healthChecker = new NettyChannelHealthChecker( settings, Clock.SYSTEM ); + + setCreationTimestamp( channel, 0 ); + Future healthy = healthChecker.isHealthy( channel ); + + assertThat( await( healthy ), is( true ) ); + } + + @Test + public void shouldKeepIdleConnectionWhenPingSucceeds() + { + testPing( true ); + } + + @Test + public void shouldDropIdleConnectionWhenPingFails() + { + testPing( false ); + } + + @Test + public void shouldKeepActiveConnections() + { + testActiveConnectionCheck( true ); + } + + @Test + public void shouldDropInactiveConnections() + { + testActiveConnectionCheck( false ); + } + + private void testPing( boolean resetMessageSuccessful ) + { + int idleTimeBeforeConnectionTest = 1000; + PoolSettings settings = new PoolSettings( DEFAULT_MAX_IDLE_CONNECTION_POOL_SIZE, + idleTimeBeforeConnectionTest, NOT_CONFIGURED, DEFAULT_MAX_CONNECTION_POOL_SIZE, + DEFAULT_CONNECTION_ACQUISITION_TIMEOUT ); + Clock clock = Clock.SYSTEM; + NettyChannelHealthChecker healthChecker = new NettyChannelHealthChecker( settings, clock ); + + setCreationTimestamp( channel, clock.millis() ); + setLastUsedTimestamp( channel, clock.millis() - idleTimeBeforeConnectionTest * 2 ); + + Future healthy = healthChecker.isHealthy( channel ); + + assertEquals( ResetMessage.RESET, single( channel.outboundMessages() ) ); + assertFalse( healthy.isDone() ); + + if ( resetMessageSuccessful ) + { + dispatcher.handleSuccessMessage( Collections.emptyMap() ); + assertThat( await( healthy ), is( true ) ); + } + else + { + dispatcher.handleFailureMessage( "Neo.ClientError.General.Unknown", "Error!" ); + assertThat( await( healthy ), is( false ) ); + } + } + + private void testActiveConnectionCheck( boolean channelActive ) + { + PoolSettings settings = new PoolSettings( DEFAULT_MAX_IDLE_CONNECTION_POOL_SIZE, + DEFAULT_IDLE_TIME_BEFORE_CONNECTION_TEST, NOT_CONFIGURED, DEFAULT_MAX_CONNECTION_POOL_SIZE, + DEFAULT_CONNECTION_ACQUISITION_TIMEOUT ); + Clock clock = Clock.SYSTEM; + NettyChannelHealthChecker healthChecker = new NettyChannelHealthChecker( settings, clock ); + + setCreationTimestamp( channel, clock.millis() ); + + if ( channelActive ) + { + Future healthy = healthChecker.isHealthy( channel ); + assertThat( await( healthy ), is( true ) ); + } + else + { + channel.close().syncUninterruptibly(); + Future healthy = healthChecker.isHealthy( channel ); + assertThat( await( healthy ), is( false ) ); + } + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/cluster/RoutingPooledConnectionErrorHandlingTest.java b/driver/src/test/java/org/neo4j/driver/internal/cluster/RoutingPooledConnectionErrorHandlingTest.java index 91b54067c2..b8ad0242ea 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/cluster/RoutingPooledConnectionErrorHandlingTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/cluster/RoutingPooledConnectionErrorHandlingTest.java @@ -31,6 +31,7 @@ import java.util.List; import org.neo4j.driver.internal.cluster.loadbalancing.LoadBalancer; +import org.neo4j.driver.internal.handlers.NoOpResponseHandler; import org.neo4j.driver.internal.net.BoltServerAddress; import org.neo4j.driver.internal.net.pooling.PoolSettings; import org.neo4j.driver.internal.net.pooling.SocketConnectionPool; @@ -60,8 +61,8 @@ import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; import static org.neo4j.driver.internal.net.pooling.PoolSettings.DEFAULT_MAX_IDLE_CONNECTION_POOL_SIZE; import static org.neo4j.driver.internal.net.pooling.PoolSettings.INFINITE_CONNECTION_LIFETIME; +import static org.neo4j.driver.internal.net.pooling.PoolSettings.NOT_CONFIGURED; import static org.neo4j.driver.internal.net.pooling.PoolSettings.NO_IDLE_CONNECTION_TEST; -import static org.neo4j.driver.internal.spi.Collector.NO_OP; import static org.neo4j.driver.internal.util.Matchers.containsReader; import static org.neo4j.driver.internal.util.Matchers.containsRouter; import static org.neo4j.driver.internal.util.Matchers.containsWriter; @@ -353,7 +354,7 @@ private static ConnectionPool newConnectionPool( Connector connector, BoltServer { int maxIdleConnections = DEFAULT_MAX_IDLE_CONNECTION_POOL_SIZE; PoolSettings settings = new PoolSettings( maxIdleConnections, NO_IDLE_CONNECTION_TEST, - INFINITE_CONNECTION_LIFETIME ); + INFINITE_CONNECTION_LIFETIME, NOT_CONFIGURED, NOT_CONFIGURED ); SocketConnectionPool pool = new SocketConnectionPool( settings, connector, Clock.SYSTEM, DEV_NULL_LOGGING ); // force pool to create and memorize some connections @@ -400,7 +401,8 @@ private static class Run implements ConnectionMethod @Override public void invoke( Connection connection ) { - connection.run( "CREATE (n:Node {name: {value}})", singletonMap( "value", value( "A" ) ), NO_OP ); + connection.run( "CREATE (n:Node {name: {value}})", singletonMap( "value", value( "A" ) ), + NoOpResponseHandler.INSTANCE ); } } @@ -409,7 +411,7 @@ private static class DiscardAll implements ConnectionMethod @Override public void invoke( Connection connection ) { - connection.discardAll( NO_OP ); + connection.discardAll( NoOpResponseHandler.INSTANCE ); } } @@ -418,7 +420,7 @@ private static class PullAll implements ConnectionMethod @Override public void invoke( Connection connection ) { - connection.pullAll( NO_OP ); + connection.pullAll( NoOpResponseHandler.INSTANCE ); } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/handlers/PingResponseHandlerTest.java b/driver/src/test/java/org/neo4j/driver/internal/handlers/PingResponseHandlerTest.java new file mode 100644 index 0000000000..d60319f85e --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/handlers/PingResponseHandlerTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.handlers; + +import io.netty.util.concurrent.ImmediateEventExecutor; +import io.netty.util.concurrent.Promise; +import org.junit.Test; + +import java.util.Collections; + +import org.neo4j.driver.v1.Value; + +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class PingResponseHandlerTest +{ + @Test + public void shouldResolvePromiseOnSuccess() + { + Promise promise = newPromise(); + PingResponseHandler handler = new PingResponseHandler( promise ); + + handler.onSuccess( Collections.emptyMap() ); + + assertTrue( promise.isSuccess() ); + assertTrue( promise.getNow() ); + } + + @Test + public void shouldResolvePromiseOnFailure() + { + Promise promise = newPromise(); + PingResponseHandler handler = new PingResponseHandler( promise ); + + handler.onFailure( new RuntimeException() ); + + assertTrue( promise.isSuccess() ); + assertFalse( promise.getNow() ); + } + + @Test + public void shouldNotSupportRecordMessages() + { + PingResponseHandler handler = new PingResponseHandler( newPromise() ); + + try + { + handler.onRecord( new Value[0] ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( UnsupportedOperationException.class ) ); + } + } + + private static Promise newPromise() + { + return ImmediateEventExecutor.INSTANCE.newPromise(); + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/FragmentedMessageDeliveryTest.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/FragmentedMessageDeliveryTest.java index a3b2fd66d7..03e948d369 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/messaging/FragmentedMessageDeliveryTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/FragmentedMessageDeliveryTest.java @@ -29,6 +29,7 @@ import java.util.ArrayList; import java.util.Collections; +import org.neo4j.driver.internal.net.BufferingChunkedInput; import org.neo4j.driver.internal.net.ChunkedOutput; import org.neo4j.driver.v1.Value; import org.neo4j.driver.v1.util.DumpMessage; @@ -102,7 +103,8 @@ private void testPermutation( byte[] unfragmented, ByteBuffer[] fragments ) thro } ReadableByteChannel fragmentedChannel = packets( channels ); - MessageFormat.Reader reader = format.newReader( fragmentedChannel ); + BufferingChunkedInput input = new BufferingChunkedInput( fragmentedChannel ); + MessageFormat.Reader reader = format.newReader( input ); ArrayList packedMessages = new ArrayList<>(); DumpMessage.unpack( packedMessages, reader ); diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/MessageFormatTest.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/MessageFormatTest.java index b011e3a693..b181b9b958 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/messaging/MessageFormatTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/MessageFormatTest.java @@ -33,6 +33,7 @@ import org.neo4j.driver.internal.InternalNode; import org.neo4j.driver.internal.InternalPath; import org.neo4j.driver.internal.InternalRelationship; +import org.neo4j.driver.internal.net.BufferingChunkedInput; import org.neo4j.driver.internal.net.ChunkedOutput; import org.neo4j.driver.internal.packstream.PackStream; import org.neo4j.driver.internal.util.BytePrinter; @@ -135,7 +136,8 @@ private void assertSerializes( Message... messages ) throws IOException { // Pack final ByteArrayOutputStream out = new ByteArrayOutputStream( 128 ); - MessageFormat.Writer writer = format.newWriter( Channels.newChannel( out ), true ); + ChunkedOutput output = new ChunkedOutput( Channels.newChannel( out ) ); + MessageFormat.Writer writer = format.newWriter( output, true ); for ( Message message : messages ) { writer.write( message ); @@ -151,8 +153,9 @@ private ArrayList unpack( MessageFormat format, byte[] bytes ) throws I { try { - ByteArrayInputStream input = new ByteArrayInputStream( bytes ); - MessageFormat.Reader reader = format.newReader( Channels.newChannel( input ) ); + ByteArrayInputStream inputStream = new ByteArrayInputStream( bytes ); + BufferingChunkedInput input = new BufferingChunkedInput( Channels.newChannel( inputStream ) ); + MessageFormat.Reader reader = format.newReader( input ); ArrayList messages = new ArrayList<>(); DumpMessage.unpack( messages, reader ); return messages; diff --git a/driver/src/test/java/org/neo4j/driver/internal/net/ChunkedOutputTest.java b/driver/src/test/java/org/neo4j/driver/internal/net/ChunkedOutputTest.java index 06d030d075..89569c3974 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/net/ChunkedOutputTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/net/ChunkedOutputTest.java @@ -21,6 +21,8 @@ import org.hamcrest.MatcherAssert; import org.junit.Test; +import java.util.Arrays; + import org.neo4j.driver.internal.util.BytePrinter; import org.neo4j.driver.v1.util.RecordingByteChannel; @@ -65,7 +67,7 @@ public void shouldChunkMessageSpanningMultipleChunks() throws Throwable public void shouldReserveSpaceForChunkHeaderWhenWriteDataToNewChunk() throws Throwable { // Given 2 bytes left in buffer + chunk is closed - out.writeBytes( new byte[10], 0, 10 ); // 2 (header) + 10 + out.writeBytes( new byte[10] ); // 2 (header) + 10 out.messageBoundaryHook().run(); // 2 (ending) // When write 2 bytes @@ -87,7 +89,7 @@ public void shouldSendOutDataWhoseSizeIsGreaterThanOutputBufferCapacity() throws } // When - out.writeBytes( data, 4, 16 ); + out.writeBytes( Arrays.copyOfRange( data, 4, 16 ) ); out.messageBoundaryHook().run(); out.flush(); diff --git a/driver/src/test/java/org/neo4j/driver/internal/net/LoggingResponseHandlerTest.java b/driver/src/test/java/org/neo4j/driver/internal/net/LoggingResponseHandlerTest.java index 4ad146eeea..0f3294260d 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/net/LoggingResponseHandlerTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/net/LoggingResponseHandlerTest.java @@ -27,6 +27,7 @@ import java.util.Arrays; import java.util.HashMap; +import org.neo4j.driver.internal.handlers.NoOpResponseHandler; import org.neo4j.driver.internal.messaging.DiscardAllMessage; import org.neo4j.driver.internal.messaging.FailureMessage; import org.neo4j.driver.internal.messaging.IgnoredMessage; @@ -37,7 +38,6 @@ import org.neo4j.driver.internal.messaging.ResetMessage; import org.neo4j.driver.internal.messaging.RunMessage; import org.neo4j.driver.internal.messaging.SuccessMessage; -import org.neo4j.driver.internal.spi.Collector; import org.neo4j.driver.v1.Logger; import org.neo4j.driver.v1.Value; @@ -120,7 +120,7 @@ public void shouldLogAckFailureMessage() throws Throwable public void shouldLogSuccessMessage() throws Throwable { // When - handler.appendResultCollector( Collector.NO_OP ); + handler.appendResponseHandler( NoOpResponseHandler.INSTANCE ); handler.handleSuccessMessage( new HashMap() ); // Then @@ -132,7 +132,7 @@ public void shouldLogSuccessMessage() throws Throwable public void shouldLogRecordMessage() throws Throwable { // When - handler.appendResultCollector( Collector.NO_OP ); + handler.appendResponseHandler( NoOpResponseHandler.INSTANCE ); handler.handleRecordMessage( new Value[]{} ); // Then @@ -144,7 +144,7 @@ public void shouldLogRecordMessage() throws Throwable public void shouldLogFailureMessage() throws Throwable { // When - handler.appendResultCollector( Collector.NO_OP ); + handler.appendResponseHandler( NoOpResponseHandler.INSTANCE ); handler.handleFailureMessage( "code.error", "message" ); // Then @@ -156,7 +156,7 @@ public void shouldLogFailureMessage() throws Throwable public void shouldLogIgnoredMessage() throws Throwable { // When - handler.appendResultCollector( Collector.NO_OP ); + handler.appendResponseHandler( NoOpResponseHandler.INSTANCE ); handler.handleIgnoredMessage(); // Then diff --git a/driver/src/test/java/org/neo4j/driver/internal/net/SocketResponseHandlerTest.java b/driver/src/test/java/org/neo4j/driver/internal/net/SocketResponseHandlerTest.java deleted file mode 100644 index 2d7970d705..0000000000 --- a/driver/src/test/java/org/neo4j/driver/internal/net/SocketResponseHandlerTest.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (c) 2002-2017 "Neo Technology," - * Network Engine for Objects in Lund AB [http://neotechnology.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.net; - -import org.junit.Before; -import org.junit.Test; - -import java.util.Collections; -import java.util.Map; - -import org.neo4j.driver.internal.spi.Collector; -import org.neo4j.driver.internal.summary.InternalSummaryCounters; -import org.neo4j.driver.v1.Value; -import org.neo4j.driver.v1.summary.Plan; -import org.neo4j.driver.v1.summary.StatementType; -import org.neo4j.driver.v1.summary.SummaryCounters; - -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.neo4j.driver.internal.summary.InternalPlan.plan; -import static org.neo4j.driver.v1.Values.parameters; -import static org.neo4j.driver.v1.Values.value; -import static org.neo4j.driver.v1.Values.ofValue; -import static org.neo4j.driver.v1.Values.values; - -public class SocketResponseHandlerTest -{ - private final SocketResponseHandler handler = new SocketResponseHandler(); - private final Collector collector = mock( Collector.class ); - - @Before - public void setup() - { - handler.appendResultCollector( collector ); - } - - @Test - public void shouldCollectRecords() throws Throwable - { - // Given - Value[] record = values( 1, 2, 3 ); - - // When - handler.handleRecordMessage( record ); - - // Then - verify( collector ).record( record ); - } - - @Test - public void shouldCollectFieldNames() throws Throwable - { - // Given - String[] fieldNames = new String[] { "name", "age", "income" }; - Value fields = value( fieldNames ); - Map data = parameters( "fields", fields ).asMap( ofValue()); - - // When - handler.handleSuccessMessage( data ); - - // Then - verify( collector ).keys( fieldNames ); - } - - @Test - public void shouldCollectBasicMetadata() throws Throwable - { - // Given - Map data = parameters( - "type", "rw", - "stats", parameters( - "nodes-created", 1, - "properties-set", 12 - ) - ).asMap( ofValue()); - SummaryCounters stats = new InternalSummaryCounters( 1, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0); - - // When - handler.handleSuccessMessage( data ); - - // Then - verify( collector ).statementType( StatementType.READ_WRITE ); - verify( collector ).statementStatistics( stats ); - } - - @Test - public void shouldCollectPlan() throws Throwable - { - // Given - Map data = parameters( - "type", "rw", - "stats", parameters( - "nodes-created", 1, - "properties-set", 12 - ), - "plan", parameters( - "operatorType", "ProduceResults", - "identifiers", values( value( "num" ) ), - "args", parameters( "KeyNames", "num", "EstimatedRows", 1.0 ), - "children", values( - parameters( - "operatorType", "Projection", - "identifiers", values( value( "num" ) ), - "args", parameters( "A", "x", "B", 2 ), - "children", emptyList() - ) - ) - ) - ).asMap( ofValue()); - - SummaryCounters stats = new InternalSummaryCounters( 1, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0 ); - Plan plan = plan( - "ProduceResults", - parameters( "KeyNames", "num", "EstimatedRows", 1.0 ).asMap( ofValue()), singletonList( "num" ), - singletonList( - plan( "Projection", parameters( "A", "x", "B", 2 ).asMap( ofValue()), singletonList( "num" ), Collections - .emptyList() ) - ) - ); - - // When - handler.handleSuccessMessage( data ); - - // Then - verify( collector ).statementType( StatementType.READ_WRITE ); - verify( collector ).statementStatistics( stats ); - verify( collector ).plan( plan ); - } -} diff --git a/driver/src/test/java/org/neo4j/driver/internal/net/pooling/ConnectionInvalidationTest.java b/driver/src/test/java/org/neo4j/driver/internal/net/pooling/ConnectionInvalidationTest.java index d3ea6c4e72..438c3c4560 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/net/pooling/ConnectionInvalidationTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/net/pooling/ConnectionInvalidationTest.java @@ -24,11 +24,12 @@ import java.io.IOException; import java.util.HashMap; +import org.neo4j.driver.internal.handlers.NoOpResponseHandler; import org.neo4j.driver.internal.net.BoltServerAddress; -import org.neo4j.driver.internal.spi.Collector; import org.neo4j.driver.internal.spi.Connection; import org.neo4j.driver.internal.spi.ConnectionPool; import org.neo4j.driver.internal.spi.PooledConnection; +import org.neo4j.driver.internal.spi.ResponseHandler; import org.neo4j.driver.internal.util.Clock; import org.neo4j.driver.internal.util.Consumers; import org.neo4j.driver.v1.Value; @@ -66,7 +67,7 @@ public void shouldNotInvalidateConnectionThatIsUnableToRun() throws Throwable { // Given a connection that's broken Mockito.doThrow( new ClientException( "That didn't work" ) ) - .when( delegate ).run( anyString(), anyMap(), any( Collector.class ) ); + .when( delegate ).run( anyString(), anyMap(), any( ResponseHandler.class ) ); PooledConnection conn = new PooledSocketConnection( delegate, Consumers.noOp(), clock ); PooledConnectionValidator validator = new PooledConnectionValidator( pool( true ) ); @@ -137,12 +138,12 @@ public void shouldInvalidateOnProtocolViolationExceptions() throws Throwable private void assertUnrecoverable( Neo4jException exception ) { doThrow( exception ).when( delegate ) - .run( eq( "assert unrecoverable" ), anyMap(), any( Collector.class ) ); + .run( eq( "assert unrecoverable" ), anyMap(), any( ResponseHandler.class ) ); // When try { - conn.run( "assert unrecoverable", new HashMap(), Collector.NO_OP ); + conn.run( "assert unrecoverable", new HashMap(), NoOpResponseHandler.INSTANCE ); fail( "Should've rethrown exception" ); } catch ( Neo4jException e ) @@ -165,12 +166,12 @@ private void assertUnrecoverable( Neo4jException exception ) @SuppressWarnings( "unchecked" ) private void assertRecoverable( Neo4jException exception ) { - doThrow( exception ).when( delegate ).run( eq( "assert recoverable" ), anyMap(), any( Collector.class ) ); + doThrow( exception ).when( delegate ).run( eq( "assert recoverable" ), anyMap(), any( ResponseHandler.class ) ); // When try { - conn.run( "assert recoverable", new HashMap(), Collector.NO_OP ); + conn.run( "assert recoverable", new HashMap(), NoOpResponseHandler.INSTANCE ); fail( "Should've rethrown exception" ); } catch ( Neo4jException e ) diff --git a/driver/src/test/java/org/neo4j/driver/internal/net/pooling/PoolSettingsTest.java b/driver/src/test/java/org/neo4j/driver/internal/net/pooling/PoolSettingsTest.java index e69afd2db3..0bb5a6c729 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/net/pooling/PoolSettingsTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/net/pooling/PoolSettingsTest.java @@ -20,10 +20,8 @@ import org.junit.Test; -import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; public class PoolSettingsTest @@ -31,7 +29,7 @@ public class PoolSettingsTest @Test public void idleTimeBeforeConnectionTestWhenConfigured() { - PoolSettings settings = new PoolSettings( 10, 42, 10 ); + PoolSettings settings = new PoolSettings( 10, 42, 10, 5, -1 ); assertTrue( settings.idleTimeBeforeConnectionTestEnabled() ); assertEquals( 42, settings.idleTimeBeforeConnectionTest() ); } @@ -40,7 +38,7 @@ public void idleTimeBeforeConnectionTestWhenConfigured() public void idleTimeBeforeConnectionTestWhenSetToZero() { //Always test idle time during acquisition - PoolSettings settings = new PoolSettings( 10, 0, 10 ); + PoolSettings settings = new PoolSettings( 10, 0, 10, 5, -1 ); assertTrue( settings.idleTimeBeforeConnectionTestEnabled() ); assertEquals( 0, settings.idleTimeBeforeConnectionTest() ); } @@ -57,7 +55,7 @@ public void idleTimeBeforeConnectionTestWhenSetToNegativeValue() @Test public void maxConnectionLifetimeWhenConfigured() { - PoolSettings settings = new PoolSettings( 10, 10, 42 ); + PoolSettings settings = new PoolSettings( 10, 10, 42, 5, -1 ); assertTrue( settings.maxConnectionLifetimeEnabled() ); assertEquals( 42, settings.maxConnectionLifetime() ); } @@ -65,7 +63,7 @@ public void maxConnectionLifetimeWhenConfigured() @Test public void maxConnectionLifetimeWhenSetToZeroOrNegativeValue() { - testMaxConnectionLifetimeWithIllegalValue( 0 ); // testing for thrashing + testMaxConnectionLifetimeWithIllegalValue( 0 ); testMaxConnectionLifetimeWithIllegalValue( -1 ); testMaxConnectionLifetimeWithIllegalValue( -42 ); testMaxConnectionLifetimeWithIllegalValue( Integer.MIN_VALUE ); @@ -73,13 +71,13 @@ public void maxConnectionLifetimeWhenSetToZeroOrNegativeValue() private static void testIdleTimeBeforeConnectionTestWithIllegalValue( int value ) { - PoolSettings settings = new PoolSettings( 10, value, 10 ); + PoolSettings settings = new PoolSettings( 10, value, 10, 5, -1 ); assertFalse( settings.idleTimeBeforeConnectionTestEnabled() ); } private static void testMaxConnectionLifetimeWithIllegalValue( int value ) { - PoolSettings settings = new PoolSettings( 10, 10, value ); + PoolSettings settings = new PoolSettings( 10, 10, value, 5, -1 ); assertFalse( settings.maxConnectionLifetimeEnabled() ); } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/net/pooling/PooledConnectionValidatorTest.java b/driver/src/test/java/org/neo4j/driver/internal/net/pooling/PooledConnectionValidatorTest.java index d7f209cc1d..05a6e103e0 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/net/pooling/PooledConnectionValidatorTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/net/pooling/PooledConnectionValidatorTest.java @@ -30,10 +30,10 @@ import org.neo4j.driver.internal.net.BoltServerAddress; import org.neo4j.driver.internal.net.SocketClient; import org.neo4j.driver.internal.net.SocketConnection; -import org.neo4j.driver.internal.spi.Collector; import org.neo4j.driver.internal.spi.Connection; import org.neo4j.driver.internal.spi.ConnectionPool; import org.neo4j.driver.internal.spi.PooledConnection; +import org.neo4j.driver.internal.spi.ResponseHandler; import org.neo4j.driver.internal.summary.InternalServerInfo; import org.neo4j.driver.internal.util.Clock; import org.neo4j.driver.internal.util.Consumers; @@ -77,7 +77,7 @@ public void isNotReusableWhenHasUnrecoverableErrors() { Connection connection = mock( Connection.class ); DatabaseException runError = new DatabaseException( "", "" ); - doThrow( runError ).when( connection ).run( anyString(), any( Map.class ), any( Collector.class ) ); + doThrow( runError ).when( connection ).run( anyString(), any( Map.class ), any( ResponseHandler.class ) ); PooledConnection pooledConnection = newPooledConnection( connection ); diff --git a/driver/src/test/java/org/neo4j/driver/internal/net/pooling/SocketConnectionPoolTest.java b/driver/src/test/java/org/neo4j/driver/internal/net/pooling/SocketConnectionPoolTest.java index 35f9bd3fc2..9610c736af 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/net/pooling/SocketConnectionPoolTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/net/pooling/SocketConnectionPoolTest.java @@ -786,7 +786,8 @@ private static SocketConnectionPool newPool( Connector connector, Clock clock, l private static SocketConnectionPool newPool( Connector connector, Clock clock, long idleTimeBeforeConnectionTest, long maxConnectionLifetime ) { - PoolSettings poolSettings = new PoolSettings( 42, idleTimeBeforeConnectionTest, maxConnectionLifetime ); + PoolSettings poolSettings = new PoolSettings( 42, idleTimeBeforeConnectionTest, maxConnectionLifetime, + 42, -1 ); Logging logging = mock( Logging.class, RETURNS_MOCKS ); return new SocketConnectionPool( poolSettings, connector, clock, logging ); } diff --git a/driver/src/test/java/org/neo4j/driver/internal/packstream/BufferedChannelInput.java b/driver/src/test/java/org/neo4j/driver/internal/packstream/BufferedChannelInput.java index e01585236a..31dbb7c75e 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/packstream/BufferedChannelInput.java +++ b/driver/src/test/java/org/neo4j/driver/internal/packstream/BufferedChannelInput.java @@ -114,6 +114,18 @@ public byte peekByte() throws IOException return buffer.get( buffer.position() ); } + @Override + public Runnable messageBoundaryHook() + { + return new Runnable() + { + @Override + public void run() + { + } + }; + } + private boolean attempt( int numBytes ) throws IOException { if ( buffer.remaining() >= numBytes ) diff --git a/driver/src/test/java/org/neo4j/driver/internal/packstream/BufferedChannelOutput.java b/driver/src/test/java/org/neo4j/driver/internal/packstream/BufferedChannelOutput.java index 70bc8ff29e..393b7007b5 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/packstream/BufferedChannelOutput.java +++ b/driver/src/test/java/org/neo4j/driver/internal/packstream/BufferedChannelOutput.java @@ -49,8 +49,9 @@ public BufferedChannelOutput flush() throws IOException } @Override - public PackOutput writeBytes( byte[] data, int offset, int length ) throws IOException + public PackOutput writeBytes( byte[] data ) throws IOException { + int length = data.length; int index = 0; while ( index < length ) { @@ -61,7 +62,7 @@ public PackOutput writeBytes( byte[] data, int offset, int length ) throws IOExc int amountToWrite = Math.min( buffer.remaining(), length - index ); - buffer.put( data, offset + index, amountToWrite ); + buffer.put( data, index, amountToWrite ); index += amountToWrite; } return this; @@ -107,6 +108,18 @@ public PackOutput writeDouble( double value ) throws IOException return this; } + @Override + public Runnable messageBoundaryHook() + { + return new Runnable() + { + @Override + public void run() + { + } + }; + } + private void ensure( int size ) throws IOException { if ( buffer.remaining() < size ) diff --git a/driver/src/test/java/org/neo4j/driver/internal/util/MessageToByteBufWriter.java b/driver/src/test/java/org/neo4j/driver/internal/util/MessageToByteBufWriter.java new file mode 100644 index 0000000000..d120ddf8ad --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/util/MessageToByteBufWriter.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.util; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import java.io.IOException; + +import org.neo4j.driver.internal.messaging.Message; +import org.neo4j.driver.internal.messaging.PackStreamMessageFormatV1; +import org.neo4j.driver.internal.packstream.PackOutput; + +public final class MessageToByteBufWriter +{ + private MessageToByteBufWriter() + { + } + + public static ByteBuf asByteBuf( Message message ) + { + try + { + ByteBuf buf = Unpooled.buffer(); + ByteBufOutput output = new ByteBufOutput( buf ); + new PackStreamMessageFormatV1.Writer( output, output.messageBoundaryHook(), true ).write( message ); + return buf; + } + catch ( IOException e ) + { + throw new RuntimeException( e ); + } + } + + private static class ByteBufOutput implements PackOutput + { + final ByteBuf buf; + + ByteBufOutput( ByteBuf buf ) + { + this.buf = buf; + } + + @Override + public PackOutput flush() throws IOException + { + return this; + } + + @Override + public PackOutput writeByte( byte value ) throws IOException + { + buf.writeByte( value ); + return this; + } + + @Override + public PackOutput writeBytes( byte[] data ) throws IOException + { + buf.writeBytes( data ); + return this; + } + + @Override + public PackOutput writeShort( short value ) throws IOException + { + buf.writeShort( value ); + return this; + } + + @Override + public PackOutput writeInt( int value ) throws IOException + { + buf.writeInt( value ); + return this; + } + + @Override + public PackOutput writeLong( long value ) throws IOException + { + buf.writeLong( value ); + return this; + } + + @Override + public PackOutput writeDouble( double value ) throws IOException + { + buf.writeDouble( value ); + return this; + } + + @Override + public Runnable messageBoundaryHook() + { + return new Runnable() + { + @Override + public void run() + { + } + }; + } + } +} diff --git a/driver/src/test/java/org/neo4j/driver/v1/ConfigTest.java b/driver/src/test/java/org/neo4j/driver/v1/ConfigTest.java index 3c7420d4a7..5714b7785f 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/ConfigTest.java +++ b/driver/src/test/java/org/neo4j/driver/v1/ConfigTest.java @@ -111,7 +111,7 @@ public void shouldSupportMaxConnectionLifetimeSetting() throws Throwable { Config config = Config.build().withMaxConnectionLifetime( 42, TimeUnit.SECONDS ).toConfig(); - assertEquals( TimeUnit.SECONDS.toMillis( 42 ), config.maxConnectionLifetime() ); + assertEquals( TimeUnit.SECONDS.toMillis( 42 ), config.maxConnectionLifetimeMillis() ); } @Test @@ -119,7 +119,7 @@ public void shouldAllowZeroConnectionMaxConnectionLifetime() throws Throwable { Config config = Config.build().withMaxConnectionLifetime( 0, TimeUnit.SECONDS ).toConfig(); - assertEquals( 0, config.maxConnectionLifetime() ); + assertEquals( 0, config.maxConnectionLifetimeMillis() ); } @Test @@ -127,7 +127,7 @@ public void shouldAllowNegativeConnectionMaxConnectionLifetime() throws Throwabl { Config config = Config.build().withMaxConnectionLifetime( -42, TimeUnit.SECONDS ).toConfig(); - assertEquals( TimeUnit.SECONDS.toMillis( -42 ), config.maxConnectionLifetime() ); + assertEquals( TimeUnit.SECONDS.toMillis( -42 ), config.maxConnectionLifetimeMillis() ); } @Test diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/BookmarkIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/BookmarkIT.java index d9bae97fc2..c0673c0ece 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/BookmarkIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/BookmarkIT.java @@ -125,7 +125,6 @@ public void shouldThrowForUnreachableBookmark() try { - // todo: configure bookmark wait timeout to be lower than default 30sec when neo4j supports this session.beginTransaction( session.lastBookmark() + 42 ); fail( "Exception expected" ); } diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/CausalClusteringIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/CausalClusteringIT.java index 94323ea79d..8578b2caf3 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/CausalClusteringIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/CausalClusteringIT.java @@ -302,7 +302,6 @@ public void beginTransactionThrowsForUnreachableBookmark() try { - // todo: configure bookmark wait timeout to be lower than default 30sec when neo4j supports this session.beginTransaction( newBookmark ); fail( "Exception expected" ); } diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/ConnectionHandlingIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/ConnectionHandlingIT.java index bd60e3eacb..7e5a2d850f 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/ConnectionHandlingIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/ConnectionHandlingIT.java @@ -361,7 +361,8 @@ protected ConnectionPool createConnectionPool( AuthToken authToken, SecurityPlan { ConnectionSettings connectionSettings = new ConnectionSettings( authToken, 1000 ); PoolSettings poolSettings = new PoolSettings( config.maxIdleConnectionPoolSize(), - config.idleTimeBeforeConnectionTest(), config.maxConnectionLifetime() ); + config.idleTimeBeforeConnectionTest(), config.maxConnectionLifetimeMillis(), + config.maxConnectionPoolSize(), config.connectionAcquisitionTimeoutMillis() ); Connector connector = createConnector( connectionSettings, securityPlan, config.logging() ); connectionPool = new MemorizingConnectionPool( poolSettings, connector, createClock(), config.logging() ); return connectionPool; diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/ConnectionPoolIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/ConnectionPoolIT.java index 7b1f2c700b..5a5c18d5b7 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/ConnectionPoolIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/ConnectionPoolIT.java @@ -67,7 +67,7 @@ public void shouldRecoverFromDownedServer() throws Throwable sessionGrabber.start(); // When - neo4j.restart(); + neo4j.restartDb(); // Then we accept a hump with failing sessions, but demand that failures stop as soon as the server is back up. sessionGrabber.assertSessionsAvailableWithin( 60 ); @@ -198,7 +198,6 @@ public void run() } catch ( Throwable e ) { - e.printStackTrace(); lastExceptionFromDriver = e; throw new RuntimeException( e ); } diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/CredentialsIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/CredentialsIT.java index 49dbaa04b5..020a031187 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/CredentialsIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/CredentialsIT.java @@ -62,7 +62,7 @@ public class CredentialsIT @BeforeClass public static void enableAuth() throws Exception { - neo4j.restart( Neo4jSettings.TEST_SETTINGS + neo4j.restartDb( Neo4jSettings.TEST_SETTINGS .updateWith( Neo4jSettings.AUTH_ENABLED, "true" ) .updateWith( Neo4jSettings.DATA_DIR, tempDir.getRoot().getAbsolutePath().replace( "\\", "/" ) ) ); diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/ServerKilledIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/ServerKilledIT.java index 97ce0c9bf3..28bafc6d54 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/ServerKilledIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/ServerKilledIT.java @@ -82,7 +82,7 @@ public void shouldRecoverFromServerRestart() throws Throwable acquireAndReleaseConnections( 4, driver ); // When - neo4j.forceRestart(); + neo4j.forceRestartDb(); // Then we should be able to start using sessions again, at most O(numSessions) session calls later int toleratedFailures = 4; @@ -123,7 +123,7 @@ public void shouldDropBrokenOldSessions() throws Throwable acquireAndReleaseConnections( 5, driver ); // restart database to invalidate all idle connections in the pool - neo4j.forceRestart(); + neo4j.forceRestartDb(); // move clock forward more than configured liveness check timeout clock.progress( TimeUnit.MINUTES.toMillis( livenessCheckTimeoutMinutes + 1 ) ); diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java new file mode 100644 index 0000000000..4edfff25ac --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java @@ -0,0 +1,460 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.v1.integration; + +import io.netty.util.concurrent.GlobalEventExecutor; +import io.netty.util.concurrent.Promise; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.neo4j.driver.v1.Record; +import org.neo4j.driver.v1.Response; +import org.neo4j.driver.v1.ResponseListener; +import org.neo4j.driver.v1.Session; +import org.neo4j.driver.v1.Statement; +import org.neo4j.driver.v1.StatementResultCursor; +import org.neo4j.driver.v1.Value; +import org.neo4j.driver.v1.exceptions.ClientException; +import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; +import org.neo4j.driver.v1.summary.ResultSummary; +import org.neo4j.driver.v1.summary.StatementType; +import org.neo4j.driver.v1.types.Node; +import org.neo4j.driver.v1.util.TestNeo4j; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.neo4j.driver.internal.util.Iterables.single; +import static org.neo4j.driver.v1.Values.parameters; +import static org.neo4j.driver.v1.util.TestUtil.await; +import static org.neo4j.driver.v1.util.TestUtil.awaitAll; + +public class SessionAsyncIT +{ + @Rule + public final TestNeo4j neo4j = new TestNeo4j(); + + private Session session; + + @Before + public void setUp() throws Exception + { + session = neo4j.driver().session(); + } + + @After + public void tearDown() throws Exception + { + await( session.closeAsync() ); + } + + @Test + public void shouldRunQueryWithEmptyResult() + { + StatementResultCursor cursor = await( session.runAsync( "CREATE (:Person)" ) ); + + assertThat( await( cursor.fetchAsync() ), is( false ) ); + } + + @Test + public void shouldRunQueryWithSingleResult() + { + StatementResultCursor cursor = await( session.runAsync( "CREATE (p:Person {name: 'Nick Fury'}) RETURN p" ) ); + + assertThat( await( cursor.fetchAsync() ), is( true ) ); + + Record record = cursor.current(); + Node node = record.get( 0 ).asNode(); + assertEquals( "Person", single( node.labels() ) ); + assertEquals( "Nick Fury", node.get( "name" ).asString() ); + + assertThat( await( cursor.fetchAsync() ), is( false ) ); + } + + @Test + public void shouldRunQueryWithMultipleResults() + { + StatementResultCursor cursor = await( session.runAsync( "UNWIND [1,2,3] AS x RETURN x" ) ); + + assertThat( await( cursor.fetchAsync() ), is( true ) ); + assertEquals( 1, cursor.current().get( 0 ).asInt() ); + + assertThat( await( cursor.fetchAsync() ), is( true ) ); + assertEquals( 2, cursor.current().get( 0 ).asInt() ); + + assertThat( await( cursor.fetchAsync() ), is( true ) ); + assertEquals( 3, cursor.current().get( 0 ).asInt() ); + + assertThat( await( cursor.fetchAsync() ), is( false ) ); + } + + @Test + public void shouldFailForIncorrectQuery() + { + try + { + await( session.runAsync( "RETURN" ) ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertSyntaxError( e ); + } + } + + @Test + public void shouldFailWhenQueryFailsAtRuntime() + { + StatementResultCursor cursor = await( session.runAsync( "UNWIND [1, 2, 0] AS x RETURN 10 / x" ) ); + + assertThat( await( cursor.fetchAsync() ), is( true ) ); + assertEquals( 10, cursor.current().get( 0 ).asInt() ); + + assertThat( await( cursor.fetchAsync() ), is( true ) ); + assertEquals( 5, cursor.current().get( 0 ).asInt() ); + + try + { + await( cursor.fetchAsync() ); + System.out.println( cursor.current() ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertArithmeticError( e ); + } + } + + @Test + public void shouldFailWhenServerIsRestarted() throws Exception + { + StatementResultCursor cursor = await( session.runAsync( + "UNWIND range(0, 1000000) AS x " + + "CREATE (n1:Node {value: x})-[r:LINKED {value: x}]->(n2:Node {value: x}) " + + "DETACH DELETE n1, n2 " + + "RETURN x" ) ); + + try + { + Response recordAvailable = cursor.fetchAsync(); + + // kill db after receiving the first record + // do it from a listener so that event loop thread executes the kill operation + recordAvailable.addListener( new KillDbListener( neo4j ) ); + + while ( await( recordAvailable ) ) + { + assertNotNull( cursor.current() ); + recordAvailable = cursor.fetchAsync(); + } + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( ServiceUnavailableException.class ) ); + } + } + + @Test + public void shouldAllowNestedQueries() + { + StatementResultCursor cursor = + await( session.runAsync( "UNWIND [1, 2, 3] AS x CREATE (p:Person {id: x}) RETURN p" ) ); + + Future>> queriesExecuted = runNestedQueries( cursor ); + List> futures = await( queriesExecuted ); + + List futureResults = awaitAll( futures ); + assertEquals( 7, futureResults.size() ); + + StatementResultCursor personCursor = await( session.runAsync( "MATCH (p:Person) RETURN p ORDER BY p.id" ) ); + + List personNodes = new ArrayList<>(); + while ( await( personCursor.fetchAsync() ) ) + { + personNodes.add( personCursor.current().get( 0 ).asNode() ); + } + assertEquals( 3, personNodes.size() ); + + Node node1 = personNodes.get( 0 ); + assertEquals( 1, node1.get( "id" ).asInt() ); + assertEquals( 10, node1.get( "age" ).asInt() ); + + Node node2 = personNodes.get( 1 ); + assertEquals( 2, node2.get( "id" ).asInt() ); + assertEquals( 20, node2.get( "age" ).asInt() ); + + Node node3 = personNodes.get( 2 ); + assertEquals( 3, node3.get( "id" ).asInt() ); + assertEquals( 30, personNodes.get( 2 ).get( "age" ).asInt() ); + } + + @Test + public void shouldAllowMultipleAsyncRunsWithoutConsumingResults() throws InterruptedException + { + int queryCount = 13; + List> cursors = new ArrayList<>(); + for ( int i = 0; i < queryCount; i++ ) + { + cursors.add( session.runAsync( "CREATE (:Person)" ) ); + } + + List> fetches = new ArrayList<>(); + for ( StatementResultCursor cursor : awaitAll( cursors ) ) + { + fetches.add( cursor.fetchAsync() ); + } + + awaitAll( fetches ); + + await( session.closeAsync() ); + session = neo4j.driver().session(); + + StatementResultCursor cursor = await( session.runAsync( "MATCH (p:Person) RETURN count(p)" ) ); + assertThat( await( cursor.fetchAsync() ), is( true ) ); + + Record record = cursor.current(); + assertEquals( queryCount, record.get( 0 ).asInt() ); + } + + @Test + public void shouldExposeStatementKeysForColumnsWithAliases() + { + StatementResultCursor cursor = await( session.runAsync( "RETURN 1 AS one, 2 AS two, 3 AS three, 4 AS five" ) ); + + assertEquals( Arrays.asList( "one", "two", "three", "five" ), cursor.keys() ); + } + + @Test + public void shouldExposeStatementKeysForColumnsWithoutAliases() + { + StatementResultCursor cursor = await( session.runAsync( "RETURN 1, 2, 3, 5" ) ); + + assertEquals( Arrays.asList( "1", "2", "3", "5" ), cursor.keys() ); + } + + @Test + public void shouldExposeResultSummaryForSimpleQuery() + { + String query = "CREATE (:Node {id: $id, name: $name})"; + Value params = parameters( "id", 1, "name", "TheNode" ); + + StatementResultCursor cursor = await( session.runAsync( query, params ) ); + ResultSummary summary = await( cursor.summaryAsync() ); + + assertEquals( new Statement( query, params ), summary.statement() ); + assertEquals( 1, summary.counters().nodesCreated() ); + assertEquals( 1, summary.counters().labelsAdded() ); + assertEquals( 2, summary.counters().propertiesSet() ); + assertEquals( 0, summary.counters().relationshipsCreated() ); + assertEquals( StatementType.WRITE_ONLY, summary.statementType() ); + assertFalse( summary.hasPlan() ); + assertFalse( summary.hasProfile() ); + assertNull( summary.plan() ); + assertNull( summary.profile() ); + assertEquals( 0, summary.notifications().size() ); + assertThat( summary.resultAvailableAfter( TimeUnit.MILLISECONDS ), greaterThanOrEqualTo( 0L ) ); + assertThat( summary.resultConsumedAfter( TimeUnit.MILLISECONDS ), greaterThanOrEqualTo( 0L ) ); + } + + @Test + public void shouldExposeResultSummaryForExplainQuery() + { + String query = "EXPLAIN CREATE (),() WITH * MATCH (n)-->(m) CREATE (n)-[:HI {id: 'id'}]->(m) RETURN n, m"; + + StatementResultCursor cursor = await( session.runAsync( query ) ); + ResultSummary summary = await( cursor.summaryAsync() ); + + assertEquals( new Statement( query ), summary.statement() ); + assertEquals( 0, summary.counters().nodesCreated() ); + assertEquals( 0, summary.counters().propertiesSet() ); + assertEquals( 0, summary.counters().relationshipsCreated() ); + assertEquals( StatementType.READ_WRITE, summary.statementType() ); + assertTrue( summary.hasPlan() ); + assertFalse( summary.hasProfile() ); + assertNotNull( summary.plan() ); + // asserting on plan is a bit fragile and can break when server side changes or with different + // server versions; that is why do fuzzy assertions in this test based on string content + String planAsString = summary.plan().toString(); + assertThat( planAsString, containsString( "CreateNode" ) ); + assertThat( planAsString, containsString( "Expand" ) ); + assertThat( planAsString, containsString( "AllNodesScan" ) ); + assertNull( summary.profile() ); + assertEquals( 0, summary.notifications().size() ); + assertThat( summary.resultAvailableAfter( TimeUnit.MILLISECONDS ), greaterThanOrEqualTo( 0L ) ); + assertThat( summary.resultConsumedAfter( TimeUnit.MILLISECONDS ), greaterThanOrEqualTo( 0L ) ); + } + + @Test + public void shouldExposeResultSummaryForProfileQuery() + { + String query = "PROFILE CREATE (:Node)-[:KNOWS]->(:Node) WITH * MATCH (n) RETURN n"; + + StatementResultCursor cursor = await( session.runAsync( query ) ); + ResultSummary summary = await( cursor.summaryAsync() ); + + assertEquals( new Statement( query ), summary.statement() ); + assertEquals( 2, summary.counters().nodesCreated() ); + assertEquals( 0, summary.counters().propertiesSet() ); + assertEquals( 1, summary.counters().relationshipsCreated() ); + assertEquals( StatementType.READ_WRITE, summary.statementType() ); + assertTrue( summary.hasPlan() ); + assertTrue( summary.hasProfile() ); + assertNotNull( summary.plan() ); + assertNotNull( summary.profile() ); + // asserting on profile is a bit fragile and can break when server side changes or with different + // server versions; that is why do fuzzy assertions in this test based on string content + String profileAsString = summary.profile().toString(); + assertThat( profileAsString, containsString( "DbHits" ) ); + assertThat( profileAsString, containsString( "PageCacheHits" ) ); + assertThat( profileAsString, containsString( "CreateNode" ) ); + assertThat( profileAsString, containsString( "CreateRelationship" ) ); + assertEquals( 0, summary.notifications().size() ); + assertThat( summary.resultAvailableAfter( TimeUnit.MILLISECONDS ), greaterThanOrEqualTo( 0L ) ); + assertThat( summary.resultConsumedAfter( TimeUnit.MILLISECONDS ), greaterThanOrEqualTo( 0L ) ); + } + + private Future>> runNestedQueries( StatementResultCursor inputCursor ) + { + Promise>> resultPromise = GlobalEventExecutor.INSTANCE.newPromise(); + runNestedQueries( inputCursor, new ArrayList>(), resultPromise ); + return resultPromise; + } + + private void runNestedQueries( final StatementResultCursor inputCursor, final List> futures, + final Promise>> resultPromise ) + { + final Response inputAvailable = inputCursor.fetchAsync(); + futures.add( inputAvailable ); + + inputAvailable.addListener( new ResponseListener() + { + @Override + public void operationCompleted( Boolean inputAvailable, Throwable error ) + { + if ( error != null ) + { + resultPromise.setFailure( error ); + } + else if ( inputAvailable ) + { + runNestedQuery( inputCursor, futures, resultPromise ); + } + else + { + resultPromise.setSuccess( futures ); + } + } + } ); + } + + private void runNestedQuery( final StatementResultCursor inputCursor, final List> futures, + final Promise>> resultPromise ) + { + Record record = inputCursor.current(); + Node node = record.get( 0 ).asNode(); + long id = node.get( "id" ).asLong(); + long age = id * 10; + + Response response = + session.runAsync( "MATCH (p:Person {id: $id}) SET p.age = $age RETURN p", + parameters( "id", id, "age", age ) ); + + response.addListener( new ResponseListener() + { + @Override + public void operationCompleted( StatementResultCursor result, Throwable error ) + { + if ( error != null ) + { + resultPromise.setFailure( error ); + } + else + { + futures.add( result.fetchAsync() ); + runNestedQueries( inputCursor, futures, resultPromise ); + } + } + } ); + } + + private static void assertSyntaxError( Exception e ) + { + assertThat( e, instanceOf( ClientException.class ) ); + assertThat( ((ClientException) e).code(), containsString( "SyntaxError" ) ); + assertThat( e.getMessage(), startsWith( "Unexpected end of input" ) ); + } + + private static void assertArithmeticError( Exception e ) + { + assertThat( e, instanceOf( ClientException.class ) ); + assertThat( ((ClientException) e).code(), containsString( "ArithmeticError" ) ); + } + + private static class KillDbListener implements ResponseListener + { + final TestNeo4j neo4j; + volatile boolean shouldKillDb = true; + + KillDbListener( TestNeo4j neo4j ) + { + this.neo4j = neo4j; + } + + @Override + public void operationCompleted( Boolean result, Throwable error ) + { + if ( shouldKillDb ) + { + killDb(); + shouldKillDb = false; + } + } + + void killDb() + { + try + { + neo4j.killDb(); + } + catch ( IOException e ) + { + throw new RuntimeException( e ); + } + } + } +} diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/SocketClientIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/SocketClientIT.java index a21d64fcef..fe0d15dda4 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/SocketClientIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/SocketClientIT.java @@ -81,7 +81,7 @@ public void shouldCloseConnectionWhenReceivingProtocolViolationError() throws Ex SocketResponseHandler handler = mock( SocketResponseHandler.class ); when( handler.protocolViolationErrorOccurred() ).thenReturn( true ); - when( handler.collectorsWaiting() ).thenReturn( 2, 1, 0 ); + when( handler.handlersWaiting() ).thenReturn( 2, 1, 0 ); when( handler.serverFailure() ).thenReturn( new ClientException( "Neo.ClientError.Request.InvalidFormat", "Hello, world!" ) ); diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/TLSSocketChannelIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/TLSSocketChannelIT.java index f296f84017..195990ec07 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/TLSSocketChannelIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/TLSSocketChannelIT.java @@ -129,7 +129,7 @@ public void shouldPerformTLSHandshakeWithTrustedCert() throws Throwable finally { // always restore the db default settings - neo4j.restart(); + neo4j.restartDb(); } } @@ -172,7 +172,7 @@ public void shouldNotPerformTLSHandshakeWithNonSystemCert() throws Throwable finally { // always restore the db default settings - neo4j.restart(); + neo4j.restartDb(); } } @@ -231,7 +231,7 @@ private void createFakeServerCertPairInKnownCerts( BoltServerAddress address, Fi public void shouldFailTLSHandshakeDueToServerCertNotSignedByKnownCA() throws Throwable { // Given - neo4j.restart( Neo4jSettings.TEST_SETTINGS.updateWith( + neo4j.restartDb( Neo4jSettings.TEST_SETTINGS.updateWith( Neo4jSettings.CERT_DIR, folder.getRoot().getAbsolutePath().replace( "\\", "/" ) ) ); SocketChannel channel = SocketChannel.open(); diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java new file mode 100644 index 0000000000..f21ebc8da9 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java @@ -0,0 +1,525 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.v1.integration; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.neo4j.driver.v1.Response; +import org.neo4j.driver.v1.Session; +import org.neo4j.driver.v1.Statement; +import org.neo4j.driver.v1.StatementResult; +import org.neo4j.driver.v1.StatementResultCursor; +import org.neo4j.driver.v1.Transaction; +import org.neo4j.driver.v1.Value; +import org.neo4j.driver.v1.exceptions.ClientException; +import org.neo4j.driver.v1.summary.ResultSummary; +import org.neo4j.driver.v1.summary.StatementType; +import org.neo4j.driver.v1.types.Node; +import org.neo4j.driver.v1.util.TestNeo4j; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.neo4j.driver.internal.util.Iterables.single; +import static org.neo4j.driver.v1.Values.parameters; +import static org.neo4j.driver.v1.util.TestUtil.await; + +public class TransactionAsyncIT +{ + @Rule + public final TestNeo4j neo4j = new TestNeo4j(); + + private Session session; + + @Before + public void setUp() throws Exception + { + session = neo4j.driver().session(); + } + + @After + public void tearDown() throws Exception + { + await( session.closeAsync() ); + } + + @Test + public void shouldBePossibleToCommitEmptyTx() + { + String bookmarkBefore = session.lastBookmark(); + + Transaction tx = await( session.beginTransactionAsync() ); + assertThat( await( tx.commitAsync() ), is( nullValue() ) ); + + String bookmarkAfter = session.lastBookmark(); + + assertNotNull( bookmarkAfter ); + assertNotEquals( bookmarkBefore, bookmarkAfter ); + } + + @Test + public void shouldBePossibleToRollbackEmptyTx() + { + String bookmarkBefore = session.lastBookmark(); + + Transaction tx = await( session.beginTransactionAsync() ); + assertThat( await( tx.rollbackAsync() ), is( nullValue() ) ); + + String bookmarkAfter = session.lastBookmark(); + assertEquals( bookmarkBefore, bookmarkAfter ); + } + + @Test + public void shouldBePossibleToRunSingleStatementAndCommit() + { + Transaction tx = await( session.beginTransactionAsync() ); + + StatementResultCursor cursor = await( tx.runAsync( "CREATE (n:Node {id: 42}) RETURN n" ) ); + assertThat( await( cursor.fetchAsync() ), is( true ) ); + Node node = cursor.current().get( 0 ).asNode(); + assertEquals( "Node", single( node.labels() ) ); + assertEquals( 42, node.get( "id" ).asInt() ); + + assertNull( await( tx.commitAsync() ) ); + assertEquals( 1, countNodes( 42 ) ); + } + + @Test + public void shouldBePossibleToRunSingleStatementAndRollback() + { + Transaction tx = await( session.beginTransactionAsync() ); + + StatementResultCursor cursor = await( tx.runAsync( "CREATE (n:Node {id: 4242}) RETURN n" ) ); + assertThat( await( cursor.fetchAsync() ), is( true ) ); + Node node = cursor.current().get( 0 ).asNode(); + assertEquals( "Node", single( node.labels() ) ); + assertEquals( 4242, node.get( "id" ).asInt() ); + + assertNull( await( tx.rollbackAsync() ) ); + assertEquals( 0, countNodes( 4242 ) ); + } + + @Test + public void shouldBePossibleToRunMultipleStatementsAndCommit() + { + Transaction tx = await( session.beginTransactionAsync() ); + + StatementResultCursor cursor1 = await( tx.runAsync( "CREATE (n:Node {id: 1})" ) ); + assertThat( await( cursor1.fetchAsync() ), is( false ) ); + + StatementResultCursor cursor2 = await( tx.runAsync( "CREATE (n:Node {id: 2})" ) ); + assertThat( await( cursor2.fetchAsync() ), is( false ) ); + + StatementResultCursor cursor3 = await( tx.runAsync( "CREATE (n:Node {id: 2})" ) ); + assertThat( await( cursor3.fetchAsync() ), is( false ) ); + + assertNull( await( tx.commitAsync() ) ); + assertEquals( 1, countNodes( 1 ) ); + assertEquals( 2, countNodes( 2 ) ); + } + + @Test + public void shouldBePossibleToRunMultipleStatementsAndCommitWithoutWaiting() + { + Transaction tx = await( session.beginTransactionAsync() ); + + tx.runAsync( "CREATE (n:Node {id: 1})" ); + tx.runAsync( "CREATE (n:Node {id: 2})" ); + tx.runAsync( "CREATE (n:Node {id: 1})" ); + + assertNull( await( tx.commitAsync() ) ); + assertEquals( 1, countNodes( 2 ) ); + assertEquals( 2, countNodes( 1 ) ); + } + + @Test + public void shouldBePossibleToRunMultipleStatementsAndRollback() + { + Transaction tx = await( session.beginTransactionAsync() ); + + StatementResultCursor cursor1 = await( tx.runAsync( "CREATE (n:Node {id: 1})" ) ); + assertThat( await( cursor1.fetchAsync() ), is( false ) ); + + StatementResultCursor cursor2 = await( tx.runAsync( "CREATE (n:Node {id: 42})" ) ); + assertThat( await( cursor2.fetchAsync() ), is( false ) ); + + assertNull( await( tx.rollbackAsync() ) ); + assertEquals( 0, countNodes( 1 ) ); + assertEquals( 0, countNodes( 42 ) ); + } + + @Test + public void shouldBePossibleToRunMultipleStatementsAndRollbackWithoutWaiting() + { + Transaction tx = await( session.beginTransactionAsync() ); + + tx.runAsync( "CREATE (n:Node {id: 1})" ); + tx.runAsync( "CREATE (n:Node {id: 42})" ); + + assertNull( await( tx.rollbackAsync() ) ); + assertEquals( 0, countNodes( 1 ) ); + assertEquals( 0, countNodes( 42 ) ); + } + + @Test + public void shouldFailToCommitAfterSingleWrongStatement() + { + Transaction tx = await( session.beginTransactionAsync() ); + + try + { + await( tx.runAsync( "RETURN" ) ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertSyntaxError( e ); + } + + try + { + await( tx.commitAsync() ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( ClientException.class ) ); + } + } + + @Test + public void shouldAllowRollbackAfterSingleWrongStatement() + { + Transaction tx = await( session.beginTransactionAsync() ); + + try + { + await( tx.runAsync( "RETURN" ) ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertSyntaxError( e ); + } + + assertThat( await( tx.rollbackAsync() ), is( nullValue() ) ); + } + + @Test + public void shouldFailToCommitAfterCoupleCorrectAndSingleWrongStatement() + { + Transaction tx = await( session.beginTransactionAsync() ); + + StatementResultCursor cursor1 = await( tx.runAsync( "CREATE (n:Node) RETURN n" ) ); + assertThat( await( cursor1.fetchAsync() ), is( true ) ); + assertTrue( cursor1.current().get( 0 ).asNode().hasLabel( "Node" ) ); + + StatementResultCursor cursor2 = await( tx.runAsync( "RETURN 42" ) ); + assertThat( await( cursor2.fetchAsync() ), is( true ) ); + assertEquals( 42, cursor2.current().get( 0 ).asInt() ); + + try + { + await( tx.runAsync( "RETURN" ) ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertSyntaxError( e ); + } + + try + { + await( tx.commitAsync() ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( ClientException.class ) ); + } + } + + @Test + public void shouldAllowRollbackAfterCoupleCorrectAndSingleWrongStatement() + { + Transaction tx = await( session.beginTransactionAsync() ); + + StatementResultCursor cursor1 = await( tx.runAsync( "RETURN 4242" ) ); + assertThat( await( cursor1.fetchAsync() ), is( true ) ); + assertEquals( 4242, cursor1.current().get( 0 ).asInt() ); + + StatementResultCursor cursor2 = await( tx.runAsync( "CREATE (n:Node) DELETE n RETURN 42" ) ); + assertThat( await( cursor2.fetchAsync() ), is( true ) ); + assertEquals( 42, cursor2.current().get( 0 ).asInt() ); + + try + { + await( tx.runAsync( "RETURN" ) ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertSyntaxError( e ); + } + + assertThat( await( tx.rollbackAsync() ), is( nullValue() ) ); + } + + @Test + public void shouldNotAllowNewStatementsAfterAnIncorrectStatement() + { + Transaction tx = await( session.beginTransactionAsync() ); + + try + { + await( tx.runAsync( "RETURN" ) ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertSyntaxError( e ); + } + + try + { + tx.runAsync( "CREATE ()" ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( ClientException.class ) ); + assertThat( e.getMessage(), startsWith( "Cannot run more statements in this transaction" ) ); + } + } + + @Test + public void shouldFailBoBeginTxWithInvalidBookmark() + { + Session session = neo4j.driver().session( "InvalidBookmark" ); + + try + { + await( session.beginTransactionAsync() ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( ClientException.class ) ); + assertThat( e.getMessage(), containsString( "InvalidBookmark" ) ); + } + } + + @Test + public void shouldBePossibleToCommitWhenCommitted() + { + Transaction tx = await( session.beginTransactionAsync() ); + tx.runAsync( "CREATE ()" ); + assertNull( await( tx.commitAsync() ) ); + + Response secondCommit = tx.commitAsync(); + // second commit should return a completed future + assertTrue( secondCommit.isDone() ); + assertNull( await( secondCommit ) ); + } + + @Test + public void shouldBePossibleToRollbackWhenRolledBack() + { + Transaction tx = await( session.beginTransactionAsync() ); + tx.runAsync( "CREATE ()" ); + assertNull( await( tx.rollbackAsync() ) ); + + Response secondRollback = tx.rollbackAsync(); + // second rollback should return a completed future + assertTrue( secondRollback.isDone() ); + assertNull( await( secondRollback ) ); + } + + @Test + public void shouldFailToCommitWhenRolledBack() + { + Transaction tx = await( session.beginTransactionAsync() ); + tx.runAsync( "CREATE ()" ); + assertNull( await( tx.rollbackAsync() ) ); + + try + { + // should not be possible to commit after rollback + await( tx.commitAsync() ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( ClientException.class ) ); + assertThat( e.getMessage(), containsString( "transaction has already been rolled back" ) ); + } + } + + @Test + public void shouldFailToRollbackWhenCommitted() + { + Transaction tx = await( session.beginTransactionAsync() ); + tx.runAsync( "CREATE ()" ); + assertNull( await( tx.commitAsync() ) ); + + try + { + // should not be possible to rollback after commit + await( tx.rollbackAsync() ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertThat( e, instanceOf( ClientException.class ) ); + assertThat( e.getMessage(), containsString( "transaction has already been committed" ) ); + } + } + + @Test + public void shouldExposeStatementKeysForColumnsWithAliases() + { + Transaction tx = await( session.beginTransactionAsync() ); + StatementResultCursor cursor = await( tx.runAsync( "RETURN 1 AS one, 2 AS two, 3 AS three, 4 AS five" ) ); + + assertEquals( Arrays.asList( "one", "two", "three", "five" ), cursor.keys() ); + } + + @Test + public void shouldExposeStatementKeysForColumnsWithoutAliases() + { + Transaction tx = await( session.beginTransactionAsync() ); + StatementResultCursor cursor = await( tx.runAsync( "RETURN 1, 2, 3, 5" ) ); + + assertEquals( Arrays.asList( "1", "2", "3", "5" ), cursor.keys() ); + } + + @Test + public void shouldExposeResultSummaryForSimpleQuery() + { + String query = "CREATE (p1:Person {name: $name1})-[:KNOWS]->(p2:Person {name: $name2}) RETURN p1, p2"; + Value params = parameters( "name1", "Bob", "name2", "John" ); + + Transaction tx = await( session.beginTransactionAsync() ); + StatementResultCursor cursor = await( tx.runAsync( query, params ) ); + ResultSummary summary = await( cursor.summaryAsync() ); + + assertEquals( new Statement( query, params ), summary.statement() ); + assertEquals( 2, summary.counters().nodesCreated() ); + assertEquals( 2, summary.counters().labelsAdded() ); + assertEquals( 2, summary.counters().propertiesSet() ); + assertEquals( 1, summary.counters().relationshipsCreated() ); + assertEquals( StatementType.READ_WRITE, summary.statementType() ); + assertFalse( summary.hasPlan() ); + assertFalse( summary.hasProfile() ); + assertNull( summary.plan() ); + assertNull( summary.profile() ); + assertEquals( 0, summary.notifications().size() ); + assertThat( summary.resultAvailableAfter( TimeUnit.MILLISECONDS ), greaterThanOrEqualTo( 0L ) ); + assertThat( summary.resultConsumedAfter( TimeUnit.MILLISECONDS ), greaterThanOrEqualTo( 0L ) ); + } + + @Test + public void shouldExposeResultSummaryForExplainQuery() + { + String query = "EXPLAIN MATCH (n) RETURN n"; + + Transaction tx = await( session.beginTransactionAsync() ); + StatementResultCursor cursor = await( tx.runAsync( query ) ); + ResultSummary summary = await( cursor.summaryAsync() ); + + assertEquals( new Statement( query ), summary.statement() ); + assertEquals( 0, summary.counters().nodesCreated() ); + assertEquals( 0, summary.counters().propertiesSet() ); + assertEquals( StatementType.READ_ONLY, summary.statementType() ); + assertTrue( summary.hasPlan() ); + assertFalse( summary.hasProfile() ); + assertNotNull( summary.plan() ); + // asserting on plan is a bit fragile and can break when server side changes or with different + // server versions; that is why do fuzzy assertions in this test based on string content + assertThat( summary.plan().toString(), containsString( "AllNodesScan" ) ); + assertNull( summary.profile() ); + assertEquals( 0, summary.notifications().size() ); + assertThat( summary.resultAvailableAfter( TimeUnit.MILLISECONDS ), greaterThanOrEqualTo( 0L ) ); + assertThat( summary.resultConsumedAfter( TimeUnit.MILLISECONDS ), greaterThanOrEqualTo( 0L ) ); + } + + @Test + public void shouldExposeResultSummaryForProfileQuery() + { + String query = "PROFILE MERGE (n {name: $name}) " + + "ON CREATE SET n.created = timestamp() " + + "ON MATCH SET n.counter = coalesce(n.counter, 0) + 1"; + + Value params = parameters( "name", "Bob" ); + + Transaction tx = await( session.beginTransactionAsync() ); + StatementResultCursor cursor = await( tx.runAsync( query, params ) ); + ResultSummary summary = await( cursor.summaryAsync() ); + + assertEquals( new Statement( query, params ), summary.statement() ); + assertEquals( 1, summary.counters().nodesCreated() ); + assertEquals( 2, summary.counters().propertiesSet() ); + assertEquals( 0, summary.counters().relationshipsCreated() ); + assertEquals( StatementType.WRITE_ONLY, summary.statementType() ); + assertTrue( summary.hasPlan() ); + assertTrue( summary.hasProfile() ); + assertNotNull( summary.plan() ); + assertNotNull( summary.profile() ); + // asserting on profile is a bit fragile and can break when server side changes or with different + // server versions; that is why do fuzzy assertions in this test based on string content + String profileAsString = summary.profile().toString(); + System.out.println( profileAsString ); + assertThat( profileAsString, containsString( "DbHits" ) ); + assertThat( profileAsString, containsString( "PageCacheHits" ) ); + assertThat( profileAsString, containsString( "AllNodesScan" ) ); + assertEquals( 0, summary.notifications().size() ); + assertThat( summary.resultAvailableAfter( TimeUnit.MILLISECONDS ), greaterThanOrEqualTo( 0L ) ); + assertThat( summary.resultConsumedAfter( TimeUnit.MILLISECONDS ), greaterThanOrEqualTo( 0L ) ); + } + + private int countNodes( Object id ) + { + StatementResult result = session.run( "MATCH (n:Node {id: $id}) RETURN count(n)", parameters( "id", id ) ); + return result.single().get( 0 ).asInt(); + } + + private static void assertSyntaxError( Exception e ) + { + assertThat( e, instanceOf( ClientException.class ) ); + assertThat( ((ClientException) e).code(), containsString( "SyntaxError" ) ); + assertThat( e.getMessage(), startsWith( "Unexpected end of input" ) ); + } +} diff --git a/driver/src/test/java/org/neo4j/driver/v1/tck/DriverAuthSteps.java b/driver/src/test/java/org/neo4j/driver/v1/tck/DriverAuthSteps.java index cb4a07890c..d5ae7ffa47 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/tck/DriverAuthSteps.java +++ b/driver/src/test/java/org/neo4j/driver/v1/tck/DriverAuthSteps.java @@ -65,7 +65,7 @@ public void reset() { driver.close(); } - neo4j.restart(); + neo4j.restartDb(); } catch ( Exception e ) { @@ -140,7 +140,7 @@ public void aProtocolErrorIsRaised() throws Throwable private Driver configureCredentials( String name, String oldPassword, String newPassword ) throws Exception { - neo4j.restart( Neo4jSettings.TEST_SETTINGS + neo4j.restartDb( Neo4jSettings.TEST_SETTINGS .updateWith( Neo4jSettings.AUTH_ENABLED, "true" ) .updateWith( Neo4jSettings.DATA_DIR, tempDir.getAbsolutePath().replace("\\", "/") )); diff --git a/driver/src/test/java/org/neo4j/driver/v1/tck/DriverSecurityComplianceSteps.java b/driver/src/test/java/org/neo4j/driver/v1/tck/DriverSecurityComplianceSteps.java index 89cc6b5fbc..b4ec805789 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/tck/DriverSecurityComplianceSteps.java +++ b/driver/src/test/java/org/neo4j/driver/v1/tck/DriverSecurityComplianceSteps.java @@ -304,7 +304,7 @@ public void clearAfterEachScenario() throws Throwable @After( "@modifies_db_config" ) public void resetDbWithDefaultSettings() throws Throwable { - neo4j.restart(); + neo4j.restartDb(); } private File tempFile( String prefix, String suffix ) throws Throwable diff --git a/driver/src/test/java/org/neo4j/driver/v1/util/Neo4jRunner.java b/driver/src/test/java/org/neo4j/driver/v1/util/Neo4jRunner.java index 6ddfd3403e..5d6e50c905 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/util/Neo4jRunner.java +++ b/driver/src/test/java/org/neo4j/driver/v1/util/Neo4jRunner.java @@ -67,6 +67,7 @@ public class Neo4jRunner public static final String HOME_DIR = new File( NEO4J_DIR, "neo4jHome" ).getAbsolutePath(); private Driver driver; + private boolean restartDriver; /** Global runner controlling a single server, used to avoid having to restart the server between tests */ public static synchronized Neo4jRunner getOrCreateGlobalRunner() throws IOException @@ -110,6 +111,16 @@ public void ensureRunning( Neo4jSettings neo4jSettings ) throws IOException, Int public Driver driver() { + if ( restartDriver ) + { + restartDriver = false; + if ( driver != null ) + { + driver.close(); + driver = null; + } + } + if ( driver == null ) { driver = GraphDatabase.driver( DEFAULT_URI, DEFAULT_AUTH_TOKEN ); @@ -159,17 +170,26 @@ public synchronized void stopNeo4j() throws IOException { return; } - if( driver != null ) - { - driver.close(); - driver = null; - } + restartDriver = true; debug( "Stopping server..." ); executeCommand( "neoctrl-stop", HOME_DIR ); debug( "Server stopped." ); } + public void killNeo4j() throws IOException + { + if ( serverStatus() == ServerStatus.OFFLINE ) + { + return; + } + restartDriver = true; + + debug( "Killing server..." ); + executeCommand( "neoctrl-stop", "-k", HOME_DIR ); + debug( "Server killed." ); + } + public void forceToRestart() throws IOException { stopNeo4j(); diff --git a/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4j.java b/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4j.java index 9637a68aab..81594bee30 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4j.java +++ b/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4j.java @@ -82,17 +82,17 @@ public Driver driver() return runner.driver(); } - public void restart() throws Exception + public void restartDb() throws Exception { runner.restartNeo4j(); } - public void forceRestart() throws Exception + public void forceRestartDb() throws Exception { runner.forceToRestart(); } - public void restart( Neo4jSettings neo4jSettings ) throws Exception + public void restartDb( Neo4jSettings neo4jSettings ) throws Exception { runner.restartNeo4j( neo4jSettings ); } @@ -160,13 +160,18 @@ public void ensureProcedures( String jarName ) throws IOException } } - public void start() throws IOException + public void startDb() throws IOException { runner.startNeo4j(); } - public void stop() throws IOException + public void stopDb() throws IOException { runner.stopNeo4j(); } + + public void killDb() throws IOException + { + runner.killNeo4j(); + } } diff --git a/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4jSession.java b/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4jSession.java index 42947e0463..429c5f62e8 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4jSession.java +++ b/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4jSession.java @@ -24,8 +24,10 @@ import java.util.Map; import org.neo4j.driver.v1.Record; +import org.neo4j.driver.v1.Response; import org.neo4j.driver.v1.Session; import org.neo4j.driver.v1.StatementResult; +import org.neo4j.driver.v1.StatementResultCursor; import org.neo4j.driver.v1.Transaction; import org.neo4j.driver.v1.TransactionWork; import org.neo4j.driver.v1.Value; @@ -95,6 +97,18 @@ public void close() throw new UnsupportedOperationException( "Disallowed on this test session" ); } + @Override + public Response closeAsync() + { + throw new UnsupportedOperationException( "Disallowed on this test session" ); + } + + @Override + public Response runAsync( String statement, Map params ) + { + return realSession.runAsync( statement, params ); + } + @Override public Transaction beginTransaction() { @@ -108,6 +122,12 @@ public Transaction beginTransaction( String bookmark ) return realSession.beginTransaction( bookmark ); } + @Override + public Response beginTransactionAsync() + { + return realSession.beginTransactionAsync(); + } + @Override public T readTransaction( TransactionWork work ) { @@ -145,24 +165,48 @@ public StatementResult run( String statementText, Value parameters ) return realSession.run( statementText, parameters ); } + @Override + public Response runAsync( String statementText, Value parameters ) + { + return realSession.runAsync( statementText, parameters ); + } + @Override public StatementResult run( String statementText, Record parameters ) { return realSession.run( statementText, parameters ); } + @Override + public Response runAsync( String statementTemplate, Record statementParameters ) + { + return realSession.runAsync( statementTemplate, statementParameters ); + } + @Override public StatementResult run( String statementTemplate ) { return realSession.run( statementTemplate ); } + @Override + public Response runAsync( String statementTemplate ) + { + return realSession.runAsync( statementTemplate ); + } + @Override public StatementResult run( org.neo4j.driver.v1.Statement statement ) { return realSession.run( statement.text(), statement.parameters() ); } + @Override + public Response runAsync( org.neo4j.driver.v1.Statement statement ) + { + return realSession.runAsync( statement ); + } + @Override public TypeSystem typeSystem() { diff --git a/driver/src/test/java/org/neo4j/driver/v1/util/TestUtil.java b/driver/src/test/java/org/neo4j/driver/v1/util/TestUtil.java new file mode 100644 index 0000000000..91c4701673 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/v1/util/TestUtil.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.v1.util; + +import io.netty.buffer.ByteBuf; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeoutException; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public final class TestUtil +{ + private TestUtil() + { + } + + public static > T get( F future ) + { + if ( !future.isDone() ) + { + throw new IllegalArgumentException( "Given future is not yet completed" ); + } + return await( future ); + } + + public static > List awaitAll( List futures ) + { + List result = new ArrayList<>(); + for ( F future : futures ) + { + result.add( await( future ) ); + } + return result; + } + + public static > T await( F future ) + { + try + { + return future.get( 1, MINUTES ); + } + catch ( InterruptedException e ) + { + Thread.currentThread().interrupt(); + throw new AssertionError( "Interrupted while waiting for future: " + future, e ); + } + catch ( ExecutionException e ) + { + Throwable cause = e.getCause(); + StackTraceElement[] originalStackTrace = cause.getStackTrace(); + RuntimeException exceptionWithOriginalStackTrace = new RuntimeException(); + cause.setStackTrace( exceptionWithOriginalStackTrace.getStackTrace() ); + exceptionWithOriginalStackTrace.setStackTrace( originalStackTrace ); + cause.addSuppressed( exceptionWithOriginalStackTrace ); + throwException( cause ); + return null; + } + catch ( TimeoutException e ) + { + throw new AssertionError( "Given future did not complete in time: " + future ); + } + } + + public static void assertByteBufContains( ByteBuf buf, Number... values ) + { + try + { + assertNotNull( buf ); + int expectedReadableBytes = 0; + for ( Number value : values ) + { + expectedReadableBytes += bytesCount( value ); + } + assertEquals( "Unexpected number of bytes", expectedReadableBytes, buf.readableBytes() ); + for ( Number expectedValue : values ) + { + Number actualValue = read( buf, expectedValue.getClass() ); + String valueType = actualValue.getClass().getSimpleName(); + assertEquals( valueType + " values not equal", expectedValue, actualValue ); + } + } + finally + { + buf.release(); + } + } + + public static void assertByteBufEquals( ByteBuf expected, ByteBuf actual ) + { + try + { + assertEquals( expected, actual ); + } + finally + { + expected.release(); + actual.release(); + } + } + + private static void throwException( Throwable t ) + { + TestUtil.doThrowException( t ); + } + + @SuppressWarnings( "unchecked" ) + private static void doThrowException( Throwable t ) throws E + { + throw (E) t; + } + + private static Number read( ByteBuf buf, Class type ) + { + if ( type == Byte.class ) + { + return buf.readByte(); + } + else if ( type == Short.class ) + { + return buf.readShort(); + } + else if ( type == Integer.class ) + { + return buf.readInt(); + } + else if ( type == Long.class ) + { + return buf.readLong(); + } + else if ( type == Float.class ) + { + return buf.readFloat(); + } + else if ( type == Double.class ) + { + return buf.readDouble(); + } + else + { + throw new IllegalArgumentException( "Unexpected numeric type: " + type ); + } + } + + private static int bytesCount( Number value ) + { + if ( value instanceof Byte ) + { + return 1; + } + else if ( value instanceof Short ) + { + return 2; + } + else if ( value instanceof Integer ) + { + return 4; + } + else if ( value instanceof Long ) + { + return 8; + } + else if ( value instanceof Float ) + { + return 4; + } + else if ( value instanceof Double ) + { + return 8; + } + else + { + throw new IllegalArgumentException( + "Unexpected number: '" + value + "' or type" + value.getClass() ); + } + } +} diff --git a/driver/src/test/resources/multiple_bookmarks.script b/driver/src/test/resources/multiple_bookmarks.script index 5e7344103c..2648731dca 100644 --- a/driver/src/test/resources/multiple_bookmarks.script +++ b/driver/src/test/resources/multiple_bookmarks.script @@ -9,8 +9,8 @@ S: SUCCESS {} C: RUN "CREATE (n {name:'Bob'})" {} PULL_ALL S: SUCCESS {} - SUCCESS {"bookmark": "neo4j:bookmark:v1:tx95"} + SUCCESS {} C: RUN "COMMIT" {} PULL_ALL S: SUCCESS {} - SUCCESS {} + SUCCESS {"bookmark": "neo4j:bookmark:v1:tx95"} diff --git a/driver/src/test/resources/read_tx_with_bookmarks.script b/driver/src/test/resources/read_tx_with_bookmarks.script index 47e9244506..325a8ab651 100644 --- a/driver/src/test/resources/read_tx_with_bookmarks.script +++ b/driver/src/test/resources/read_tx_with_bookmarks.script @@ -11,8 +11,8 @@ C: RUN "MATCH (n) RETURN n.name AS name" {} S: SUCCESS {"fields": ["name"]} RECORD ["Bob"] RECORD ["Alice"] - SUCCESS {"bookmark": "NewBookmark"} + SUCCESS {} C: RUN "COMMIT" {} PULL_ALL S: SUCCESS {} - SUCCESS {} + SUCCESS {"bookmark": "NewBookmark"} diff --git a/driver/src/test/resources/write_read_tx_with_bookmarks.script b/driver/src/test/resources/write_read_tx_with_bookmarks.script index f01feff289..4bc0c613ea 100644 --- a/driver/src/test/resources/write_read_tx_with_bookmarks.script +++ b/driver/src/test/resources/write_read_tx_with_bookmarks.script @@ -9,11 +9,11 @@ S: SUCCESS {} C: RUN "CREATE (n {name:'Bob'})" {} PULL_ALL S: SUCCESS {} - SUCCESS {"bookmark": "BookmarkB"} + SUCCESS {} C: RUN "COMMIT" {} PULL_ALL S: SUCCESS {} - SUCCESS {} + SUCCESS {"bookmark": "BookmarkB"} C: RUN "BEGIN" {"bookmark": "BookmarkB", "bookmarks": ["BookmarkB"]} PULL_ALL S: SUCCESS {} @@ -22,8 +22,8 @@ C: RUN "MATCH (n) RETURN n.name AS name" {} PULL_ALL S: SUCCESS {"fields": ["name"]} RECORD ["Bob"] - SUCCESS {"bookmark": "BookmarkC"} + SUCCESS {} C: RUN "COMMIT" {} PULL_ALL S: SUCCESS {} - SUCCESS {} + SUCCESS {"bookmark": "BookmarkC"} diff --git a/driver/src/test/resources/write_tx_with_bookmarks.script b/driver/src/test/resources/write_tx_with_bookmarks.script index 4413266b16..2744b31313 100644 --- a/driver/src/test/resources/write_tx_with_bookmarks.script +++ b/driver/src/test/resources/write_tx_with_bookmarks.script @@ -9,8 +9,8 @@ S: SUCCESS {} C: RUN "CREATE (n {name:'Bob'})" {} PULL_ALL S: SUCCESS {} - SUCCESS {"bookmark": "NewBookmark"} + SUCCESS {} C: RUN "COMMIT" {} PULL_ALL S: SUCCESS {} - SUCCESS {} + SUCCESS {"bookmark": "NewBookmark"} 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 838d6e2fa6..79e0b452f6 100644 --- a/examples/src/test/java/org/neo4j/docs/driver/ExamplesIT.java +++ b/examples/src/test/java/org/neo4j/docs/driver/ExamplesIT.java @@ -282,14 +282,14 @@ public void testShouldRunServiceUnavailableExample() throws IOException try { // When - neo4j.stop(); + neo4j.stopDb(); // Then assertThat( example.addItem(), equalTo( false ) ); } finally { - neo4j.start(); + neo4j.startDb(); } }