Deadlock in JDBCCacheLoader
gpklimek Jan 8, 2009 12:49 PMIt is possible to have a deadlock in JDBCCacheLoader when concurrently trying to put two different entries into it. Timing has to be right of course and even then chances are small (like 1 to 16384). But still it is a problem.
I have found it in version 1.4.1.SP8 but it is still a problem in 3.0.1.GA
I have put together a unit test (for 3.0.1.GA) which demonstrates the issue: trying to put Fqns "/a/b" and "/c/d" at the same time is fine, but "/a/m" and "/l/p" is not
import static org.easymock.EasyMock.*;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Hashtable;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.spi.InitialContextFactory;
import javax.sql.DataSource;
import junit.framework.TestCase;
import org.easymock.Capture;
import org.easymock.IAnswer;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.Fqn;
import org.jboss.cache.config.CacheLoaderConfig.IndividualCacheLoaderConfig;
import org.jboss.cache.io.ByteBuffer;
import org.jboss.cache.loader.JDBCCacheLoader;
import org.jboss.cache.marshall.Marshaller;
public class JDBCCacheLoaderTest extends TestCase {
private static final long EXECUTE_QUERY_DELAY = 1000;
private static final long MAX_TIME_FOR_PUT = 10000;
private static Context context;
public static class SimpleContextFactory implements InitialContextFactory {
public Context getInitialContext(Hashtable<?, ?> environment) throws NamingException {
return context;
}
}
private JDBCCacheLoader jdbcCacheLoader;
@Override
protected void setUp() throws Exception {
System.setProperty("java.naming.factory.initial", "JDBCCacheLoaderTest$SimpleContextFactory");
//JDBC mocks
context = createNiceMock("context", Context.class);
final CacheSPI cacheSPI = createNiceMock("cacheSPI", CacheSPI.class);
final Marshaller marshaller = createNiceMock("marshaller", Marshaller.class);
final DataSource dataSource = createNiceMock("dataSource", DataSource.class);
final Connection connection = createNiceMock("connection", Connection.class);
final DatabaseMetaData databaseMetaData = createNiceMock("databaseMetaData", DatabaseMetaData.class);
final PreparedStatement preparedStatement = createNiceMock("preparedStatement", PreparedStatement.class);
final ResultSet resultSet = createNiceMock("resultSet", ResultSet.class);
makeThreadSafe(cacheSPI, true);
makeThreadSafe(marshaller, true);
makeThreadSafe(dataSource, true);
makeThreadSafe(connection, true);
makeThreadSafe(databaseMetaData, true);
makeThreadSafe(preparedStatement, true);
makeThreadSafe(resultSet, true);
//mock expectations
expect(context.lookup("java:/JDBCCacheLoader")).andStubReturn(dataSource);
expect(cacheSPI.getMarshaller()).andStubReturn(marshaller);
expect(marshaller.objectToBuffer(capture(new Capture<Object>()))).andStubReturn(new ByteBuffer(new byte[] {'a'}, 0, 1));
expect(dataSource.getConnection()).andStubReturn(connection);
expect(connection.getMetaData()).andStubReturn(databaseMetaData);
expect(databaseMetaData.getDriverName()).andStubReturn("a driver name");
expect(connection.prepareStatement(capture(new Capture<String>()))).andStubReturn(preparedStatement);
expect(preparedStatement.executeQuery()).andStubAnswer(new IAnswer<ResultSet>() {
public ResultSet answer() throws Throwable {
Thread.sleep(EXECUTE_QUERY_DELAY);
return resultSet;
}
});
preparedStatement.executeUpdate();
expectLastCall().andStubReturn(1);
replay(context, cacheSPI, marshaller, dataSource, connection, databaseMetaData, preparedStatement, resultSet);
//JDBCCacheLoader init
Properties props = new Properties();
props.put("cache.jdbc.table.name","TREE_CACHE");
props.put("cache.jdbc.table.create","false");
props.put("cache.jdbc.fqn.column","FQN");
props.put("cache.jdbc.node.column","NODE");
props.put("cache.jdbc.datasource","java:/JDBCCacheLoader");
IndividualCacheLoaderConfig config = new IndividualCacheLoaderConfig();
config.setProperties(props);
jdbcCacheLoader = new JDBCCacheLoader();
jdbcCacheLoader.setConfig(config);
jdbcCacheLoader.setCache(cacheSPI);
jdbcCacheLoader.start();
}
public void testPutCaseDeadlock() throws Exception {
Thread thread1 = new Thread(new Putter("/a/m"));
Thread thread2 = new Thread(new Putter("/l/p"));
thread1.start();
thread2.start();
thread1.join(MAX_TIME_FOR_PUT);
thread2.join(MAX_TIME_FOR_PUT);
assertEquals(Thread.State.TERMINATED, thread1.getState());
assertEquals(Thread.State.TERMINATED, thread2.getState());
}
public void testPutCaseNoDeadlock() throws Exception {
Thread thread1 = new Thread(new Putter("/a/b"));
Thread thread2 = new Thread(new Putter("/c/d"));
thread1.start();
thread2.start();
thread1.join(MAX_TIME_FOR_PUT);
thread2.join(MAX_TIME_FOR_PUT);
assertEquals(Thread.State.TERMINATED, thread1.getState());
assertEquals(Thread.State.TERMINATED, thread2.getState());
}
private class Putter implements Runnable {
private String name;
public Putter(String name) {
this.name = name;
}
public void run() {
try {
jdbcCacheLoader.put(Fqn.fromString(name), "a key", "a value");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}