/*
* Copyright (C) 2006-2013 Bitronix Software (http://www.bitronix.be)
*
* 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 bitronix.tm.recovery;
import bitronix.tm.BitronixTransaction;
import bitronix.tm.BitronixTransactionManager;
import bitronix.tm.BitronixXid;
import bitronix.tm.TransactionManagerServices;
import bitronix.tm.internal.TransactionStatusChangeListener;
import bitronix.tm.journal.Journal;
import bitronix.tm.mock.events.Event;
import bitronix.tm.mock.events.EventRecorder;
import bitronix.tm.mock.events.JournalLogEvent;
import bitronix.tm.mock.resource.MockJournal;
import bitronix.tm.mock.resource.MockXAResource;
import bitronix.tm.mock.resource.MockXid;
import bitronix.tm.mock.resource.jdbc.MockitoXADataSource;
import bitronix.tm.resource.ResourceRegistrar;
import bitronix.tm.resource.common.ResourceBean;
import bitronix.tm.resource.jdbc.JdbcPooledConnection;
import bitronix.tm.resource.jdbc.PooledConnectionProxy;
import bitronix.tm.resource.jdbc.PoolingDataSource;
import bitronix.tm.utils.Uid;
import bitronix.tm.utils.UidGenerator;
import junit.framework.TestCase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.transaction.Status;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import java.io.File;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
/**
*
* @author Ludovic Orban
*/
public class RecovererTest extends TestCase {
private final static Logger log = LoggerFactory.getLogger(RecovererTest.class);
private MockXAResource xaResource;
private PoolingDataSource pds;
private Journal journal;
@Override
protected void setUp() throws Exception {
Iterator<String> it = ResourceRegistrar.getResourcesUniqueNames().iterator();
while (it.hasNext()) {
String name = it.next();
ResourceRegistrar.unregister(ResourceRegistrar.get(name));
}
pds = new PoolingDataSource();
pds.setClassName(MockitoXADataSource.class.getName());
pds.setUniqueName("mock-xads");
pds.setMinPoolSize(1);
pds.setMaxPoolSize(1);
pds.init();
new File(TransactionManagerServices.getConfiguration().getLogPart1Filename()).delete();
new File(TransactionManagerServices.getConfiguration().getLogPart2Filename()).delete();
EventRecorder.clear();
Connection connection1 = pds.getConnection();
PooledConnectionProxy handle = (PooledConnectionProxy) connection1;
xaResource = (MockXAResource) handle.getPooledConnection().getXAResource();
connection1.close();
// test the clustered recovery as its logic is more complex and covers the non-clustered logic
TransactionManagerServices.getConfiguration().setCurrentNodeOnlyRecovery(true);
// recoverer needs the journal to be open to be run manually
journal = TransactionManagerServices.getJournal();
journal.open();
}
@Override
protected void tearDown() throws Exception {
if (TransactionManagerServices.isTransactionManagerRunning())
TransactionManagerServices.getTransactionManager().shutdown();
journal.close();
pds.close();
TransactionManagerServices.getJournal().close();
new File(TransactionManagerServices.getConfiguration().getLogPart1Filename()).delete();
new File(TransactionManagerServices.getConfiguration().getLogPart2Filename()).delete();
EventRecorder.clear();
}
/**
* Create 3 XIDs on the resource that are not in the journal -> recoverer presumes they have aborted and rolls
* them back.
* @throws Exception
*/
public void testRecoverPresumedAbort() throws Exception {
byte[] gtrid = UidGenerator.generateUid().getArray();
xaResource.addInDoubtXid(new MockXid(0, gtrid, BitronixXid.FORMAT_ID));
xaResource.addInDoubtXid(new MockXid(1, gtrid, BitronixXid.FORMAT_ID));
xaResource.addInDoubtXid(new MockXid(2, gtrid, BitronixXid.FORMAT_ID));
TransactionManagerServices.getRecoverer().run();
assertEquals(0, TransactionManagerServices.getRecoverer().getCommittedCount());
assertEquals(3, TransactionManagerServices.getRecoverer().getRolledbackCount());
assertEquals(0, xaResource.recover(XAResource.TMSTARTRSCAN | XAResource.TMENDRSCAN).length);
}
/**
* Create 3 XIDs on the resource that are not in the journal -> recoverer presumes they have aborted and rolls
* them back.
* @throws Exception
*/
public void testIncrementalRecoverPresumedAbort() throws Exception {
byte[] gtrid = UidGenerator.generateUid().getArray();
xaResource.addInDoubtXid(new MockXid(0, gtrid, BitronixXid.FORMAT_ID));
xaResource.addInDoubtXid(new MockXid(1, gtrid, BitronixXid.FORMAT_ID));
xaResource.addInDoubtXid(new MockXid(2, gtrid, BitronixXid.FORMAT_ID));
IncrementalRecoverer.recover(pds);
assertEquals(0, xaResource.recover(XAResource.TMSTARTRSCAN | XAResource.TMENDRSCAN).length);
}
/**
* Create 3 XIDs on the resource that are in the journal -> recoverer commits them.
* @throws Exception
*/
public void testRecoverCommitting() throws Exception {
Xid xid0 = new MockXid(0, UidGenerator.generateUid().getArray(), BitronixXid.FORMAT_ID);
xaResource.addInDoubtXid(xid0);
Xid xid1 = new MockXid(1, UidGenerator.generateUid().getArray(), BitronixXid.FORMAT_ID);
xaResource.addInDoubtXid(xid1);
Xid xid2 = new MockXid(2, UidGenerator.generateUid().getArray(), BitronixXid.FORMAT_ID);
xaResource.addInDoubtXid(xid2);
Set<String> names = new HashSet<String>();
names.add(pds.getUniqueName());
journal.log(Status.STATUS_COMMITTING, new Uid(xid0.getGlobalTransactionId()), names);
journal.log(Status.STATUS_COMMITTING, new Uid(xid1.getGlobalTransactionId()), names);
journal.log(Status.STATUS_COMMITTING, new Uid(xid2.getGlobalTransactionId()), names);
TransactionManagerServices.getRecoverer().run();
assertEquals(3, TransactionManagerServices.getRecoverer().getCommittedCount());
assertEquals(0, TransactionManagerServices.getRecoverer().getRolledbackCount());
assertEquals(0, xaResource.recover(XAResource.TMSTARTRSCAN | XAResource.TMENDRSCAN).length);
}
/**
* Create 3 XIDs on the resource that are in the journal -> recoverer commits them.
* @throws Exception
*/
public void testIncrementalRecoverCommitting() throws Exception {
Xid xid0 = new MockXid(0, UidGenerator.generateUid().getArray(), BitronixXid.FORMAT_ID);
xaResource.addInDoubtXid(xid0);
Xid xid1 = new MockXid(1, UidGenerator.generateUid().getArray(), BitronixXid.FORMAT_ID);
xaResource.addInDoubtXid(xid1);
Xid xid2 = new MockXid(2, UidGenerator.generateUid().getArray(), BitronixXid.FORMAT_ID);
xaResource.addInDoubtXid(xid2);
Set<String> names = new HashSet<String>();
names.add(pds.getUniqueName());
journal.log(Status.STATUS_COMMITTING, new Uid(xid0.getGlobalTransactionId()), names);
journal.log(Status.STATUS_COMMITTING, new Uid(xid1.getGlobalTransactionId()), names);
journal.log(Status.STATUS_COMMITTING, new Uid(xid2.getGlobalTransactionId()), names);
IncrementalRecoverer.recover(pds);
assertEquals(0, xaResource.recover(XAResource.TMSTARTRSCAN | XAResource.TMENDRSCAN).length);
}
public void testSkipInFlightRollback() throws Exception {
BitronixTransactionManager btm = TransactionManagerServices.getTransactionManager();
Uid uid0 = UidGenerator.generateUid();
Xid xid0 = new MockXid(0, uid0.getArray(), BitronixXid.FORMAT_ID);
xaResource.addInDoubtXid(xid0);
assertNull(btm.getCurrentTransaction());
Thread.sleep(30); // let the clock run a bit so that in-flight TX is a bit older than the journaled one
btm.begin();
Xid xid1 = new MockXid(1, UidGenerator.generateUid().getArray(), BitronixXid.FORMAT_ID);
xaResource.addInDoubtXid(xid1);
TransactionManagerServices.getRecoverer().run();
btm.rollback();
assertEquals(0, TransactionManagerServices.getRecoverer().getCommittedCount());
assertEquals(1, TransactionManagerServices.getRecoverer().getRolledbackCount());
assertEquals(1, xaResource.recover(XAResource.TMSTARTRSCAN | XAResource.TMENDRSCAN).length);
btm.shutdown();
TransactionManagerServices.getJournal().open();
TransactionManagerServices.getRecoverer().run();
assertEquals(0, TransactionManagerServices.getRecoverer().getCommittedCount());
assertEquals(1, TransactionManagerServices.getRecoverer().getRolledbackCount());
assertEquals(0, xaResource.recover(XAResource.TMSTARTRSCAN | XAResource.TMENDRSCAN).length);
}
public void testSkipInFlightCommit() throws Exception {
BitronixTransactionManager btm = TransactionManagerServices.getTransactionManager();
Uid uid0 = UidGenerator.generateUid();
Xid xid0 = new MockXid(0, uid0.getArray(), BitronixXid.FORMAT_ID);
xaResource.addInDoubtXid(xid0);
Set<String> names = new HashSet<String>();
names.add(pds.getUniqueName());
journal.log(Status.STATUS_COMMITTING, new Uid(xid0.getGlobalTransactionId()), names);
assertNull(btm.getCurrentTransaction());
Thread.sleep(30); // let the clock run a bit so that in-flight TX is a bit older than the journaled one
btm.begin();
Xid xid1 = new MockXid(1, UidGenerator.generateUid().getArray(), BitronixXid.FORMAT_ID);
xaResource.addInDoubtXid(xid1);
names = new HashSet<String>();
names.add(pds.getUniqueName());
journal.log(Status.STATUS_COMMITTING, new Uid(xid1.getGlobalTransactionId()), names);
TransactionManagerServices.getRecoverer().run();
btm.rollback();
assertEquals(1, xaResource.recover(XAResource.TMSTARTRSCAN | XAResource.TMENDRSCAN).length);
btm.shutdown();
TransactionManagerServices.getJournal().open();
TransactionManagerServices.getRecoverer().run();
assertEquals(0, xaResource.recover(XAResource.TMSTARTRSCAN | XAResource.TMENDRSCAN).length);
}
public void testRecoverMissingResource() throws Exception {
final Xid xid0 = new MockXid(0, UidGenerator.generateUid().getArray(), BitronixXid.FORMAT_ID);
xaResource.addInDoubtXid(xid0);
Set<String> names = new HashSet<String>();
names.add("no-such-registered-resource");
journal.log(Status.STATUS_COMMITTING, new Uid(xid0.getGlobalTransactionId()), names);
assertEquals(1, TransactionManagerServices.getJournal().collectDanglingRecords().size());
// the TM must run the recoverer in this scenario
TransactionManagerServices.getTransactionManager();
assertEquals(1, TransactionManagerServices.getJournal().collectDanglingRecords().size());
assertNull(TransactionManagerServices.getRecoverer().getCompletionException());
assertEquals(0, TransactionManagerServices.getRecoverer().getCommittedCount());
assertEquals(1, TransactionManagerServices.getRecoverer().getRolledbackCount());
assertEquals(0, xaResource.recover(XAResource.TMSTARTRSCAN | XAResource.TMENDRSCAN).length);
// the TM is running, adding this resource will kick incremental recovery on it
PoolingDataSource pds = new PoolingDataSource() {
@Override
public JdbcPooledConnection createPooledConnection(Object xaFactory, ResourceBean bean) throws Exception {
JdbcPooledConnection pc = super.createPooledConnection(xaFactory, bean);
MockXAResource xaResource = (MockXAResource) pc.getXAResource();
xaResource.addInDoubtXid(UidGenerator.generateXid(new Uid(xid0.getGlobalTransactionId())));
return pc;
}
};
pds.setClassName(MockitoXADataSource.class.getName());
pds.setUniqueName("no-such-registered-resource");
pds.setMinPoolSize(1);
pds.setMaxPoolSize(1);
pds.init();
Connection connection = pds.getConnection();
PooledConnectionProxy handle = (PooledConnectionProxy) connection;
XAResource xaResource = handle.getPooledConnection().getXAResource();
connection.close();
assertEquals(0, xaResource.recover(XAResource.TMSTARTRSCAN | XAResource.TMENDRSCAN).length);
assertEquals(0, TransactionManagerServices.getJournal().collectDanglingRecords().size());
pds.close();
TransactionManagerServices.getTransactionManager().shutdown();
}
volatile boolean listenerExecuted = false;
public void testBackgroundRecovererSkippingInFlightTransactions() throws Exception {
// change disk journal into mock journal
Field field = TransactionManagerServices.class.getDeclaredField("journalRef");
field.setAccessible(true);
@SuppressWarnings("unchecked")
AtomicReference<Journal> journalRef = (AtomicReference<Journal>) field.get(TransactionManagerServices.class);
journalRef.set(new MockJournal());
pds.setMaxPoolSize(2);
BitronixTransactionManager btm = TransactionManagerServices.getTransactionManager();
final Recoverer recoverer = TransactionManagerServices.getRecoverer();
try {
btm.begin();
BitronixTransaction tx = btm.getCurrentTransaction();
tx.addTransactionStatusChangeListener(new TransactionStatusChangeListener() {
@Override
public void statusChanged(int oldStatus, int newStatus) {
if (newStatus != Status.STATUS_COMMITTING)
return;
recoverer.run();
assertEquals(0, recoverer.getCommittedCount());
assertEquals(0, recoverer.getRolledbackCount());
assertNull(recoverer.getCompletionException());
listenerExecuted = true;
}
});
Connection c = pds.getConnection();
c.createStatement();
c.close();
xaResource.addInDoubtXid(new MockXid(new byte[] {0, 1, 2}, tx.getResourceManager().getGtrid().getArray(), BitronixXid.FORMAT_ID));
btm.commit();
}
finally {
btm.shutdown();
}
assertTrue("recoverer did not run between phases 1 and 2", listenerExecuted);
int committedCount = 0;
List events = EventRecorder.getOrderedEvents();
for (int i = 0; i < events.size(); i++) {
Event event = (Event) events.get(i);
if (event instanceof JournalLogEvent) {
if (((JournalLogEvent) event).getStatus() == Status.STATUS_COMMITTED)
committedCount++;
}
}
assertEquals("TX has been committed more or less times than just once", 1, committedCount);
}
public void testReentrance() throws Exception {
log.debug("Start test RecovererTest.testReentrance()");
final int THREAD_COUNT = 10;
Recoverer recoverer = new Recoverer();
xaResource.setRecoveryDelay(1000);
List<Thread> threads = new ArrayList<Thread>();
//create
for (int i=0; i< THREAD_COUNT;i++) {
Thread t = new Thread(recoverer);
threads.add(t);
}
//start
for (int i=0; i< THREAD_COUNT;i++) {
Thread t = threads.get(i);
t.start();
}
//join
for (int i=0; i< THREAD_COUNT;i++) {
Thread t = threads.get(i);
t.join();
}
assertEquals(1, recoverer.getExecutionsCount());
}
}