From 80dbc69529a2f812472198fe03a706e92ad0d403 Mon Sep 17 00:00:00 2001 From: Scott Marlow Date: Thu, 4 Feb 2010 00:07:31 +0000 Subject: [PATCH] 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 --- .../hibernate/dialect/Oracle9iDialect.java | 36 ++- .../PessimisticReadSelectLockingStrategy.java | 16 +- ...PessimisticWriteSelectLockingStrategy.java | 16 +- .../def/AbstractLockUpgradeEventListener.java | 2 +- .../java/org/hibernate/sql/SimpleSelect.java | 16 +- .../org/hibernate/ejb/test/lock/LockTest.java | 294 +++++++++++++++++- 6 files changed, 352 insertions(+), 28 deletions(-) diff --git a/core/src/main/java/org/hibernate/dialect/Oracle9iDialect.java b/core/src/main/java/org/hibernate/dialect/Oracle9iDialect.java index caa1ed08c3..cf9e376e1a 100644 --- a/core/src/main/java/org/hibernate/dialect/Oracle9iDialect.java +++ b/core/src/main/java/org/hibernate/dialect/Oracle9iDialect.java @@ -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 ); + } + } diff --git a/core/src/main/java/org/hibernate/dialect/lock/PessimisticReadSelectLockingStrategy.java b/core/src/main/java/org/hibernate/dialect/lock/PessimisticReadSelectLockingStrategy.java index 82ec608a07..be114f29d4 100644 --- a/core/src/main/java/org/hibernate/dialect/lock/PessimisticReadSelectLockingStrategy.java +++ b/core/src/main/java/org/hibernate/dialect/lock/PessimisticReadSelectLockingStrategy.java @@ -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(), "=?" ); diff --git a/core/src/main/java/org/hibernate/dialect/lock/PessimisticWriteSelectLockingStrategy.java b/core/src/main/java/org/hibernate/dialect/lock/PessimisticWriteSelectLockingStrategy.java index 7575e62404..073ae722df 100644 --- a/core/src/main/java/org/hibernate/dialect/lock/PessimisticWriteSelectLockingStrategy.java +++ b/core/src/main/java/org/hibernate/dialect/lock/PessimisticWriteSelectLockingStrategy.java @@ -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(), "=?" ); diff --git a/core/src/main/java/org/hibernate/event/def/AbstractLockUpgradeEventListener.java b/core/src/main/java/org/hibernate/event/def/AbstractLockUpgradeEventListener.java index d300dc2f12..b365cb26e5 100644 --- a/core/src/main/java/org/hibernate/event/def/AbstractLockUpgradeEventListener.java +++ b/core/src/main/java/org/hibernate/event/def/AbstractLockUpgradeEventListener.java @@ -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); } diff --git a/core/src/main/java/org/hibernate/sql/SimpleSelect.java b/core/src/main/java/org/hibernate/sql/SimpleSelect.java index 6a0d2e97a9..5035eeb5d4 100644 --- a/core/src/main/java/org/hibernate/sql/SimpleSelect.java +++ b/core/src/main/java/org/hibernate/sql/SimpleSelect.java @@ -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() ); diff --git a/entitymanager/src/test/java/org/hibernate/ejb/test/lock/LockTest.java b/entitymanager/src/test/java/org/hibernate/ejb/test/lock/LockTest.java index c5398b2cf1..e19845780d 100644 --- a/entitymanager/src/test/java/org/hibernate/ejb/test/lock/LockTest.java +++ b/entitymanager/src/test/java/org/hibernate/ejb/test/lock/LockTest.java @@ -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() { - - 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 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 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( 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 props = new HashMap(); + // 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 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( 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 props = new HashMap(); + // 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 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( 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 props = new HashMap(); + // 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,