Author: smarlow(a)redhat.com
Date: 2010-02-03 19:07:31 -0500 (Wed, 03 Feb 2010)
New Revision: 18688
Modified:
core/trunk/core/src/main/java/org/hibernate/dialect/Oracle9iDialect.java
core/trunk/core/src/main/java/org/hibernate/dialect/lock/PessimisticReadSelectLockingStrategy.java
core/trunk/core/src/main/java/org/hibernate/dialect/lock/PessimisticWriteSelectLockingStrategy.java
core/trunk/core/src/main/java/org/hibernate/event/def/AbstractLockUpgradeEventListener.java
core/trunk/core/src/main/java/org/hibernate/sql/SimpleSelect.java
core/trunk/entitymanager/src/test/java/org/hibernate/ejb/test/lock/LockTest.java
Log:
HHH-4765 Enhance Dialect support for JPA-2 locking. pessimistic no wait/timed locking and
additional pessimistic locking tests. Oracle support for pessimistic locking
Modified: core/trunk/core/src/main/java/org/hibernate/dialect/Oracle9iDialect.java
===================================================================
--- core/trunk/core/src/main/java/org/hibernate/dialect/Oracle9iDialect.java 2010-02-03
21:47:31 UTC (rev 18687)
+++ core/trunk/core/src/main/java/org/hibernate/dialect/Oracle9iDialect.java 2010-02-04
00:07:31 UTC (rev 18688)
@@ -26,6 +26,7 @@
import java.sql.Types;
+import org.hibernate.LockOptions;
import org.hibernate.sql.CaseFragment;
import org.hibernate.sql.ANSICaseFragment;
@@ -58,9 +59,13 @@
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 @@
}
if ( isForUpdate ) {
- pagingSelect.append( " for update" );
+ pagingSelect.append( " " );
+ pagingSelect.append( forUpdateClause );
}
return pagingSelect.toString();
@@ -98,4 +104,28 @@
// 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 );
+ }
+
}
Modified:
core/trunk/core/src/main/java/org/hibernate/dialect/lock/PessimisticReadSelectLockingStrategy.java
===================================================================
---
core/trunk/core/src/main/java/org/hibernate/dialect/lock/PessimisticReadSelectLockingStrategy.java 2010-02-03
21:47:31 UTC (rev 18687)
+++
core/trunk/core/src/main/java/org/hibernate/dialect/lock/PessimisticReadSelectLockingStrategy.java 2010-02-04
00:07:31 UTC (rev 18688)
@@ -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 PessimisticReadSelectLockingStrategy(Lockable lockable, LockMode lockMode) {
this.lockable = lockable;
this.lockMode = lockMode;
- this.sql = generateLockString();
+ this.sql = generateLockString(LockOptions.WAIT_FOREVER);
}
/**
@@ -83,6 +84,13 @@
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 @@
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(), "=?" );
Modified:
core/trunk/core/src/main/java/org/hibernate/dialect/lock/PessimisticWriteSelectLockingStrategy.java
===================================================================
---
core/trunk/core/src/main/java/org/hibernate/dialect/lock/PessimisticWriteSelectLockingStrategy.java 2010-02-03
21:47:31 UTC (rev 18687)
+++
core/trunk/core/src/main/java/org/hibernate/dialect/lock/PessimisticWriteSelectLockingStrategy.java 2010-02-04
00:07:31 UTC (rev 18688)
@@ -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 PessimisticWriteSelectLockingStrategy(Lockable lockable, LockMode lockMode) {
this.lockable = lockable;
this.lockMode = lockMode;
- this.sql = generateLockString();
+ this.sql = generateLockString(LockOptions.WAIT_FOREVER);
}
/**
@@ -83,6 +84,13 @@
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 @@
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(), "=?" );
Modified:
core/trunk/core/src/main/java/org/hibernate/event/def/AbstractLockUpgradeEventListener.java
===================================================================
---
core/trunk/core/src/main/java/org/hibernate/event/def/AbstractLockUpgradeEventListener.java 2010-02-03
21:47:31 UTC (rev 18687)
+++
core/trunk/core/src/main/java/org/hibernate/event/def/AbstractLockUpgradeEventListener.java 2010-02-04
00:07:31 UTC (rev 18688)
@@ -107,7 +107,7 @@
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);
}
Modified: core/trunk/core/src/main/java/org/hibernate/sql/SimpleSelect.java
===================================================================
--- core/trunk/core/src/main/java/org/hibernate/sql/SimpleSelect.java 2010-02-03 21:47:31
UTC (rev 18687)
+++ core/trunk/core/src/main/java/org/hibernate/sql/SimpleSelect.java 2010-02-04 00:07:31
UTC (rev 18688)
@@ -33,6 +33,7 @@
import java.util.Set;
import org.hibernate.LockMode;
+import org.hibernate.LockOptions;
import org.hibernate.dialect.Dialect;
/**
@@ -51,7 +52,7 @@
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 @@
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 @@
}
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 @@
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() );
Modified:
core/trunk/entitymanager/src/test/java/org/hibernate/ejb/test/lock/LockTest.java
===================================================================
---
core/trunk/entitymanager/src/test/java/org/hibernate/ejb/test/lock/LockTest.java 2010-02-03
21:47:31 UTC (rev 18687)
+++
core/trunk/entitymanager/src/test/java/org/hibernate/ejb/test/lock/LockTest.java 2010-02-04
00:07:31 UTC (rev 18688)
@@ -3,14 +3,22 @@
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 @@
Lock lock = new Lock();
Thread t = null;
try {
- lock.setName( "contendedLock" );
+ lock.setName( "testContendedPessimisticLock" );
em.getTransaction().begin();
em.persist( lock );
@@ -204,21 +212,24 @@
t = new Thread( new Runnable() {
public void run() {
-
- em2.getTransaction().begin();
- log.info("testContendedPessimisticLock: (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("testContendedPessimisticLock: (BG) read write-locked entity");
- em2.lock( lock2, LockModeType.PESSIMISTIC_READ);
- log.info("testContendedPessimisticLock: (BG) got read lock on entity");
- em2.getTransaction().commit();
- latch.countDown(); // signal that we got the read lock
+ try {
+ em2.getTransaction().begin();
+ log.info("testContendedPessimisticLock: (BG) about to issue (PESSIMISTIC_READ)
query against write-locked entity");
+ // we should block on the following read
+ 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 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,