HHH-4765 Enhance Dialect support for JPA-2 locking. pessimistic no wait/timed locking and additional pessimistic locking tests. Oracle support for pessimistic locking

git-svn-id: https://svn.jboss.org/repos/hibernate/core/trunk@18688 1b8cb986-b30d-0410-93ca-fae66ebed9b2
This commit is contained in:
Scott Marlow 2010-02-04 00:07:31 +00:00
parent 46bc41c226
commit 80dbc69529
6 changed files with 352 additions and 28 deletions

View File

@ -26,6 +26,7 @@ package org.hibernate.dialect;
import java.sql.Types;
import org.hibernate.LockOptions;
import org.hibernate.sql.CaseFragment;
import org.hibernate.sql.ANSICaseFragment;
@ -58,9 +59,13 @@ public class Oracle9iDialect extends Oracle8iDialect {
public String getLimitString(String sql, boolean hasOffset) {
sql = sql.trim();
String forUpdateClause = null;
boolean isForUpdate = false;
if ( sql.toLowerCase().endsWith(" for update") ) {
sql = sql.substring( 0, sql.length()-11 );
final int forUpdateIndex = sql.toLowerCase().lastIndexOf( "for update") ;
if ( forUpdateIndex > -1 ) {
// save 'for update ...' and then remove it
forUpdateClause = sql.substring( forUpdateIndex );
sql = sql.substring( 0, forUpdateIndex-1 );
isForUpdate = true;
}
@ -80,7 +85,8 @@ public class Oracle9iDialect extends Oracle8iDialect {
}
if ( isForUpdate ) {
pagingSelect.append( " for update" );
pagingSelect.append( " " );
pagingSelect.append( forUpdateClause );
}
return pagingSelect.toString();
@ -98,4 +104,28 @@ public class Oracle9iDialect extends Oracle8iDialect {
// the standard SQL function name is current_timestamp...
return "current_timestamp";
}
// locking support
public String getForUpdateString() {
return " for update";
}
public String getWriteLockString(int timeout) {
if ( timeout == LockOptions.NO_WAIT ) {
return " for update nowait";
}
else if ( timeout > 0 ) {
// convert from milliseconds to seconds
float seconds = timeout / 1000.0f;
timeout = Math.round(seconds);
return " for update wait " + timeout;
}
else
return " for update";
}
public String getReadLockString(int timeout) {
return getWriteLockString( timeout );
}
}

View File

@ -24,6 +24,7 @@
*/
package org.hibernate.dialect.lock;
import org.hibernate.LockOptions;
import org.hibernate.persister.entity.Lockable;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.engine.SessionFactoryImplementor;
@ -72,7 +73,7 @@ public class PessimisticReadSelectLockingStrategy implements LockingStrategy {
public PessimisticReadSelectLockingStrategy(Lockable lockable, LockMode lockMode) {
this.lockable = lockable;
this.lockMode = lockMode;
this.sql = generateLockString();
this.sql = generateLockString(LockOptions.WAIT_FOREVER);
}
/**
@ -83,6 +84,13 @@ public class PessimisticReadSelectLockingStrategy implements LockingStrategy {
Object version,
Object object,
int timeout, SessionImplementor session) throws StaleObjectStateException, JDBCException {
String sql = this.sql;
if ( timeout == LockOptions.NO_WAIT ) {
sql = generateLockString( LockOptions.NO_WAIT );
}
else if ( timeout > 0) {
sql = generateLockString( timeout );
}
SessionFactoryImplementor factory = session.getFactory();
try {
@ -132,10 +140,12 @@ public class PessimisticReadSelectLockingStrategy implements LockingStrategy {
return lockMode;
}
protected String generateLockString() {
protected String generateLockString(int lockTimeout) {
SessionFactoryImplementor factory = lockable.getFactory();
LockOptions lockOptions = new LockOptions(this.lockMode);
lockOptions.setTimeOut( lockTimeout );
SimpleSelect select = new SimpleSelect( factory.getDialect() )
.setLockMode( lockMode )
.setLockOptions( lockOptions )
.setTableName( lockable.getRootTableName() )
.addColumn( lockable.getRootTableIdentifierColumnNames()[0] )
.addCondition( lockable.getRootTableIdentifierColumnNames(), "=?" );

View File

@ -24,6 +24,7 @@
*/
package org.hibernate.dialect.lock;
import org.hibernate.LockOptions;
import org.hibernate.persister.entity.Lockable;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.engine.SessionFactoryImplementor;
@ -72,7 +73,7 @@ public class PessimisticWriteSelectLockingStrategy implements LockingStrategy {
public PessimisticWriteSelectLockingStrategy(Lockable lockable, LockMode lockMode) {
this.lockable = lockable;
this.lockMode = lockMode;
this.sql = generateLockString();
this.sql = generateLockString(LockOptions.WAIT_FOREVER);
}
/**
@ -83,6 +84,13 @@ public class PessimisticWriteSelectLockingStrategy implements LockingStrategy {
Object version,
Object object,
int timeout, SessionImplementor session) throws StaleObjectStateException, JDBCException {
String sql = this.sql;
if ( timeout == LockOptions.NO_WAIT ) {
sql = generateLockString( LockOptions.NO_WAIT );
}
else if ( timeout > 0) {
sql = generateLockString( timeout );
}
SessionFactoryImplementor factory = session.getFactory();
try {
@ -132,10 +140,12 @@ public class PessimisticWriteSelectLockingStrategy implements LockingStrategy {
return lockMode;
}
protected String generateLockString() {
protected String generateLockString(int lockTimeout) {
SessionFactoryImplementor factory = lockable.getFactory();
LockOptions lockOptions = new LockOptions(this.lockMode);
lockOptions.setTimeOut( lockTimeout );
SimpleSelect select = new SimpleSelect( factory.getDialect() )
.setLockMode( lockMode )
.setLockOptions( lockOptions )
.setTableName( lockable.getRootTableName() )
.addColumn( lockable.getRootTableIdentifierColumnNames()[0] )
.addCondition( lockable.getRootTableIdentifierColumnNames(), "=?" );

View File

@ -107,7 +107,7 @@ public class AbstractLockUpgradeEventListener extends AbstractReassociateEventLi
entry.forceLocked( object, nextVersion );
}
else {
persister.lock( entry.getId(), entry.getVersion(), object, requestedLockMode, source );
persister.lock( entry.getId(), entry.getVersion(), object, lockOptions, source );
}
entry.setLockMode(requestedLockMode);
}

View File

@ -33,6 +33,7 @@ import java.util.Map;
import java.util.Set;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.dialect.Dialect;
/**
@ -51,7 +52,7 @@ public class SimpleSelect {
private String tableName;
private String orderBy;
private Dialect dialect;
private LockMode lockMode = LockMode.READ;
private LockOptions lockOptions = new LockOptions( LockMode.READ);
private String comment;
private List columns = new ArrayList();
@ -99,8 +100,13 @@ public class SimpleSelect {
return this;
}
public SimpleSelect setLockOptions( LockOptions lockOptions ) {
LockOptions.copy(lockOptions, this.lockOptions);
return this;
}
public SimpleSelect setLockMode(LockMode lockMode) {
this.lockMode = lockMode;
this.lockOptions.setLockMode( lockMode );
return this;
}
@ -172,7 +178,7 @@ public class SimpleSelect {
}
buf.append(" from ")
.append( dialect.appendLockHint(lockMode, tableName) );
.append( dialect.appendLockHint(lockOptions.getLockMode(), tableName) );
if ( whereTokens.size() > 0 ) {
buf.append(" where ")
@ -181,8 +187,8 @@ public class SimpleSelect {
if (orderBy!=null) buf.append(orderBy);
if (lockMode!=null) {
buf.append( dialect.getForUpdateString(lockMode) );
if (lockOptions!=null) {
buf.append( dialect.getForUpdateString(lockOptions) );
}
return dialect.transformSelectString( buf.toString() );

View File

@ -3,14 +3,22 @@ package org.hibernate.ejb.test.lock;
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.LockTimeoutException;
import javax.persistence.OptimisticLockException;
import javax.persistence.Query;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.dialect.HSQLDialect;
import org.hibernate.dialect.Oracle10gDialect;
import org.hibernate.ejb.test.TestCase;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
/**
@ -187,7 +195,7 @@ public class LockTest extends TestCase {
Lock lock = new Lock();
Thread t = null;
try {
lock.setName( "contendedLock" );
lock.setName( "testContendedPessimisticLock" );
em.getTransaction().begin();
em.persist( lock );
@ -204,21 +212,24 @@ public class LockTest extends TestCase {
t = new Thread( new Runnable() {
public void run() {
try {
em2.getTransaction().begin();
log.info("testContendedPessimisticLock: (BG) about to read write-locked entity");
log.info("testContendedPessimisticLock: (BG) about to issue (PESSIMISTIC_READ) query against write-locked entity");
// we should block on the following read
Lock lock2 = em2.getReference( Lock.class, id );
lock2.getName(); // force entity to be read
log.info("testContendedPessimisticLock: (BG) read write-locked entity");
em2.lock( lock2, LockModeType.PESSIMISTIC_READ);
log.info("testContendedPessimisticLock: (BG) got read lock on entity");
Query query = em2.createQuery(
"select L from Lock_ L where L.id < 10000 ");
query.setLockMode(LockModeType.PESSIMISTIC_READ);
List<Lock> resultList = query.getResultList();
resultList.get(0).getName(); // force entity to be read
}
finally {
em2.getTransaction().commit();
latch.countDown(); // signal that we got the read lock
}
}
} );
// t.setDaemon( true );
t.setDaemon( true );
t.setName("LockTest read lock");
t.start();
log.info("testContendedPessimisticLock: wait on BG thread");
@ -245,6 +256,263 @@ public class LockTest extends TestCase {
}
}
public void testContendedPessimisticReadLockTimeout() throws Exception {
EntityManager em = getOrCreateEntityManager();
final EntityManager em2 = createIsolatedEntityManager();
// TODO: replace dialect instanceof test with a Dialect.hasCapability (e.g. supportsPessimisticLockTimeout)
if ( ! (getDialect() instanceof Oracle10gDialect)) {
log.info("skipping testContendedPessimisticReadLockTimeout");
return;
}
Lock lock = new Lock();
Thread t = null;
FutureTask<Boolean> bgTask = null;
final CountDownLatch latch = new CountDownLatch(1);
try {
lock.setName( "testContendedPessimisticReadLockTimeout" );
em.getTransaction().begin();
em.persist( lock );
em.getTransaction().commit();
em.clear();
em.getTransaction().begin();
lock = em.getReference( Lock.class, lock.getId() );
em.lock( lock, LockModeType.PESSIMISTIC_WRITE );
final Integer id = lock.getId();
lock.getName(); // force entity to be read
log.info("testContendedPessimisticReadLockTimeout: got write lock");
bgTask = new FutureTask<Boolean>( new Callable() {
public Boolean call() {
try {
boolean timedOut = false; // true (success) if LockTimeoutException occurred
em2.getTransaction().begin();
log.info("testContendedPessimisticReadLockTimeout: (BG) about to read write-locked entity");
// we should block on the following read
Lock lock2 = em2.getReference( Lock.class, id );
lock2.getName(); // force entity to be read
log.info("testContendedPessimisticReadLockTimeout: (BG) read write-locked entity");
Map<String,Object> props = new HashMap<String, Object>();
// timeout is in milliseconds
props.put("javax.persistence.lock.timeout", new Integer(1000));
try {
em2.lock( lock2, LockModeType.PESSIMISTIC_READ, props);
}
catch( LockTimeoutException e) {
// success
log.info("testContendedPessimisticReadLockTimeout: (BG) got expected timeout exception");
timedOut = true;
}
catch ( Throwable e) {
log.info("Expected LockTimeoutException but got unexpected exception", e);
}
em2.getTransaction().commit();
return new Boolean(timedOut);
}
finally {
latch.countDown(); // signal that we finished
}
}
} );
t = new Thread(bgTask);
t.setDaemon( true );
t.setName("Lock timeout Test (bg)");
t.start();
boolean latchSet = latch.await( 10, TimeUnit.SECONDS ); // should return quickly on success
assertTrue( "background test thread finished (lock timeout is broken)", latchSet);
assertTrue( "background test thread timed out on lock attempt", bgTask.get().booleanValue() );
em.getTransaction().commit();
}
finally {
if ( em.getTransaction().isActive() ) {
em.getTransaction().rollback();
}
if ( t != null) { // wait for background thread to finish before deleting entity
t.join();
}
em.getTransaction().begin();
lock = em.getReference( Lock.class, lock.getId() );
em.remove( lock );
em.getTransaction().commit();
em.close();
em2.close();
}
}
public void testContendedPessimisticWriteLockTimeout() throws Exception {
EntityManager em = getOrCreateEntityManager();
final EntityManager em2 = createIsolatedEntityManager();
// TODO: replace dialect instanceof test with a Dialect.hasCapability (e.g. supportsPessimisticLockTimeout)
if ( ! (getDialect() instanceof Oracle10gDialect)) {
log.info("skipping testContendedPessimisticWriteLockTimeout");
return;
}
Lock lock = new Lock();
Thread t = null;
FutureTask<Boolean> bgTask = null;
final CountDownLatch latch = new CountDownLatch(1);
try {
lock.setName( "testContendedPessimisticWriteLockTimeout" );
em.getTransaction().begin();
em.persist( lock );
em.getTransaction().commit();
em.clear();
em.getTransaction().begin();
lock = em.getReference( Lock.class, lock.getId() );
em.lock( lock, LockModeType.PESSIMISTIC_WRITE );
final Integer id = lock.getId();
lock.getName(); // force entity to be read
log.info("testContendedPessimisticWriteLockTimeout: got write lock");
bgTask = new FutureTask<Boolean>( new Callable() {
public Boolean call() {
try {
boolean timedOut = false; // true (success) if LockTimeoutException occurred
em2.getTransaction().begin();
log.info("testContendedPessimisticWriteLockTimeout: (BG) about to read write-locked entity");
// we should block on the following read
Lock lock2 = em2.getReference( Lock.class, id );
lock2.getName(); // force entity to be read
log.info("testContendedPessimisticWriteLockTimeout: (BG) read write-locked entity");
Map<String,Object> props = new HashMap<String, Object>();
// timeout is in milliseconds
props.put("javax.persistence.lock.timeout", new Integer(1000));
try {
em2.lock( lock2, LockModeType.PESSIMISTIC_WRITE, props);
}
catch( LockTimeoutException e) {
// success
log.info("testContendedPessimisticWriteLockTimeout: (BG) got expected timeout exception");
timedOut = true;
}
catch ( Throwable e) {
log.info("Expected LockTimeoutException but got unexpected exception", e);
}
em2.getTransaction().commit();
return new Boolean(timedOut);
}
finally {
latch.countDown(); // signal that we finished
}
}
} );
t = new Thread(bgTask);
t.setDaemon( true );
t.setName("Lock timeout Test (bg)");
t.start();
boolean latchSet = latch.await( 10, TimeUnit.SECONDS ); // should return quickly on success
assertTrue( "background test thread finished (lock timeout is broken)", latchSet);
assertTrue( "background test thread timed out on lock attempt", bgTask.get().booleanValue() );
em.getTransaction().commit();
}
finally {
if ( em.getTransaction().isActive() ) {
em.getTransaction().rollback();
}
if ( t != null) { // wait for background thread to finish before deleting entity
t.join();
}
em.getTransaction().begin();
lock = em.getReference( Lock.class, lock.getId() );
em.remove( lock );
em.getTransaction().commit();
em.close();
em2.close();
}
}
public void testContendedPessimisticWriteLockNoWait() throws Exception {
EntityManager em = getOrCreateEntityManager();
final EntityManager em2 = createIsolatedEntityManager();
// TODO: replace dialect instanceof test with a Dialect.hasCapability (e.g. supportsPessimisticLockTimeout)
if ( ! (getDialect() instanceof Oracle10gDialect)) {
log.info("skipping testContendedPessimisticWriteLockNoWait");
return;
}
Lock lock = new Lock();
Thread t = null;
FutureTask<Boolean> bgTask = null;
final CountDownLatch latch = new CountDownLatch(1);
try {
lock.setName( "testContendedPessimisticWriteLockNoWait" );
em.getTransaction().begin();
em.persist( lock );
em.getTransaction().commit();
em.clear();
em.getTransaction().begin();
lock = em.getReference( Lock.class, lock.getId() );
em.lock( lock, LockModeType.PESSIMISTIC_WRITE );
final Integer id = lock.getId();
lock.getName(); // force entity to be read
log.info("testContendedPessimisticWriteLockNoWait: got write lock");
bgTask = new FutureTask<Boolean>( new Callable() {
public Boolean call() {
try {
boolean timedOut = false; // true (success) if LockTimeoutException occurred
em2.getTransaction().begin();
log.info("testContendedPessimisticWriteLockNoWait: (BG) about to read write-locked entity");
// we should block on the following read
Lock lock2 = em2.getReference( Lock.class, id );
lock2.getName(); // force entity to be read
log.info("testContendedPessimisticWriteLockNoWait: (BG) read write-locked entity");
Map<String,Object> props = new HashMap<String, Object>();
// timeout of zero means no wait (for lock)
props.put("javax.persistence.lock.timeout", new Integer(0));
try {
em2.lock( lock2, LockModeType.PESSIMISTIC_WRITE, props);
}
catch( LockTimeoutException e) {
// success
log.info("testContendedPessimisticWriteLockNoWait: (BG) got expected timeout exception");
timedOut = true;
}
catch ( Throwable e) {
log.info("Expected LockTimeoutException but got unexpected exception", e);
}
em2.getTransaction().commit();
return new Boolean(timedOut);
}
finally {
latch.countDown(); // signal that we finished
}
}
} );
t = new Thread(bgTask);
t.setDaemon( true );
t.setName("Lock timeout Test (bg)");
t.start();
boolean latchSet = latch.await( 10, TimeUnit.SECONDS ); // should return quickly on success
assertTrue( "background test thread finished (lock timeout is broken)", latchSet);
assertTrue( "background test thread timed out on lock attempt", bgTask.get().booleanValue() );
em.getTransaction().commit();
}
finally {
if ( em.getTransaction().isActive() ) {
em.getTransaction().rollback();
}
if ( t != null) { // wait for background thread to finish before deleting entity
t.join();
}
em.getTransaction().begin();
lock = em.getReference( Lock.class, lock.getId() );
em.remove( lock );
em.getTransaction().commit();
em.close();
em2.close();
}
}
public Class[] getAnnotatedClasses() {
return new Class[]{
Lock.class,