ch.maxant.generic_jca_adapter.TransactionAssistanceXAResource Maven / Gradle / Ivy
/*
Copyright 2015 Ant Kutschera
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.
*/
package ch.maxant.generic_jca_adapter;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import ch.maxant.generic_jca_adapter.TransactionAssistanceFactory.CommitRollbackRecoveryCallback;
/**
* {@link XAResource} used by the transaction assistance JCA Adapter to bind callbacks
* into transactions.
*/
public class TransactionAssistanceXAResource implements XAResource, Serializable {
private static final long serialVersionUID = 1L;
/** If tracking state internally, then this filter finds
* files related to incomplete transactions */
private static FileFilter executeFilter = new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.getName().toLowerCase().startsWith("exec.");
}
};
private final Logger log = Logger.getLogger(this.getClass().getName());
/** the resource related to this {@link XAResource} */
private ManagedTransactionAssistance conn;
/** default timeout, as well as that set by the system */
private int timeout = 300;
public TransactionAssistanceXAResource(ManagedTransactionAssistance conn) {
this.conn = conn;
}
@Override
public void commit(Xid xid, boolean onePhase) throws XAException {
String s = XidImpl.asString(xid);
log.log(Level.INFO, "COMMIT " + onePhase + "/" + s);
//regardless of onePhase, we need to tell the external system that we are done and
//the previous call to the EXECUTE method should be COMMITTED.
try{
conn.getCommitRollbackRecoveryCallback().commit(s);
if(conn.isHandleRecoveryInternally()){
cleanupInternalTransactionState(xid);
}
conn.cleanup();
}catch(Exception e){
log.log(Level.SEVERE, "Failed to COMMIT", e);
int var = XAException.XA_RETRY;
throw new XAException(var);
}
}
@Override
public void end(Xid xid, int flags) throws XAException {
String s = "-";
if(flags == TMSUSPEND){
s = "TMSUSPEND";
}else if(flags == TMFAIL){
s = "TMFAIL";
}else if(flags == TMSUCCESS){
s = "TMSUCCESS";
}
log.log(Level.INFO, "END flags=" + s + "(" + flags + ")" + "/" + XidImpl.asString(xid));
//there is nothing to do at this stage, it is purely informational
}
@Override
public void forget(Xid xid) throws XAException {
log.log(Level.INFO, "FORGET " + XidImpl.asString(xid));
if(conn.isHandleRecoveryInternally()){
cleanupInternalTransactionState(xid);
}else{
//TODO send this to the callback?
}
}
@Override
public int getTransactionTimeout() throws XAException {
// TODO from config?
return timeout;
}
@Override
public boolean isSameRM(XAResource xares) throws XAException {
log.log(Level.FINEST, "isSameRM " + xares);
//TODO hmm not sure about this
if(xares instanceof TransactionAssistanceXAResource){
TransactionAssistanceXAResource other = (TransactionAssistanceXAResource) xares;
if(this.conn.getManagedConnectionFactoryId().equals(other.conn.getManagedConnectionFactoryId())){
return true;
}
}
return false;
}
@Override
public int prepare(Xid xid) throws XAException {
log.log(Level.INFO, "PREPARE " + XidImpl.asString(xid));
//if the EXECUTE was unsuccessful, we must not let a COMMIT occur, since it will fail
//7.6.2.8 of the JCA spec 1.6 says we must not erase knowledge
//of the transaction branch until commit or rollback is called.
// => do not call cleanup yet!
if(!conn.wasExecuteSuccessful()){
//vote to rollback :o(
throw new XAException(XAException.XA_RBROLLBACK);
}else{
//a successful execute is a guarantee that we can commit => therefore vote "OK to commit"
return XAResource.XA_OK;
}
}
@Override
public Xid[] recover(int flag) throws XAException {
List xids = new ArrayList();
switch(flag) {
case (XAResource.TMSTARTRSCAN):
log.log(Level.INFO, "RECOVER TMSTARTRSCAN");
CommitRollbackRecoveryCallback callback = conn.getCommitRollbackRecoveryCallback();
if(callback != null){
final String[] unfinishedTxIds;
if(conn.isHandleRecoveryInternally()){
unfinishedTxIds = getTransactionsInNeedOfRecovery();
}else{
unfinishedTxIds = callback.getTransactionsInNeedOfRecovery();
}
if(unfinishedTxIds != null){
for(String txId : unfinishedTxIds){
log.log(Level.INFO, "recovery required for " + txId);
Xid xid = XidImpl.getXid(txId);
xids.add(xid);
}
}
}
break;
case (XAResource.TMENDRSCAN):
log.log(Level.INFO, "RECOVER TMENDRSCAN");
break;
case (XAResource.TMNOFLAGS):
log.log(Level.INFO, "RECOVER TMNOFLAGS");
break;
default:
log.log(Level.INFO, "RECOVER " + flag);
}
return xids.toArray(new Xid[0]);
}
@Override
public void rollback(Xid xid) throws XAException {
String s = XidImpl.asString(xid);
log.log(Level.INFO, "ROLLBACK " + s);
try{
conn.getCommitRollbackRecoveryCallback().rollback(s);
if(conn.isHandleRecoveryInternally()){
cleanupInternalTransactionState(xid);
}
conn.cleanup();
}catch(Exception e){
log.log(Level.WARNING, "failed to rollback for txid " + s, e);
throw new XAException(XAException.XA_RETRY);
}
}
private void cleanupInternalTransactionState(Xid xid) {
boolean found = false;
for(File f : conn.getRecoveryStatePersistenceDirectory().listFiles(executeFilter)){
try{
final String content = read(f);
if(content.equals(XidImpl.asString(xid))){
found = true;
if(!f.delete()){
log.log(Level.WARNING, "Failed to delete file '" + f.getAbsolutePath() + "'. Please do this manually!");
}else{
log.log(Level.FINE, "Transaction cleaned up: " + f.getName());
}
break;
}
}catch(NoSuchFileException e){
//this can happen during concurrent attempts to commit/rollback.
//the concurrent attempts are not trying to find the same file,
//rather the one XAResource has simply cleaned up a file by the
//time the second XAResource tries to read it, since its
//list of files has become outdated. since another XAResource
//has already tidied up the file, it did not contain
//the Xid we are searching for, so we do nothing, just as though
//we had successfully read the file and it had not been relevant
//to the transaction we are searching for.
//summed up: move along, nothing to see here!
}
}
if(!found){
log.log(Level.WARNING, "Unable to clean up internal state for transaction '" + xid + "' (" + XidImpl.asString(xid) + ") because no record of the transaction was found. Please report this as a bug.");
}
}
private String read(File f) throws NoSuchFileException {
try{
return new String(Files.readAllBytes(f.toPath()), StandardCharsets.UTF_8);
}catch(NoSuchFileException e){
throw e;
}catch(IOException e){
throw new RuntimeException("failed to read transaction state for file " + f.getAbsolutePath(), e);
}
}
private String[] getTransactionsInNeedOfRecovery() {
List unfinishedTxs = new ArrayList();
for(File f : conn.getRecoveryStatePersistenceDirectory().listFiles(executeFilter)){
try {
if(getFileAgeInMs(f) > 30000){ //TODO use transaction timeout?? or maybe we just list all EXECUTEs, and TX Manager is clever enough to know its in the middle of committing/rolling back some of them, ie the ones that are not in need of recovery??
String content = read(f);
unfinishedTxs.add(content);
log.log(Level.INFO, "Unfinished transaction found by generic resource adapter: " + f.getName());
}
} catch (NoSuchFileException e) {
//the list of files has become out of date because
//another XAResource has tidied away the transaction.
//that means it no longer needs recovering :-)
//summary: move along, nothing to see here.
}
}
return unfinishedTxs.toArray(new String[0]);
}
private long getFileAgeInMs(File f) throws NoSuchFileException {
try{
BasicFileAttributes attr = Files.readAttributes(f.toPath(), BasicFileAttributes.class);
return System.currentTimeMillis() - attr.creationTime().toMillis();
}catch(NoSuchFileException e){
throw e;
}catch(IOException e){
return Integer.MAX_VALUE; //otherwise theres a danger it will never be considered for recovery!
}
}
@Override
public boolean setTransactionTimeout(int timeout) throws XAException {
log.log(Level.INFO, "SET TRANSACTION TIMEOUT " + timeout);
this.timeout = timeout;
//TODO this is called on startup -> provide the callback with this?
return true;
}
@Override
public void start(Xid xid, int flags) throws XAException {
//called when getConnection is called in the code which uses this resource. xid comes from the TM.
//remember the xid, since once started, we need to remember the result of EXECUTE
//as it is important for the PREPARE phase.
String s = XidImpl.asString(xid);
log.log(Level.INFO, "START " + flags + "/" + s);
//note the xid, since its needed in the call to EXECUTE
conn.setCurrentTxId(s);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy