/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you 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.apache.cassandra.utils.concurrent;

import static org.apache.cassandra.utils.Throwables.maybeFail;
import static org.apache.cassandra.utils.Throwables.merge;

/**
 * An abstraction for Transactional behaviour. An object implementing this interface has a lifetime
 * of the following pattern:
 *
 * Throwable failure = null;
 * try (Transactional t1, t2 = ...)
 * {
 *     // do work with t1 and t2
 *     t1.prepareToCommit();
 *     t2.prepareToCommit();
 *     failure = t1.commit(failure);
 *     failure = t2.commit(failure);
 * }
 * logger.error(failure);
 *
 * If something goes wrong before commit() is called on any transaction, then on exiting the try block
 * the auto close method should invoke cleanup() and then abort() to reset any state.
 * If everything completes normally, then on exiting the try block the auto close method will invoke cleanup
 * to release any temporary state/resources
 *
 * All exceptions and assertions that may be thrown should be checked and ruled out during commit preparation.
 * Commit should generally never throw an exception unless there is a real correctness-affecting exception that
 * cannot be moved to prepareToCommit, in which case this operation MUST be executed before any other commit
 * methods in the object graph.
 *
 * If exceptions are generated by commit after this initial moment, it is not at all clear what the correct behaviour
 * of the system should be, and so simply logging the exception is likely best (since it may have been an issue
 * during cleanup, say), and rollback cannot now occur. As such all exceptions and assertions that may be thrown
 * should be checked and ruled out during commit preparation.
 *
 * Since Transactional implementations will abort any changes they've made if calls to prepareToCommit() and commit()
 * aren't made prior to calling close(), the semantics of its close() method differ significantly from
 * most AutoCloseable implementations.
 */
public interface Transactional extends AutoCloseable
{
    /**
     * A simple abstract implementation of Transactional behaviour.
     * In general this should be used as the base class for any transactional implementations.
     *
     * If the implementation wraps any internal Transactional objects, it must proxy every
     * commit() and abort() call onto each internal object to ensure correct behaviour
     */
    abstract class AbstractTransactional implements Transactional
    {
        public enum State
        {
            IN_PROGRESS,
            READY_TO_COMMIT,
            COMMITTED,
            ABORTED;
        }

        private State state = State.IN_PROGRESS;

        // the methods for actually performing the necessary behaviours, that are themselves protected against
        // improper use by the external implementations provided by this class. empty default implementations
        // could be provided, but we consider it safer to force implementers to consider explicitly their presence

        protected abstract Throwable doCommit(Throwable accumulate);
        protected abstract Throwable doAbort(Throwable accumulate);

        // these only needs to perform cleanup of state unique to this instance; any internal
        // Transactional objects will perform cleanup in the commit() or abort() calls

        /**
         * perform an exception-safe pre-abort/commit cleanup;
         * this will be run after prepareToCommit (so before commit), and before abort
         */
        protected Throwable doPreCleanup(Throwable accumulate){ return accumulate; }

        /**
         * perform an exception-safe post-abort cleanup
         */
        protected Throwable doPostCleanup(Throwable accumulate){ return accumulate; }

        /**
         * Do any preparatory work prior to commit. This method should throw any exceptions that can be encountered
         * during the finalization of the behaviour.
         */
        protected abstract void doPrepare();

        /**
         * commit any effects of this transaction object graph, then cleanup; delegates first to doCommit, then to doCleanup
         */
        public final Throwable commit(Throwable accumulate)
        {
            if (state != State.READY_TO_COMMIT)
                throw new IllegalStateException("Cannot commit unless READY_TO_COMMIT; state is " + state);
            accumulate = doCommit(accumulate);
            accumulate = doPostCleanup(accumulate);
            state = State.COMMITTED;
            return accumulate;
        }

        /**
         * rollback any effects of this transaction object graph; delegates first to doCleanup, then to doAbort
         */
        public final Throwable abort(Throwable accumulate)
        {
            if (state == State.ABORTED)
                return accumulate;
            if (state == State.COMMITTED)
            {
                try
                {
                    throw new IllegalStateException("Attempted to abort a committed operation");
                }
                catch (Throwable t)
                {
                    accumulate = merge(accumulate, t);
                }
                return accumulate;
            }
            state = State.ABORTED;
            // we cleanup first so that, e.g., file handles can be released prior to deletion
            accumulate = doPreCleanup(accumulate);
            accumulate = doAbort(accumulate);
            accumulate = doPostCleanup(accumulate);
            return accumulate;
        }

        // if we are committed or aborted, then we are done; otherwise abort
        public final void close()
        {
            switch (state)
            {
                case COMMITTED:
                case ABORTED:
                    break;
                default:
                    abort();
            }
        }

        /**
         * The first phase of commit: delegates to doPrepare(), with valid state transition enforcement.
         * This call should be propagated onto any child objects participating in the transaction
         */
        public final void prepareToCommit()
        {
            if (state != State.IN_PROGRESS)
                throw new IllegalStateException("Cannot prepare to commit unless IN_PROGRESS; state is " + state);

            doPrepare();
            maybeFail(doPreCleanup(null));
            state = State.READY_TO_COMMIT;
        }

        /**
         * convenience method to both prepareToCommit() and commit() in one operation;
         * only of use to outer-most transactional object of an object graph
         */
        public Object finish()
        {
            prepareToCommit();
            commit();
            return this;
        }

        // convenience method wrapping abort, and throwing any exception encountered
        // only of use to (and to be used by) outer-most object in a transactional graph
        public final void abort()
        {
            maybeFail(abort(null));
        }

        // convenience method wrapping commit, and throwing any exception encountered
        // only of use to (and to be used by) outer-most object in a transactional graph
        public final void commit()
        {
            maybeFail(commit(null));
        }

        public final State state()
        {
            return state;
        }
    }

    // commit should generally never throw an exception, and preferably never generate one,
    // but if it does generate one it should accumulate it in the parameter and return the result
    // IF a commit implementation has a real correctness affecting exception that cannot be moved to
    // prepareToCommit, it MUST be executed before any other commit methods in the object graph
    Throwable commit(Throwable accumulate);

    // release any resources, then rollback all state changes (unless commit() has already been invoked)
    Throwable abort(Throwable accumulate);

    void prepareToCommit();

    // close() does not throw
    public void close();
}
