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

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

/*
 * Copyright (c) 2002-2020 "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.EnumSet;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.function.BiFunction;

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.ClientException;
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 org.neo4j.driver.internal.util.Futures;

import static org.neo4j.driver.internal.util.Futures.completedWithNull;
import static org.neo4j.driver.internal.util.Futures.failedFuture;

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
    }

    /**
     * This is a holder so that we can have ony the state volatile in the tx without having to synchronize the whole block.
     */
    private static final class StateHolder
    {
        private static final EnumSet OPEN_STATES = EnumSet.of( State.ACTIVE, State.TERMINATED );
        private static final StateHolder ACTIVE_HOLDER = new StateHolder( State.ACTIVE, null );
        private static final StateHolder COMMITTED_HOLDER = new StateHolder( State.COMMITTED, null );
        private static final StateHolder ROLLED_BACK_HOLDER = new StateHolder( State.ROLLED_BACK, null );

        /**
         * The actual state.
         */
        final State value;

        /**
         * If this holder contains a state of {@link State#TERMINATED}, this represents the cause if any.
         */
        final Throwable causeOfTermination;

        static StateHolder of( State value )
        {
            switch ( value )
            {
            case ACTIVE:
                return ACTIVE_HOLDER;
            case COMMITTED:
                return COMMITTED_HOLDER;
            case ROLLED_BACK:
                return ROLLED_BACK_HOLDER;
            case TERMINATED:
            default:
                throw new IllegalArgumentException( "Cannot provide a default state holder for state " + value );
            }
        }

        static StateHolder terminatedWith( Throwable cause )
        {
            return new StateHolder( State.TERMINATED, cause );
        }

        private StateHolder( State value, Throwable causeOfTermination )
        {
            this.value = value;
            this.causeOfTermination = causeOfTermination;
        }

        boolean isOpen()
        {
            return OPEN_STATES.contains( this.value );
        }
    }

    private final Connection connection;
    private final BoltProtocol protocol;
    private final BookmarkHolder bookmarkHolder;
    private final ResultCursorsHolder resultCursors;
    private final long fetchSize;

    private volatile StateHolder state = StateHolder.of( State.ACTIVE );

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

    public CompletionStage beginAsync(Bookmark initialBookmark, TransactionConfig config )
    {
        return protocol.beginTransaction( connection, initialBookmark, config )
                .handle( ( ignore, beginError ) ->
                {
                    if ( beginError != null )
                    {
                        // release connection if begin failed, transaction can't be started
                        connection.release();
                        throw Futures.asCompletionException( beginError );
                    }
                    return this;
                } );
    }

    public CompletionStage closeAsync()
    {
        if ( isOpen() )
        {
            return rollbackAsync();
        }
        else
        {
            return completedWithNull();
        }
    }

    public CompletionStage commitAsync()
    {
        if ( state.value == State.COMMITTED )
        {
            return failedFuture( new ClientException( "Can't commit, transaction has been committed" ) );
        }
        else if ( state.value == State.ROLLED_BACK )
        {
            return failedFuture( new ClientException( "Can't commit, transaction has been rolled back" ) );
        }
        else
        {
            return resultCursors.retrieveNotConsumedError()
                    .thenCompose( error -> doCommitAsync().handle( handleCommitOrRollback( error ) ) )
                    .whenComplete( ( ignore, error ) -> transactionClosed( error == null ) );
        }
    }

    public CompletionStage rollbackAsync()
    {
        if ( state.value == State.COMMITTED )
        {
            return failedFuture( new ClientException( "Can't rollback, transaction has been committed" ) );
        }
        else if ( state.value == State.ROLLED_BACK )
        {
            return failedFuture( new ClientException( "Can't rollback, transaction has been rolled back" ) );
        }
        else
        {
            return resultCursors.retrieveNotConsumedError()
                    .thenCompose( error -> doRollbackAsync().handle( handleCommitOrRollback( error ) ) )
                    .whenComplete( ( ignore, error ) -> transactionClosed( false ) );
        }
    }

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

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

    public boolean isOpen()
    {
        return state.isOpen();
    }

    public void markTerminated( Throwable cause )
    {
        state = StateHolder.terminatedWith( cause );
    }

    public Connection connection()
    {
        return connection;
    }

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

    private CompletionStage doCommitAsync()
    {
        if ( state.value == State.TERMINATED )
        {
            return failedFuture( new ClientException( "Transaction can't be committed. " +
                                                      "It has been rolled back either because of an error or explicit termination", state.causeOfTermination ) );
        }
        return protocol.commitTransaction( connection ).thenAccept( bookmarkHolder::setBookmark );
    }

    private CompletionStage doRollbackAsync()
    {
        if ( state.value == State.TERMINATED )
        {
            return completedWithNull();
        }
        return protocol.rollbackTransaction( connection );
    }

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

    private void transactionClosed( boolean isCommitted )
    {
        if ( isCommitted )
        {
            state = StateHolder.of( State.COMMITTED );
        }
        else
        {
            state = StateHolder.of( State.ROLLED_BACK );
        }
        connection.release(); // release in background
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy