/*
* The contents of this file are subject to the Open Software License
* Version 3.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.rosenlaw.com/OSL3.0.htm
*
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
* the License for the specific language governing rights and limitations
* under the License.
*
* This file is an original work developed by Netymon Pty Ltd
* (http://www.netymon.com, mailto:mail@netymon.com) under contract to
* Topaz Foundation. Portions created under this contract are
* Copyright (c) 2007 Topaz Foundation
* All Rights Reserved.
*/
package org.mulgara.resolver;
// Java 2 enterprise packages
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import javax.transaction.xa.XAException;
// Third party packages
import org.apache.log4j.Logger;
// Local packages
import org.mulgara.resolver.spi.DatabaseMetadata;
import org.mulgara.resolver.spi.EnlistableResource;
import org.mulgara.query.MulgaraTransactionException;
import org.mulgara.query.TuplesException;
import org.mulgara.query.QueryException;
/**
* @created 2007-11-06
*
* @author <a href="mailto:andrae@netymon.com">Andrae Muys</a>
*
* @company <a href="mailto:mail@netymon.com">Netymon Pty Ltd</a>
*
* @copyright ©2007 <a href="http://www.topazproject.org/">Topaz Foundation</a>
*
* @licence Open Software License v3.0
*/
public class MulgaraExternalTransaction implements MulgaraTransaction {
private static final Logger logger =
Logger.getLogger(MulgaraExternalTransaction.class.getName());
private static enum ResourceState { IDLE, ACTIVE, SUSPENDED, FINISHED };
private Xid xid;
private Set<EnlistableResource> enlisted;
private Set<EnlistableResource> started;
private Set<EnlistableResource> needRollback;
private Set<EnlistableResource> prepared;
private Set<EnlistableResource> committed;
private Set<EnlistableResource> rollbacked;
private Map<EnlistableResource, XAResource> xaResources;
private ResourceState xaResState;
private int inuse;
private MulgaraExternalTransactionFactory factory;
private DatabaseOperationContext context;
private boolean inXACompletion;
private boolean hRollback;
private int heurCode;
private boolean rollback;
private String rollbackCause;
private boolean completed;
private volatile long lastActive;
MulgaraExternalTransaction(MulgaraExternalTransactionFactory factory, Xid xid, DatabaseOperationContext context)
throws QueryException {
this.factory = factory;
this.context = context;
this.xid = xid;
this.enlisted = new HashSet<EnlistableResource>();
this.started = new HashSet<EnlistableResource>();
this.needRollback = new HashSet<EnlistableResource>();
this.prepared = new HashSet<EnlistableResource>();
this.committed = new HashSet<EnlistableResource>();
this.rollbacked = new HashSet<EnlistableResource>();
this.xaResources = new HashMap<EnlistableResource, XAResource>();
this.xaResState = ResourceState.IDLE;
this.inuse = 0;
this.inXACompletion = false;
this.hRollback = false;
this.heurCode = 0;
this.rollback = false;
this.completed = false;
this.lastActive = System.currentTimeMillis();
this.context.initiate(this);
}
// We ignore reference counting in external transactions
public void reference() throws MulgaraTransactionException {}
public void dereference() throws MulgaraTransactionException {}
/**
* Calls through to {@link #abortTransaction(String,Throwable)} passing the message in
* the cause as the message for the transaction abort.
* @param cause The state triggering the abort.
* @return The exception for aborting.
* @throws MulgaraTransactionException Indicated failure to cleanly abort.
*/
public MulgaraTransactionException abortTransaction(Throwable cause) throws MulgaraTransactionException {
return abortTransaction(cause.getMessage(), cause);
}
public MulgaraTransactionException abortTransaction(String errorMessage, Throwable cause)
throws MulgaraTransactionException {
report("abortTransaction");
// we should actually already have the mutex, but let's make sure
acquireMutex(0L, true, MulgaraTransactionException.class);
try {
if (rollbackCause == null)
rollbackCause = errorMessage;
try {
for (EnlistableResource resource : enlisted) {
try {
resource.abort();
} catch (Throwable throw_away) {
logger.warn("Difficulty aborting enlisted resource while aborting transaction", throw_away);
}
}
for (EnlistableResource resource : prepared) {
try {
resource.abort();
} catch (Throwable throw_away) {
logger.warn("Difficulty aborting prepared resource while aborting transaction", throw_away);
}
}
return new MulgaraTransactionException(errorMessage, cause);
} finally {
completed = true;
factory.transactionComplete(this, rollbackCause);
}
} finally {
releaseMutex();
}
}
public void heuristicRollback(String cause) throws MulgaraTransactionException {
report("heuristicRollback: " + cause);
synchronized (factory.getMutexLock()) {
if (factory.getMutexHolder() != null && factory.getMutexHolder() != Thread.currentThread()) {
if (inXACompletion) {
return; // this txn is already being cleaned up, so let it go
}
}
factory.acquireMutexWithInterrupt(0L, MulgaraTransactionException.class);
inXACompletion = true;
}
try {
if (hRollback)
return;
hRollback = true;
if (rollbackCause == null)
rollbackCause = cause;
try {
rollback(xid);
} catch (XAException xa) {
throw new MulgaraTransactionException("Failed heuristic rollback", xa);
} finally {
heurCode = heurCode == 0 ? XAException.XA_HEURRB : heurCode;
}
} finally {
releaseMutex();
}
}
public void execute(Operation operation, DatabaseMetadata metadata) throws MulgaraTransactionException {
acquireMutex(0, false, MulgaraTransactionException.class);
try {
checkActive(MulgaraTransactionException.class);
try {
activateXARes(MulgaraTransactionException.class);
try {
long la = lastActive;
lastActive = -1;
operation.execute(context,
context.getSystemResolver(),
metadata);
lastActive = (la != -1) ? System.currentTimeMillis() : -1;
} finally {
deactivateXARes(MulgaraTransactionException.class);
}
} catch (Throwable th) {
try {
heuristicRollback(th.toString());
} catch (MulgaraTransactionException ex) {
logger.error("Error in rollback after operation failure", ex);
}
throw new MulgaraTransactionException("Operation failed", th);
}
} finally {
releaseMutex();
}
}
public AnswerOperationResult execute(AnswerOperation ao) throws TuplesException {
acquireMutex(0, false, TuplesException.class);
try {
checkActive(TuplesException.class);
try {
activateXARes(TuplesException.class);
try {
long la = lastActive;
lastActive = -1;
ao.execute();
lastActive = (la != -1) ? System.currentTimeMillis() : -1;
return ao.getResult();
} finally {
deactivateXARes(TuplesException.class);
}
} catch (Throwable th) {
try {
logger.warn("Error in answer operation triggered rollback", th);
heuristicRollback(th.toString());
} catch (MulgaraTransactionException ex) {
logger.error("Error in rollback after answer-operation failure", ex);
}
throw new TuplesException("Request failed", th);
}
} finally {
releaseMutex();
}
}
// FIXME: See if we can't rearrange things to allow this to be deleted.
public void execute(TransactionOperation to) throws MulgaraTransactionException {
acquireMutex(0, false, MulgaraTransactionException.class);
try {
checkActive(MulgaraTransactionException.class);
activateXARes(MulgaraTransactionException.class);
try {
long la = lastActive;
lastActive = -1;
to.execute();
lastActive = (la != -1) ? System.currentTimeMillis() : -1;
} finally {
deactivateXARes(MulgaraTransactionException.class);
}
} finally {
releaseMutex();
}
}
private <T extends Throwable> void checkActive(Class<T> exc) throws T {
if (hRollback)
throw MulgaraTransactionFactory.newException(exc, "Transaction was heuristically rolled back. Reason: " + rollbackCause);
if (rollback)
throw MulgaraTransactionFactory.newException(exc, "Transaction was rolled back. Reason: " + rollbackCause);
if (completed)
throw MulgaraTransactionFactory.newException(exc, "Transaction has been completed");
}
private <T extends Throwable> void activateXARes(Class<T> exc) throws T {
assert xaResState != ResourceState.FINISHED : "Unexpected resource-state: state=" + xaResState;
if (xaResState == ResourceState.ACTIVE) {
inuse++;
return;
}
assert inuse == 0 : "Unexpected use-count: state=" + xaResState + ", inuse=" + inuse;
boolean wasStarted = (xaResState == ResourceState.SUSPENDED);
xaResState = ResourceState.ACTIVE;
int flags = wasStarted ? XAResource.TMRESUME : XAResource.TMNOFLAGS;
for (EnlistableResource eres : wasStarted ? started : enlisted) {
XAResource res = xaResources.get(eres);
try {
res.start(xid, flags);
if (!wasStarted) {
started.add(eres);
}
} catch (XAException xae) {
started.remove(eres);
if (isRollback(xae)) needRollback.add(eres);
// end started resources so we're in a consistent state
flags = wasStarted ? XAResource.TMSUSPEND : XAResource.TMFAIL;
for (EnlistableResource eres2 : wasStarted ? started : enlisted) {
XAResource res2 = xaResources.get(eres2);
if (res2 == res) {
break;
}
try {
res2.end(xid, flags);
} catch (XAException xae2) {
logger.error("Error ending resource '" + res2 + "' after start failure", xae2);
}
}
xaResState = wasStarted ? ResourceState.SUSPENDED : ResourceState.FINISHED;
throw MulgaraTransactionFactory.newExceptionOrCause(exc, "Error starting resource '" + res + "'", xae);
}
}
inuse = 1;
}
private <T extends Throwable> void deactivateXARes(Class<T> exc) throws T {
if (xaResState == ResourceState.FINISHED) {
return;
}
assert xaResState == ResourceState.ACTIVE : "Unexpected resource-state: state=" + xaResState;
assert inuse > 0 : "Unexpected use-count: state=" + xaResState + ", inuse=" + inuse;
inuse--;
if (inuse > 0) {
return;
}
int flags = XAResource.TMSUSPEND;
T error = null;
for (Iterator<EnlistableResource> iter = started.iterator(); iter.hasNext(); ) {
EnlistableResource eres = iter.next();
XAResource res = xaResources.get(eres);
try {
res.end(xid, flags);
} catch (XAException xae) {
iter.remove();
if (isRollback(xae)) needRollback.add(eres);
if (error == null) {
error = MulgaraTransactionFactory.newExceptionOrCause(exc, "Error ending resource '" + res + "'", xae);
} else {
logger.error("Error ending resource '" + res + "'", xae);
}
}
}
xaResState = ResourceState.SUSPENDED;
if (error != null) {
throw error;
}
}
private void endXARes(boolean success) throws XAException {
if (xaResState != ResourceState.SUSPENDED && xaResState != ResourceState.ACTIVE) {
return;
}
int flags = success ? XAResource.TMSUCCESS : XAResource.TMFAIL;
XAException error = null;
for (Iterator<EnlistableResource> iter = started.iterator(); iter.hasNext(); ) {
EnlistableResource eres = iter.next();
XAResource res = xaResources.get(eres);
try {
res.end(xid, flags);
} catch (XAException xae) {
iter.remove();
if (isRollback(xae)) needRollback.add(eres);
if (error == null) {
error = xae;
} else {
logger.error("Error ending resource '" + res + "'", xae);
}
}
}
xaResState = ResourceState.FINISHED;
if (error != null) {
throw error;
}
}
public void enlist(EnlistableResource enlistable) throws MulgaraTransactionException {
acquireMutex(0, false, MulgaraTransactionException.class);
try {
try {
XAResource res = enlistable.getXAResource();
for (EnlistableResource eres : enlisted) {
if (res.isSameRM(xaResources.get(eres))) {
return;
}
}
enlisted.add(enlistable);
xaResources.put(enlistable, res);
if (xaResState == ResourceState.ACTIVE) {
res.start(xid, XAResource.TMNOFLAGS);
started.add(enlistable);
} else if (xaResState == ResourceState.SUSPENDED) {
res.start(xid, XAResource.TMNOFLAGS);
res.end(xid, XAResource.TMSUSPEND);
started.add(enlistable);
}
} catch (XAException ex) {
if (isRollback(ex)) needRollback.add(enlistable);
throw new MulgaraTransactionException("Failed to enlist resource", ex);
}
} finally {
releaseMutex();
}
}
public long lastActive() {
return lastActive;
}
//
// Methods used to manage transaction from XAResource.
//
void commit(Xid xid) throws XAException {
report("commit");
acquireMutex(0, true, XAException.class);
try {
lastActive = -1;
// FIXME: Consider the possiblity prepare failed, or was incomplete.
for (EnlistableResource er : prepared) {
xaResources.get(er).commit(xid, false);
committed.add(er);
}
cleanupTransaction();
} finally {
releaseMutex();
}
}
boolean isHeuristicallyRollbacked() {
return hRollback;
}
boolean isHeuristicallyCommitted() {
return false;
}
int getHeuristicCode() {
return heurCode;
}
String getRollbackCause() {
return rollbackCause;
}
boolean isRollbacked() {
return rollback;
}
void prepare(Xid xid) throws XAException {
report("prepare");
acquireMutex(0, true, XAException.class);
try {
long la = lastActive;
lastActive = -1;
endXARes(true);
for (Iterator<EnlistableResource> iter = started.iterator(); iter.hasNext(); ) {
EnlistableResource er = iter.next();
try {
if (xaResources.get(er).prepare(xid) == XAResource.XA_OK) {
prepared.add(er);
} else {
iter.remove();
}
} catch (XAException xae) {
if (isRollback(xae)) iter.remove();
throw xae;
}
}
lastActive = (la != -1) ? System.currentTimeMillis() : -1;
} finally {
releaseMutex();
}
}
/**
* Perform rollback. Only throws exception if transaction is subject to
* Heuristic Completion.
*/
void rollback(Xid xid) throws XAException {
report("rollback");
acquireMutex(0, true, XAException.class);
try {
lastActive = -1;
try {
rollback = true;
Map<EnlistableResource, XAException> rollbackFailed = new HashMap<EnlistableResource, XAException>();
try {
endXARes(false);
} catch (XAException ex) {
logger.error("Error ending resources - attempting to rollback anyway", ex);
}
started.addAll(needRollback);
for (EnlistableResource er : started) {
try {
if (!committed.contains(er)) {
xaResources.get(er).rollback(xid);
rollbacked.add(er);
}
} catch (XAException ex) {
logger.error("Attempt to rollback resource failed", ex);
rollbackFailed.put(er, ex);
}
}
if (rollbackFailed.isEmpty()) {
if (committed.isEmpty()) { // Clean failure and rollback
return; // SUCCESSFUL ROLLBACK - RETURN
} else { // No rollback-failure, but partial commit
heurCode = XAException.XA_HEURMIX;
throw new XAException(heurCode);
}
} else {
// Something went wrong - start by assuming if one committed all committed
heurCode = (committed.isEmpty()) ? 0 : XAException.XA_HEURCOM;
// Then check every rollback failure code for a contradiction to all committed.
for (XAException xaex : rollbackFailed.values()) {
switch (xaex.errorCode) {
case XAException.XA_HEURHAZ:
case XAException.XAER_NOTA:
case XAException.XAER_RMERR:
case XAException.XAER_RMFAIL:
case XAException.XAER_INVAL:
case XAException.XAER_PROTO:
// All these amount to not knowing the result - so we have a hazard
// unless we already know we have a mixed result.
if (heurCode != XAException.XA_HEURMIX) {
heurCode = XAException.XA_HEURHAZ;
}
break;
case XAException.XA_HEURCOM:
if (!rollbacked.isEmpty() || heurCode == XAException.XA_HEURRB) {
// We know something else was rollbacked, so we know we have a mixed result.
heurCode = XAException.XA_HEURMIX;
} else if (heurCode == 0) {
heurCode = XAException.XA_HEURCOM;
} // else it's a HEURHAZ or a HEURCOM and stays that way.
break;
case XAException.XA_HEURRB:
if (!committed.isEmpty() || heurCode == XAException.XA_HEURCOM) {
heurCode = XAException.XA_HEURMIX;
} else if (heurCode == 0) {
heurCode = XAException.XA_HEURRB;
} // else it's a HEURHAZ or a HEURRB and stays that way.
break;
case XAException.XA_HEURMIX:
// It can't get worse than, we know we have a mixed result.
heurCode = XAException.XA_HEURMIX;
break;
default:
// The codes above are the only codes permitted from a rollback() so
// anything else indicates a serious error in the resource-manager.
throw new XAException(XAException.XAER_RMERR);
}
}
throw new XAException(heurCode);
}
} finally {
cleanupTransaction();
}
} finally {
releaseMutex();
}
}
Xid getXid() {
return xid;
}
private void cleanupTransaction() throws XAException {
report("cleanupTransaction");
try {
factory.transactionComplete(this, rollbackCause);
} catch (MulgaraTransactionException em) {
try {
logger.error("Failed to cleanup transaction", em);
abortTransaction("Failure in cleanup", em);
throw new XAException(XAException.XAER_RMERR);
} catch (MulgaraTransactionException em2) {
logger.error("Failed to abort transaction on cleanup failure", em2);
throw new XAException(XAException.XAER_RMFAIL);
}
} finally {
completed = true;
}
}
private <T extends Throwable> void acquireMutex(long timeout, boolean isXACompletion, Class<T> exc) throws T {
synchronized (factory.getMutexLock()) {
factory.acquireMutex(timeout, exc);
inXACompletion = isXACompletion;
}
}
private void releaseMutex() {
factory.releaseMutex();
}
private static boolean isRollback(XAException xae) {
return xae.errorCode >= XAException.XA_RBBASE && xae.errorCode <= XAException.XA_RBEND;
}
private void report(String desc) {
if (logger.isInfoEnabled()) {
logger.info(desc + ": " + System.identityHashCode(this));
}
}
}