blob: abb876c221916e6e8a8a2530dc3437563d29f651 [file] [log] [blame]
/*
* 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();
}