diff --git a/briar-tests/src/org/briarproject/db/TransactionIsolationTest.java b/briar-tests/src/org/briarproject/db/TransactionIsolationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b01f5770a9413b9a2f0508a1b6e57aa05a37ff03 --- /dev/null +++ b/briar-tests/src/org/briarproject/db/TransactionIsolationTest.java @@ -0,0 +1,277 @@ +package org.briarproject.db; + +import org.briarproject.BriarTestCase; +import org.briarproject.TestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class TransactionIsolationTest extends BriarTestCase { + + private static final String DROP_TABLE = "DROP TABLE foo IF EXISTS"; + private static final String CREATE_TABLE = "CREATE TABLE foo" + + " (key INT NOT NULL," + + " counter INT NOT NULL)"; + private static final String INSERT_ROW = + "INSERT INTO foo (key, counter) VALUES (1, 123)"; + private static final String GET_COUNTER = + "SELECT counter FROM foo WHERE key = 1"; + private static final String SET_COUNTER = + "UPDATE foo SET counter = ? WHERE key = 1"; + + private final File testDir = TestUtils.getTestDirectory(); + private final File db = new File(testDir, "db"); + private final String withMvcc = "jdbc:h2:" + db.getAbsolutePath() + + ";MV_STORE=TRUE;MVCC=TRUE"; + private final String withoutMvcc = "jdbc:h2:" + db.getAbsolutePath() + + ";MV_STORE=FALSE;MVCC=FALSE;LOCK_MODE=1"; + + @Before + public void setUp() throws Exception { + assertTrue(testDir.mkdirs()); + Class.forName("org.h2.Driver"); + } + + @After + public void tearDown() throws Exception { + TestUtils.deleteTestDirectory(testDir); + } + + @Test + public void testDoesNotReadUncommittedWritesWithMvcc() throws Exception { + Connection connection = openConnection(true); + try { + createTableAndInsertRow(connection); + } finally { + connection.close(); + } + // Start the first transaction + Connection txn1 = openConnection(true); + try { + txn1.setAutoCommit(false); + // The first transaction should read the initial value + assertEquals(123, getCounter(txn1)); + // The first transaction updates the value but doesn't commit it + assertEquals(1, setCounter(txn1, 234)); + // Start the second transaction + Connection txn2 = openConnection(true); + try { + txn2.setAutoCommit(false); + // The second transaction should still read the initial value + assertEquals(123, getCounter(txn2)); + // Commit the second transaction + txn2.commit(); + } finally { + txn2.close(); + } + // Commit the first transaction + txn1.commit(); + } finally { + txn1.close(); + } + } + + @Test + public void testLastWriterWinsWithMvcc() throws Exception { + Connection connection = openConnection(true); + try { + createTableAndInsertRow(connection); + } finally { + connection.close(); + } + // Start the first transaction + Connection txn1 = openConnection(true); + try { + txn1.setAutoCommit(false); + // The first transaction should read the initial value + assertEquals(123, getCounter(txn1)); + // The first transaction updates the value but doesn't commit it + assertEquals(1, setCounter(txn1, 234)); + // Start the second transaction + Connection txn2 = openConnection(true); + try { + txn2.setAutoCommit(false); + // The second transaction should still read the initial value + assertEquals(123, getCounter(txn2)); + // Commit the first transaction + txn1.commit(); + // The second transaction updates the value + assertEquals(1, setCounter(txn2, 345)); + // Commit the second transaction + txn2.commit(); + } finally { + txn2.close(); + } + } finally { + txn1.close(); + } + // The second transaction was the last writer, so it should win + connection = openConnection(true); + try { + assertEquals(345, getCounter(connection)); + } finally { + connection.close(); + } + } + + @Test + public void testLockTimeoutOnRowWithMvcc() throws Exception { + Connection connection = openConnection(true); + try { + createTableAndInsertRow(connection); + } finally { + connection.close(); + } + // Start the first transaction + Connection txn1 = openConnection(true); + try { + txn1.setAutoCommit(false); + // The first transaction should read the initial value + assertEquals(123, getCounter(txn1)); + // Start the second transaction + Connection txn2 = openConnection(true); + try { + txn2.setAutoCommit(false); + // The second transaction should read the initial value + assertEquals(123, getCounter(txn2)); + // The first transaction updates the value but doesn't commit it + assertEquals(1, setCounter(txn1, 234)); + // The second transaction tries to update the value + try { + setCounter(txn2, 345); + fail(); + } catch (SQLException expected) { + // Expected: the row is locked by the first transaction + } + // Abort the transactions + txn1.rollback(); + txn2.rollback(); + } finally { + txn2.close(); + } + } finally { + txn1.close(); + } + } + + @Test + public void testReadLockTimeoutOnTableWithoutMvcc() throws Exception { + Connection connection = openConnection(false); + try { + createTableAndInsertRow(connection); + } finally { + connection.close(); + } + // Start the first transaction + Connection txn1 = openConnection(false); + try { + txn1.setAutoCommit(false); + // The first transaction should read the initial value + assertEquals(123, getCounter(txn1)); + // Start the second transaction + Connection txn2 = openConnection(false); + try { + txn2.setAutoCommit(false); + // The second transaction should read the initial value + assertEquals(123, getCounter(txn2)); + // The first transaction tries to update the value + try { + setCounter(txn1, 345); + fail(); + } catch (SQLException expected) { + // Expected: the table is locked by the second transaction + } + // Abort the transactions + txn1.rollback(); + txn2.rollback(); + } finally { + txn2.close(); + } + } finally { + txn1.close(); + } + } + + @Test + public void testWriteLockTimeoutOnTableWithoutMvcc() throws Exception { + Connection connection = openConnection(false); + try { + createTableAndInsertRow(connection); + } finally { + connection.close(); + } + // Start the first transaction + Connection txn1 = openConnection(false); + try { + txn1.setAutoCommit(false); + // The first transaction should read the initial value + assertEquals(123, getCounter(txn1)); + // The first transaction updates the value but doesn't commit yet + assertEquals(1, setCounter(txn1, 345)); + // Start the second transaction + Connection txn2 = openConnection(false); + try { + txn2.setAutoCommit(false); + // The second transaction tries to read the value + try { + getCounter(txn2); + fail(); + } catch (SQLException expected) { + // Expected: the table is locked by the first transaction + } + // Abort the transactions + txn1.rollback(); + txn2.rollback(); + } finally { + txn2.close(); + } + } finally { + txn1.close(); + } + } + + private Connection openConnection(boolean mvcc) throws SQLException { + return DriverManager.getConnection(mvcc ? withMvcc : withoutMvcc); + } + + private void createTableAndInsertRow(Connection c) throws SQLException { + Statement s = c.createStatement(); + s.executeUpdate(DROP_TABLE); + s.executeUpdate(CREATE_TABLE); + s.executeUpdate(INSERT_ROW); + s.close(); + } + + private int getCounter(Connection c) throws SQLException { + Statement s = c.createStatement(); + ResultSet rs = s.executeQuery(GET_COUNTER); + assertTrue(rs.next()); + int counter = rs.getInt(1); + assertFalse(rs.next()); + rs.close(); + s.close(); + return counter; + } + + private int setCounter(Connection c, int counter) + throws SQLException { + PreparedStatement ps = c.prepareStatement(SET_COUNTER); + ps.setInt(1, counter); + int rowsAffected = ps.executeUpdate(); + ps.close(); + return rowsAffected; + } +}