diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index cdc503dd145d..e4bc9bdfc83c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -23,21 +23,25 @@ */ package org.hibernate.dialect; -import java.sql.SQLException; -import java.sql.Types; - -import org.jboss.logging.Logger; - +import org.hibernate.JDBCException; +import org.hibernate.PessimisticLockException; import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.function.AvgWithArgumentCastFunction; import org.hibernate.dialect.function.NoArgSQLFunction; import org.hibernate.dialect.function.StandardSQLFunction; import org.hibernate.dialect.function.VarArgsSQLFunction; +import org.hibernate.exception.LockAcquisitionException; +import org.hibernate.exception.spi.SQLExceptionConversionDelegate; import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtracter; import org.hibernate.exception.spi.ViolatedConstraintNameExtracter; import org.hibernate.internal.CoreMessageLogger; +import org.hibernate.internal.util.JdbcExceptionHelper; import org.hibernate.internal.util.ReflectHelper; import org.hibernate.type.StandardBasicTypes; +import org.jboss.logging.Logger; + +import java.sql.SQLException; +import java.sql.Types; /** * A dialect compatible with the H2 database. @@ -292,6 +296,33 @@ public String extractConstraintName(SQLException sqle) { return constraintName; } }; + + @Override + public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { + SQLExceptionConversionDelegate delegate = super.buildSQLExceptionConversionDelegate(); + if (delegate == null) { + delegate = new SQLExceptionConversionDelegate() { + + @Override + public JDBCException convert(SQLException sqlException, String message, String sql) { + JDBCException exception = null; + + int errorCode = JdbcExceptionHelper.extractErrorCode(sqlException); + + if (40001 == errorCode) { // DEADLOCK DETECTED + exception = new LockAcquisitionException(message, sqlException, sql); + } + + if (50200 == errorCode) { // LOCK NOT AVAILABLE + exception = new PessimisticLockException(message, sqlException, sql); + } + + return exception; + } + }; + } + return delegate; + } @Override public boolean supportsTemporaryTables() { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQL81Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQL81Dialect.java index 0d1102a9c852..38a15c7ebacd 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQL81Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQL81Dialect.java @@ -28,13 +28,17 @@ import java.sql.SQLException; import java.sql.Types; +import org.hibernate.JDBCException; import org.hibernate.LockOptions; +import org.hibernate.PessimisticLockException; import org.hibernate.cfg.Environment; import org.hibernate.dialect.function.NoArgSQLFunction; import org.hibernate.dialect.function.PositionSubstringFunction; import org.hibernate.dialect.function.SQLFunctionTemplate; import org.hibernate.dialect.function.StandardSQLFunction; import org.hibernate.dialect.function.VarArgsSQLFunction; +import org.hibernate.exception.LockAcquisitionException; +import org.hibernate.exception.spi.SQLExceptionConversionDelegate; import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtracter; import org.hibernate.exception.spi.ViolatedConstraintNameExtracter; import org.hibernate.id.SequenceGenerator; @@ -367,6 +371,34 @@ public String extractConstraintName(SQLException sqle) { } }; + @Override + public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { + SQLExceptionConversionDelegate delegate = super.buildSQLExceptionConversionDelegate(); + if (delegate == null) { + delegate = new SQLExceptionConversionDelegate() { + @Override + public JDBCException convert(SQLException sqlException, String message, String sql) { + JDBCException exception = null; + + if (exception == null) { + String sqlState = JdbcExceptionHelper.extractSqlState(sqlException); + + if ("40P01".equals(sqlState)) { // DEADLOCK DETECTED + exception = new LockAcquisitionException(message, sqlException, sql); + } + + if ("55P03".equals(sqlState)) { // LOCK NOT AVAILABLE + exception = new PessimisticLockException(message, sqlException, sql); + } + } + + return exception; + } + }; + } + return delegate; + } + public int registerResultSetOutParameter(CallableStatement statement, int col) throws SQLException { // Register the type of the out param - PostgreSQL uses Types.OTHER statement.registerOutParameter(col++, Types.OTHER); diff --git a/hibernate-core/src/test/java/org/hibernate/dialect/PostgreSQL81DialectTestCase.java b/hibernate-core/src/test/java/org/hibernate/dialect/PostgreSQL81DialectTestCase.java new file mode 100644 index 000000000000..a2640c07b0af --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/dialect/PostgreSQL81DialectTestCase.java @@ -0,0 +1,68 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2009 by Red Hat Inc and/or its affiliates or by + * third-party contributors as indicated by either @author tags or express + * copyright attribution statements applied by the authors. All + * third-party contributions are distributed under license by Red Hat Inc. + * + * This copyrighted material is made available to anyone wishing to use, modify, + * copy, or redistribute it subject to the terms and conditions of the GNU + * Lesser General Public License, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this distribution; if not, write to: + * Free Software Foundation, Inc. + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301 USA + */ +package org.hibernate.dialect; + +import org.junit.Test; + +import java.sql.SQLException; +import org.hibernate.JDBCException; +import org.hibernate.PessimisticLockException; +import org.hibernate.exception.LockAcquisitionException; +import org.hibernate.exception.spi.SQLExceptionConversionDelegate; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseUnitTestCase; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNotNull; + + +/** + * Testing of patched support for PostgreSQL Lock error detection. HHH-7251 + * + * @author Bryan Varner + */ +@TestForIssue( jiraKey = "HHH-7251" ) +public class PostgreSQL81DialectTestCase extends BaseUnitTestCase { + + @Test + public void testDeadlockException() { + PostgreSQL81Dialect dialect = new PostgreSQL81Dialect(); + SQLExceptionConversionDelegate delegate = dialect.buildSQLExceptionConversionDelegate(); + assertNotNull(delegate); + + JDBCException exception = delegate.convert(new SQLException("Deadlock Detected", "40P01"), "", ""); + assertTrue(exception instanceof LockAcquisitionException); + } + + @Test + public void testTimeoutException() { + PostgreSQL81Dialect dialect = new PostgreSQL81Dialect(); + SQLExceptionConversionDelegate delegate = dialect.buildSQLExceptionConversionDelegate(); + assertNotNull(delegate); + + JDBCException exception = delegate.convert(new SQLException("Lock Not Available", "55P03"), "", ""); + assertTrue(exception instanceof PessimisticLockException); + } +} diff --git a/hibernate-entitymanager/src/main/java/org/hibernate/ejb/AbstractEntityManagerImpl.java b/hibernate-entitymanager/src/main/java/org/hibernate/ejb/AbstractEntityManagerImpl.java index 8c7e84f965a1..e436096bd9ca 100755 --- a/hibernate-entitymanager/src/main/java/org/hibernate/ejb/AbstractEntityManagerImpl.java +++ b/hibernate-entitymanager/src/main/java/org/hibernate/ejb/AbstractEntityManagerImpl.java @@ -792,9 +792,10 @@ public A find(Class entityClass, Object primaryKey, LockModeType lockMode try { getSession().setCacheMode( cacheMode ); if ( lockModeType != null ) { + lockOptions = getLockRequest( lockModeType, properties ); return ( A ) getSession().get( - entityClass, ( Serializable ) primaryKey, - getLockRequest( lockModeType, properties ) + entityClass, ( Serializable ) primaryKey, + lockOptions ); } else { @@ -919,7 +920,8 @@ public void refresh(Object entity, LockModeType lockModeType, Map p if ( !contains( entity ) ) { throw new IllegalArgumentException( "entity not in the persistence context" ); } - getSession().buildLockRequest( ( lockOptions = getLockRequest( lockModeType, properties ) ) ) - .lock( entity ); + lockOptions = getLockRequest( lockModeType, properties ); + getSession().buildLockRequest( lockOptions ).lock( entity ); } catch ( HibernateException he ) { throw convert( he, lockOptions ); diff --git a/hibernate-entitymanager/src/matrix/java/org/hibernate/ejb/test/lock/LockTest.java b/hibernate-entitymanager/src/matrix/java/org/hibernate/ejb/test/lock/LockTest.java index eaff6c99c762..147483c28db5 100644 --- a/hibernate-entitymanager/src/matrix/java/org/hibernate/ejb/test/lock/LockTest.java +++ b/hibernate-entitymanager/src/matrix/java/org/hibernate/ejb/test/lock/LockTest.java @@ -23,6 +23,17 @@ */ package org.hibernate.ejb.test.lock; +import org.hibernate.dialect.HSQLDialect; +import org.hibernate.dialect.Oracle10gDialect; +import org.hibernate.dialect.PostgreSQL81Dialect; +import org.hibernate.dialect.SybaseASE15Dialect; +import org.hibernate.ejb.AvailableSettings; +import org.hibernate.ejb.test.BaseEntityManagerFunctionalTestCase; +import org.hibernate.testing.*; +import org.jboss.logging.Logger; +import org.junit.Test; + +import javax.persistence.*; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -30,27 +41,8 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; -import javax.persistence.EntityManager; -import javax.persistence.LockModeType; -import javax.persistence.LockTimeoutException; -import javax.persistence.OptimisticLockException; -import javax.persistence.Query; -import javax.persistence.QueryTimeoutException; - -import org.jboss.logging.Logger; -import org.junit.Test; -import org.hibernate.dialect.HSQLDialect; -import org.hibernate.dialect.Oracle10gDialect; -import org.hibernate.dialect.SybaseASE15Dialect; -import org.hibernate.ejb.AvailableSettings; -import org.hibernate.ejb.test.BaseEntityManagerFunctionalTestCase; -import org.hibernate.testing.SkipForDialect; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; /** * @author Emmanuel Bernard @@ -83,7 +75,58 @@ public void testFindWithTimeoutHint() { em.getTransaction().commit(); em.close(); } + + @Test + @TestForIssue( jiraKey = "HHH-7252" ) + @RequiresDialectFeature( value = DialectChecks.SupportsLockTimeouts.class, + comment = "Test verifies proper exception throwing when a lock timeout is specified.", + jiraKey = "HHH-7252" ) + public void testFindWithPessimisticWriteLockTimeoutException() { + Lock lock = new Lock(); + lock.setName( "name" ); + EntityManager em = getOrCreateEntityManager(); + em.getTransaction().begin(); + em.persist( lock ); + em.getTransaction().commit(); + em.close(); + EntityManager em2 = createIsolatedEntityManager(); + em2.getTransaction().begin(); + Map properties = new HashMap(); + properties.put( AvailableSettings.LOCK_TIMEOUT, 0L ); + Lock lock2 = em2.find( Lock.class, lock.getId(), LockModeType.PESSIMISTIC_WRITE, properties ); + assertEquals( "lock mode should be PESSIMISTIC_WRITE ", LockModeType.PESSIMISTIC_WRITE, em2.getLockMode( lock2 ) ); + + EntityManager em3 = createIsolatedEntityManager(); + em3.getTransaction().begin(); + try { + em3.find( Lock.class, lock.getId(), LockModeType.PESSIMISTIC_WRITE, properties ); + assertFalse("Exception should be thrown", true); + } catch (LockTimeoutException lte) { + assertTrue("Proper exception thrown for dialect supporting lock timeouts when an immediate timeout is set.", true); + } catch (PessimisticLockException pe) { + assertTrue("Find with immediate timeout should have thrown LockTimeoutException.", false); + } catch (PersistenceException pe) { + log.info("EntityManager.find() for PESSIMISTIC_WRITE with timeout of 0 threw a PersistenceException.\n" + + "This is likely a consequence of " + getDialect().getClass().getName() + " not properly mapping SQL errors into the correct HibernateException subtypes.\n" + + "See HHH-7251 for an example of one such situation.", pe); + assertTrue("EntityManager should be throwing LockTimeoutException.", false); + } finally { + if (em3.getTransaction().getRollbackOnly()) { + em3.getTransaction().rollback(); + } else { + em3.getTransaction().commit(); + } + em3.close(); + } + + em2.getTransaction().commit(); + em2.getTransaction().begin(); + em2.remove( lock2 ); + em2.getTransaction().commit(); + em2.close(); + } + @Test public void testLockRead() throws Exception { Lock lock = new Lock(); @@ -394,14 +437,11 @@ private Lock createAndPersistLockInstance(EntityManager em) { } @Test + @RequiresDialect( value = Oracle10gDialect.class ) + @RequiresDialectFeature( value = DialectChecks.SupportsLockTimeouts.class ) 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; @@ -490,15 +530,12 @@ public Boolean call() { } @Test + @RequiresDialect( value = Oracle10gDialect.class ) + @RequiresDialectFeature( value = DialectChecks.SupportsLockTimeouts.class ) 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; @@ -582,15 +619,12 @@ public Boolean call() { } @Test + @RequiresDialect( value = { Oracle10gDialect.class, PostgreSQL81Dialect.class }) + @RequiresDialectFeature( value = DialectChecks.SupportsLockTimeouts.class ) 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; @@ -674,15 +708,12 @@ public Boolean call() { } @Test + @RequiresDialect( value = Oracle10gDialect.class ) + @RequiresDialectFeature( value = DialectChecks.SupportsLockTimeouts.class ) public void testQueryTimeout() throws Exception { EntityManager em = getOrCreateEntityManager(); final EntityManager em2 = createIsolatedEntityManager(); - // TODO: replace dialect instanceof test with a Dialect.hasCapability - if ( !( getDialect() instanceof Oracle10gDialect ) ) { - log.info( "skipping testQueryTimeout" ); - return; - } Lock lock = new Lock(); Thread t = null; FutureTask bgTask; @@ -770,12 +801,9 @@ public Boolean call() { } @Test + @RequiresDialect( value = Oracle10gDialect.class ) + @RequiresDialectFeature( value = DialectChecks.SupportsLockTimeouts.class ) public void testQueryTimeoutEMProps() throws Exception { - // TODO: replace dialect instanceof test with a Dialect.hasCapability - if ( !( getDialect() instanceof Oracle10gDialect ) ) { - log.info( "skipping testQueryTimeout" ); - return; - } EntityManager em = getOrCreateEntityManager(); Map queryTimeoutProps = new HashMap(); queryTimeoutProps.put( "javax.persistence.query.timeout", 500 ); // 1 sec timeout (should round up) @@ -866,17 +894,14 @@ public Boolean call() { } @Test + @RequiresDialect( value = Oracle10gDialect.class ) + @RequiresDialectFeature( value = DialectChecks.SupportsLockTimeouts.class ) public void testLockTimeoutEMProps() throws Exception { EntityManager em = getOrCreateEntityManager(); Map TimeoutProps = new HashMap(); TimeoutProps.put( "javax.persistence.lock.timeout", 1000 ); // 1 second timeout final EntityManager em2 = createIsolatedEntityManager( TimeoutProps ); - // TODO: replace dialect instanceof test with a Dialect.hasCapability (e.g. supportsPessimisticLockTimeout) - if ( !( getDialect() instanceof Oracle10gDialect ) ) { - log.info( "skipping testLockTimeoutEMProps" ); - return; - } Lock lock = new Lock(); Thread t = null; FutureTask bgTask; diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/DialectChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/DialectChecks.java index 46ab68c59a47..a2e56fdda219 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/DialectChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/DialectChecks.java @@ -163,4 +163,10 @@ public boolean isMatch(Dialect dialect) { return dialect.supportsLobValueChangePropogation(); } } + + public static class SupportsLockTimeouts implements DialectCheck { + public boolean isMatch(Dialect dialect) { + return dialect.supportsLockTimeouts(); + } + } }