org.neo4j.driver.internal.async.NetworkSession Maven / Gradle / Ivy
/*
* Copyright (c) 2002-2019 "Neo4j,"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.neo4j.driver.internal.async;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.atomic.AtomicBoolean;
import org.neo4j.driver.AccessMode;
import org.neo4j.driver.Logger;
import org.neo4j.driver.Logging;
import org.neo4j.driver.Statement;
import org.neo4j.driver.TransactionConfig;
import org.neo4j.driver.async.StatementResultCursor;
import org.neo4j.driver.exceptions.ClientException;
import org.neo4j.driver.Bookmark;
import org.neo4j.driver.internal.BookmarkHolder;
import org.neo4j.driver.internal.FailableCursor;
import org.neo4j.driver.internal.InternalBookmark;
import org.neo4j.driver.internal.cursor.InternalStatementResultCursor;
import org.neo4j.driver.internal.cursor.RxStatementResultCursor;
import org.neo4j.driver.internal.cursor.StatementResultCursorFactory;
import org.neo4j.driver.internal.logging.PrefixedLogger;
import org.neo4j.driver.internal.retry.RetryLogic;
import org.neo4j.driver.internal.spi.Connection;
import org.neo4j.driver.internal.spi.ConnectionProvider;
import org.neo4j.driver.internal.util.Futures;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static org.neo4j.driver.internal.util.Futures.completedWithNull;
public class NetworkSession
{
private static final String LOG_NAME = "Session";
private final ConnectionProvider connectionProvider;
private final NetworkSessionConnectionContext connectionContext;
private final AccessMode mode;
private final RetryLogic retryLogic;
protected final Logger logger;
private final BookmarkHolder bookmarkHolder;
private volatile CompletionStage transactionStage = completedWithNull();
private volatile CompletionStage connectionStage = completedWithNull();
private volatile CompletionStage extends FailableCursor> resultCursorStage = completedWithNull();
private final AtomicBoolean open = new AtomicBoolean( true );
public NetworkSession( ConnectionProvider connectionProvider, RetryLogic retryLogic, String databaseName, AccessMode mode,
BookmarkHolder bookmarkHolder, Logging logging )
{
this.connectionProvider = connectionProvider;
this.mode = mode;
this.retryLogic = retryLogic;
this.logger = new PrefixedLogger( "[" + hashCode() + "]", logging.getLog( LOG_NAME ) );
this.bookmarkHolder = bookmarkHolder;
this.connectionContext = new NetworkSessionConnectionContext( databaseName, bookmarkHolder.getBookmark() );
}
public CompletionStage runAsync( Statement statement, TransactionConfig config, boolean waitForRunResponse )
{
CompletionStage newResultCursorStage =
buildResultCursorFactory( statement, config, waitForRunResponse ).thenCompose( StatementResultCursorFactory::asyncResult );
resultCursorStage = newResultCursorStage.exceptionally( error -> null );
return newResultCursorStage.thenApply( cursor -> cursor ); // convert the return type
}
public CompletionStage runRx( Statement statement, TransactionConfig config )
{
CompletionStage newResultCursorStage =
buildResultCursorFactory( statement, config, true ).thenCompose( StatementResultCursorFactory::rxResult );
resultCursorStage = newResultCursorStage.exceptionally( error -> null );
return newResultCursorStage;
}
public CompletionStage beginTransactionAsync( TransactionConfig config )
{
return this.beginTransactionAsync( mode, config );
}
public CompletionStage beginTransactionAsync( AccessMode mode, TransactionConfig config )
{
ensureSessionIsOpen();
// create a chain that acquires connection and starts a transaction
CompletionStage newTransactionStage = ensureNoOpenTxBeforeStartingTx()
.thenCompose( ignore -> acquireConnection( mode ) )
.thenCompose( connection ->
{
ExplicitTransaction tx = new ExplicitTransaction( connection, bookmarkHolder );
return tx.beginAsync( bookmarkHolder.getBookmark(), config );
} );
// update the reference to the only known transaction
CompletionStage currentTransactionStage = transactionStage;
transactionStage = newTransactionStage
.exceptionally( error -> null ) // ignore errors from starting new transaction
.thenCompose( tx ->
{
if ( tx == null )
{
// failed to begin new transaction, keep reference to the existing one
return currentTransactionStage;
}
// new transaction started, keep reference to it
return completedFuture( tx );
} );
return newTransactionStage;
}
public CompletionStage resetAsync()
{
return existingTransactionOrNull()
.thenAccept( tx ->
{
if ( tx != null )
{
tx.markTerminated();
}
} )
.thenCompose( ignore -> connectionStage )
.thenCompose( connection ->
{
if ( connection != null )
{
// there exists an active connection, send a RESET message over it
return connection.reset();
}
return completedWithNull();
} );
}
public RetryLogic retryLogic()
{
return retryLogic;
}
public Bookmark lastBookmark()
{
return bookmarkHolder.getBookmark();
}
public CompletionStage releaseConnectionAsync()
{
return connectionStage.thenCompose( connection ->
{
if ( connection != null )
{
// there exists connection, try to release it back to the pool
return connection.release();
}
// no connection so return null
return completedWithNull();
} );
}
public CompletionStage connectionAsync()
{
return connectionStage;
}
public boolean isOpen()
{
return open.get();
}
public CompletionStage closeAsync()
{
if ( open.compareAndSet( true, false ) )
{
return resultCursorStage.thenCompose( cursor ->
{
if ( cursor != null )
{
// there exists a cursor with potentially unconsumed error, try to extract and propagate it
return cursor.failureAsync();
}
// no result cursor exists so no error exists
return completedWithNull();
} ).thenCompose( cursorError -> closeTransactionAndReleaseConnection().thenApply( txCloseError ->
{
// now we have cursor error, active transaction has been closed and connection has been released
// back to the pool; try to propagate cursor and transaction close errors, if any
CompletionException combinedError = Futures.combineErrors( cursorError, txCloseError );
if ( combinedError != null )
{
throw combinedError;
}
return null;
} ) );
}
return completedWithNull();
}
protected CompletionStage currentConnectionIsOpen()
{
return connectionStage.handle( ( connection, error ) ->
error == null && // no acquisition error
connection != null && // some connection has actually been acquired
connection.isOpen() ); // and it's still open
}
private CompletionStage buildResultCursorFactory( Statement statement, TransactionConfig config, boolean waitForRunResponse )
{
ensureSessionIsOpen();
return ensureNoOpenTxBeforeRunningQuery()
.thenCompose( ignore -> acquireConnection( mode ) )
.thenCompose( connection -> {
try
{
StatementResultCursorFactory factory = connection.protocol()
.runInAutoCommitTransaction( connection, statement, bookmarkHolder, config, waitForRunResponse );
return completedFuture( factory );
}
catch ( Throwable e )
{
return Futures.failedFuture( e );
}
} );
}
private CompletionStage acquireConnection( AccessMode mode )
{
CompletionStage currentConnectionStage = connectionStage;
CompletionStage newConnectionStage = resultCursorStage.thenCompose( cursor ->
{
if ( cursor == null )
{
return completedWithNull();
}
// make sure previous result is fully consumed and connection is released back to the pool
return cursor.failureAsync();
} ).thenCompose( error ->
{
if ( error == null )
{
// there is no unconsumed error, so one of the following is true:
// 1) this is first time connection is acquired in this session
// 2) previous result has been successful and is fully consumed
// 3) previous result failed and error has been consumed
// return existing connection, which should've been released back to the pool by now
return currentConnectionStage.exceptionally( ignore -> null );
}
else
{
// there exists unconsumed error, re-throw it
throw new CompletionException( error );
}
} ).thenCompose( existingConnection ->
{
if ( existingConnection != null && existingConnection.isOpen() )
{
// there somehow is an existing open connection, this should not happen, just a precondition
throw new IllegalStateException( "Existing open connection detected" );
}
return connectionProvider.acquireConnection( connectionContext.contextWithMode( mode ) );
} );
connectionStage = newConnectionStage.exceptionally( error -> null );
return newConnectionStage;
}
private CompletionStage closeTransactionAndReleaseConnection()
{
return existingTransactionOrNull().thenCompose( tx ->
{
if ( tx != null )
{
// there exists an open transaction, let's close it and propagate the error, if any
return tx.closeAsync()
.thenApply( ignore -> (Throwable) null )
.exceptionally( error -> error );
}
// no open transaction so nothing to close
return completedWithNull();
} ).thenCompose( txCloseError ->
// then release the connection and propagate transaction close error, if any
releaseConnectionAsync().thenApply( ignore -> txCloseError ) );
}
private CompletionStage ensureNoOpenTxBeforeRunningQuery()
{
return ensureNoOpenTx( "Statements cannot be run directly on a session with an open transaction; " +
"either run from within the transaction or use a different session." );
}
private CompletionStage ensureNoOpenTxBeforeStartingTx()
{
return ensureNoOpenTx( "You cannot begin a transaction on a session with an open transaction; " +
"either run from within the transaction or use a different session." );
}
private CompletionStage ensureNoOpenTx( String errorMessage )
{
return existingTransactionOrNull().thenAccept( tx ->
{
if ( tx != null )
{
throw new ClientException( errorMessage );
}
} );
}
private CompletionStage existingTransactionOrNull()
{
return transactionStage
.exceptionally( error -> null ) // handle previous connection acquisition and tx begin failures
.thenApply( tx -> tx != null && tx.isOpen() ? tx : null );
}
private void ensureSessionIsOpen()
{
if ( !open.get() )
{
throw new ClientException(
"No more interaction with this session are allowed as the current session is already closed. " );
}
}
/**
* A {@link Connection} shall fulfil this {@link ImmutableConnectionContext} when acquired from a connection provider.
*/
private class NetworkSessionConnectionContext implements ConnectionContext
{
private final String databaseName;
private AccessMode mode;
// This bookmark is only used for rediscovery.
// It has to be the initial bookmark given at the creation of the session.
// As only that bookmark could carry extra system bookmarks
private final InternalBookmark rediscoveryBookmark;
private NetworkSessionConnectionContext( String databaseName, InternalBookmark bookmark )
{
this.databaseName = databaseName;
this.rediscoveryBookmark = bookmark;
}
private ConnectionContext contextWithMode( AccessMode mode )
{
this.mode = mode;
return this;
}
@Override
public String databaseName()
{
return databaseName;
}
@Override
public AccessMode mode()
{
return mode;
}
@Override
public InternalBookmark rediscoveryBookmark()
{
return rediscoveryBookmark;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy