blob: 2e159c069f5ead590664585e22a8aaf0cb2b5047 [file] [log] [blame]
/*
* Derby - Class org.apache.derbyTesting.functionTests.tests.store.IndexSplitDeadlockTest
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.derbyTesting.functionTests.tests.store;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import junit.framework.Test;
import org.apache.derbyTesting.functionTests.util.Barrier;
import org.apache.derbyTesting.junit.BaseJDBCTestCase;
import org.apache.derbyTesting.junit.CleanDatabaseTestSetup;
import org.apache.derbyTesting.junit.DatabasePropertyTestSetup;
import org.apache.derbyTesting.junit.JDBC;
import org.apache.derbyTesting.junit.TestConfiguration;
/**
* Test that executes the code paths changed by the fix for the index split
* deadlock (DERBY-2991). The main purpose is to test that index scans are
* able to reposition in cases where they release the latch on the leaf page
* on which they are positioned (typically because they had to wait for a
* lock, or because they returned control to the caller after fetching a
* bulk of rows).
*/
public class IndexSplitDeadlockTest extends BaseJDBCTestCase {
/**
* List of threads (AsyncThread objects) to wait for after running the test.
*/
private List<AsyncThread> threads = new ArrayList<AsyncThread>();
public IndexSplitDeadlockTest(String name) {
super(name);
}
public static Test suite() {
Test test = TestConfiguration.embeddedSuite(
IndexSplitDeadlockTest.class);
// DERBY-4273: Include the lock table in the error message to help
// debugging in case of lock timeouts.
test = DatabasePropertyTestSetup.singleProperty(
test, "derby.locks.deadlockTrace", "true");
test = new CleanDatabaseTestSetup(test);
return test;
}
protected void tearDown() throws Exception {
// Rollback all uncommitted operations so that we don't hold any
// locks that may block the other threads.
rollback();
for (AsyncThread thread : threads) {
thread.waitFor();
}
threads = null;
// All the other threads have finished. Now, remove everything from
// the APP schema so that we don't leave anything around for subsequent
// tests.
setAutoCommit(false); // required by JDBC.dropSchema()
JDBC.dropSchema(getConnection().getMetaData(), "APP");
super.tearDown();
}
// --------------------------------------------------------------------
// Test cases for calls to BTreeScan.reposition() in BTreeMaxScan
// --------------------------------------------------------------------
// NOTE: There is a call in fetchMax() that cannot be reached because the
// scan state is alway SCAN_INIT when that method is called, and it only
// calls reposition() if the scan state is SCAN_INPROGRESS. Therefore,
// there's no test case for fetchMax().
public void testBTreeMaxScan_fetchMaxRowFromBeginning() throws Exception {
setAutoCommit(false);
Statement s = createStatement();
s.executeUpdate("create table max_scan(x int)");
s.executeUpdate("create index idx on max_scan(x)");
// We need to make sure that we have at least two leaf pages. Each
// 4K index page can hold ~200 rows.
PreparedStatement ins = prepareStatement(
"insert into max_scan values ?");
for (int i = 0; i < 500; i++) {
ins.setInt(1, i * 2);
ins.executeUpdate();
}
commit();
// Now make sure that the right-most leaf is empty, so that we must
// fetch the max value from the beginning.
s.executeUpdate("delete from max_scan where x > 50");
// Obtain lock in another thread to block scans. Release lock after
// two seconds.
obstruct("update max_scan set x = x where x = 10", 2000);
// Give the other thread time to obtain the lock.
Thread.sleep(1000);
// Perform a max scan (from beginning because last page is empty).
// Will force repositioning because we must wait for the lock and
// release the latch.
JDBC.assertSingleValueResultSet(s.executeQuery(
"select max(x) from max_scan --DERBY-PROPERTIES index=IDX"),
"50");
}
// --------------------------------------------------------------------
// Test cases for calls to BTreeScan.reposition() in BTreeForwardScan
// --------------------------------------------------------------------
/**
* Test first call to reposition() in BTreeForwardScan.fetchRows().
* This call happens when a new batch of rows is requested from a scan
* that's in progress.
*/
public void testBTreeForwardScan_fetchRows_resumeAfterSplit()
throws SQLException {
// Create a table and an index and populate them
Statement s = createStatement();
s.executeUpdate("create table t (x int)");
s.executeUpdate("create index idx on t(x)");
PreparedStatement ins = prepareStatement("insert into t values ?");
for (int i = 0; i < 400; i++) {
ins.setInt(1, i);
ins.executeUpdate();
}
// Start an index scan and fetch some rows so that it's in the
// INPROGRESS state. Just fetch a small number of rows so that we
// are still positioned on the left-most leaf page.
ResultSet rs = s.executeQuery(
"select * from t --DERBY-PROPERTIES index=IDX");
for (int i = 0; i < 30; i++) {
assertTrue(rs.next());
assertEquals(i, rs.getInt(1));
}
// In another transaction, insert values smaller than the values
// currently in the index. This causes a split of the left-most leaf.
// Before DERBY-2991 we'd get a lock timeout here.
Connection c2 = openDefaultConnection();
Statement s2 = c2.createStatement();
for (int i = 0; i < 300; i++) {
s2.executeUpdate("insert into t values -1");
}
s2.close();
c2.close();
// Continue the index scan. This will trigger a full repositioning
// from the root of the B-tree since the page on which we were
// positioned has been split.
for (int i = 30; i < 400; i++) {
assertTrue(rs.next());
assertEquals(i, rs.getInt(1));
}
assertFalse(rs.next());
rs.close();
}
/**
* Test that we can reposition on a holdable cursor after a commit and
* a split on the leaf page of the current position. This tests the
* second call to reposition() in BTreeForwardScan.fetchRows().
*/
public void testBTreeForwardScan_fetchRows_resumeScanAfterCommitAndSplit()
throws SQLException {
setAutoCommit(false);
// Create a table and an index and populate them
Statement s1 = createStatement();
s1.executeUpdate("create table t (x int)");
s1.executeUpdate("create index idx on t(x)");
PreparedStatement ins = prepareStatement("insert into t values ?");
for (int i = 0; i < 1000; i++) {
ins.setInt(1, i);
ins.executeUpdate();
}
commit();
// Start an index scan with a holdable cursor, and fetch some rows
// to move the position to the middle of the index.
assertEquals("This test must use a holdable cursor",
ResultSet.HOLD_CURSORS_OVER_COMMIT,
s1.getResultSetHoldability());
ResultSet rs = s1.executeQuery(
"select * from t --DERBY-PROPERTIES index=IDX");
for (int i = 0; i < 500; i++) {
assertTrue(rs.next());
assertEquals(i, rs.getInt(1));
}
commit();
// Insert rows right before the one we're positioned on in order to
// split that page.
Statement s2 = createStatement();
for (int i = 0; i < 300; i++) {
s2.executeUpdate("insert into t values 498");
}
// Check that the index scan can continue where we left it, even
// though we committed and released the latches.
for (int i = 500; i < 1000; i++) {
assertTrue(rs.next());
assertEquals(i, rs.getInt(1));
}
assertFalse(rs.next());
rs.close();
}
/**
* Test that we can reposition on a holdable cursor after a commit and
* a compress that removes the leaf page of the current position. This
* tests the second call to reposition() in BTreeForwardScan.fetchRows().
*/
public void testBTreeForwardScan_fetchRows_resumeScanAfterCompress()
throws Exception {
setAutoCommit(false);
// Create a table and an index and populate them
Statement s1 = createStatement();
s1.executeUpdate("create table t (x int)");
s1.executeUpdate("create index idx on t(x)");
PreparedStatement ins = prepareStatement("insert into t values ?");
for (int i = 0; i < 1000; i++) {
ins.setInt(1, i);
ins.executeUpdate();
}
commit();
// Start an index scan with a holdable cursor, and fetch some rows
// to move the position to the middle of the index.
assertEquals("This test must use a holdable cursor",
ResultSet.HOLD_CURSORS_OVER_COMMIT,
s1.getResultSetHoldability());
ResultSet rs = s1.executeQuery(
"select * from t --DERBY-PROPERTIES index=IDX");
for (int i = 0; i < 500; i++) {
assertTrue(rs.next());
assertEquals(i, rs.getInt(1));
}
commit();
// Delete all rows and compress the table so that the leaf page on
// which the result set is positioned disappears.
Statement s2 = createStatement();
s2.executeUpdate("delete from t");
commit();
// Sleep for a little while, otherwise SYSCS_INPLACE_COMPRESS_TABLE
// doesn't free any space in the index (waiting for the background
// thread to perform post-commit work?)
Thread.sleep(1000);
s2.execute("call syscs_util.syscs_inplace_compress_table" +
"('APP','T',1,1,1)");
commit();
// Check that we are able to reposition. We may or may not see more
// rows, since some rows may still be available in the cache in the
// result set. The point of the code below is to see that calls to
// ResultSet.next() don't fail when the page has disappeared, not to
// test how many of the deleted rows are returned.
int expected = 500;
while (rs.next()) {
assertTrue(expected < 1000);
assertEquals(expected, rs.getInt(1));
expected++;
}
rs.close();
}
/**
* Test that BTreeForwardScan.fetchRows() can reposition after releasing
* latches because it had to wait for a lock. This tests the third call
* to reposition() in fetchRows(), which is only called if the index is
* unique.
*/
public void testBTreeForwardScan_fetchRows_resumeAfterWait_unique()
throws Exception {
setAutoCommit(false);
// Populate a table with a unique index
Statement s = createStatement();
s.executeUpdate("create table t (x int, constraint c primary key(x))");
PreparedStatement ins = prepareStatement("insert into t values ?");
for (int i = 0; i < 300; i++) {
ins.setInt(1, i);
ins.executeUpdate();
}
commit();
// Hold a lock in a different thread to stop the index scan
obstruct("delete from t where x = 100", 2000);
// Give the other thread time to obtain the lock
Thread.sleep(1000);
// Perform an index scan. Will be blocked for a while when fetching
// the row where x=100, but should be able to resume the scan.
ResultSet rs = s.executeQuery(
"select * from t --DERBY-PROPERTIES constraint=C");
for (int i = 0; i < 300; i++) {
assertTrue(rs.next());
assertEquals(i, rs.getInt(1));
}
assertFalse(rs.next());
rs.close();
}
/**
* Test that BTreeForwardScan.fetchRows() can reposition after releasing
* latches because it had to wait for a lock, and the leaf page on which
* the scan is positioned has been split. This tests the third call
* to reposition() in fetchRows(), which is only called if the index is
* unique.
*/
public void testBTreeForwardScan_fetchRows_resumeAfterWait_unique_split()
throws Exception {
setAutoCommit(false);
// Populate a table with a unique index
Statement s = createStatement();
s.executeUpdate("create table t (x int, constraint c primary key(x))");
PreparedStatement ins = prepareStatement("insert into t values ?");
for (int i = 0; i < 300; i++) {
ins.setInt(1, i);
ins.executeUpdate();
}
commit();
// Object used for synchronization between the main thread and the
// helper thread. The main thread uses it to tell the helper thread
// that it has started the index scan. The helper thread uses it
// to tell the main thread that it has locked row 40 and is ready to
// insert more values. Both threads should wait until the other thread
// has reached the barrier before continuing.
final Barrier barrier = new Barrier(2);
// Lock a row on the first page in a different thread to stop the
// index scan. Then split the first leaf by inserting many values
// less than zero.
new AsyncThread(new AsyncTask() {
public void doWork(Connection conn) throws Exception {
conn.setAutoCommit(false);
Statement s = conn.createStatement();
s.executeUpdate("update t set x = x where x = 40");
s.close();
// Tell the main thread that we've locked the row and that
// it can go ahead with the index scan. Wait here until the
// main thread has started the scan.
barrier.await();
// The main thread has started the index scan. Give it a
// second to get to the row we have locked.
Thread.sleep(1000L);
// Split the first leaf
PreparedStatement ps = conn.prepareStatement(
"insert into t values ?");
for (int i = -1; i > -300; i--) {
ps.setInt(1, i);
ps.executeUpdate();
}
ps.close();
conn.commit();
}
});
// Prepare the index scan.
ResultSet rs = s.executeQuery(
"select * from t --DERBY-PROPERTIES constraint=C");
// Perform an index scan. Will be blocked for a while when fetching
// the row where x=40, but should be able to resume the scan.
for (int i = 0; i < 300; i++) {
assertTrue(rs.next());
assertEquals(i, rs.getInt(1));
// Once we have fetched the first row, tell the helper thread we
// have started the index scan, and wait until it has locked the
// row that should block the scan (x=40).
if (i == 0) {
barrier.await();
}
}
assertFalse(rs.next());
rs.close();
}
/**
* Test that BTreeForwardScan.fetchRows() can reposition after releasing
* latches because it had to wait for a lock. This tests the fourth call
* to reposition() in fetchRows(), which is only called if the index is
* non-unique.
*/
public void testBTreeForwardScan_fetchRows_resumeAfterWait_nonUnique()
throws Exception {
setAutoCommit(false);
// Populate a table with a non-unique index
Statement s = createStatement();
s.executeUpdate("create table t (x int)");
s.executeUpdate("create index idx on t(x)");
PreparedStatement ins = prepareStatement("insert into t values ?");
for (int i = 0; i < 300; i++) {
ins.setInt(1, i);
ins.executeUpdate();
}
commit();
// Hold a lock in a different thread to stop the index scan
obstruct("delete from t where x = 100", 2000);
// Give the other thread time to obtain the lock
Thread.sleep(1000);
// Perform an index scan. Will be blocked for a while when fetching
// the row where x=100, but should be able to resume the scan.
ResultSet rs = s.executeQuery(
"select * from t --DERBY-PROPERTIES index=IDX");
for (int i = 0; i < 300; i++) {
assertTrue(rs.next());
assertEquals(i, rs.getInt(1));
}
assertFalse(rs.next());
rs.close();
}
/**
* Test that BTreeForwardScan.fetchRows() can reposition after releasing
* latches because it had to wait for a lock, and the leaf page on which
* the scan is positioned has been split. This tests the fourth call
* to reposition() in fetchRows(), which is only called if the index is
* non-unique.
*/
public void testBTreeForwardScan_fetchRows_resumeAfterWait_nonUnique_split()
throws Exception {
setAutoCommit(false);
// Populate a table with a non-unique index
Statement s = createStatement();
s.executeUpdate("create table t (x int)");
s.executeUpdate("create index idx on t(x)");
PreparedStatement ins = prepareStatement("insert into t values ?");
for (int i = 0; i < 300; i++) {
ins.setInt(1, i);
ins.executeUpdate();
}
commit();
// Object used for synchronization between main thread and helper
// thread. They should both wait for the other thread to reach the
// barrier point before continuing.
final Barrier barrier = new Barrier(2);
// Hold a lock in a different thread to stop the index scan, then
// split the first leaf (on which the scan is positioned) before the
// lock is released.
new AsyncThread(new AsyncTask() {
public void doWork(Connection conn) throws Exception {
conn.setAutoCommit(false);
Statement s = conn.createStatement();
s.executeUpdate("update t set x = x where x = 40");
// Tell the main thread we have locked the row, and wait for
// it to start the index scan.
barrier.await();
// Give the index scan time to get to the row we have locked.
Thread.sleep(1000);
// The index scan should be blocked now. Split the first leaf
// by inserting more values just before the lowest key, so
// that we can verify that the index scan is able to reposition
// correctly after a page split.
for (int i = 0; i < 300; i++) {
s.executeUpdate("insert into t values -1");
}
s.close();
conn.commit();
}
});
// Perform an index scan. Will be blocked for a while when fetching
// the row where x=40, but should be able to resume the scan after
// the helper thread commits and releases its locks.
ResultSet rs = s.executeQuery(
"select * from t --DERBY-PROPERTIES index=IDX");
for (int i = 0; i < 300; i++) {
assertTrue(rs.next());
assertEquals(i, rs.getInt(1));
// Once we have fetched the first row, tell the helper thread we
// have started the index scan, and wait until it has locked the
// row that should block the scan (x=40).
if (i == 0) {
barrier.await();
}
}
assertFalse(rs.next());
rs.close();
}
// --------------------------------------------------------------------
// Test cases for calls to BTreeScan.reposition() in BTreeScan
// --------------------------------------------------------------------
// There's a call to reposition() from positionAtDoneScanFromClose(), but
// I'm not sure how to reach it. According to the code coverage reports
// there's no other tests that reach that call to reposition().
//
// Not testing the first call to reposition() in delete() since it will
// be exercised by all code that deletes or modifies index rows, so it's
// already exercised by other tests. The existing tests do not make the
// this call do a full repositioning from the root of the B-tree, but
// this is very difficult to test because a page split needs to happen in
// the very short window between the scan releases the latch and delete()
// reobtains the latch.
//
// The other call to reposition() in delete() is only used if
// init_useUpdateLocks is true. No other tests reach that call, according
// to the code coverage reports, and I'm not sure how/if it can be
// reached from the public API. Leaving it untested for now.
//
// There's a call to reposition() in BTreeScan.doesCurrentPositionQualify()
// too. The only caller (except test code bypassing the public API) is
// TableScanResultSet.getCurrentRow(), which is only called from trigger
// code (for before and after result sets) and CurrentOfResultSets. It
// doesn't look like these will ever use a TableScanResultSet wrapping a
// index scan, so there's no test for this method here. (The method is
// exercised from T_b2i by using the internal API directly.)
//
// Same comment as above goes for BTreeScan.isCurrentPositionDeleted(), as
// it is used the same places as doesCurrentPositionQualify().
//
// The call to reposition() from BTreeScan.fetch() is also hard to reach.
// It can be reached from getConstraintDescriptorViaIndex(), which is
// frequently exercised by other tests, so I'm not adding a test case here.
// In order to test repositioning after a split in this method, we should
// rather have a test case calls the internal API directly (e.g., in
// T_b2i).
//
// Similarly, BTreeScan.reopenScan() has a call to reposition() that's
// exercised frequently by other tests, but to test a split right before
// the repositioning, we'd probably need to use the internal API for that
// method too.
// --------------------------------------------------------------------
// Test cases for bugs related to saving position and repositioning
// --------------------------------------------------------------------
/**
* Test that a max scan works when it needs to wait more than once in order
* to lock the last record in the index. This used to cause an assert
* failure in sane builds before DERBY-4193.
*/
public void testMultipleLastKeyWaitsInMaxScan() throws Exception {
setAutoCommit(false);
// Create a table with an index and a couple of rows.
Statement s = createStatement();
s.execute("create table max_scan(x int, y int)");
s.execute("create index idx on max_scan(x)");
s.execute("insert into max_scan(x) values 1,2,3");
commit();
// Start a thread that (1) obtains an exclusive lock on the last
// row, (2) waits for the main thread to perform a max scan that will
// be blocked by the lock, (3) inserts values greater than the current
// max so that the main thread needs to rescan when it wakes up, (4)
// commit to allow the main thread to continue, and (5) immediately
// insert more rows greater than the previous max so that the main
// thread is likely to have to wait for a lock a second time.
new AsyncThread(new AsyncTask() {
public void doWork(Connection conn) throws Exception {
conn.setAutoCommit(false);
Statement s = conn.createStatement();
s.execute("update max_scan set y = x where x = 3");
s.close();
// Give the main thread time to start executing select max(x)
// and wait for the lock.
Thread.sleep(2000);
// Insert rows greater than the current max.
PreparedStatement ps = conn.prepareStatement(
"insert into max_scan(x) values 4");
for (int i = 0; i < 300; i++) {
ps.execute();
}
// Commit and release locks to allow the main thread to
// continue.
conn.commit();
// Insert some more rows so that the main thread is likely to
// have to wait again. Note that there is a possibility that
// the main thread manages to obtain the lock on the last row
// before we manage to insert a new row, in which case it
// won't have to wait for us and we're not actually testing
// a max scan that needs to wait more than once to lock the
// last row.
for (int i = 0; i < 300; i++) {
ps.execute();
}
// Block for a while before releasing locks, so that the main
// thread will have to wait if it didn't obtain the lock on the
// last row before we did.
Thread.sleep(500);
conn.commit();
ps.close();
}
});
// Give the other thread a little while to start and obtain the
// lock on the last record. We expect two locks in the lock table
// when the other thread is ready. Don't wait more than a minute
// as something must have gone wrong.
int totalWait = 0;
do {
totalWait += 500;
Thread.sleep(500);
} while (numlocks() < 2 && totalWait < 60000);
// The last record should be locked now, so this call will have to
// wait initially. This statement used to cause an assert failure in
// debug builds before DERBY-4193.
JDBC.assertSingleValueResultSet(
s.executeQuery("select max(x) from max_scan " +
"--DERBY-PROPERTIES index=IDX"),
"4");
}
/**
* Get the number of locks in the lock table
* @return number of locks
* @throws SQLException
*/
private int numlocks() throws SQLException {
Statement s = createStatement();
ResultSet rs = s.executeQuery("SELECT count(*) from syscs_diag.lock_table");
rs.next();
int num = rs.getInt(1);
rs.close();
return num;
}
/**
* Test that a forward scan works even in the case that it has to wait
* for the previous key lock more than once. This used to cause an assert
* failure in sane builds before DERBY-4193.
*/
public void testMultiplePrevKeyWaitsInForwardScan() throws Exception {
setAutoCommit(false);
// Isolation level should be serializable so that the scan needs
// a previous key lock.
getConnection().setTransactionIsolation(
Connection.TRANSACTION_SERIALIZABLE);
// Create a table with an index and a couple of rows.
Statement s = createStatement();
s.execute("create table fw_scan(x int)");
s.execute("create index idx on fw_scan(x)");
s.execute("insert into fw_scan(x) values 100,200,300");
commit();
new AsyncThread(new AsyncTask() {
public void doWork(Connection conn) throws Exception {
conn.setAutoCommit(false);
PreparedStatement ps =
conn.prepareStatement("insert into fw_scan values 1");
// Insert one row right before the first row to be returned
// by the scan. This will be the previous key that the scan
// will attempt to lock. Wait for two seconds to allow the
// scan to start and attempt to lock the record.
ps.execute();
Thread.sleep(2000);
// Before we commit and release the lock, insert more rows
// between the locked row and the first row of the scan, so
// that another row holds the previous key for the scan when
// it wakes up.
for (int i = 0; i < 300; i++) {
ps.execute();
}
conn.commit();
// The scan will wake up and try to lock the row that has
// now become the row immediately to the left of its starting
// position. Try to beat it to it so that it has to wait a
// second time in order to obtain the previous key lock. This
// used to trigger an assert failure in the scan before
// DERBY-4193.
for (int i = 0; i < 300; i++) {
ps.execute();
}
// Wait a little while to give the scan enough time to wake
// up and make another attempt to lock the previous key before
// we release the locks.
Thread.sleep(500);
conn.rollback();
ps.close();
}
});
// Give the other thread a second to start and obtain a lock that
// blocks the scan.
Thread.sleep(1000);
// The key to the left of the first key to be returned by the scan
// should be locked now. This call will have to wait for the previous
// key lock at least once. If it has to wait a second time (dependent
// on the exact timing between this thread and the other thread) the
// assert error from DERBY-4193 will be exposed.
JDBC.assertSingleValueResultSet(
s.executeQuery("select x from fw_scan " +
"--DERBY-PROPERTIES index=IDX\n" +
"where x >= 100 and x < 200"),
"100");
}
// --------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------
/**
* <p>
* In a separate thread, and in a separate transaction, execute the
* SQL text and wait for the specified period of time, before the
* transaction is rolled back. This method can be used to hold locks
* and thereby block the main thread for a certain amount of time.
* </p>
*
* <p>
* If an exception is thrown while executing the SQL, the exception is
* stored and rethrown from the tearDown() method in the main execution
* thread, so that it is detected by the JUnit framework.
* </p>
*
* @param sql the SQL text to execute
* @param blockMillis how many milliseconds to wait until the transaction
* is rolled back
*/
private void obstruct(final String sql, final long blockMillis) {
AsyncTask task = new AsyncTask() {
public void doWork(Connection conn) throws Exception {
conn.setAutoCommit(false);
Statement s = conn.createStatement();
s.execute(sql);
s.close();
Thread.sleep(blockMillis);
}
};
new AsyncThread(task);
}
/**
* Interface that should be implemented by classes that define a
* database task that is to be executed asynchronously in a separate
* transaction.
*/
private static interface AsyncTask {
void doWork(Connection conn) throws Exception;
}
/**
* Class that executes an {@code AsyncTask} object.
*/
private class AsyncThread implements Runnable {
private final Thread thread = new Thread(this);
private final AsyncTask task;
private Exception error;
/**
* Create an {@code AsyncThread} object and starts a thread executing
* the task. Also put the {@code AsyncThread} object in the list of
* threads in the parent object to make sure the thread is waited for
* and its errors detected in the {@code tearDown()} method.
*
* @param task the task to perform
*/
public AsyncThread(AsyncTask task) {
this.task = task;
thread.start();
threads.add(this);
}
/**
* Open a database connection and perform the task. Roll back the
* transaction when finished. Any exception thrown will be caught and
* rethrown when the {@code waitFor()} method is called.
*/
public void run() {
try {
Connection conn = openDefaultConnection();
try {
task.doWork(conn);
} finally {
JDBC.cleanup(conn);
}
} catch (Exception e) {
error = e;
}
}
/**
* Wait for the thread to complete. If an error was thrown during
* execution, rethrow the execption here.
* @throws Exception if an error happened while performing the task
*/
void waitFor() throws Exception {
thread.join();
if (error != null) {
throw error;
}
}
}
}