/* * Copyright 2013 Gordon Burgett and individual contributors * * 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 org.xflatdb.xflat.db; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.xflatdb.xflat.transaction.Transaction; import org.xflatdb.xflat.transaction.TransactionException; import org.hamcrest.Matchers; import org.hamcrest.TypeSafeMatcher; import org.junit.After; import org.junit.Before; import org.junit.Test; import static org.junit.Assert.*; import static org.mockito.Mockito.*; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.xflatdb.xflat.transaction.IllegalTransactionStateException; import org.xflatdb.xflat.transaction.Propagation; import org.xflatdb.xflat.transaction.TransactionOptions; import org.xflatdb.xflat.transaction.TransactionPropagationException; import org.xflatdb.xflat.transaction.TransactionScope; /** * * @author Gordon */ public abstract class EngineTransactionManagerTestBase { public EngineTransactionManagerTestBase() { } @Before public void setUp() { } @After public void tearDown() { } protected abstract EngineTransactionManager getInstance(); @Test public void testBeginTransaction_TransactionIsOpen() throws Exception { System.out.println("testBeginTransaction_TransactionIsOpen"); try(EngineTransactionManager instance = getInstance()) { TransactionScope scope = instance.openTransaction(); Transaction txTransaction = instance.getTransaction(); assertFalse("TX should not be committed", scope.isCommitted()); assertEquals("TX should not be committed", -1, instance.isTransactionCommitted(txTransaction.getTransactionId())); assertFalse("TX should not be reverted", scope.isReverted()); assertFalse("TX should not be reverted", instance.isTransactionReverted(txTransaction.getTransactionId())); assertTrue("Should be one open TX", instance.anyOpenTransactions()); assertEquals(txTransaction.getTransactionId(), instance.getLowestOpenTransaction()); scope.commit(); assertTrue("TX should be committed", scope.isCommitted()); assertEquals("TX should be committed", txTransaction.getCommitId(), instance.isTransactionCommitted(txTransaction.getTransactionId())); assertFalse("TX should not be reverted", scope.isReverted()); assertFalse("TX should not be reverted", instance.isTransactionReverted(txTransaction.getTransactionId())); assertThat("TX should have higher commit ID", txTransaction.getCommitId(), Matchers.greaterThan(txTransaction.getTransactionId())); scope.close(); assertFalse("Should be no open TX", instance.anyOpenTransactions()); assertEquals(Long.MAX_VALUE, instance.getLowestOpenTransaction()); } } @Test public void testRevertTransaction_TransactionIsReverted() throws Exception { System.out.println("testRevertTransaction_TransactionIsReverted"); try(EngineTransactionManager instance = getInstance()) { TransactionScope scope = instance.openTransaction(); Transaction txTransaction = instance.getTransaction(); scope.revert(); assertFalse("TX should not be committed", scope.isCommitted()); assertEquals("TX should not be committed", -1, instance.isTransactionCommitted(txTransaction.getTransactionId())); assertTrue("TX should be reverted", scope.isReverted()); assertTrue("TX should be reverted", instance.isTransactionReverted(txTransaction.getTransactionId())); assertEquals("TX should have no commit ID", -1, txTransaction.getCommitId()); scope.close(); assertFalse("Should be no open TX", instance.anyOpenTransactions()); assertEquals(Long.MAX_VALUE, instance.getLowestOpenTransaction()); } } @Test public void testGetTransaction_GetsCurrentTransaction() throws Exception { System.out.println("testGetTransaction_GetsCurrentTransaction"); try(EngineTransactionManager instance = getInstance()) { TransactionScope scope = instance.openTransaction(); Transaction current = instance.getTransaction(); assertNotNull("should have current transaction", current); assertThat("current should have an ID", current.getTransactionId(), Matchers.not(Matchers.equalTo(-1L))); scope.close(); assertNull("getTransaction should be null after closing", instance.getTransaction()); } } @Test public void testTransactionlessCommitId_BeforeAndAfterTx_LTAndGTTxId() throws Exception { System.out.println("testTransactionlessCommitId_BeforeAndAfterTx_LTAndGTTxId"); try(EngineTransactionManager instance = getInstance()) { long id1 = instance.transactionlessCommitId(); TransactionScope scope = instance.openTransaction(); Transaction tx = instance.getTransaction(); long id2 = instance.transactionlessCommitId(); scope.close(); assertThat(id1, Matchers.lessThan(tx.getTransactionId())); assertThat(id2, Matchers.greaterThan(tx.getTransactionId())); } } @Test public void testTransactionIDs_HeavyUse_ThreadSafe() throws Exception { System.out.println("testTransactionIDs_HeavyUse_ThreadSafe"); try(final EngineTransactionManager instance = getInstance()) { final Set<List<Long>> ids = Collections.synchronizedSet(new HashSet<List<Long>>()); final int iterations = 10000; Runnable r = new Runnable(){ @Override public void run() { List<Long> idList = new ArrayList<>(iterations); for(int i = 0; i < iterations; i++){ idList.add(instance.transactionlessCommitId()); } ids.add(idList); } }; int cores = Runtime.getRuntime().availableProcessors(); if(cores < 2) cores = 2; //need to get at least some multithreading going System.out.println(String.format("Running with %d cores", cores)); int threads = cores - 1; List<Thread> threadList = new ArrayList<>(); for(int i = 0; i < threads; i++){ threadList.add(new Thread(r)); } long start = instance.transactionlessCommitId(); for(int i = 0; i < threads; i++){ threadList.get(i).start(); } r.run(); for(int i = 0; i < threads; i++){ threadList.get(i).join(); } long end = instance.transactionlessCommitId(); int maxUniquifier = 0; try{ Set<Long> finalIds = new HashSet<>(); for(List<Long> idList : ids){ for(Long l : idList){ if(!finalIds.add(l)){ fail("Duplicate IDs generated: " + Long.toHexString(l) + " (" + l + ")"); } assertThat("id not greater than start", l, Matchers.greaterThan(start)); assertThat("id not less than end", l, Matchers.lessThan(end)); int i = (int)(l.longValue() & 0xFFFFL); maxUniquifier = i > maxUniquifier ? i : maxUniquifier; } } } finally { System.out.println("Max uniquifier: " + Integer.toHexString(maxUniquifier)); } } } @Test public void testBindEngine_BoundEngineNotifiedOfRevert() throws Exception { System.out.println("testBindEngine_BoundEngineNotifiedOfRevert"); try(EngineTransactionManager instance = getInstance()) { TransactionScope scope = instance.openTransaction(); EngineBase e = mock(EngineBase.class); instance.bindEngineToCurrentTransaction(e); scope.revert(); verify(e).revert(instance.getTransaction().getTransactionId(), false); scope.close(); } } @Test public void testBindEngine_BoundEngineNotifiedOfCommit() throws Exception { System.out.println("testBindEngine_BoundEngineNotifiedOfCommit"); try(EngineTransactionManager instance = getInstance()) { TransactionScope scope = instance.openTransaction(); Transaction tx = instance.getTransaction(); EngineBase e = mock(EngineBase.class); instance.bindEngineToCurrentTransaction(e); scope.commit(); verify(e).commit(argThat(matchesTransaction(tx)), argThat(Matchers.equalTo(TransactionOptions.DEFAULT))); scope.close(); } } @Test public void testBindEngine_ExceptionDuringCommit_BoundEngineReverted() throws Exception { System.out.println("testBindEngine_ExceptionDuringCommit_BoundEngineReverted"); try(EngineTransactionManager instance = getInstance()) { TransactionScope scope = instance.openTransaction(); Transaction tx = instance.getTransaction(); EngineBase e = mock(EngineBase.class); doThrow(new TransactionException("Test"){}) .when(e).commit(any(Transaction.class), any(TransactionOptions.class)); instance.bindEngineToCurrentTransaction(e); try{ scope.commit(); fail("Did not throw TransactionException"); } catch(TransactionException ex){ //expected } verify(e).revert(tx.getTransactionId(), false); scope.close(); } } @Test public void testMultipleBoundEngines_SecondEngineThrows_FirstBoundEngineRevertedAfterCommit() throws Exception { System.out.println("testMultipleBoundEngines_SecondEngineThrows_FirstBoundEngineRevertedAfterCommit"); try(EngineTransactionManager instance = getInstance()) { TransactionScope scope = instance.openTransaction(); Transaction tx = instance.getTransaction(); final AtomicReference<EngineBase> committedEngine = new AtomicReference<>(null); Answer a = new Answer(){ @Override public Object answer(InvocationOnMock invocation) throws Throwable { EngineBase eng = (EngineBase)invocation.getMock(); if(!committedEngine.compareAndSet(null, eng)){ //the second one should throw throw new TransactionException("Test"){}; } return null; } }; EngineBase e = mock(EngineBase.class); EngineBase e2 = mock(EngineBase.class); doAnswer(a) .when(e).commit(any(Transaction.class), any(TransactionOptions.class)); doAnswer(a) .when(e2).commit(any(Transaction.class), any(TransactionOptions.class)); instance.bindEngineToCurrentTransaction(e); instance.bindEngineToCurrentTransaction(e2); try{ scope.commit(); fail("Did not throw TransactionException"); } catch(TransactionException ex){ //expected } verify(e).revert(tx.getTransactionId(), false); verify(e2).revert(tx.getTransactionId(), false); verify(committedEngine.get()).commit(tx, TransactionOptions.DEFAULT); scope.close(); } } @Test public void testBoundEngineFails_InstanceClosed_Recovers() throws Exception { System.out.println("testBoundEngineFails_InstanceClosed_Recovers"); long txId; try(EngineTransactionManager instance = getInstance()) { TransactionScope tx = instance.openTransaction(); Transaction txTransaction = instance.getTransaction(); txId = txTransaction.getTransactionId(); EngineBase e = mock(EngineBase.class); doThrow(new TransactionException("Test"){}) .when(e).commit(any(Transaction.class), any(TransactionOptions.class)); doThrow(new Error("Expected")) .when(e).revert(any(Long.class), anyBoolean()); doReturn("Name") .when(e).getTableName(); instance.bindEngineToCurrentTransaction(e); try{ tx.commit(); fail("Did not throw TransactionException"); } catch(Error err){ if(!"Expected".equals(err.getMessage())) throw err; //expected } tx.close(); } EngineBase e = mock(EngineBase.class); XFlatDatabase db = mock(XFlatDatabase.class); when(db.getEngine("Name")) .thenReturn(e); try(EngineTransactionManager instance = getInstance()) { instance.recover(db); } //verify recovered verify(e).revert(txId, true); } //<editor-fold desc="propagation"> @Test public void testMandatoryPropagation_NoTransaction_ThrowsException() throws Exception { System.out.println("testMandatoryPropagation_NoTransaction_ThrowsException"); try(EngineTransactionManager instance = getInstance()) { try{ instance.openTransaction(new TransactionOptions().withPropagation(Propagation.MANDATORY)); fail("should have thrown TransactionPropagationException"); }catch(TransactionPropagationException ex){ //expected } assertFalse("Should not be any more open transactions", instance.anyOpenTransactions()); } } @Test public void testMandatoryPropagation_TransactionExists_AmbientTransactionRequiresBothCommits() throws Exception { System.out.println("testMandatoryPropagation_TransactionExists_AmbientTransactionRequiresBothCommits"); try(EngineTransactionManager instance = getInstance()) { try(TransactionScope outer = instance.openTransaction()){ //act TransactionScope inner = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.MANDATORY)); inner.commit(); assertFalse("Ambient transaction should not yet be committed", inner.isCommitted()); inner.close(); //assert assertFalse("Ambient transaction should not yet be committed", outer.isCommitted()); outer.commit(); assertTrue("Ambient transaction should be committed", inner.isCommitted()); } assertFalse("Should not be any more open transactions", instance.anyOpenTransactions()); } } @Test public void testMandatoryPropagation_TransactionExists_CloseDoesNotCloseOuterScope() throws Exception { System.out.println("testMandatoryPropagation_TransactionExists_CloseDoesNotCloseOuterScope"); try(EngineTransactionManager instance = getInstance()) { try(TransactionScope outer = instance.openTransaction()){ TransactionScope inner = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.MANDATORY)); //act inner.close(); //assert assertTrue("inner close should revert outer", outer.isReverted()); try{ outer.commit(); fail("should have thrown IllegalTransactionStateException on commit after revert"); }catch(IllegalTransactionStateException ex){ //expected } } assertFalse("Should not be any more open transactions", instance.anyOpenTransactions()); } } @Test public void testMandatoryPropagationReadOnly_TransactionExists_CloseDoesNotCloseOuterScope() throws Exception { System.out.println("testMandatoryPropagation_TransactionExists_CloseDoesNotCloseOuterScope"); try(EngineTransactionManager instance = getInstance()) { try(TransactionScope outer = instance.openTransaction()){ TransactionScope inner = instance.openTransaction(new TransactionOptions() .withPropagation(Propagation.MANDATORY) .withReadOnly(true)); //act inner.close(); //assert assertFalse("inner close should not revert outer", outer.isReverted()); outer.commit(); assertTrue("outer should still be capable of commit", outer.isCommitted()); } assertFalse("Should not be any more open transactions", instance.anyOpenTransactions()); } } @Test public void testNeverPropagation_TransactionExists_ThrowsException() throws Exception { System.out.println("testNeverPropagation_TransactionExists_ThrowsException"); try(EngineTransactionManager instance = getInstance()) { try(TransactionScope outer = instance.openTransaction()){ try{ TransactionScope inner = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.NEVER)); fail("should have thrown TransactionPropagationException"); }catch(TransactionPropagationException ex){ //expected } } assertFalse("Should not be any more open transactions", instance.anyOpenTransactions()); } } @Test public void testNeverPropagation_TransactionDoesNotExist_CurrentTransactionIsNull() throws Exception { System.out.println("testNeverPropagation_TransactionDoesNotExist_CurrentTransactionIsNull"); try(EngineTransactionManager instance = getInstance()) { TransactionScope inner = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.NEVER)); Transaction current = instance.getTransaction(); assertNull("Current TX should be null", current); assertFalse("Should not be any more open transactions", instance.anyOpenTransactions()); } } @Test public void testNotSupportedPropagation_TransactionExists_ExistingIsSuspended() throws Exception { System.out.println("testNotSupportedPropagation_TransactionExists_ExistingIsSuspended"); try(EngineTransactionManager instance = getInstance()) { try(TransactionScope outer = instance.openTransaction()){ Transaction outerTx = instance.getTransaction(); //act try(TransactionScope inner = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.NOT_SUPPORTED))){ //assert Transaction current = instance.getTransaction(); assertNull("Current TX should be null", current); } Transaction current = instance.getTransaction(); assertEquals("Should restore original transaction", outerTx.getCommitId(), current.getCommitId()); } assertFalse("Should not be any more open transactions", instance.anyOpenTransactions()); } } @Test public void testNotSupportedPropagation_TransactionDoesntExist_OperatesNonTransactionally() throws Exception { System.out.println("testNotSupportedPropagation_TransactionDoesntExist_OperatesNonTransactionally"); try(EngineTransactionManager instance = getInstance()) { //act try(TransactionScope outer = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.NOT_SUPPORTED))){ Transaction outerTx = instance.getTransaction(); assertNull("Current TX should be null", outerTx); try(TransactionScope inner = instance.openTransaction()){ Transaction innerTx = instance.getTransaction(); assertNotNull("Should have inner transaction", innerTx); assertThat("Should have inner transaction", innerTx.getTransactionId(), Matchers.greaterThan(0L)); inner.revert(); } //assert outerTx = instance.getTransaction(); assertNull("Current TX should still be null", outerTx); assertFalse("Revert on inner should not propagate to outer", outer.isReverted()); } assertFalse("Should not be any more open transactions", instance.anyOpenTransactions()); } } @Test public void testRequiredPropagation_TransactionDoesNotExist_CreatesNewTransaction() throws Exception { System.out.println("testRequiredPropagation_TransactionDoesNotExist_ThrowsException"); try(EngineTransactionManager instance = getInstance()) { //act try(TransactionScope outer = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.REQUIRED))){ Transaction current = instance.getTransaction(); assertNotNull("Current transaction should exist", current); assertThat("Should have TX ID", current.getTransactionId(), Matchers.greaterThan(0L)); outer.revert(); current = instance.getTransaction(); assertTrue("Current TX should be reverted", current.isReverted()); } assertFalse("Should not be any more open transactions", instance.anyOpenTransactions()); } } @Test public void testRequiredPropagation_TransactionExists_WrapsTransaction() throws Exception { System.out.println("testRequiredPropagation_TransactionExists_WrapsTransaction"); try(EngineTransactionManager instance = getInstance()) { try(TransactionScope outer = instance.openTransaction()){ Transaction outerTx = instance.getTransaction(); try (TransactionScope inner = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.REQUIRED))) { Transaction current = instance.getTransaction(); assertEquals("Current should be outer transaction", outerTx.getTransactionId(), current.getTransactionId()); inner.commit(); } //assert assertFalse("Commit should not yet have been finalized", outer.isCommitted()); outer.commit(); assertTrue("Commit should now be finalized", outer.isCommitted()); } assertFalse("Should not be any more open transactions", instance.anyOpenTransactions()); } } @Test public void testRequiresNewPropagation_NoTransactionExists_NewTransactionCreated() throws Exception { System.out.println("testRequiresNewPropagation_NoTransactionExists_NewTransactionCreated"); try(EngineTransactionManager instance = getInstance()) { //act try(TransactionScope outer = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.REQUIRES_NEW))){ Transaction current = instance.getTransaction(); assertNotNull("Current transaction should exist", current); assertThat("Should have TX ID", current.getTransactionId(), Matchers.greaterThan(0L)); outer.commit(); current = instance.getTransaction(); assertTrue("Current TX should be committed", current.isCommitted()); } assertFalse("Should not be any more open transactions", instance.anyOpenTransactions()); } } @Test public void testRequiresNewPropagation_TransactionExists_TransactionIsSuspended() throws Exception { System.out.println("testRequiresNewPropagation_TransactionExists_TransactionIsSuspended"); try(EngineTransactionManager instance = getInstance()) { try(TransactionScope outer = instance.openTransaction()){ Transaction outerTx = instance.getTransaction(); //act try(TransactionScope inner = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.REQUIRES_NEW))){ //assert Transaction current = instance.getTransaction(); assertNotNull("Current transaction should exist", current); assertThat("Should have TX ID", current.getTransactionId(), Matchers.greaterThan(0L)); assertThat("Inner TX should not be outer", current.getTransactionId(), Matchers.not(Matchers.equalTo(outerTx.getTransactionId()))); inner.revert(); } Transaction current = instance.getTransaction(); assertEquals("Should restore original transaction", outerTx.getTransactionId(), current.getTransactionId()); assertFalse("revert on inner should propagate to outer", outer.isReverted()); assertFalse("revert on inner should propagate to outer", current.isReverted()); } assertFalse("Should not be any more open transactions", instance.anyOpenTransactions()); } } @Test public void testSupportsPropagation_TransactionDoesNotExist_ExecutesNonTransactionally() throws Exception { System.out.println("testSupportsPropagation_TransactionDoesNotExist_ExecutesNonTransactionally"); try(EngineTransactionManager instance = getInstance()) { //act try(TransactionScope outer = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.SUPPORTS))){ //assert Transaction current = instance.getTransaction(); assertNull("Current TX should be null", current); try(TransactionScope inner = instance.openTransaction()){ current = instance.getTransaction(); assertNotNull("Should now have inner transaction", current); inner.revert(); } //assert current = instance.getTransaction(); assertNull("Current TX should be null", current); assertFalse("Revert on inner should not propagate to outer", outer.isReverted()); } assertFalse("Should not be any more open transactions", instance.anyOpenTransactions()); } } @Test public void testSupportsPropagation_TransactionExists_WrapsCurrentTransaction() throws Exception { System.out.println("testSupportsPropagation_TransactionExists_WrapsCurrentTransaction"); try(EngineTransactionManager instance = getInstance()) { try(TransactionScope outer = instance.openTransaction()){ Transaction outerTx = instance.getTransaction(); try (TransactionScope inner = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.SUPPORTS))) { Transaction current = instance.getTransaction(); assertEquals("Current should be outer transaction", outerTx.getTransactionId(), current.getTransactionId()); inner.commit(); } //assert assertFalse("Commit should have not yet been finalized", outer.isCommitted()); Transaction current = instance.getTransaction(); assertEquals("current should be outer TX", outerTx.getTransactionId(), current.getTransactionId()); outer.commit(); assertTrue("Commit should now be finalized", outer.isCommitted()); } assertFalse("Should not be any more open transactions", instance.anyOpenTransactions()); } } //</editor-fold> private Matcher<Transaction> matchesTransaction(final Transaction tx){ return new TypeSafeMatcher<Transaction>(){ @Override protected boolean matchesSafely(Transaction item) { if(item.getTransactionId() != tx.getTransactionId()) return false; if(item.getCommitId() != tx.getCommitId()) return false; if(item.isCommitted() != tx.isCommitted()) return false; if(item.isReverted() != tx.isReverted()) return false; if(item.isReadOnly() != tx.isReadOnly()) return false; return true; } @Override public void describeTo(Description description) { description.appendText("Transaction with ID").appendValue(tx.getTransactionId()); } }; } }