package org.multiverse.stms.gamma.transactions;
import org.multiverse.api.Txn;
import org.multiverse.api.TxnStatus;
import org.multiverse.api.blocking.DefaultRetryLatch;
import org.multiverse.api.blocking.RetryLatch;
import org.multiverse.api.exceptions.*;
import org.multiverse.api.functions.Function;
import org.multiverse.api.lifecycle.TxnEvent;
import org.multiverse.api.lifecycle.TxnListener;
import org.multiverse.stms.gamma.GammaConstants;
import org.multiverse.stms.gamma.GammaObjectPool;
import org.multiverse.stms.gamma.transactionalobjects.BaseGammaTxnRef;
import org.multiverse.stms.gamma.transactionalobjects.GammaObject;
import org.multiverse.stms.gamma.transactionalobjects.Tranlocal;
import java.util.ArrayList;
import static java.lang.String.format;
import static org.multiverse.stms.gamma.GammaStmUtils.toDebugString;
/**
* Abstract GammaTxn to be used by all the concrete GammaTxn implementations.
*
* @author Peter Veentjer.
*/
@SuppressWarnings({"OverlyComplexClass", "ClassWithTooManyFields", "OverlyCoupledClass"})
public abstract class GammaTxn implements GammaConstants, Txn {
public final GammaObjectPool pool = new GammaObjectPool();
public int status = TX_ACTIVE;
public GammaTxnConfig config;
public int attempt;
public long remainingTimeoutNs;
public boolean hasWrites;
public final int transactionType;
public boolean richmansMansConflictScan;
public boolean abortOnly = false;
public final RetryLatch retryListener = new DefaultRetryLatch();
public ArrayList<TxnListener> listeners;
public boolean commitConflict;
public boolean evaluatingCommute = false;
public GammaTxn(GammaTxnConfig config, int transactionType) {
config.init();
init(config);
this.transactionType = transactionType;
}
protected void notifyListeners(TxnEvent event) {
if (listeners != null) {
boolean abort = true;
try {
for (int k = 0; k < listeners.size(); k++) {
listeners.get(k).notify(this, event);
}
abort = false;
} finally {
if (abort) {
abortIfAlive();
}
}
}
final ArrayList<TxnListener> permanentListeners = config.permanentListeners;
if (permanentListeners != null) {
boolean abort = true;
try {
for (int k = 0; k < permanentListeners.size(); k++) {
permanentListeners.get(k).notify(this, event);
}
abort = false;
} finally {
if (abort) {
abortIfAlive();
}
}
}
}
protected RetryError newRetryError() {
return config.controlFlowErrorsReused ? RetryError.INSTANCE : new RetryError(true);
}
public final boolean isLean() {
return transactionType == TRANSACTIONTYPE_LEAN_MONO || transactionType == TRANSACTIONTYPE_LEAN_FIXED_LENGTH;
}
public final void abortIfAlive() {
if (isAlive()) {
abort();
}
}
public AbortOnlyException abortPrepareOnAbortOnly() {
abortIfAlive();
return new AbortOnlyException(
format("[%s] Failed to execute transaction.prepare, reason: the transaction was configured as abortOnly",
config.familyName));
}
public AbortOnlyException abortCommitOnAbortOnly() {
abortIfAlive();
return new AbortOnlyException(
format("[%s] Failed to execute transaction.commit, reason: the transaction was configured as abortOnly",
config.familyName));
}
public final ReadWriteConflict abortOnReadWriteConflict(GammaObject object) {
abortIfAlive();
if (attempt == config.maxRetries || !config.controlFlowErrorsReused) {
return new ReadWriteConflict(
format("[%s] Failed transaction, reason: object [%s] contains a read/write-conflict",
config.familyName, toDebugString(object)));
} else {
return ReadWriteConflict.INSTANCE;
}
}
public DeadTxnException failAbortOnAlreadyCommitted() {
return new DeadTxnException(
format("[%s] Failed to execute transaction.abort, reason: the transaction is already committed",
config.familyName));
}
// ================= open for read =============================
public SpeculativeConfigurationError abortOpenForReadOrWriteOnExplicitLockingDetected(BaseGammaTxnRef ref) {
config.updateSpeculativeConfigurationToUseExplicitLocking();
abortIfAlive();
if (config.controlFlowErrorsReused) {
return SpeculativeConfigurationError.INSTANCE;
}
return new SpeculativeConfigurationError(
format("[%s] Failed to execute TxnRef.openForRead/openForWrite '%s', reason: the transaction is lean, " +
"but explicit locking is required",
config.familyName, toDebugString(ref)));
}
public SpeculativeConfigurationError abortOpenForReadOnNonRefTypeDetected(BaseGammaTxnRef ref) {
config.updateSpeculativeConfigurationToUseNonRefType();
abortIfAlive();
if (config.controlFlowErrorsReused) {
return SpeculativeConfigurationError.INSTANCE;
}
return new SpeculativeConfigurationError(
format("[%s] Failed to execute TxnRef.openForRead/openForWrite '%s', reason: the transaction is lean," +
" but explicit locking is required",
config.familyName, toDebugString(ref)));
}
public final StmMismatchException abortOpenForReadOnBadStm(GammaObject o) {
abortIfAlive();
return new StmMismatchException(
format("[%s] Failed to execute TxnRef.openForRead '%s', reason: the stm the ref was created " +
"with is a different stm than the stm of the transaction",
config.familyName, toDebugString(o)));
}
public IllegalTxnStateException abortOpenForReadOnNullLockMode(BaseGammaTxnRef object) {
switch (status) {
case TX_PREPARED:
abort();
return new PreparedTxnException(
format("[%s] Failed to execute TxnRef.openForRead '%s', reason: the LockMode is null",
config.familyName, toDebugString(object)));
case TX_ABORTED:
return new DeadTxnException(
format("[%s] Failed to execute TxnRef.openForRead '%s', reason: the Lockmode is null",
config.familyName, toDebugString(object)));
case TX_COMMITTED:
return new DeadTxnException(
format("[%s] Failed to execute TxnRef.openForRead '%s', reason: the LockMode is null",
config.familyName, toDebugString(object)));
default:
throw new IllegalStateException();
}
}
public final IllegalTxnStateException abortOpenForReadOnBadStatus(GammaObject object) {
switch (status) {
case TX_PREPARED:
abort();
return new PreparedTxnException(
format("[%s] Failed to execute TxnRef.openForRead '%s', reason: the transaction is prepared",
config.familyName, toDebugString(object)));
case TX_ABORTED:
return new DeadTxnException(
format("[%s] Failed to execute TxnRef.openForRead '%s', reason: the transaction is aborted",
config.familyName, toDebugString(object)));
case TX_COMMITTED:
return new DeadTxnException(
format("[%s] Failed to execute TxnRef.openForRead '%s', reason: the transaction is committed",
config.familyName, toDebugString(object)));
default:
throw new IllegalStateException();
}
}
// ============== open for write ============================
public final ReadonlyException abortOpenForWriteOnReadonly(GammaObject object) {
abortIfAlive();
return new ReadonlyException(
format("[%s] Failed to TxnRef.openForWrite '%s', reason: the transaction is readonly",
config.familyName, toDebugString(object)));
}
// ============================= retry ==============================
public final IllegalTxnStateException abortRetryOnNoRetryPossible() {
abortIfAlive();
throw new RetryNotPossibleException(
format("[%s] Failed to execute TxnRef.retry, reason: there are no tracked reads",
config.familyName));
}
public final RetryNotAllowedException abortRetryOnNoBlockingAllowed() {
abortIfAlive();
return new RetryNotAllowedException(
format("[%s] Failed to execute TxnRef.retry, reason: the transaction doesn't allow blocking",
config.familyName));
}
public final IllegalTxnStateException abortRetryOnBadStatus() {
switch (status) {
case TX_PREPARED:
abort();
return new PreparedTxnException(
format("[%s] Failed to execute Txn.retry, reason: the transaction is prepared",
config.familyName));
case TX_ABORTED:
return new DeadTxnException(
format("[%s] Failed to execute Txn.retry, reason: the transaction is aborted",
config.familyName));
case TX_COMMITTED:
return new DeadTxnException(
format("[%s] Failed to execute Txn.retry, reason: the transaction is committed",
config.familyName));
default:
throw new IllegalStateException();
}
}
// ========================== open for construction ===========================
public final IllegalArgumentException abortOpenForConstructionOnBadReference(
final GammaObject ref) {
abortIfAlive();
return new IllegalArgumentException(
format("[%s] Failed to execute TxnRef.openForConstruction '%s', reason: the object is not new " +
"and has previous commits",
config.familyName, toDebugString(ref)));
}
public final IllegalTxnStateException abortOpenForConstructionOnBadStatus(GammaObject o) {
switch (status) {
case TX_PREPARED:
abort();
return new PreparedTxnException(
format("[%s] Failed to execute TxnRef.openForConstruction '%s', reason: the transaction is prepared",
config.familyName, toDebugString(o)));
case TX_ABORTED:
return new DeadTxnException(
format("[%s] Failed to execute TxnRef.openForConstruction '%s', reason: the transaction is aborted",
config.familyName, toDebugString(o)));
case TX_COMMITTED:
return new DeadTxnException(
format("[%s] Failed to execute TxnRef.openForConstruction '%s', reason: the transaction is committed",
config.familyName, toDebugString(o)));
default:
throw new IllegalStateException();
}
}
public final StmMismatchException abortOpenForConstructionOnBadStm(GammaObject o) {
abortIfAlive();
return new StmMismatchException(
format("[%s] Failed to execute TxnRef.openForConstruction '%s', reason: the stm the ref was " +
"created with is a different stm than the stm of the transaction",
config.familyName, toDebugString(o)));
}
public ReadonlyException abortOpenForConstructionOnReadonly(GammaObject o) {
abortIfAlive();
return new ReadonlyException(
format("[%s] Failed to execute TxnRef.openForConstruction '%s', reason: the transaction is readonly",
config.familyName, toDebugString(o)));
}
public SpeculativeConfigurationError abortOpenForConstructionRequired(BaseGammaTxnRef ref) {
config.updateSpeculativeConfigurationToUseConstructedObjects();
abortIfAlive();
if (config.controlFlowErrorsReused) {
return SpeculativeConfigurationError.INSTANCE;
}
return new SpeculativeConfigurationError(
format("[%s] Failed to execute TxnRef.openForConstruction '%s', reason: the transaction is lean, " +
"but explicit attachments of constructed objects is required",
config.familyName, toDebugString(ref)));
}
// ============================== open for commute ======================
public SpeculativeConfigurationError abortCommuteOnCommuteDetected(BaseGammaTxnRef ref) {
config.updateSpeculativeConfigurationToUseCommute();
abortIfAlive();
if (config.controlFlowErrorsReused) {
return SpeculativeConfigurationError.INSTANCE;
}
return new SpeculativeConfigurationError(
format("[%s] Failed to execute TxnRef.commute '%s', reason: the transaction is lean, but commute is required",
config.familyName, toDebugString(ref)));
}
public IllegalTxnStateException abortCommuteOnBadStatus(final GammaObject object, final Function function) {
switch (status) {
case TX_PREPARED:
abort();
return new PreparedTxnException(
format("[%s] Failed to execute TxnRef.commute '%s' with reference '%s', reason: the transaction is prepared",
config.familyName, toDebugString(object), function));
case TX_ABORTED:
return new DeadTxnException(
format("[%s] Failed to execute TxnRef.commute '%s' with reference '%s', reason: the transaction is aborted",
config.familyName, toDebugString(object), function));
case TX_COMMITTED:
return new DeadTxnException(
format("[%s] Failed to execute TxnRef.commute '%s' with reference '%s', reason: the transaction is prepared",
config.familyName, toDebugString(object), function));
default:
throw new IllegalStateException();
}
}
public StmMismatchException abortCommuteOnBadStm(GammaObject object) {
abortIfAlive();
return new StmMismatchException(
format("[%s] Failed to execute TxnRef.commute '%s', reason: the stm the ref was created with is a different" +
" stm than the stm of the transaction",
config.familyName, toDebugString(object)));
}
public ReadonlyException abortCommuteOnReadonly(final GammaObject object) {
abortIfAlive();
return new ReadonlyException(
format("[%s] Failed to execute TxnRef.commute '%s', reason: the transaction is readonly",
config.familyName, toDebugString(object)));
}
public NullPointerException abortCommuteOnNullFunction(final GammaObject object) {
abortIfAlive();
return new NullPointerException(
format("[%s] Failed to execute TxnRef.commute '%s', reason: the function is null",
config.familyName, toDebugString(object)));
}
// ==========================================
public final IllegalTxnStateException abortLocateOnBadStatus(GammaObject object) {
switch (status) {
case TX_PREPARED:
abort();
return new PreparedTxnException(
format("[%s] Failed to execute Txn.locate '%s' , reason: the transaction is prepared",
toDebugString(object), config.familyName));
case TX_ABORTED:
return new DeadTxnException(
format("[%s] Failed to execute Txn.locate, '%s' reason: the transaction is aborted",
toDebugString(object), config.familyName));
case TX_COMMITTED:
return new DeadTxnException(
format("[%s] Failed to execute Txn.locate, '%s' reason: the transaction is committed",
toDebugString(object), config.familyName));
default:
throw new IllegalStateException();
}
}
public final NullPointerException abortLocateOnNullArgument() {
abortIfAlive();
return new NullPointerException(
format("[%s] Failed to execute Txn.locate, reason: the reference is null",
config.familyName));
}
// ====================== register ==========================================
private NullPointerException abortRegisterOnNullListener() {
abortIfAlive();
return new NullPointerException(
format("[%s] Failed to execute Txn.register , reason: the listener is null",
config.familyName));
}
private IllegalTxnStateException abortRegisterOnBadStatus() {
switch (status) {
case TX_PREPARED:
abort();
return new PreparedTxnException(
format("[%s] Failed to execute Txn.register, reason: the transaction is prepared",
config.familyName));
case TX_ABORTED:
return new DeadTxnException(
format("[%s] Failed to execute Txn.register, reason: the transaction is aborted",
config.familyName));
case TX_COMMITTED:
return new DeadTxnException(
format("[%s] Failed to execute Txn.register, reason: the transaction is prepared",
config.familyName));
default:
throw new IllegalStateException();
}
}
public SpeculativeConfigurationError abortRegisterOnListenerRequired() {
config.updateSpeculativeConfigurationToUseListeners();
abortIfAlive();
if (config.controlFlowErrorsReused) {
return SpeculativeConfigurationError.INSTANCE;
}
return new SpeculativeConfigurationError(
format("[%s] Failed to execute Txn.register, reason: the transaction is lean, but listeners are required",
config.familyName));
}
public final IllegalTxnStateException abortPrepareOnBadStatus() {
switch (status) {
case TX_ABORTED:
return new DeadTxnException(
format("[%s] Failed to execute Txn.prepare, reason: the transaction already is aborted",
config.familyName));
case TX_COMMITTED:
return new DeadTxnException(
format("[%s] Failed to execute Txn.prepare, reason: the transaction already is committed",
config.familyName));
default:
throw new IllegalStateException();
}
}
public final IllegalTxnStateException abortCommitOnBadStatus() {
abortIfAlive();
return new DeadTxnException(
format("[%s] Failed to execute Txn.commit, reason: the transaction already is aborted",
config.familyName));
}
public TxnExecutionException abortOnOpenForConstructionWhileEvaluatingCommute(GammaObject o) {
abort();
return new IllegalCommuteException(
format("[%s] Failed to execute TxnRef.openForConstruction '%s', " +
"reason: the transaction is already evaluating a commuting function",
config.familyName, toDebugString(o)));
}
public TxnExecutionException abortOnOpenForReadWhileEvaluatingCommute(GammaObject o) {
abortIfAlive();
return new IllegalCommuteException(
format("[%s] Failed to execute TxnRef.openForRead '%s', " +
"reason: the transaction is already evaluating a commuting function",
config.familyName, toDebugString(o)));
}
public TxnExecutionException abortOnOpenForCommuteWhileEvaluatingCommute(GammaObject o) {
abortIfAlive();
return new IllegalCommuteException(
format("[%s] Failed to execute TxnRef.openForCommute '%s', " +
"reason: the transaction is already evaluating a commuting function",
config.familyName, toDebugString(o)));
}
public IllegalTxnStateException abortEnsureOnBadStatus(BaseGammaTxnRef o) {
switch (status) {
case TX_PREPARED:
abort();
return new PreparedTxnException(
format("[%s] Failed to execute TxnRef.ensure with reference '%s', reason: the transaction is prepared",
config.familyName, toDebugString(o)));
case TX_ABORTED:
return new DeadTxnException(
format("[%s] Failed to execute TxnRef.ensure with reference '%s', reason: the transaction is aborted",
config.familyName, toDebugString(o)));
case TX_COMMITTED:
return new DeadTxnException(
format("[%s] Failed to execute TxnRef.ensure with reference '%s', reason: the transaction is committed",
config.familyName, toDebugString(o)));
default:
throw new IllegalStateException();
}
}
public final SpeculativeConfigurationError abortOnTransactionTooSmall(int minimalSize) {
config.updateSpeculativeConfigurationToUseMinimalTransactionLength(minimalSize);
abortIfAlive();
if (config.controlFlowErrorsReused) {
return SpeculativeConfigurationError.INSTANCE;
}
return new SpeculativeConfigurationError(
format("[%s] Failed to execute opening a TxnRef, reason: the transaction is too small for this operation",
config.familyName));
}
public final SpeculativeConfigurationError abortOnRichmanConflictScanDetected() {
config.updateSpeculativeConfigurationToUseRichMansConflictScan();
abortIfAlive();
if (config.controlFlowErrorsReused) {
return SpeculativeConfigurationError.INSTANCE;
}
return new SpeculativeConfigurationError(
format("[%s] Failed to execute evaluate the Txn read consistency, reason: the transaction is large to be used" +
" in combination for a poor mans conflictscan",
config.familyName));
}
public SpeculativeConfigurationError abortEnsureOnEnsureDetected(GammaObject o) {
config.updateSpeculativeConfigurationToUseEnsure();
abortIfAlive();
if (config.controlFlowErrorsReused) {
return SpeculativeConfigurationError.INSTANCE;
}
return new SpeculativeConfigurationError(
format("[%s] Failed to execute evaluate the TxnRef.ensure [%s], reason: the transaction lean and a fat one needs to be used",
config.familyName, toDebugString(o)));
}
public final NullPointerException abortAcquireOnNullLockMode(GammaObject o) {
switch (status) {
case TX_ACTIVE:
abort();
return new NullPointerException();
case TX_PREPARED:
abort();
return new NullPointerException();
case TX_ABORTED:
return new NullPointerException();
case TX_COMMITTED:
return new NullPointerException();
default:
throw new IllegalStateException();
}
}
public final boolean hasWrites() {
return hasWrites;
}
public abstract void commit();
public abstract void abort();
public abstract Tranlocal locate(BaseGammaTxnRef o);
@Override
public final GammaTxnConfig getConfig() {
return config;
}
@Override
public final int getAttempt() {
return attempt;
}
@Override
public final long getRemainingTimeoutNs() {
return remainingTimeoutNs;
}
@Override
public boolean isAbortOnly() {
switch (status) {
case TX_ACTIVE:
return abortOnly;
case TX_PREPARED:
return abortOnly;
case TX_COMMITTED:
throw new DeadTxnException(
format("[%s] Failed to execute Txn.isAbortOnly, reason: the transaction is committed",
config.familyName));
case TX_ABORTED:
throw new DeadTxnException(
format("[%s] Failed to execute Txn.isAbortOnly, reason: the transaction is aborted",
config.familyName));
default:
throw new IllegalStateException();
}
}
@Override
public final void setAbortOnly() {
switch (status) {
case TX_ACTIVE:
if (isLean()) {
config.updateSpeculativeConfigureToUseAbortOnly();
abort();
if (config.controlFlowErrorsReused) {
throw SpeculativeConfigurationError.INSTANCE;
}
throw new SpeculativeConfigurationError(
format("[%s] Failed to execute Txn.setAbortOnly, reason: the transaction is lean, " +
"but a fat one is required for dealing with the abortOnly",
config.familyName));
}
abortOnly = true;
break;
case TX_PREPARED:
abort();
throw new PreparedTxnException(
format("[%s] Failed to execute Txn.setAbortOnly, reason: the transaction is prepared",
config.familyName));
case TX_COMMITTED:
throw new DeadTxnException(
format("[%s] Failed to execute Txn.setAbortOnly, reason: the transaction is committed",
config.familyName));
case TX_ABORTED:
throw new DeadTxnException(
format("[%s] Failed to execute Txn.setAbortOnly, reason: the transaction is aborted",
config.familyName));
default:
throw new IllegalStateException();
}
}
@Override
public void register(TxnListener listener) {
if (listener == null) {
throw abortRegisterOnNullListener();
}
if (status != TX_ACTIVE) {
throw abortRegisterOnBadStatus();
}
if (transactionType == TRANSACTIONTYPE_LEAN_MONO || transactionType == TRANSACTIONTYPE_LEAN_FIXED_LENGTH) {
throw abortRegisterOnListenerRequired();
}
if (listeners == null) {
listeners = pool.takeArrayList();
}
listeners.add(listener);
}
/**
* Does a hard reset of an aborted/committed transaction. This means that it is made ready to be used by another
* transaction configuration.
*/
public abstract void hardReset();
/**
* Does a soft reset of an aborted/committed transaction. This method is called when the execution of a transaction
* fails, but needs to be retried again.
*
* @return if another attempt can be made, false otherwise.
*/
public abstract boolean softReset();
/**
* Gets the Tranlocal for a specific AbstractGammaTxnRef. This method doesn't care about the state of a
* transaction.
*
* @param ref the AbstractGammaTxnRef
* @return the found Tranlocal or null if not found.
*/
public abstract Tranlocal getRefTranlocal(BaseGammaTxnRef ref);
public final boolean isAlive() {
return status == TX_ACTIVE || status == TX_PREPARED;
}
public final void awaitUpdate() {
final long lockEra = retryListener.getEra();
if (config.timeoutNs == Long.MAX_VALUE) {
if (config.isInterruptible()) {
retryListener.await(lockEra, config.familyName);
} else {
retryListener.awaitUninterruptible(lockEra);
}
} else {
if (config.isInterruptible()) {
remainingTimeoutNs = retryListener.awaitNanos(lockEra, remainingTimeoutNs, config.familyName);
} else {
remainingTimeoutNs = retryListener.awaitNanosUninterruptible(lockEra, remainingTimeoutNs);
}
if (remainingTimeoutNs < 0) {
throw new RetryTimeoutException(
format("[%s] Txn has timed out with a total timeout of %s ns",
config.getFamilyName(), config.getTimeoutNs()));
}
}
}
public final void copyForSpeculativeFailure(GammaTxn failingTx) {
remainingTimeoutNs = failingTx.remainingTimeoutNs;
attempt = failingTx.attempt;
}
public final void init(GammaTxnConfig config) {
if (config == null) {
throw new NullPointerException();
}
this.config = config;
hardReset();
}
@SuppressWarnings({"BooleanMethodIsAlwaysInverted"})
public abstract boolean isReadConsistent(Tranlocal justAdded);
public final TxnStatus getStatus() {
switch (status) {
case TX_ACTIVE:
return TxnStatus.Active;
case TX_PREPARED:
return TxnStatus.Prepared;
case TX_COMMITTED:
return TxnStatus.Committed;
case TX_ABORTED:
return TxnStatus.Aborted;
default:
throw new IllegalStateException();
}
}
public final boolean skipPrepare() {
return config.readLockModeAsInt == LOCKMODE_EXCLUSIVE && !config.dirtyCheck;
}
/**
* Initializes the local conflict counter if the transaction has a need for it.
* It should only be initialized if there are no reads.
*/
public abstract void initLocalConflictCounter();
}