/*
* Copyright 2013, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags.
* See the copyright.txt in the distribution for a
* full listing of individual contributors.
* This copyrighted material is made available to anyone wishing to use,
* modify, copy, or redistribute it subject to the terms and conditions
* of the GNU Lesser General Public License, v. 2.1.
* This program is distributed in the hope that it will be useful, but WITHOUT A
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public License,
* v.2.1 along with this distribution; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
* (C) 2013
* @author JBoss Inc.
*/
package com.arjuna.ats.internal.jta.resources.arjunacore;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.Map;
import java.util.Vector;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import com.arjuna.ats.arjuna.exceptions.ObjectStoreException;
import com.arjuna.ats.internal.jta.resources.XAResourceErrorHandler;
import com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionSynchronizationRegistryImple;
import org.jboss.tm.ConnectableResource;
import org.jboss.tm.XAResourceWrapper;
import com.arjuna.ats.arjuna.ObjectType;
import com.arjuna.ats.arjuna.common.Uid;
import com.arjuna.ats.arjuna.coordinator.AbstractRecord;
import com.arjuna.ats.arjuna.coordinator.BasicAction;
import com.arjuna.ats.arjuna.coordinator.RecordType;
import com.arjuna.ats.arjuna.coordinator.TwoPhaseOutcome;
import com.arjuna.ats.arjuna.coordinator.TxControl;
import com.arjuna.ats.arjuna.logging.tsLogger;
import com.arjuna.ats.arjuna.recovery.RecoveryManager;
import com.arjuna.ats.arjuna.recovery.RecoveryModule;
import com.arjuna.ats.arjuna.state.InputObjectState;
import com.arjuna.ats.arjuna.state.OutputObjectState;
import com.arjuna.ats.internal.jta.recovery.arjunacore.CommitMarkableResourceRecordRecoveryModule;
import com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionImple;
import com.arjuna.ats.internal.jta.xa.XID;
import com.arjuna.ats.jta.common.JTAEnvironmentBean;
import com.arjuna.ats.jta.logging.jtaLogger;
import com.arjuna.ats.jta.utils.XAHelper;
import com.arjuna.ats.jta.xa.XidImple;
import com.arjuna.common.internal.util.propertyservice.BeanPopulator;
/**
* The CommitMarkableResourceRecord does not support nested transactions
*
* If the database is down forever that a CommitMarkableResourceRecord is linked
* to, it will have the side effect of never expiring a RecoverAtomicAction.
*
* The CommitMarkableResourceRecord assumes the following table has been
* created:
*
* syb:
*
* create table xids (xid varbinary(144), transactionManagerID varchar(64),
* actionuid varbinary(28))
*
* ora:
*
* create table xids (xid RAW(144), transactionManagerID varchar(64), actionuid
* RAW(28))
*
* psql:
*
* create table xids (xid bytea, transactionManagerID varchar(64), actionuid
* bytea)
*
* h2:
*
* create table xids (xid varbinary(144), transactionManagerID varchar(64),
* actionuid varbinary(28))
*
* sybase notes: sp_configure "lock scheme",0,datarows
*/
public class CommitMarkableResourceRecord extends AbstractRecord {
private final String tableName;
private Xid xid;
private ConnectableResource connectableResource;
private boolean onePhase = false;
private String commitMarkableJndiName;
private boolean committed;
private int heuristic;
private BasicAction basicAction;
private String productName;
private String productVersion;
private boolean hasCompleted;
private static CommitMarkableResourceRecordRecoveryModule commitMarkableResourceRecoveryModule;
private static final JTAEnvironmentBean jtaEnvironmentBean = BeanPopulator
.getDefaultInstance(JTAEnvironmentBean.class);
private static final Map<String, String> commitMarkableResourceTableNameMap = jtaEnvironmentBean
.getCommitMarkableResourceTableNameMap();
private static final String defaultTableName = jtaEnvironmentBean
.getDefaultCommitMarkableTableName();
private boolean isPerformImmediateCleanupOfBranches = jtaEnvironmentBean
.isPerformImmediateCleanupOfCommitMarkableResourceBranches();
private Connection preparedConnection;
private static final boolean isNotifyRecoveryModuleOfCompletedBranches = jtaEnvironmentBean
.isNotifyCommitMarkableResourceRecoveryModuleOfCompleteBranches();
private static final Map<String, Boolean> isPerformImmediateCleanupOfCommitMarkableResourceBranchesMap = jtaEnvironmentBean
.getPerformImmediateCleanupOfCommitMarkableResourceBranchesMap();
static {
commitMarkableResourceRecoveryModule = null;
RecoveryManager recMan = RecoveryManager.manager();
Vector recoveryModules = recMan.getModules();
if (recoveryModules != null) {
Enumeration modules = recoveryModules.elements();
while (modules.hasMoreElements()) {
RecoveryModule m = (RecoveryModule) modules.nextElement();
if (m instanceof CommitMarkableResourceRecordRecoveryModule) {
commitMarkableResourceRecoveryModule = (CommitMarkableResourceRecordRecoveryModule) m;
break;
}
}
}
}
/**
* For recovery
*/
public CommitMarkableResourceRecord() {
if (tsLogger.logger.isTraceEnabled()) {
tsLogger.logger.trace("CommitMarkableResourceRecord.CommitMarkableResourceRecord (), record id=" + order());
}
heuristic = TwoPhaseOutcome.FINISH_OK;
tableName = null;
}
public CommitMarkableResourceRecord(TransactionImple tx,
ConnectableResource xaResource, final Xid xid,
BasicAction basicAction) throws IllegalStateException,
RollbackException, SystemException {
super(new Uid(), null, ObjectType.ANDPERSISTENT);
if (tsLogger.logger.isTraceEnabled()) {
tsLogger.logger.trace("CommitMarkableResourceRecord.CommitMarkableResourceRecord ( " + tx + ", " + xaResource + ", "
+ xid + ", " + basicAction + " ), record id=" + order());
}
this.connectableResource = xaResource;
XAResourceWrapper xaResourceWrapper = ((XAResourceWrapper) xaResource);
this.commitMarkableJndiName = xaResourceWrapper.getJndiName();
this.productName = xaResourceWrapper.getProductName();
this.productVersion = xaResourceWrapper.getProductVersion();
this.xid = xid;
this.basicAction = basicAction;
heuristic = TwoPhaseOutcome.FINISH_OK;
String tableName = commitMarkableResourceTableNameMap
.get(commitMarkableJndiName);
if (tableName != null) {
this.tableName = tableName;
} else {
this.tableName = defaultTableName;
}
Boolean boolean1 = isPerformImmediateCleanupOfCommitMarkableResourceBranchesMap
.get(commitMarkableJndiName);
if (boolean1 != null) {
isPerformImmediateCleanupOfBranches = boolean1;
}
if (isPerformImmediateCleanupOfBranches) {
// a session synch may enlist a CMR in a transaction so this sycnh must be correctly ordered
new TransactionSynchronizationRegistryImple()
.registerInterposedSynchronization(new Synchronization() {
@Override
public void beforeCompletion() {
}
@Override
public void afterCompletion(int status) {
if (!onePhase && status == Status.STATUS_COMMITTED) {
Connection connection = null;
try {
connection = ((Connection) connectableResource
.getConnection());
connection.setAutoCommit(false);
String sql = "DELETE from "
+ CommitMarkableResourceRecord.this.tableName
+ " where xid in (?)";
PreparedStatement prepareStatement = connection
.prepareStatement(sql);
try {
XID toSave = ((XidImple) xid).getXID();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(
baos);
dos.writeInt(toSave.formatID);
dos.writeInt(toSave.gtrid_length);
dos.writeInt(toSave.bqual_length);
dos.writeInt(toSave.data.length);
dos.write(toSave.data);
dos.flush();
prepareStatement.setBytes(1, baos.toByteArray());
if (prepareStatement.executeUpdate() != 1) {
tsLogger.logger
.error("Update was not successfull");
connection.rollback();
} else {
connection.commit();
}
} catch (IOException e) {
tsLogger.logger
.warn("Could not generate prepareStatement paramaters",
e);
} finally {
try {
prepareStatement.close();
} catch (SQLException e) {
tsLogger.logger
.warn("Could not close the prepared statement",
e);
}
}
} catch (Throwable e1) {
tsLogger.logger
.warn("Could not delete CommitMarkableResourceRecord entry, will rely on RecoveryModule",
e1);
} finally {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
tsLogger.logger
.warn("Could not close the preparedConnection",
e);
}
}
}
}
}
});
} else if (isNotifyRecoveryModuleOfCompletedBranches) {
// a session synch may enlist a CMR in a transaction so this sycnh must be correctly ordered
new TransactionSynchronizationRegistryImple()
.registerInterposedSynchronization(new Synchronization() {
@Override
public void beforeCompletion() {
}
@Override
public void afterCompletion(int status) {
if (!onePhase && status == Status.STATUS_COMMITTED) {
commitMarkableResourceRecoveryModule
.notifyOfCompletedBranch(
commitMarkableJndiName, xid);
}
}
});
}
}
public String getProductName() {
return productName;
}
public String getProductVersion() {
return productVersion;
}
public String getJndiName() {
return commitMarkableJndiName;
}
public void updateOutcome(boolean committed) {
this.hasCompleted = true;
this.committed = committed;
}
/**
* We need to save this so we know there was a ConnectableResource in the
* intentions list.
*/
public boolean doSave() {
return true;
}
public boolean save_state(OutputObjectState os, int t) {
boolean res = false;
try {
// We store these information so that during recovery we can query
// the resource
// manager to see if it had committed prior to any potential crash
if (tsLogger.logger.isTraceEnabled()) {
tsLogger.logger.trace("pack: " + commitMarkableJndiName);
}
os.packString(commitMarkableJndiName);
if (tsLogger.logger.isTraceEnabled()) {
tsLogger.logger.trace("pack: " + xid);
}
XidImple.pack(os, xid);
os.packBoolean(hasCompleted);
if (hasCompleted) {
os.packBoolean(committed);
// os.packInt(heuristic);
}
os.packString(productName);
os.packString(productVersion);
res = super.save_state(os, t);
} catch (Exception e) {
jtaLogger.logger.warn(
"Could not save_state: " + XAHelper.xidToString(xid), e);
}
return res;
}
public boolean restore_state(InputObjectState os, int t) {
boolean res = false;
try {
commitMarkableJndiName = os.unpackString();
if (tsLogger.logger.isTraceEnabled()) {
tsLogger.logger.trace("unpack: " + commitMarkableJndiName);
}
xid = XidImple.unpack(os);
if (tsLogger.logger.isTraceEnabled()) {
tsLogger.logger.trace("unpack: " + xid);
}
if (os.unpackBoolean()) {
committed = os.unpackBoolean();
// heuristic = os.unpackInt();
} else {
// This will return true if the
// CommitMarkableRecoveryModule is
// between phases and the XID
// has not been GC'd
committed = commitMarkableResourceRecoveryModule.wasCommitted(commitMarkableJndiName, xid);
}
productName = os.unpackString();
productVersion = os.unpackString();
res = super.restore_state(os, t);
} catch (Exception e) {
String resInfo = connectableResource == null ? "" : connectableResource.toString();
jtaLogger.i18NLogger.warn_resources_arjunacore_restorestateerror(
resInfo, XAHelper.xidToString(xid), e);
}
return res;
}
/**
* This will add the required recovery data about this resource into the
* resources preparedConnection. If the preparedConnection is in read only
* mode, we do not need to persist this information.
*/
public int topLevelPrepare() {
if (tsLogger.logger.isTraceEnabled()) {
tsLogger.logger.trace("CommitMarkableResourceRecord.topLevelPrepare for " + this + ", record id=" + order());
}
try {
PreparedStatement prepareStatement = null;
preparedConnection = (Connection) connectableResource
.getConnection();
try {
prepareStatement = preparedConnection
.prepareStatement("insert into "
+ tableName
+ " (xid, transactionManagerID, actionuid) values (?,?,?)");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
XID toSave = ((XidImple) xid).getXID();
dos.writeInt(toSave.formatID);
dos.writeInt(toSave.gtrid_length);
dos.writeInt(toSave.bqual_length);
dos.writeInt(toSave.data.length);
dos.write(toSave.data);
dos.flush();
prepareStatement.setBytes(1, baos.toByteArray());
prepareStatement.setString(2, TxControl.getXANodeName());
prepareStatement.setBytes(3, basicAction.get_uid().getBytes());
if (prepareStatement.executeUpdate() != 1) {
tsLogger.logger.warn("Update was not successful");
removeConnection();
return TwoPhaseOutcome.PREPARE_NOTOK;
}
} finally {
if (prepareStatement != null)
prepareStatement.close();
}
return TwoPhaseOutcome.PREPARE_OK;
} catch (Throwable t) {
tsLogger.logger.error(
"Could not add recovery data to the 1PC resource", t);
return TwoPhaseOutcome.PREPARE_NOTOK;
}
}
public int topLevelAbort() {
if (tsLogger.logger.isTraceEnabled()) {
tsLogger.logger.trace("CommitMarkableResourceRecord.topLevelAbort for " + this + ", record id=" + order());
}
try {
try {
// This can never be null as it can only ever be called before
// crash
// when we have a reference
// on a connectableResource still. Although topLevelAbort can be
// called for RecoverAtomicAction, it
// can only do that for resources after the head position in the
// preparedList, we know this resource
// must be first
((XAResource) connectableResource).rollback(xid);
hasCompleted = true;
committed = false;
return TwoPhaseOutcome.FINISH_OK;
} catch (XAException e) {
XAResourceErrorHandler handler = new XAResourceErrorHandler(e, (XAResource) connectableResource, xid);
heuristic = handler.handleCMRRollbackError();
return heuristic;
} catch (Throwable e) {
jtaLogger.i18NLogger.warn_resources_arjunacore_rollbackerror(XAHelper.xidToString(xid),
connectableResource.toString(), "-", e);
return TwoPhaseOutcome.FINISH_ERROR;
}
} finally {
removeConnection();
}
}
public int topLevelCommit() {
if (tsLogger.logger.isTraceEnabled()) {
tsLogger.logger.trace("CommitMarkableResourceRecord.topLevelCommit for " + this + ", record id=" + order());
}
return commit(false);
}
public int topLevelOnePhaseCommit() {
if (tsLogger.logger.isTraceEnabled()) {
tsLogger.logger.trace("CommitMarkableResourceRecord.topLevelOnePhaseCommit for " + this + ", record id=" + order());
}
return commit(true);
}
private int commit(boolean onePhase) {
// As this can be called during recovery we check to see if we have the
// pre-crash reference
this.onePhase = onePhase;
if (connectableResource != null) {
try {
((XAResource) connectableResource).commit(xid, false);
hasCompleted = true;
committed = true;
return TwoPhaseOutcome.FINISH_OK;
} catch (XAException e) {
XAResourceErrorHandler handler = new XAResourceErrorHandler(e, (XAResource) connectableResource, xid);
heuristic = handler.handleCMRCommitError(onePhase);
committed = handler.isCommitted();
return heuristic;
} catch (Throwable e) {
jtaLogger.i18NLogger.warn_resources_arjunacore_commitxaerror(XAHelper.xidToString(xid),
connectableResource.toString(), "-", e);
return TwoPhaseOutcome.FINISH_ERROR;
} finally {
if (!isPerformImmediateCleanupOfBranches) {
removeConnection();
}
}
} else {
// This is a recovery scenario
if (committed) {
return TwoPhaseOutcome.FINISH_OK;
} else {
return TwoPhaseOutcome.HEURISTIC_ROLLBACK;
}
}
}
private final void removeConnection() {
if (preparedConnection != null) {
try {
preparedConnection.close();
preparedConnection = null;
} catch (SQLException e) {
tsLogger.logger.warn("Could not close the preparedConnection", e);
}
}
}
@Override
public boolean forgetHeuristic() {
// This is a no-op for CMR resources
return true;
}
public int getHeuristic() {
return heuristic;
}
public Uid order() {
return Uid.minUid();
}
public boolean propagateOnCommit() {
return false;
}
public int typeIs() {
return RecordType.COMMITMARKABLERESOURCE;
}
public String type() {
return "/StateManager/AbstractRecord/CommitMarkableResourceRecord";
}
public Object value() {
return connectableResource;
}
public void setValue(Object o) {
}
public int nestedAbort() {
return TwoPhaseOutcome.FINISH_OK;
}
public int nestedCommit() {
return TwoPhaseOutcome.FINISH_ERROR;
}
public int nestedPrepare() {
return TwoPhaseOutcome.PREPARE_NOTOK;
}
public void merge(AbstractRecord a) {
}
public void alter(AbstractRecord a) {
}
public boolean shouldAdd(AbstractRecord a) {
return false;
}
public boolean shouldAlter(AbstractRecord a) {
return false;
}
public boolean shouldMerge(AbstractRecord a) {
return false;
}
public boolean shouldReplace(AbstractRecord a) {
return false;
}
}