/*
 * 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;
            }
        }
    }
}
