Never mind :-)
I have a simple parent-child relationship, Client (parent) and Campaign (child). The following code deadlocks against MySQL:
Code:
Persistence.beginTransaction();
Client client = new Client("deadlock test", new HashSet());
Campaign campaign = new Campaign("deadlock test", client);
Persistence.save(client);
Persistence.commitTransaction();
Persistence.closeSession();
// Normally there is only ever one session per thread. But in this test case
// we require two sessions. So we use the testing-only method to get them.
Session session1 = Persistence.testingOnlyGetJdbcSessionFactory().openSession();
Session session2 = Persistence.testingOnlyGetJdbcSessionFactory().openSession();
Transaction tx1 = session1.beginTransaction();
Transaction tx2 = session2.beginTransaction();
Client client1 = (Client)session1.createQuery("from Client client where client.name = ?")
.setParameter(0, "deadlock test", Hibernate.STRING)
.uniqueResult();
Campaign campaign1 = (Campaign)client1.getCampaigns().iterator().next();
Client client2 = (Client)session2.createQuery("from Client client where client.name = ?")
.setParameter(0, "deadlock test", Hibernate.STRING)
.uniqueResult();
Campaign campaign2 = (Campaign)client2.getCampaigns().iterator().next();
// now try modifying the clients and campaigns in an interleaved order
campaign2.setName("deadlock test 2");
client1.setName("deadlock test 1");
session1.flush();
session2.flush();
client2.setName("deadlock test 2");
campaign1.setName("deadlock test 1");
session1.flush();
session2.flush();
tx1.commit();
tx2.commit();
session1.close();
session2.close();
Fails with:
Code:
[junit] Testcase: testDeadlock(com.nimblefish.core.test.persist.PersistenceManagerTest): Caused an ERROR
[junit] Could not execute JDBC batch update
[junit] net.sf.hibernate.JDBCException: Could not execute JDBC batch update
[junit] at net.sf.hibernate.impl.BatcherImpl.executeBatch(BatcherImpl.java:125)
[junit] at net.sf.hibernate.impl.SessionImpl.executeAll(SessionImpl.java:2311)
[junit] at net.sf.hibernate.impl.SessionImpl.execute(SessionImpl.java:2262)
[junit] at net.sf.hibernate.impl.SessionImpl.flush(SessionImpl.java:2187)
[junit] at com.nimblefish.core.test.persist.PersistenceManagerTest.testDeadlock(PersistenceManagerTest.java:672)
[junit] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
[junit] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
[junit] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
[junit] at junit.extensions.TestDecorator.basicRun(TestDecorator.java:22)
[junit] at junit.extensions.TestSetup$1.protect(TestSetup.java:19)
[junit] at junit.extensions.TestSetup.run(TestSetup.java:23)
[junit] Caused by: java.sql.BatchUpdateException: General error, message from server: "Lock wait timeout exceeded; Try restarting transaction"
[junit] at com.mysql.jdbc.PreparedStatement.executeBatch(PreparedStatement.java:1469)
[junit] at net.sf.hibernate.impl.BatchingBatcher.doExecuteBatch(BatchingBatcher.java:54)
[junit] at net.sf.hibernate.impl.BatcherImpl.executeBatch(BatcherImpl.java:118)
[junit] ... 24 more
So looks like I can make the database wedge on demand :-)
Now obviously the thing to do is to make a multithreaded test with well-controlled synchronization, to verify that different lock orderings either do or don't address this kind of a deadlock. (This single-threaded test can't demonstrate lock ordering making any difference since there's no way for one session to make progress while another is waiting.) I'll post the test case code I come up with.
Sorry for the simpleminded first post -- an earlier version of this test case had a simple thinko that I caught immediately after posting.
Cheers!
Rob