2

There are sometimes errors like this when using SQL Server:

Transaction (Process ID 54) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.

For more background, check out what Jeff Atwood has blogged about this issue.

I'd like to create an SQL Server deadlock programmatically with a small test using plain-old JDBC. The test should immediately create a deadlock so that I can test some retry logic.

My understanding, from reading Jeff's analysis, is that I need only have some data and read it a lot, and write it a little.

I currently have a short Java program (below) that creates a table and writes some test data to the table. The program them launches several hundred threads. Each thread either does an update, or a read of the test data. I have varied the ratio of update to read operations, but regardless of the ratio, I cannot seem to programmatically create a deadlock. This version of the test program does not have my retry logic, I'll add that once I can reliably get SQL Server deadlocks happening.

I wondered whether having all of the threads running in a single process might somehow serialize operations at the JDBC driver level, so I tried running several processes concurrently (on the same machine), but still no deadlocks.

import java.sql.*;
import java.util.*;
import java.util.concurrent.*;
import static java.util.concurrent.TimeUnit.*;

public class Deadlock {
    static final int QUERY_THREAD_COUNT = 300, MAX_OPERATIONS_ITERATION = 5000;
    static String server, db, user, pw;
    static CountDownLatch latch = new CountDownLatch(QUERY_THREAD_COUNT);

    public static void main(String... args) throws Exception {
        server = args[0];
        db     = args[1];
        user   = args[2];
        pw     = args[3];
        Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
        Connection connection = getConnection();
        Statement statement = connection.createStatement();
        statement.execute("CREATE TABLE TESTTABLE (BAR INTEGER, BAZ VARCHAR(32))");
        statement.execute("DELETE FROM TESTTABLE");
        statement.execute("INSERT INTO TESTTABLE VALUES (1, 'FOOBARBAZ')");
        connection.setAutoCommit(false);
        connection.commit();
        connection.close();
        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        for (int i = 0; i < QUERY_THREAD_COUNT; ++i) {
            scheduledExecutorService.scheduleWithFixedDelay(new Operation(), 0, 1, MILLISECONDS);
        }
        latch.await();
        System.exit(0);
    }

    static class Operation implements Runnable {
        Connection connection = getConnection();
        Statement statement = getStatement(connection);
        int iteration;

        @Override
        public void run() {
            if (++iteration > MAX_OPERATIONS_ITERATION) {
                latch.countDown();
                return;
            }
            try {
                double  random = Math.random();
                boolean update = (random < 0.01);
                if (update) {
                    statement.executeUpdate("UPDATE TESTTABLE SET BAR=" + ((int) (random * 100)) + " WHERE BAZ='FOOBARBAZ'");
                } else {
                    ResultSet rs = statement.executeQuery("SELECT BAR, BAZ FROM TESTTABLE");
                    if (! rs.next()) {
                        return;
                    }
                    int    bar = rs.getInt(1);
                    String baz = rs.getString(2);
                    if (bar > 100) {
                        System.err.println("int is greater than 100");
                    }
                    if (! baz.equals("FOOBARBAZ")) {
                        System.err.println("string is not FOOBARBAZ");
                    }
                }
                connection.commit();
            } catch (SQLException sqle) { // <-- looking for a deadlock exception here!
                System.err.println(sqle);
            }
        }
    }

    static Connection getConnection() {
        try {
            return DriverManager.getConnection("jdbc:sqlserver://" + server + ";databaseName=" + db + ";", user, pw);
        } catch (Exception e) {
            System.err.println(e);
            throw new RuntimeException(e);
        }
    }

    static Statement getStatement(Connection connection) {
        try {
            return connection.createStatement();
        } catch (Exception e) {
            System.err.println(e);
            throw new RuntimeException(e);
        }
    }
}
Greg Mattes
  • 33,090
  • 15
  • 73
  • 105

2 Answers2

5

I think this does it:

import java.sql.*;
import java.util.*;
import java.util.concurrent.*;

/**
 * Creates an SQL Server deadlock.
 *
 * <pre>
   javac SQLServerDeadlock.java && java -cp ".:sqljdbc.jar" SQLServerDeadlock <server> <db-name> <username> <password>
 * </pre>
 */
public class SQLServerDeadlock {
    static String server, db, user, pw;
    static String TABLE_A = "TABLE_A", TABLE_B = "TABLE_B";
    static CountDownLatch latch = new CountDownLatch(2);

    public static void main(String... args) throws SQLException {
        server = args[0];
        db     = args[1];
        user   = args[2];
        pw     = args[3];
        Connection connection = null;
        try {
            Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
            connection = getConnection();
            init(connection);
            Thread t1 = new Thread(new Update(TABLE_A, TABLE_B), "A-THEN-B");
            Thread t2 = new Thread(new Update(TABLE_B, TABLE_A), "B-THEN-A");
            if (Math.random() < .5) {
                t1.start();
                t2.start();
            } else {
                t2.start();
                t1.start();
            }
            t1.join();
            t2.join();
        } catch (Exception e) {
            System.err.println(e);
        } finally {
            cleanup(connection);
        }
    }

    static class Update implements Runnable {
        String table1;
        String table2;

        Update(String table1, String table2) {
            this.table1 = table1;
            this.table2 = table2;
        }

        @Override
        public void run() {
            Connection connection = null;
            try {
                connection = getConnection();
                Statement statement = connection.createStatement();
                statement.executeUpdate("UPDATE " + table1 + " SET FOO=1");
                latch.countDown();
                latch.await();
                statement.executeUpdate("UPDATE " + table2 + " SET FOO=1");
                connection.commit();
                System.err.println(Thread.currentThread().getName() + ": SUCCESS!");
            } catch (SQLException sqle) {
                if (sqle.getMessage().contains("Rerun the transaction")) {
                    System.err.println(Thread.currentThread().getName() + ": DEADLOCK VICTIM!");
                }
                System.err.println(sqle);
            } catch (InterruptedException ie) {
                System.err.println(ie);
            } finally {
                try {
                    connection.close();
                } catch (SQLException sqle) {
                    System.err.println(sqle);
                }
            }
        }
    }

    static void init(Connection connection) throws SQLException {
        Statement statement = null;
        try {
            statement = connection.createStatement();
            for (String tableName : Arrays.asList(TABLE_A, TABLE_B)) {
                if (tableExists(connection, tableName)) {
                    statement.execute("DROP TABLE " + tableName);
                }
                statement.execute("CREATE TABLE " + tableName + " (FOO INTEGER)");
                statement.execute("INSERT INTO  " + tableName + " VALUES (0)");
            }
            connection.commit();
        } finally {
            statement.close();
        }
    }

    static void cleanup(Connection connection) throws SQLException {
        if (connection == null) {
            return;
        }
        Statement statement = null;
        try {
            statement = connection.createStatement();
            for (String tableName : Arrays.asList(TABLE_A, TABLE_B)) {
                if (tableExists(connection, tableName)) {
                    statement.execute("DROP TABLE " + tableName);
                }
            }
            connection.commit();
        } finally {
            statement.close();
        }
    }

    static boolean tableExists(Connection connection, String tableName) throws SQLException {
        Statement statement = null;
        try {
            statement = connection.createStatement();
            String sql =
                " SELECT TABLE_NAME                         " +
                "   FROM INFORMATION_SCHEMA.TABLES          " +
                "  WHERE TABLE_CATALOG = '" + db        + "'" +
                "    AND TABLE_NAME    = '" + tableName + "'";
            ResultSet rs = statement.executeQuery(sql);
            return rs.next();
        } finally {
            statement.close();
        }
    }

    static Connection getConnection() throws SQLException {
        Connection connection = DriverManager.getConnection("jdbc:sqlserver://" + server + ";databaseName=" + db + ";", user, pw);
        connection.setAutoCommit(false);
        return connection;
    }
}

The randomization of thread starts isn't necessary, but doesn't affect correctness. The thread scheduler should interleave thread execution arbitrarily. However, in my environment I observed that the second thread to start was almost, but not quite always, the deadlock victim.

Greg Mattes
  • 33,090
  • 15
  • 73
  • 105
  • I ran (a modified version - maybe I broke something) this - but got no deadlock on my SQLServer (Express) - I know this is a long time ago - but you did this always give you a deadlock when you ran it ? (Do I have to some specific URL flags set to make this happen? ) Cheers – monojohnny Feb 26 '15 at 10:33
  • Actually - my fault ! I had removed the 'setAutoCommit' from your code ! It now deadlocks !great! thanks. – monojohnny Feb 26 '15 at 10:35
  • Glad it worked for you -- now good luck recovering from deadlocks in your application! – Greg Mattes Feb 26 '15 at 20:10
  • 2
    yeah - the fun has only just begun ! Thanks again - nice bit of code. Cheers – monojohnny Feb 26 '15 at 21:22
2

Here's pseudocode to create a deadlock.

thread A:
    conA.setAutoCommit(false); // use transactions
    UPDATE TABLE_A SET AVALUE=5
    sleep(5); // seconds
    UPDATE TABLE_B SET BVALUE=5
    conA.commit();

thread B:
    conB.setAutoCommit(false); // use transactions
    sleep(1); // let thread A go first
    UPDATE TABLE_B SET BVALUE=5
    UPDATE TABLE_A SET AVALUE=5
    conB.commit();
Chui Tey
  • 5,436
  • 2
  • 35
  • 44
  • Don't forget `BEGIN TRANSACTION` – Remus Rusanu Sep 06 '12 at 21:31
  • I didn't mention this in my original post, but I also had a similar, or maybe even the same idea. I tried two tables and I had my update task change both with a fixed delay of one second between updates. Since several updates were running, I thought that a deadlock condition would likely result. I didn't see one, but I'll try this approach again. Maybe some synchronization object like a latch to coordinate the two update threads... – Greg Mattes Sep 07 '12 at 00:02
  • Deadlock is caused by contention to two different resources. It is like traffic grid lock. You are on the right trying to turn left, and same with me. We are both stuck until one backs off. Your test code has only one table, and you might be able to simulate it by one thread UPDATING row 1 then row2 in a single transaction, another thread UPDATING row 2 then row1 in a separate transaction. – Chui Tey Sep 07 '12 at 03:35
  • Thanks @Chui Tey I think I've got a fairly good handle on the concept of deadlock in general (http://stackoverflow.com/a/8880501/13940), but in this case it seemed like simply reading could could cause a deadlock (see Atwood's analysis), so this particular problem was interesting in that sense. Ultimately, my solution takes a "traditional" approach, it does not rely on a read deadlocking. – Greg Mattes Sep 08 '12 at 16:15
  • sorry :) I read somewhere forcing SQL Server to provide repeatable reads would cause shared locks to be held. If you try a sequence of repeatable reads within a single transaction, and sprinkle some sleeps in between, you might see your deadlocks. – Chui Tey Sep 09 '12 at 23:06