/*
* Copyright 2008 The Topaz Foundation
*
* Licensed 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.
*
* Contributions:
*/
package org.mulgara.resolver.distributed;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import org.apache.log4j.Logger;
import org.mulgara.resolver.spi.AbstractXAResource;
import org.mulgara.resolver.spi.AbstractXAResource.RMInfo;
import org.mulgara.resolver.spi.ResolverFactory;
import org.mulgara.resolver.spi.AbstractXAResource.TxInfo;
/**
* This is an implementation of {@link XAResource} that presents a collection of xa-resources as
* a single xa-resource. In doing so, this implements parts of what a transaction-manager must
* provide in order to present a unified view. For example, a one-phase commit may need to be
* turned into a two-phase commit over the underlying resources, and all underlying resources must
* be aborted if an RMFAIL exception occurs.
*
* <p>XAResource's may be enlisted via {@link #enlistResource}; the enlistement is only valid for
* the duration of the transaction.
*
* <p>This class is partially thread-safe, as required by JTA. Specifically, {@link #prepare},
* {@link #commit}, and {@link #rollback} may be invoked concurrently with {@link #start} and
* {@link #end} so long as these do not involve the same xid. Also, there is no requirement that
* the same thread be used for any two operations on a given xid. However, {@link #start} and
* {@link #end} may not be nested, nor may any two methods be invoked concurrently with the same
* xid.
*
* <p>Limitations:
* <ul>
* <li>Transaction-id's (Xid's) are not remembered persistently, so {@link #recover} and
* {@link #forget} across a restart won't work</li>
* <li>{@link #isSameRM} cannot be properly implemented.</li>
* </ul>
*
* @created 2008-02-16
* @author Ronald Tschalär
* @copyright ©2008 <a href="http://www.topazproject.org/">Topaz Project</a>
* @licence Apache License v2.0
*/
public class MultiXAResource
extends AbstractXAResource<RMInfo<MultiXAResource.MultiTxInfo>,MultiXAResource.MultiTxInfo> {
private static final Logger logger = Logger.getLogger(MultiXAResource.class);
private final Set<XAResource> ended = new HashSet<XAResource>();
private volatile MultiTxInfo curTx;
/**
* Create a new Multi-XAResource.
*
* @param transactionTimeout transaction timeout period, in seconds
* @param resolverFactory the resolver-factory we belong to
*/
public MultiXAResource(int transactionTimeout, ResolverFactory resolverFactory) {
super(transactionTimeout, resolverFactory);
}
protected RMInfo<MultiTxInfo> newResourceManager() {
return new RMInfo<MultiTxInfo>();
}
protected MultiTxInfo newTransactionInfo() {
return new MultiTxInfo();
}
/**
* Enlist a resource in the current transaction. This may only be invoked while a transaction is
* in the ACTIVE state (between a {@link #start} and {@link #end}).
*
* @param res the resource to enlist
* @throws XAException if an error occurs
*/
public void enlistResource(XAResource res) throws XAException {
if (curTx == null)
throw new IllegalStateException("No transaction active");
if (logger.isDebugEnabled()) {
logger.debug("enlisting resource '" + res + "' in txn '" + formatXid(curTx.xid) + "'");
}
int elapsed = (int)((System.currentTimeMillis() - curTx.startTime) / 1000);
res.setTransactionTimeout(Math.max(transactionTimeout - elapsed, 10));
for (XAResource r : curTx.resources) {
if (res.isSameRM(r)) {
res.start(curTx.xid, TMJOIN);
return;
}
}
curTx.resources.add(res);
try {
res.start(curTx.xid, TMNOFLAGS);
} catch (Throwable t) {
if (isCompleted(t)) {
curTx.resources.remove(res);
// turn this into a non-rmfail exception so rollback() is called for us
t = (XAException)new XAException(XAException.XAER_RMERR).initCause(t);
} else {
ended.add(res);
}
throwExc(t);
}
}
/* flags - One of TMNOFLAGS, TMJOIN, or TMRESUME
* Possible exceptions are: XA_RB*, XAER_RMERR, XAER_RMFAIL, XAER_DUPID, XAER_OUTSIDE, XAER_NOTA,
* XAER_INVAL, or XAER_PROTO
*/
protected void doStart(MultiTxInfo txInfo, int flags, boolean isNew) throws XAException {
// check we're not already between a start and end
if (curTx != null) throw new XAException(XAException.XAER_PROTO);
// mark that we're active
curTx = txInfo;
txInfo.state = MultiTxInfo.States.ACTIVE;
// propagate the start
if (flags == TMJOIN) {
throw new XAException("Can't handle joins");
} else if (flags == XAResource.TMRESUME) {
for (Iterator<XAResource> iter = txInfo.resources.iterator(); iter.hasNext(); ) {
XAResource r = iter.next();
try {
r.start(txInfo.xid, flags);
} catch (Throwable t) {
if (isCompleted(t)) {
iter.remove();
// turn this into a non-rmfail exception so rollback() is called for us
t = (XAException)new XAException(XAException.XAER_RMERR).initCause(t);
}
for (Iterator<XAResource> iter2 = txInfo.resources.iterator(); iter2.hasNext(); ) {
XAResource r2 = iter2.next();
if (r2 == r) continue;
try {
r2.end(txInfo.xid, TMFAIL);
} catch (Throwable t2) {
logger.error("Error suspending resource '" + r2 + "' while handling aborted start", t2);
if (isCompleted(t2)) iter2.remove();
}
}
curTx = null;
txInfo.state = MultiTxInfo.States.IDLE;
throwExc(t);
}
}
}
}
/* flags - One of TMSUCCESS, TMFAIL, or TMSUSPEND
* Possible XAException values are: XAER_RMERR, XAER_RMFAIL, XAER_NOTA, XAER_INVAL, XAER_PROTO, or
* XA_RB*.
*/
protected void doEnd(MultiTxInfo txInfo, int flags) throws XAException {
// check we're between a start and end
if (curTx != txInfo) throw new XAException(XAException.XAER_PROTO);
// propagate the end
Throwable exc = null;
for (Iterator<XAResource> iter = txInfo.resources.iterator(); iter.hasNext(); ) {
XAResource r = iter.next();
if (ended.contains(r)) continue;
try {
r.end(txInfo.xid, flags);
} catch (Throwable t) {
if (isCompleted(t)) iter.remove();
else if (flags == TMSUSPEND) ended.add(r);
if (exc == null) exc = t;
else logger.error("2nd or more exception during end; resource = '" + r + "'", t);
}
}
// mark that we're idle
curTx = null;
txInfo.state = MultiTxInfo.States.IDLE;
// clean up if we failed on suspend
if (flags == TMSUSPEND && exc != null) {
for (Iterator<XAResource> iter = txInfo.resources.iterator(); iter.hasNext(); ) {
XAResource r = iter.next();
if (ended.contains(r)) continue;
try {
r.end(txInfo.xid, TMFAIL);
} catch (Throwable t) {
if (isCompleted(t)) iter.remove();
logger.error("2nd or more exception during end; resource = '" + r + "'", t);
}
}
}
ended.clear();
// turn this into a non-rmfail exception so rollback() is called for us
if (isCompleted(exc)) {
throw (XAException)new XAException(XAException.XAER_RMERR).initCause(exc);
}
if (exc != null) throwExc(exc);
}
/* Possible exception values are: XA_RB*, XAER_RMERR, XAER_RMFAIL, XAER_NOTA, XAER_INVAL, or
* XAER_PROTO
*/
protected int doPrepare(MultiTxInfo txInfo) throws XAException {
// check that this tx isn't in the ACTIVE state
if (txInfo == curTx) throw new XAException(XAException.XAER_PROTO);
// tell everyone to prepare and gather their votes
Throwable exc = null;
txInfo.state = MultiTxInfo.States.PREPARING;
for (Iterator<XAResource> iter = txInfo.resources.iterator(); iter.hasNext(); ) {
XAResource r = iter.next();
try {
if (r.prepare(txInfo.xid) == XA_RDONLY) {
iter.remove(); // read-only don't participate in commit/rollback
}
} catch (Throwable t) {
if (logger.isDebugEnabled()) {
logger.debug("prepare vetoed by '" + r + "'", t);
}
if (isCompleted(t) || isRollback(t)) iter.remove();
if (exc == null) exc = t;
}
}
txInfo.state = MultiTxInfo.States.PREPARED;
// turn this into a non-rmfail/no-rb exception so rollback() is called for us
if (isCompleted(exc) || isRollback(exc)) {
throw (XAException)new XAException(XAException.XAER_RMERR).initCause(exc);
}
if (exc != null) throwExc(exc);
//return (txInfo.resources.size() > 0) ? XA_OK : XA_RDONLY;
return XA_OK; // JOTM bug
}
/* Possible XAExceptions are XA_HEURHAZ, XA_HEURCOM, XA_HEURRB, XA_HEURMIX, XAER_RMERR,
* XAER_RMFAIL, XAER_NOTA, XAER_INVAL, or XAER_PROTO
*/
protected void doCommit(MultiTxInfo txInfo) throws XAException {
// check that this tx isn't in the ACTIVE state
if (txInfo == curTx) throw new XAException(XAException.XAER_PROTO);
Throwable exc = null;
txInfo.state = MultiTxInfo.States.COMMITTING;
try {
// ready to do commit
int numCmt = 0;
for (Iterator<XAResource> iter = txInfo.resources.iterator(); iter.hasNext(); ) {
XAResource r = iter.next();
try {
if (numCmt == 0 && exc instanceof XAException &&
(isRollback(exc) || ((XAException)exc).errorCode == XAException.XA_HEURRB)) {
r.rollback(txInfo.xid); // the first commit failed with a RB, so we roll back all
} else {
r.commit(txInfo.xid, false);
numCmt++;
}
iter.remove();
} catch (Throwable t) {
if (!isHeuristic(t)) iter.remove();
if (exc == null) {
exc = t;
} else if (isHeuristic(t) && isHeuristic(exc) &&
((XAException)t).errorCode != ((XAException)exc).errorCode &&
((XAException)exc).errorCode != XAException.XA_HEURMIX ) {
exc = (XAException)new XAException(XAException.XA_HEURMIX).initCause(exc);
logger.error("2nd or more exception during commit; resource = '" + r + "'", t);
} else {
logger.error("2nd or more exception during commit; resource = '" + r + "'", t);
}
}
}
if (exc instanceof XAException) {
XAException xae = (XAException)exc;
if (xae.errorCode == XAException.XA_HEURMIX) throw xae;
if (numCmt == 0 && xae.errorCode == XAException.XA_HEURRB) throw xae;
}
if (exc != null) {
if (numCmt == 0) throw (XAException)new XAException(XAException.XA_HEURRB).initCause(exc);
else throw (XAException)new XAException(XAException.XA_HEURMIX).initCause(exc);
}
} finally {
txInfo.state = MultiTxInfo.States.FINISHED;
}
}
/* Possible XAExceptions are XA_HEURHAZ, XA_HEURCOM, XA_HEURRB, XA_HEURMIX, XAER_RMERR,
* XAER_RMFAIL, XAER_NOTA, XAER_INVAL, or XAER_PROTO
*/
protected void doRollback(MultiTxInfo txInfo) throws XAException {
// check that this tx isn't in the ACTIVE state
if (txInfo == curTx) throw new XAException(XAException.XAER_PROTO);
Throwable exc = null;
txInfo.state = MultiTxInfo.States.ROLLINGBACK;
try {
for (Iterator<XAResource> iter = txInfo.resources.iterator(); iter.hasNext(); ) {
XAResource r = iter.next();
try {
r.rollback(txInfo.xid);
iter.remove();
} catch (Throwable t) {
if (!isHeuristic(t)) iter.remove();
if (exc == null) {
exc = t;
} else if (isHeuristic(t) && isHeuristic(exc) &&
((XAException)t).errorCode != ((XAException)exc).errorCode &&
((XAException)exc).errorCode != XAException.XA_HEURMIX ) {
exc = (XAException)new XAException(XAException.XA_HEURMIX).initCause(exc);
logger.error("2nd or more exception during rollback; resource = '" + r + "'", t);
} else {
logger.error("2nd or more exception during rollback; resource = '" + r + "'", t);
}
}
}
if (exc instanceof XAException) {
if (((XAException)exc).errorCode == XAException.XA_HEURMIX) throw (XAException)exc;
}
if (exc != null) {
throw (XAException)new XAException(XAException.XA_HEURHAZ).initCause(exc);
}
} finally {
txInfo.state = MultiTxInfo.States.FINISHED;
}
}
// Possible exception values are: XAER_RMERR, XAER_RMFAIL, XAER_NOTA, XAER_INVAL, or XAER_PROTO.
protected void doForget(MultiTxInfo txInfo) throws XAException {
Throwable exc = null;
for (Iterator<XAResource> iter = txInfo.resources.iterator(); iter.hasNext(); ) {
XAResource r = iter.next();
try {
r.forget(txInfo.xid);
iter.remove();
} catch (Throwable t) {
if (isCompleted(t)) {
logger.debug("transaction " + formatXid(txInfo.xid) + " was not active on resource '" +
r + "'", t);
iter.remove();
continue;
}
if (exc == null) {
exc = t;
} else {
logger.error("2nd or more exception during forget; resource = '" + r + "'", t);
}
}
}
if (exc != null) throwExc(exc);
}
/* flag - One of TMSTARTRSCAN, TMENDRSCAN, TMNOFLAGS
* Possible exception values are: XAER_RMERR, XAER_RMFAIL, XAER_INVAL, and XAER_PROTO
*/
public Xid[] recover(int flag) throws XAException {
Set<Xid> res = new HashSet<Xid>();
if ((flag & TMSTARTRSCAN) != 0) {
for (MultiTxInfo txInfo : resourceManager.transactions.values()) {
if (txInfo.state == MultiTxInfo.States.PREPARING ||
txInfo.state == MultiTxInfo.States.PREPARED ||
txInfo.state == MultiTxInfo.States.COMMITTING ||
txInfo.state == MultiTxInfo.States.ROLLINGBACK ||
txInfo.state == MultiTxInfo.States.FINISHED) {
res.add(txInfo.xid);
}
}
}
return res.toArray(new Xid[res.size()]);
}
/**
* Tests whether the exception indicates that the resource has completed its participation in
* the transaction. See Table 6.4 (page 62) of X/Open for RMFAIL; for NOTA see X/Open pages 37 ff
* (xa_end), page 62 (Table 6.4), page 15 ("Rollback-Only", last two sentences), page 18
* ("Unilateral RM Action"),
*
* @param t the exception to test
* @return true if the resource is done with this transaction
*/
private static boolean isCompleted(Throwable t) {
return (t instanceof XAException) &&
(((XAException)t).errorCode == XAException.XAER_RMFAIL ||
((XAException)t).errorCode == XAException.XAER_NOTA);
}
private static final void throwExc(Throwable t) throws XAException {
if (t instanceof XAException) throw (XAException)t;
if (t instanceof RuntimeException) throw (RuntimeException)t;
throw (Error)t;
}
public static class MultiTxInfo extends TxInfo {
public enum States { IDLE, ACTIVE, PREPARING, PREPARED, COMMITTING, ROLLINGBACK, FINISHED }
// should be a Set, but easier to test with List
public final List<XAResource> resources = new ArrayList<XAResource>();
public States state = States.IDLE;
public long startTime = System.currentTimeMillis();
}
}