/*
* Copyright 2007-2010 Sun Microsystems, Inc.
*
* This file is part of Project Darkstar Server.
*
* Project Darkstar Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* version 2 as published by the Free Software Foundation and
* distributed hereunder to you.
*
* Project Darkstar Server is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* --
*/
package com.sun.sgs.impl.service.transaction;
import com.sun.sgs.app.TransactionAbortedException;
import com.sun.sgs.app.TransactionNotActiveException;
import com.sun.sgs.app.TransactionTimeoutException;
import com.sun.sgs.impl.profile.ProfileCollectorHandle;
import com.sun.sgs.impl.sharedutil.LoggerWrapper;
import com.sun.sgs.profile.ProfileCollector.ProfileLevel;
import com.sun.sgs.service.NonDurableTransactionParticipant;
import com.sun.sgs.service.Transaction;
import com.sun.sgs.service.TransactionListener;
import com.sun.sgs.service.TransactionParticipant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Provides an implementation of Transaction. <p>
*
* Note that this implementation does not check that each joining
* {@code TransactionParticipant} has a unique value for {@code getTypeName}.
* Nor is this check done for {@code TransactionListener}s. If two
* participants or listeners have the same type name then their
* profiling data will be aggregated and reported as a single result.
*/
final class TransactionImpl implements Transaction {
/** Logger for this class. */
private static final LoggerWrapper logger =
new LoggerWrapper(Logger.getLogger(TransactionImpl.class.getName()));
/** The possible states of a transaction. */
private static enum State {
/** In progress */
ACTIVE,
/** Begun preparation */
PREPARING,
/** Begun aborting */
ABORTING,
/** Completed aborting */
ABORTED,
/** Begun committing */
COMMITTING,
/** Completed committing */
COMMITTED
}
/** The transaction ID. */
private final long tid;
/** The time the transaction was created. */
private final long creationTime;
/** The length of time that this transaction is allowed to run.*/
private final long timeout;
/** The thread associated with this transaction. */
private final Thread owner;
/** Whether the prepareAndCommit optimization should be used. */
private final boolean disablePrepareAndCommitOpt;
/** The state of the transaction. */
private State state;
/**
* The transaction participants. If there is a durable participant, it
* will be listed last. Participants whose prepare method returns true
* (read-only) are removed from this list.
*/
private final List<TransactionParticipant> participants =
new ArrayList<TransactionParticipant>();
/** Whether this transaction has a durable participant. */
private boolean hasDurableParticipant = false;
/**
* The exception that caused the transaction to be aborted, or null if no
* abort occurred. Callers should synchronize on the current instance when
* accessing this field unless they have checked that they are being called
* from the creating thread.
*/
private Throwable abortCause = null;
/** The collectorHandle used to report participant detail. */
private final ProfileCollectorHandle collectorHandle;
/** Collected profiling data on each participant, created only if
* global profiling is set to MEDIUM at the start of the transaction.
*/
private final HashMap<String, ProfileParticipantDetailImpl>
participantDetailMap;
/** Collected profiling data on each listener, created only if
* global profiling is set to MEDIUM at the start of the transaction.
*/
private final HashMap<String, TransactionListenerDetailImpl>
listenerDetailMap;
/**
* The registered {@code TransactionListener}s, or {@code null}. The
* listeners are stored and called in the order registered, to simplify
* testing.
*/
private List<TransactionListener> listeners = null;
/**
* Creates an instance with the specified transaction ID, timeout,
* prepare and commit optimization flag, and collectorHandle.
*/
TransactionImpl(long tid, long timeout, boolean usePrepareAndCommitOpt,
ProfileCollectorHandle collectorHandle)
{
this.tid = tid;
this.timeout = timeout;
this.disablePrepareAndCommitOpt = usePrepareAndCommitOpt;
this.collectorHandle = collectorHandle;
creationTime = System.currentTimeMillis();
owner = Thread.currentThread();
state = State.ACTIVE;
if (collectorHandle.getCollector().
getDefaultProfileLevel().ordinal() >=
ProfileLevel.MEDIUM.ordinal())
{
participantDetailMap =
new HashMap<String, ProfileParticipantDetailImpl>();
listenerDetailMap =
new HashMap<String, TransactionListenerDetailImpl>();
} else {
participantDetailMap = null;
listenerDetailMap = null;
}
logger.log(Level.FINER, "create {0}", this);
}
/* -- Implement Transaction -- */
/** {@inheritDoc} */
public byte[] getId() {
return longToBytes(tid);
}
/** {@inheritDoc} */
public long getCreationTime() {
return creationTime;
}
/** {@inheritDoc} */
public long getTimeout() {
return timeout;
}
/** {@inheritDoc} */
public void checkTimeout() {
checkThread("checkTimeout");
logger.log(Level.FINEST, "checkTimeout {0}", this);
switch (state) {
case ABORTED:
case COMMITTED:
throw new TransactionNotActiveException(
"Transaction is not active: " + state);
case ABORTING:
case COMMITTING:
return;
case ACTIVE:
case PREPARING:
break;
default:
throw new AssertionError();
}
long runningTime = System.currentTimeMillis() - getCreationTime();
if (runningTime > getTimeout()) {
TransactionTimeoutException exception =
new TransactionTimeoutException(
"transaction timed out: " + runningTime + " ms");
abort(exception);
throw exception;
}
}
/** {@inheritDoc} */
public void join(TransactionParticipant participant) {
checkThread("join");
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST, "join {0} participant:{1}", this,
getParticipantInfo(participant));
}
if (participant == null) {
throw new NullPointerException("Participant must not be null");
} else if (state == State.ABORTED) {
throw new TransactionNotActiveException(
"Transaction is not active", abortCause);
} else if (state != State.ACTIVE) {
throw new IllegalStateException(
"Transaction is not active: " + state);
}
if (!participants.contains(participant)) {
if (participant instanceof NonDurableTransactionParticipant) {
if (hasDurableParticipant) {
participants.add(participants.size() - 1, participant);
} else {
participants.add(participant);
}
} else if (!hasDurableParticipant) {
hasDurableParticipant = true;
participants.add(participant);
} else {
throw new UnsupportedOperationException(
"Attempt to add multiple durable participants");
}
if (participantDetailMap != null) {
String name = participant.getTypeName();
participantDetailMap.
put(name, new ProfileParticipantDetailImpl(name));
}
}
}
/** {@inheritDoc} */
public void abort(Throwable cause) {
checkThread("abort");
if (cause == null) {
throw new NullPointerException("The cause cannot be null");
}
logger.log(Level.FINER, "abort {0}", this);
switch (state) {
case ACTIVE:
case PREPARING:
break;
case ABORTING:
return;
case ABORTED:
throw new TransactionNotActiveException(
"Transaction is not active", abortCause);
case COMMITTING:
case COMMITTED:
throw new IllegalStateException(
"Transaction is not active: " + state, cause);
default:
throw new AssertionError();
}
state = State.ABORTING;
synchronized (this) {
abortCause = cause;
}
long startTime = 0;
for (TransactionParticipant participant : participants) {
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST, "abort {0} participant:{1}",
this, getParticipantInfo(participant));
}
if (participantDetailMap != null) {
startTime = System.currentTimeMillis();
}
try {
participant.abort(this);
} catch (RuntimeException e) {
if (logger.isLoggable(Level.WARNING)) {
logger.logThrow(
Level.WARNING, e,
"abort {0} participant:{1} failed",
this, getParticipantInfo(participant));
}
}
if (participantDetailMap != null) {
long finishTime = System.currentTimeMillis();
ProfileParticipantDetailImpl detail =
participantDetailMap.get(participant.getTypeName());
detail.setAborted(finishTime - startTime);
collectorHandle.addParticipant(detail);
}
}
state = State.ABORTED;
notifyListenersAfter(false);
}
/** {@inheritDoc} */
public synchronized boolean isAborted() {
return abortCause != null;
}
/** {@inheritDoc} */
public synchronized Throwable getAbortCause() {
return abortCause;
}
/** {@inheritDoc} */
public void registerListener(TransactionListener listener) {
checkThread("registerListener");
if (listener == null) {
throw new NullPointerException("The listener must not be null");
} else if (state != State.ACTIVE) {
throw new TransactionNotActiveException(
"Transaction is not active: " + state);
}
if (listeners == null) {
listeners = new ArrayList<TransactionListener>();
listeners.add(listener);
} else if (!listeners.contains(listener)) {
listeners.add(listener);
}
if (listenerDetailMap != null) {
String name = listener.getTypeName();
listenerDetailMap.put(name,
new TransactionListenerDetailImpl(name));
}
}
/* -- Object methods -- */
/**
* Returns a string representation of this instance.
*
* @return a string representation of this instance
*/
public String toString() {
return "TransactionImpl[tid:" + tid +
", creationTime:" + creationTime +
", timeout:" + timeout +
", state:" + state + "]";
}
/**
* Returns <code>true</code> if the argument is an instance of the same
* class with the same transaction ID.
*
* @return <code>true</code> if the argument equals this instance,
* otherwise <code>false</code>
*/
public boolean equals(Object object) {
return (object instanceof TransactionImpl) &&
tid == ((TransactionImpl) object).tid;
}
/**
* Returns a hash code value for this object.
*
* @return a hash code value for this object.
*/
public int hashCode() {
return (int) (tid >>> 32) ^ (int) tid;
}
/* -- Other methods -- */
/**
* Commits this transaction
*
* @throws TransactionNotActiveException if the transaction has been
* aborted
* @throws TransactionAbortedException if a call to {@link
* TransactionParticipant#prepare prepare} on a transaction
* participant or to {@link TransactionListener#beforeCompletion
* beforeCompletion} on a transaction listener aborts the
* transaction but does not throw an exception
* @throws IllegalStateException if {@code prepare} has been called on any
* transaction participant and {@link Transaction#abort abort} has
* not been called on the transaction, or if called from a thread
* that is not the thread that created this transaction
* @throws Exception any exception thrown when calling {@code prepare} on
* a participant or {@code beforeCompletion} on a listener
* @see TransactionHandle#commit TransactionHandle.commit
*/
void commit() throws Exception {
checkThread("commit");
logger.log(Level.FINER, "commit {0}", this);
if (state == State.ABORTED) {
throw new TransactionNotActiveException(
"Transaction is not active", abortCause);
} else if (state != State.ACTIVE) {
throw new IllegalStateException(
"Transaction is not active: " + state);
}
notifyListenersBefore();
state = State.PREPARING;
long startTime = 0;
ProfileParticipantDetailImpl detail = null;
for (Iterator<TransactionParticipant> iter = participants.iterator();
iter.hasNext(); )
{
TransactionParticipant participant = iter.next();
if (participantDetailMap != null) {
detail = participantDetailMap.get(participant.getTypeName());
startTime = System.currentTimeMillis();
}
try {
if (iter.hasNext() || disablePrepareAndCommitOpt) {
boolean readOnly = participant.prepare(this);
if (detail != null) {
detail.setPrepared(System.currentTimeMillis() -
startTime, readOnly);
}
if (readOnly) {
iter.remove();
if (detail != null) {
collectorHandle.addParticipant(detail);
}
}
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST,
"prepare {0} participant:{1} returns {2}",
this, getParticipantInfo(participant),
readOnly);
}
} else {
participant.prepareAndCommit(this);
if (detail != null) {
detail.
setCommittedDirectly(System.currentTimeMillis() -
startTime);
collectorHandle.addParticipant(detail);
}
iter.remove();
if (logger.isLoggable(Level.FINEST)) {
logger.log(
Level.FINEST,
"prepareAndCommit {0} participant:{1} returns",
this, getParticipantInfo(participant));
}
}
} catch (Exception e) {
if (logger.isLoggable(Level.FINEST)) {
logger.logThrow(
Level.FINEST, e, "{0} {1} participant:{1} throws",
iter.hasNext() ? "prepare" : "prepareAndCommit",
this, getParticipantInfo(participant));
}
if (state != State.ABORTED) {
abort(e);
}
throw e;
}
if (state == State.ABORTED) {
throw new TransactionAbortedException(
"Transaction has been aborted: " + abortCause, abortCause);
}
}
state = State.COMMITTING;
for (TransactionParticipant participant : participants) {
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST, "commit {0} participant:{1}",
this, getParticipantInfo(participant));
}
if (participantDetailMap != null) {
detail = participantDetailMap.get(participant.getTypeName());
startTime = System.currentTimeMillis();
}
try {
participant.commit(this);
if (detail != null) {
detail.setCommitted(System.currentTimeMillis() -
startTime);
collectorHandle.addParticipant(detail);
}
} catch (RuntimeException e) {
if (logger.isLoggable(Level.WARNING)) {
logger.logThrow(
Level.WARNING, e, "commit {0} participant:{1} failed",
this, getParticipantInfo(participant));
}
}
}
state = State.COMMITTED;
notifyListenersAfter(true);
}
/** Returns a byte array that represents the specified long. */
private byte[] longToBytes(long l) {
return new byte[] {
(byte) (l >>> 56), (byte) (l >>> 48), (byte) (l >>> 40),
(byte) (l >>> 32), (byte) (l >>> 24), (byte) (l >>> 16),
(byte) (l >>> 8), (byte) l };
}
/** Notify any listeners before preparing the transaction. */
private void notifyListenersBefore() {
TransactionListenerDetailImpl detail = null;
long startTime = 0;
if (listeners != null) {
/*
* Don't use foreach iteration here, so that we can handle the
* possibility that a beforeCompletion call adds another listener.
*/
for (int i = 0; i < listeners.size(); i++) {
TransactionListener listener = listeners.get(i);
try {
if (listenerDetailMap != null) {
detail = listenerDetailMap.get(listener.getTypeName());
startTime = System.currentTimeMillis();
}
listener.beforeCompletion();
if (detail != null) {
long time = System.currentTimeMillis() - startTime;
detail.setCalledBeforeCompletion(false, time);
}
} catch (RuntimeException e) {
if (detail != null) {
long time = System.currentTimeMillis() - startTime;
detail.setCalledBeforeCompletion(true, time);
}
if (logger.isLoggable(Level.FINEST)) {
logger.logThrow(
Level.FINEST, e,
"beforeCompletion {0} listener:{1} failed",
this, listener);
}
if (state != State.ABORTED) {
abort(e);
}
throw e;
}
if (state == State.ABORTED) {
throw new TransactionAbortedException(
"Transaction has been aborted: " + abortCause,
abortCause);
}
}
}
}
/** Notify any listeners after completing the transaction. */
private void notifyListenersAfter(boolean commited) {
TransactionListenerDetailImpl detail = null;
long startTime = 0;
if (listeners != null) {
for (TransactionListener listener : listeners) {
try {
if (listenerDetailMap != null) {
detail = listenerDetailMap.get(listener.getTypeName());
startTime = System.currentTimeMillis();
}
listener.afterCompletion(commited);
if (detail != null) {
long time = System.currentTimeMillis() - startTime;
detail.setCalledAfterCompletion(time);
collectorHandle.addListener(detail);
}
} catch (RuntimeException e) {
if (logger.isLoggable(Level.WARNING)) {
logger.logThrow(
Level.WARNING, e,
"afterCompletion {0} listener:{1} failed",
this, listener);
}
}
}
}
}
/** Checks that current thread is the one that created this transaction. */
private void checkThread(String methodName) {
if (Thread.currentThread() != owner) {
throw new IllegalStateException(
"The " + methodName + " method must be called from the" +
" thread that created the transaction");
}
}
/**
* Returns a string that describes the participant. Returns null if the
* participant is null.
*/
private static String getParticipantInfo(
TransactionParticipant participant)
{
return participant == null ? null
: (participant.getTypeName() + " (" + participant + ")");
}
}