/* * Bitronix Transaction Manager * * Copyright (c) 2010, Bitronix Software. * * 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, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY 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 * along with this distribution; if not, write to: * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA */ package bitronix.tm.recovery; import java.io.File; import java.lang.reflect.*; import java.sql.Connection; import java.util.*; import java.util.concurrent.atomic.AtomicReference; import javax.transaction.Status; import javax.transaction.xa.*; import junit.framework.TestCase; import org.slf4j.*; import bitronix.tm.*; import bitronix.tm.internal.TransactionStatusChangeListener; import bitronix.tm.journal.Journal; import bitronix.tm.mock.events.*; import bitronix.tm.mock.resource.*; import bitronix.tm.mock.resource.jdbc.MockitoXADataSource; import bitronix.tm.resource.ResourceRegistrar; import bitronix.tm.resource.common.*; import bitronix.tm.resource.jdbc.*; import bitronix.tm.utils.*; /** * * @author lorban */ public class RecovererTest extends TestCase { private final static Logger log = LoggerFactory.getLogger(RecovererTest.class); private MockXAResource xaResource; private PoolingDataSource pds; private Journal journal; protected void setUp() throws Exception { Iterator it = ResourceRegistrar.getResourcesUniqueNames().iterator(); while (it.hasNext()) { String name = (String) 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(); 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(); } 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 names = new HashSet(); 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 names = new HashSet(); 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 names = new HashSet(); 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(); 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 names = new HashSet(); 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() { public XAStatefulHolder createPooledConnection(Object xaFactory, ResourceBean bean) throws Exception { JdbcPooledConnection pc = (JdbcPooledConnection) 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); 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() { 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 threads = new ArrayList(); //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 = (Thread) threads.get(i); t.start(); } //join for (int i=0; i< THREAD_COUNT;i++) { Thread t = (Thread) threads.get(i); t.join(); } assertEquals(1, recoverer.getExecutionsCount()); } }