Reputation: 33969
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);
}
}
}
Upvotes: 2
Views: 4411
Reputation: 33969
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.
Upvotes: 5
Reputation: 5574
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();
Upvotes: 2