/* * 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 boolean permitRedundantTransitions; 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 (permitRedundantTransitions && state == State.COMMITTED) return 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 (permitRedundantTransitions && state == State.READY_TO_COMMIT) return; 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; } protected void permitRedundantTransitions() { permitRedundantTransitions = true; } } // 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(); }