//If the transaction has already expired or the state
//is not amenable, don't even try to continue
if ((getState() == ACTIVE) && (ensureCurrent() == false)) {
doAbort(0);
throw new CannotCommitException("Lease expired");
}
if (getState() == ABORTED)
throw new CannotCommitException("attempt to commit " +
"ABORTED transaction");
//Check to see if anyone joined the transaction. Even
//if no one has joined, at this point, attempt to
//get to the COMMITTED state through valid state changes
Vector joinvec = parthandles();
if (joinvec == null) {
if (!modifyTxnState(VOTING))
throw new CannotCommitException("attempt to commit " +
"ABORTED transaction");
if (modifyTxnState(COMMITTED))
return;
else
throw new CannotCommitException("attempt to commit " +
"ABORTED transaction");
}
try {
Enumeration joined = joinvec.elements();
int numparts = joinvec.size();
ParticipantHandle[] phs = new ParticipantHandle[numparts];
joinvec.copyInto(phs);
long now = starttime;
long transpired = 0;
long remainder = 0;
ClientLog log = logmgr.logFor(str.id);
if (transactionsLogger.isLoggable(Level.FINEST)) {
transactionsLogger.log(Level.FINEST,
"{0} TransactionParticipants have joined",
new Integer(numparts));
}
//If commit is called after recovery, do not
//log a CommitRecord since it already exists
//exists in the Log file for this transaction.
//Remember that a log is not invalidated until
//after the transaction is committed.
//
//Only new occurrences of activities requiring
//logging which happen after recovery should
//be added to the log file. So, we add records
//for voting and roll forward/back activity for
//ACTIVE participants.
//
//If the state cannot validly transition to VOTING,
//it is either because someone already aborted or
//committed. Only throw an exception if someone
//has previously aborted. In the case of a prior
//commit, fall through and wait for the CommitJob
//to complete.
int oldstate = getState();
Integer result = new Integer(ABORTED);
Exception alternateException = null;
//On an ACTIVE to VOTING transition, create
//and schedule a Prepare or PrepareAndCommitJob.
//If the state transition is VOTING to VOTING,
//then the PrepareJob has already been created
//in the past, so just fall through and wait
//for it to complete.
//
//Only log the commit on ACTIVE to VOTING
//transitions.
if (modifyTxnState(VOTING)) {
if (oldstate == ACTIVE)
log.write(new CommitRecord(phs));
//preparing a participant can never override
//the other activities (abort or commit),
//so only set when the job is null.
synchronized (jobLock) {
if (job == null) {
if (phs.length == 1)
job = new
PrepareAndCommitJob(
str, threadpool, wm, log, phs[0]);
else
job = new PrepareJob(str, threadpool, wm, log, phs);
job.scheduleTasks();
}
}
//Wait for the PrepareJob to complete.
//PrepareJobs are given maximum time for
//completion. This is required in order to
//know the transaction's completion status.
//Remember that the timeout ONLY controls how
//long the caller is willing to wait to inform
//participants. This means that a completion
//status for the transaction MUST be computed
//before consulting the timeout.
//Timeout is ignored until completion status
//is known. If extra time is left, wait for
//the remainder to inform participants.
//We must explicitly check for Job type
//because someone else could have aborted
//the transaction at this point.
synchronized (jobLock) {
if ((job instanceof PrepareJob) ||
(job instanceof PrepareAndCommitJob)) {
try {
if (job.isCompleted(Long.MAX_VALUE)) {
result = (Integer) job.computeResult();
if (result.intValue() == ABORTED &&
job instanceof PrepareAndCommitJob) {
PrepareAndCommitJob pj =
(PrepareAndCommitJob)job;
alternateException =
pj.getAlternateException();
}
}
} catch (JobNotStartedException jnse) {
//no participants voted, so do nothing
result = new Integer(NOTCHANGED);
} catch (ResultNotReadyException rnre) {
//consider aborted
} catch (JobException je) {
//consider aborted
}
}
}
} else {
//Cannot be VOTING, so we either have
//an abort or commit in progress.
if (getState() == ABORTED)
throw new CannotCommitException("transaction ABORTED");
//If a CommitJob is already in progress
//(the state is COMMITTED) cause a fall
//through to the code which waits for
//the CommitJob to complete.
if (getState() == COMMITTED)
result = new Integer(COMMITTED);
}
if (transactionsLogger.isLoggable(Level.FINEST)) {
transactionsLogger.log(Level.FINEST,
"Voting result: {0}",
TxnConstants.getName(result.intValue()));
}
switch (result.intValue()) {
case NOTCHANGED:
break;
case ABORTED:
now = System.currentTimeMillis();
transpired = now - starttime;
remainder = waitFor - transpired;
if (remainder >=0)
doAbort(remainder);
else
doAbort(0);
if (alternateException == null) {
throw new CannotCommitException(
"Unable to commit transaction: "
+ getParticipantInfo());
} else {
throw new RemoteException(
"Problem communicating with participant",
alternateException);
}
case PREPARED:
//This entrypoint is entered if a PrepareJob
//tallied the votes with an outcome of
//PREPARED. In order to inform participants,
//a CommitJob must be scheduled.
if(modifyTxnState(COMMITTED)) {
//TODO - log committed state record?
synchronized (jobLock) {
job = new CommitJob(str, threadpool, wm, log, phs);
job.scheduleTasks();
}
} else {
throw new CannotCommitException("attempt to commit " +
"ABORTED transaction");
}
//Fall through to code with waits
//for CommitJob to complete.
case COMMITTED:
//This entrypoint is the starting place for the code
//which waits for a CommitJob to complete and
//computes its resulting outcome. In addition,
//the wait time is enforced. Should the wait time
//expire, a SettlerTask is scheduled on a thread
//pool. The SettlerTask is needed to complete
//the commit (instruct participants to roll-forward)
//on behalf of the thread which exits when
//the TimeoutExpiredException is thrown.
//
//It is reached when...
//
// a) A commit was called on the same transaction twice.
// When the thread comes in on the commit call, a
// CommitJob already exists and the state is COMMITTED.
//
// b) The normal case where a PrepareJob was found to
// have prepared the transaction and the tally of
// votes resulted in a PREPARED outcome. This causes
// the state to be changed to COMMITTED and a
// CommitJob to be created.
//
// c) A PrepareAndCommitJob has successfully prepared
// a participant which rolled its changes forward.
//
//Note: By checking to see if the CommitJob is already
// present, this check allows us to use the same
// wait-for-CommitJob code for the regular
// PREPARE/COMMIT, the COMMIT/COMMIT and
// the PREPAREANDCOMMIT cases.
synchronized (jobLock) {
//A prepareAndCommitJob is done at this
//point since the TransactionParticipant
//would have instructed itself to roll
//forward.
if (job instanceof PrepareAndCommitJob) {
if(!modifyTxnState(COMMITTED))
throw new CannotCommitException("transaction " +
"ABORTED");
break;
}
//If the abort already arrived, then stop
if (job instanceof AbortJob)
throw new CannotCommitException("transaction " +
"ABORTED");
}
if (getState() != COMMITTED)
throw new
InternalManagerException("TxnManagerTransaction: " +
"commit: " + job + " got bad state: " +
TxnConstants.getName(result.intValue()));
now = System.currentTimeMillis();
transpired = now - starttime;
boolean committed = false;
//If the commit is asynchronous then...
//
// a) check to see if the wait time has transpired
//
// b) If it hasn't, sleep for what's left from the wait time
try {
remainder = waitFor - transpired;
synchronized (jobLock) {
if (remainder <= 0 || !job.isCompleted(remainder)) {
/*
* Note - SettlerTask will kick off another Commit/Abort task for the same txn
* which will try go through the VOTING->Commit states again.
*/
//TODO - Kill off existing task? Postpone SettlerTask?
settler.noteUnsettledTxn(str.id);
throw new TimeoutExpiredException(
"timeout expired", true);
} else {
result = (Integer) job.computeResult();
committed = true;
}
}
} catch (ResultNotReadyException rnre) {
//this should not happen, so flag
//as an error.
} catch (JobNotStartedException jnse) {
//an error
} catch (JobException je) {
//an error
}
if (committed)
break;
default:
throw new InternalManagerException("TxnManagerTransaction: " +
"commit: " + job + " got bad state: " +
TxnConstants.getName(result.intValue()));
}
//We don't care about the result from
//the CommitJob
log.invalidate();
} catch (RuntimeException rte) {
if (transactionsLogger.isLoggable(Level.FINEST)) {
transactionsLogger.log(Level.FINEST,
"Problem committing transaction",
rte);
}
throw rte;
} catch (LogException le) {
if (transactionsLogger.isLoggable(Level.FINEST)) {
transactionsLogger.log(Level.FINEST,
"Problem persisting transaction",
le);
}
throw new CannotCommitException("Unable to log");
}
if (operationsLogger.isLoggable(Level.FINER)) {
operationsLogger.exiting(TxnManagerTransaction.class.getName(),
"commit");
}