All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.neo4j.driver.internal.async.UnmanagedTransaction Maven / Gradle / Ivy

There is a newer version: 5.27.0
Show newest version
/*
 * Copyright (c) "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.Arrays;
import java.util.EnumSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiFunction;
import java.util.function.Function;

import org.neo4j.driver.Bookmark;
import org.neo4j.driver.Query;
import org.neo4j.driver.Session;
import org.neo4j.driver.TransactionConfig;
import org.neo4j.driver.async.ResultCursor;
import org.neo4j.driver.exceptions.AuthorizationExpiredException;
import org.neo4j.driver.exceptions.ClientException;
import org.neo4j.driver.exceptions.ConnectionReadTimeoutException;
import org.neo4j.driver.internal.BookmarkHolder;
import org.neo4j.driver.internal.cursor.AsyncResultCursor;
import org.neo4j.driver.internal.cursor.RxResultCursor;
import org.neo4j.driver.internal.messaging.BoltProtocol;
import org.neo4j.driver.internal.spi.Connection;

import static org.neo4j.driver.internal.util.Futures.asCompletionException;
import static org.neo4j.driver.internal.util.Futures.combineErrors;
import static org.neo4j.driver.internal.util.Futures.completedWithNull;
import static org.neo4j.driver.internal.util.Futures.failedFuture;
import static org.neo4j.driver.internal.util.Futures.futureCompletingConsumer;
import static org.neo4j.driver.internal.util.LockUtil.executeWithLock;

public class UnmanagedTransaction
{
    private enum State
    {
        /**
         * The transaction is running with no explicit success or failure marked
         */
        ACTIVE,

        /**
         * This transaction has been terminated either because of explicit {@link Session#reset()} or because of a fatal connection error.
         */
        TERMINATED,

        /**
         * This transaction has successfully committed
         */
        COMMITTED,

        /**
         * This transaction has been rolled back
         */
        ROLLED_BACK
    }

    protected static final String CANT_COMMIT_COMMITTED_MSG = "Can't commit, transaction has been committed";
    protected static final String CANT_ROLLBACK_COMMITTED_MSG = "Can't rollback, transaction has been committed";
    protected static final String CANT_COMMIT_ROLLED_BACK_MSG = "Can't commit, transaction has been rolled back";
    protected static final String CANT_ROLLBACK_ROLLED_BACK_MSG = "Can't rollback, transaction has been rolled back";
    protected static final String CANT_COMMIT_ROLLING_BACK_MSG = "Can't commit, transaction has been requested to be rolled back";
    protected static final String CANT_ROLLBACK_COMMITTING_MSG = "Can't rollback, transaction has been requested to be committed";
    private static final EnumSet OPEN_STATES = EnumSet.of( State.ACTIVE, State.TERMINATED );

    private final Connection connection;
    private final BoltProtocol protocol;
    private final BookmarkHolder bookmarkHolder;
    private final ResultCursorsHolder resultCursors;
    private final long fetchSize;
    private final Lock lock = new ReentrantLock();
    private State state = State.ACTIVE;
    private CompletableFuture commitFuture;
    private CompletableFuture rollbackFuture;
    private Throwable causeOfTermination;

    public UnmanagedTransaction( Connection connection, BookmarkHolder bookmarkHolder, long fetchSize )
    {
        this( connection, bookmarkHolder, fetchSize, new ResultCursorsHolder() );
    }

    protected UnmanagedTransaction( Connection connection, BookmarkHolder bookmarkHolder, long fetchSize, ResultCursorsHolder resultCursors )
    {
        this.connection = connection;
        this.protocol = connection.protocol();
        this.bookmarkHolder = bookmarkHolder;
        this.resultCursors = resultCursors;
        this.fetchSize = fetchSize;
    }

    public CompletionStage beginAsync( Bookmark initialBookmark, TransactionConfig config )
    {
        return protocol.beginTransaction( connection, initialBookmark, config )
                       .handle( ( ignore, beginError ) ->
                                {
                                    if ( beginError != null )
                                    {
                                        if ( beginError instanceof AuthorizationExpiredException )
                                        {
                                            connection.terminateAndRelease( AuthorizationExpiredException.DESCRIPTION );
                                        }
                                        else if ( beginError instanceof ConnectionReadTimeoutException )
                                        {
                                            connection.terminateAndRelease( beginError.getMessage() );
                                        }
                                        else
                                        {
                                            connection.release();
                                        }
                                        throw asCompletionException( beginError );
                                    }
                                    return this;
                                } );
    }

    public CompletionStage closeAsync()
    {
        return closeAsync( false );
    }

    public CompletionStage closeAsync( boolean commit )
    {
        return closeAsync( commit, true );
    }

    public CompletionStage commitAsync()
    {
        return closeAsync( true, false );
    }

    public CompletionStage rollbackAsync()
    {
        return closeAsync( false, false );
    }

    public CompletionStage runAsync( Query query )
    {
        ensureCanRunQueries();
        CompletionStage cursorStage =
                protocol.runInUnmanagedTransaction( connection, query, this, fetchSize ).asyncResult();
        resultCursors.add( cursorStage );
        return cursorStage.thenCompose( AsyncResultCursor::mapSuccessfulRunCompletionAsync ).thenApply( cursor -> cursor );
    }

    public CompletionStage runRx( Query query )
    {
        ensureCanRunQueries();
        CompletionStage cursorStage =
                protocol.runInUnmanagedTransaction( connection, query, this, fetchSize ).rxResult();
        resultCursors.add( cursorStage );
        return cursorStage;
    }

    public boolean isOpen()
    {
        return OPEN_STATES.contains( executeWithLock( lock, () -> state ) );
    }

    public void markTerminated( Throwable cause )
    {
        executeWithLock( lock, () ->
        {
            if ( state == State.TERMINATED )
            {
                if ( causeOfTermination != null )
                {
                    addSuppressedWhenNotCaptured( causeOfTermination, cause );
                }
            }
            else
            {
                state = State.TERMINATED;
                causeOfTermination = cause;
            }
        } );
    }

    private void addSuppressedWhenNotCaptured( Throwable currentCause, Throwable newCause )
    {
        if ( currentCause != newCause )
        {
            boolean noneMatch = Arrays.stream( currentCause.getSuppressed() ).noneMatch( suppressed -> suppressed == newCause );
            if ( noneMatch )
            {
                currentCause.addSuppressed( newCause );
            }
        }
    }

    public Connection connection()
    {
        return connection;
    }

    private void ensureCanRunQueries()
    {
        executeWithLock( lock, () ->
        {
            if ( state == State.COMMITTED )
            {
                throw new ClientException( "Cannot run more queries in this transaction, it has been committed" );
            }
            else if ( state == State.ROLLED_BACK )
            {
                throw new ClientException( "Cannot run more queries in this transaction, it has been rolled back" );
            }
            else if ( state == State.TERMINATED )
            {
                throw new ClientException( "Cannot run more queries in this transaction, " +
                                           "it has either experienced an fatal error or was explicitly terminated", causeOfTermination );
            }
        } );
    }

    private CompletionStage doCommitAsync( Throwable cursorFailure )
    {
        ClientException exception = executeWithLock(
                lock, () -> state == State.TERMINATED
                            ? new ClientException( "Transaction can't be committed. " +
                                                   "It has been rolled back either because of an error or explicit termination",
                                                   cursorFailure != causeOfTermination ? causeOfTermination : null )
                            : null
        );
        return exception != null ? failedFuture( exception ) : protocol.commitTransaction( connection ).thenAccept( bookmarkHolder::setBookmark );
    }

    private CompletionStage doRollbackAsync()
    {
        return executeWithLock( lock, () -> state ) == State.TERMINATED ? completedWithNull() : protocol.rollbackTransaction( connection );
    }

    private static BiFunction handleCommitOrRollback( Throwable cursorFailure )
    {
        return ( ignore, commitOrRollbackError ) ->
        {
            CompletionException combinedError = combineErrors( cursorFailure, commitOrRollbackError );
            if ( combinedError != null )
            {
                throw combinedError;
            }
            return null;
        };
    }

    private void handleTransactionCompletion( boolean commitAttempt, Throwable throwable )
    {
        executeWithLock( lock, () ->
        {
            if ( commitAttempt && throwable == null )
            {
                state = State.COMMITTED;
            }
            else
            {
                state = State.ROLLED_BACK;
            }
        } );
        if ( throwable instanceof AuthorizationExpiredException )
        {
            connection.terminateAndRelease( AuthorizationExpiredException.DESCRIPTION );
        }
        else if ( throwable instanceof ConnectionReadTimeoutException )
        {
            connection.terminateAndRelease( throwable.getMessage() );
        }
        else
        {
            connection.release(); // release in background
        }
    }

    private CompletionStage closeAsync( boolean commit, boolean completeWithNullIfNotOpen )
    {
        CompletionStage stage = executeWithLock( lock, () ->
        {
            CompletionStage resultStage = null;
            if ( completeWithNullIfNotOpen && !isOpen() )
            {
                resultStage = completedWithNull();
            }
            else if ( state == State.COMMITTED )
            {
                resultStage = failedFuture( new ClientException( commit ? CANT_COMMIT_COMMITTED_MSG : CANT_ROLLBACK_COMMITTED_MSG ) );
            }
            else if ( state == State.ROLLED_BACK )
            {
                resultStage = failedFuture( new ClientException( commit ? CANT_COMMIT_ROLLED_BACK_MSG : CANT_ROLLBACK_ROLLED_BACK_MSG ) );
            }
            else
            {
                if ( commit )
                {
                    if ( rollbackFuture != null )
                    {
                        resultStage = failedFuture( new ClientException( CANT_COMMIT_ROLLING_BACK_MSG ) );
                    }
                    else if ( commitFuture != null )
                    {
                        resultStage = commitFuture;
                    }
                    else
                    {
                        commitFuture = new CompletableFuture<>();
                    }
                }
                else
                {
                    if ( commitFuture != null )
                    {
                        resultStage = failedFuture( new ClientException( CANT_ROLLBACK_COMMITTING_MSG ) );
                    }
                    else if ( rollbackFuture != null )
                    {
                        resultStage = rollbackFuture;
                    }
                    else
                    {
                        rollbackFuture = new CompletableFuture<>();
                    }
                }
            }
            return resultStage;
        } );

        if ( stage == null )
        {
            CompletableFuture targetFuture;
            Function> targetAction;
            if ( commit )
            {
                targetFuture = commitFuture;
                targetAction = throwable -> doCommitAsync( throwable ).handle( handleCommitOrRollback( throwable ) );
            }
            else
            {
                targetFuture = rollbackFuture;
                targetAction = throwable -> doRollbackAsync().handle( handleCommitOrRollback( throwable ) );
            }
            resultCursors.retrieveNotConsumedError()
                         .thenCompose( targetAction )
                         .whenComplete( ( ignored, throwable ) -> handleTransactionCompletion( commit, throwable ) )
                         .whenComplete( futureCompletingConsumer( targetFuture ) );
            stage = targetFuture;
        }

        return stage;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy