/* * Copyright (c) 2013 Nuxeo SA (http://nuxeo.com/) and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Florent Guillaume */ package org.nuxeo.ecm.core; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import javax.transaction.Transaction; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.nuxeo.ecm.core.storage.StorageException; import org.nuxeo.ecm.core.storage.sql.DatabaseDerby; import org.nuxeo.ecm.core.storage.sql.DatabaseH2; import org.nuxeo.ecm.core.storage.sql.SQLRepositoryTestCase; import org.nuxeo.ecm.core.storage.sql.jdbc.JDBCConnection; import org.nuxeo.ecm.core.storage.sql.jdbc.XAResourceConnectionAdapter; import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.datasource.ConnectionHelper; import org.nuxeo.runtime.osgi.OSGiRuntimeService; import org.nuxeo.runtime.transaction.TransactionHelper; /** * Test that transaction management does the right thing with single-datasource * mode. */ public class TestSingleDataSource extends SQLRepositoryTestCase { @Override protected OSGiRuntimeService handleNewRuntime(OSGiRuntimeService runtime) { runtime = super.handleNewRuntime(runtime); Framework.getProperties().setProperty(ConnectionHelper.SINGLE_DS, "jdbc/NuxeoTestDS"); return runtime; } @Override protected void deployRepositoryContrib() throws Exception { super.deployRepositoryContrib(); deployBundle("org.nuxeo.runtime.jtajca"); } @Before @Override public void setUp() throws Exception { super.setUp(); // database setUp deletes all tables fireFrameworkStarted(); TransactionHelper.startTransaction(); // no openSession() done here } @After @Override public void tearDown() throws Exception { try { if (session != null) { session.cancel(); closeSession(); } if (TransactionHelper.isTransactionActiveOrMarkedRollback()) { TransactionHelper.setTransactionRollbackOnly(); TransactionHelper.commitOrRollbackTransaction(); } } finally { Framework.getProperties().remove(ConnectionHelper.SINGLE_DS); super.tearDown(); } } /** * H2 cannot have one connection doing an insert in a tx and annother using * the same table, as it waits for a lock. */ protected boolean canUseTwoConnections() { return !(database instanceof DatabaseH2 // || database instanceof DatabaseDerby); } protected String getValidationQuery(Connection connection) throws StorageException { return Dialect.createDialect(connection, null, null).getValidationQuery(); } protected static void assertEqualsInt(int expected, ResultSet rs) throws SQLException { assertTrue(rs.next()); int actual = rs.getInt(1); assertEquals(expected, actual); assertFalse(rs.next()); rs.close(); } protected static void assertSharedConnectionCount(int n) { assertEquals(n, ConnectionHelper.countConnectionReferences()); } @Test public void testNoTxNoBegin() throws Exception { TransactionHelper.commitOrRollbackTransaction(); // end tx Connection connection = ConnectionHelper.getConnection(null); try { assertTrue(connection.getAutoCommit()); connection.setAutoCommit(true); // already true, no effect connection.setAutoCommit(false); connection.setAutoCommit(true); Statement st = connection.createStatement(); String sql = getValidationQuery(connection); st.execute(sql); assertSharedConnectionCount(0); } finally { assertFalse(connection.isClosed()); connection.close(); assertTrue(connection.isClosed()); connection.close(); // close twice ok assertTrue(connection.isClosed()); } // use after close is forbidden try { connection.createStatement(); fail("use after close should fail"); } catch (SQLException e) { assertTrue(e.getMessage().contains("Connection is closed")); } } @Test public void testNoTxBegin() throws Exception { TransactionHelper.commitOrRollbackTransaction(); // end tx Connection connection = ConnectionHelper.getConnection(null); try { // first thing set autoCommit=false, but no tx -> no sharing connection.setAutoCommit(false); connection.setAutoCommit(false); // already false, no effect Statement st = connection.createStatement(); String sql = getValidationQuery(connection); st.execute(sql); assertSharedConnectionCount(0); connection.commit(); // needed for DB2 before close connection.setAutoCommit(true); } finally { connection.close(); } } /* * Transaction in non-ACTIVE state. */ @Test public void testBadTxBegin() throws Exception { TransactionHelper.setTransactionRollbackOnly(); // not ACTIVE Connection connection = ConnectionHelper.getConnection(null); try { // first thing set autoCommit=false, but no tx -> no sharing connection.setAutoCommit(false); connection.setAutoCommit(false); // already false, no effect Statement st = connection.createStatement(); String sql = getValidationQuery(connection); st.execute(sql); assertSharedConnectionCount(0); connection.commit(); // needed for DB2 before close } finally { connection.close(); } } @Test public void testNoTxSwitchAutoCommit() throws Exception { TransactionHelper.commitOrRollbackTransaction(); // end tx Connection connection = ConnectionHelper.getConnection(null); try { // use connection with autoCommit=true connection.createStatement(); // then set autoCommit=false, but no tx -> no sharing connection.setAutoCommit(false); connection.setAutoCommit(false); // already false, no effect Statement st = connection.createStatement(); String sql = getValidationQuery(connection); st.execute(sql); assertSharedConnectionCount(0); connection.commit(); // needed for DB2 before close connection.setAutoCommit(true); } finally { connection.close(); } } @Test public void testNoBegin() throws Exception { Connection connection = ConnectionHelper.getConnection(null); try { Statement st = connection.createStatement(); String sql = getValidationQuery(connection); st.execute(sql); assertSharedConnectionCount(0); } finally { assertFalse(connection.isClosed()); connection.close(); assertTrue(connection.isClosed()); } } /* * Does begin as the first thing the connection does. */ @Test public void testManualBegin1() throws Exception { Connection connection = ConnectionHelper.getConnection(null); try { assertSharedConnectionCount(0); // first thing set autoCommit=false => starts sharing connection.setAutoCommit(false); // lazy, still not created assertSharedConnectionCount(0); Statement st = connection.createStatement(); // shared connection created assertSharedConnectionCount(1); String sql = getValidationQuery(connection); st.execute(sql); assertSharedConnectionCount(1); connection.commit(); // shared connection kept around, may have other uses assertSharedConnectionCount(1); } finally { assertFalse(connection.isClosed()); connection.close(); assertTrue(connection.isClosed()); } assertSharedConnectionCount(1); TransactionHelper.commitOrRollbackTransaction(); // tx synchronizer removes the shared connection assertSharedConnectionCount(0); } /* * Does begin after the connection has already been used. */ @Test public void testManualBegin2() throws Exception { Connection connection = ConnectionHelper.getConnection(null); try { assertSharedConnectionCount(0); // first thing use connection with autoCommit=true String sql = getValidationQuery(connection); assertSharedConnectionCount(0); // switch to shared connection.setAutoCommit(false); assertSharedConnectionCount(1); Statement st = connection.createStatement(); st.execute(sql); assertSharedConnectionCount(1); connection.commit(); // shared connection kept around, may have other uses assertSharedConnectionCount(1); } finally { assertFalse(connection.isClosed()); connection.close(); assertTrue(connection.isClosed()); } assertSharedConnectionCount(1); TransactionHelper.commitOrRollbackTransaction(); // tx synchronizer removes the shared connection assertSharedConnectionCount(0); } /* * Does work, commit, then more work. */ @Test public void testCommitThenMoreWork() throws Exception { Connection connection = ConnectionHelper.getConnection(null); try { assertSharedConnectionCount(0); connection.setAutoCommit(false); assertSharedConnectionCount(0); connection.createStatement(); assertSharedConnectionCount(1); connection.commit(); assertSharedConnectionCount(1); // keep working in transaction mode after commit connection.createStatement(); assertSharedConnectionCount(1); } finally { assertFalse(connection.isClosed()); connection.close(); assertTrue(connection.isClosed()); } assertSharedConnectionCount(1); TransactionHelper.commitOrRollbackTransaction(); // tx synchronizer removes the shared connection assertSharedConnectionCount(0); } /* * Does begin, no commit, then close; checks that close auto-commits. */ @Test public void testCloseWithoutCommit() throws Exception { Connection connection = ConnectionHelper.getConnection(null); try { connection.setAutoCommit(false); Statement st = connection.createStatement(); String sql = getValidationQuery(connection); st.execute(sql); assertSharedConnectionCount(1); // don't commit, close() will do it automatically } finally { assertFalse(connection.isClosed()); connection.close(); assertTrue(connection.isClosed()); } assertSharedConnectionCount(1); TransactionHelper.commitOrRollbackTransaction(); // tx synchronizer removes the shared connection assertSharedConnectionCount(0); } /* * Does begin, no commit, then setAutoCommit=true; checks that autoCommit * change auto-commits. */ @Test public void testEndWithoutCommit() throws Exception { Connection connection = ConnectionHelper.getConnection(null); try { connection.setAutoCommit(false); Statement st = connection.createStatement(); String sql = getValidationQuery(connection); st.execute(sql); assertSharedConnectionCount(1); // don't commit connection.setAutoCommit(true); // commits automatically } finally { assertFalse(connection.isClosed()); connection.close(); assertTrue(connection.isClosed()); } assertSharedConnectionCount(1); TransactionHelper.commitOrRollbackTransaction(); // tx synchronizer removes the shared connection assertSharedConnectionCount(0); } /* * Test shared connection use after the transaction has ended. */ @Test public void testUseAfterTxEnd() throws Exception { Connection connection = ConnectionHelper.getConnection(null); try { connection.setAutoCommit(false); Statement st = connection.createStatement(); String sql = getValidationQuery(connection); st.execute(sql); assertSharedConnectionCount(1); // tx just commits now // this will log an ERROR TransactionHelper.commitOrRollbackTransaction(); assertSharedConnectionCount(0); // now keep using the connection connection.createStatement(); } finally { assertFalse(connection.isClosed()); connection.close(); assertTrue(connection.isClosed()); } assertSharedConnectionCount(0); } /* * Re-begin in a new transaction after a previous use and commit in a * previous transaction. */ @Test public void testSeveralTx() throws Exception { Connection connection = ConnectionHelper.getConnection(null); try { connection.setAutoCommit(false); Statement st = connection.createStatement(); String sql = getValidationQuery(connection); st.execute(sql); assertSharedConnectionCount(1); connection.commit(); connection.setAutoCommit(true); TransactionHelper.commitOrRollbackTransaction(); assertSharedConnectionCount(0); // new tx TransactionHelper.startTransaction(); connection.setAutoCommit(false); st = connection.createStatement(); st.execute(sql); assertSharedConnectionCount(1); connection.commit(); connection.setAutoCommit(true); TransactionHelper.commitOrRollbackTransaction(); assertSharedConnectionCount(0); } finally { assertFalse(connection.isClosed()); connection.close(); assertTrue(connection.isClosed()); } } /* * Test through XAResource wrapper and transaction enlisting. * * As a side effect the connection commit is called while the transaction is * in STATUS_COMMITTING. */ @Test public void testXAResourceBeginDoStuffCommit() throws Exception { JDBCConnection jdbc = new JDBCConnection(); jdbc.connection = ConnectionHelper.getConnection(null); try { Transaction transaction = TransactionHelper.lookupTransactionManager().getTransaction(); XAResourceConnectionAdapter xaresource = new XAResourceConnectionAdapter( jdbc); transaction.enlistResource(xaresource); // lazy so not yet allocated assertSharedConnectionCount(0); // use connection jdbc.connection.createStatement(); assertSharedConnectionCount(1); // then commit TransactionHelper.commitOrRollbackTransaction(); } finally { assertFalse(jdbc.connection.isClosed()); jdbc.connection.close(); assertTrue(jdbc.connection.isClosed()); } } /* * Test through XAResource wrapper and transaction enlisting. * * But do nothing between tx start and end. */ @Test public void testXAResourceBeginDoNothingCommit() throws Exception { JDBCConnection jdbc = new JDBCConnection(); jdbc.connection = ConnectionHelper.getConnection(null); try { Transaction transaction = TransactionHelper.lookupTransactionManager().getTransaction(); XAResourceConnectionAdapter xaresource = new XAResourceConnectionAdapter( jdbc); transaction.enlistResource(xaresource); // lazy so not yet allocated assertSharedConnectionCount(0); // do nothing between start and end TransactionHelper.commitOrRollbackTransaction(); } finally { assertFalse(jdbc.connection.isClosed()); jdbc.connection.close(); assertTrue(jdbc.connection.isClosed()); } } /* * Test through XAResource wrapper and transaction enlisting. * * But do nothing between tx start and end, and do a rollback. */ @Test public void testXAResourceBeginDoNothingRollback() throws Exception { JDBCConnection jdbc = new JDBCConnection(); jdbc.connection = ConnectionHelper.getConnection(null); try { Transaction transaction = TransactionHelper.lookupTransactionManager().getTransaction(); XAResourceConnectionAdapter xaresource = new XAResourceConnectionAdapter( jdbc); transaction.enlistResource(xaresource); // lazy so not yet allocated assertSharedConnectionCount(0); // do nothing between start and end // provoke rollback TransactionHelper.setTransactionRollbackOnly(); TransactionHelper.commitOrRollbackTransaction(); } finally { assertFalse(jdbc.connection.isClosed()); jdbc.connection.close(); assertTrue(jdbc.connection.isClosed()); } } @Test public void testTwoConnections() throws Exception { assumeTrue(canUseTwoConnections()); // separate connection to check results Connection checker = ConnectionHelper.getConnection(null, true); Statement chst = checker.createStatement(); chst.execute("CREATE TABLE foo (i INTEGER)"); Connection connection = ConnectionHelper.getConnection(null); Connection connection2 = ConnectionHelper.getConnection(null); try { assertSharedConnectionCount(0); // first thing set autoCommit=false => starts sharing connection.setAutoCommit(false); connection2.setAutoCommit(false); // lazy, still not created assertSharedConnectionCount(0); Statement st = connection.createStatement(); // shared connection created assertSharedConnectionCount(1); st.execute("INSERT INTO foo (i) VALUES (1)"); // not committed yet assertEqualsInt(0, chst.executeQuery("SELECT COUNT(*) FROM foo")); Statement st2 = connection2.createStatement(); // really shared assertSharedConnectionCount(1); st2.execute("INSERT INTO foo (i) VALUES (2)"); connection.commit(); assertSharedConnectionCount(1); // still not committed yet assertEqualsInt(0, chst.executeQuery("SELECT COUNT(*) FROM foo")); connection2.commit(); assertSharedConnectionCount(1); // last commit() committed all statements assertEqualsInt(2, chst.executeQuery("SELECT COUNT(*) FROM foo")); } finally { connection.close(); connection2.close(); checker.close(); } assertSharedConnectionCount(1); TransactionHelper.commitOrRollbackTransaction(); // tx synchronizer removes the shared connection assertSharedConnectionCount(0); } /* * First connection does a rollback. */ @Test public void testTwoConnectionsWithFirstRollback() throws Exception { assumeTrue(canUseTwoConnections()); // separate connection to check results Connection checker = ConnectionHelper.getConnection(null, true); Statement chst = checker.createStatement(); chst.execute("CREATE TABLE foo (i INTEGER)"); Connection connection = ConnectionHelper.getConnection(null); Connection connection2 = ConnectionHelper.getConnection(null); try { assertSharedConnectionCount(0); // first thing set autoCommit=false => starts sharing connection.setAutoCommit(false); connection2.setAutoCommit(false); // lazy, still not created assertSharedConnectionCount(0); Statement st = connection.createStatement(); // shared connection created assertSharedConnectionCount(1); st.execute("INSERT INTO foo (i) VALUES (1)"); // not committed yet assertEqualsInt(0, chst.executeQuery("SELECT COUNT(*) FROM foo")); Statement st2 = connection2.createStatement(); // really shared assertSharedConnectionCount(1); st2.execute("INSERT INTO foo (i) VALUES (2)"); connection.rollback(); assertSharedConnectionCount(1); // still not committed yet assertEqualsInt(0, chst.executeQuery("SELECT COUNT(*) FROM foo")); // connection 2 can still be used, even though in the end // it will rollback connection2.createStatement(); connection2.commit(); assertSharedConnectionCount(1); // last commit() does actually rollback assertEqualsInt(0, chst.executeQuery("SELECT COUNT(*) FROM foo")); } finally { connection.close(); connection2.close(); checker.close(); } assertSharedConnectionCount(1); TransactionHelper.commitOrRollbackTransaction(); // tx synchronizer removes the shared connection assertSharedConnectionCount(0); } /* * Second connection does a rollback. */ @Test public void testTwoConnectionsWithSecondRollback() throws Exception { assumeTrue(canUseTwoConnections()); // separate connection to check results Connection checker = ConnectionHelper.getConnection(null, true); Statement chst = checker.createStatement(); chst.execute("CREATE TABLE foo (i INTEGER)"); Connection connection = ConnectionHelper.getConnection(null); Connection connection2 = ConnectionHelper.getConnection(null); try { assertSharedConnectionCount(0); // first thing set autoCommit=false => starts sharing connection.setAutoCommit(false); connection2.setAutoCommit(false); // lazy, still not created assertSharedConnectionCount(0); Statement st = connection.createStatement(); // shared connection created assertSharedConnectionCount(1); st.execute("INSERT INTO foo (i) VALUES (1)"); // not committed yet assertEqualsInt(0, chst.executeQuery("SELECT COUNT(*) FROM foo")); Statement st2 = connection2.createStatement(); // really shared assertSharedConnectionCount(1); st2.execute("INSERT INTO foo (i) VALUES (2)"); connection.commit(); assertSharedConnectionCount(1); // still not committed yet assertEqualsInt(0, chst.executeQuery("SELECT COUNT(*) FROM foo")); connection2.rollback(); assertSharedConnectionCount(1); // last rollback() does actually rollback assertEqualsInt(0, chst.executeQuery("SELECT COUNT(*) FROM foo")); } finally { connection.close(); connection2.close(); checker.close(); } assertSharedConnectionCount(1); TransactionHelper.commitOrRollbackTransaction(); // tx synchronizer removes the shared connection assertSharedConnectionCount(0); } }