HHH-10654 - LockOptions.SKIP_LOCKED semantics implementation on MSSQL

This commit is contained in:
Vlad Mihalcea 2017-03-23 12:05:03 +02:00
parent 6bfe281589
commit 2a9c0fe0dd
16 changed files with 424 additions and 29 deletions

View File

@ -258,4 +258,7 @@ test.dependsOn ":hibernate-orm-modules:prepareWildFlyForTests"
test {
systemProperty "file.encoding", "utf-8"
beforeTest { descriptor ->
//println "Starting test: " + descriptor
}
}

View File

@ -2882,6 +2882,15 @@ public boolean supportsValuesList() {
return false;
}
/**
* Does this dialect/database support SKIP_LOCKED timeout.
*
* @return {@code true} if SKIP_LOCKED is supported
*/
public boolean supportsSkipLocked() {
return false;
}
public boolean isLegacyLimitHandlerBehaviorEnabled() {
return legacyLimitHandlerBehavior;
}

View File

@ -85,4 +85,9 @@ public int registerResultSetOutParameter(CallableStatement statement, String nam
public ResultSet getResultSet(CallableStatement statement, String name) throws SQLException {
return (ResultSet) statement.getObject( name );
}
@Override
public boolean supportsSkipLocked() {
return true;
}
}

View File

@ -62,4 +62,9 @@ public String getForUpdateSkipLockedString() {
public String getForUpdateSkipLockedString(String aliases) {
return getForUpdateString() + " of " + aliases + " skip locked";
}
@Override
public boolean supportsSkipLocked() {
return true;
}
}

View File

@ -64,24 +64,31 @@ protected LimitHandler getDefaultLimitHandler() {
@Override
public String appendLockHint(LockOptions lockOptions, String tableName) {
// NOTE : since SQLServer2005 the nowait hint is supported
if ( lockOptions.getLockMode() == LockMode.UPGRADE_NOWAIT ) {
return tableName + " with (updlock, rowlock, nowait)";
LockMode lockMode = lockOptions.getAliasSpecificLockMode( tableName );
if(lockMode == null) {
lockMode = lockOptions.getLockMode();
}
final LockMode mode = lockOptions.getLockMode();
final boolean isNoWait = lockOptions.getTimeOut() == LockOptions.NO_WAIT;
final String noWaitStr = isNoWait ? ", nowait" : "";
switch ( mode ) {
final String writeLockStr = lockOptions.getTimeOut() == LockOptions.SKIP_LOCKED ? "updlock" : "updlock, holdlock";
final String readLockStr = lockOptions.getTimeOut() == LockOptions.SKIP_LOCKED ? "updlock" : "holdlock";
final String noWaitStr = lockOptions.getTimeOut() == LockOptions.NO_WAIT ? ", nowait" : "";
final String skipLockStr = lockOptions.getTimeOut() == LockOptions.SKIP_LOCKED ? ", readpast" : "";
switch ( lockMode ) {
case UPGRADE:
case PESSIMISTIC_WRITE:
case WRITE: {
return tableName + " with (updlock, rowlock" + noWaitStr + ")";
return tableName + " with (" + writeLockStr + ", rowlock" + noWaitStr + skipLockStr + ")";
}
case PESSIMISTIC_READ: {
return tableName + " with (holdlock, rowlock" + noWaitStr + ")";
}case UPGRADE_SKIPLOCKED:
return tableName + " with (" + readLockStr + ", rowlock" + noWaitStr + skipLockStr + ")";
}
case UPGRADE_SKIPLOCKED:
return tableName + " with (updlock, rowlock, readpast" + noWaitStr + ")";
case UPGRADE_NOWAIT:
return tableName + " with (updlock, holdlock, rowlock, nowait)";
default: {
return tableName;
}
@ -110,4 +117,9 @@ public JDBCException convert(SQLException sqlException, String message, String s
public boolean supportsNonQueryWithCTE() {
return true;
}
@Override
public boolean supportsSkipLocked() {
return true;
}
}

View File

@ -446,7 +446,7 @@ public void testAppendLockHintPessimisticReadNoTimeOut() {
@Test
@TestForIssue(jiraKey = "HHH-9635")
public void testAppendLockHintWrite() {
final String expectedLockHint = "tab1 with (updlock, rowlock)";
final String expectedLockHint = "tab1 with (updlock, holdlock, rowlock)";
LockOptions lockOptions = new LockOptions( LockMode.WRITE );
String lockHint = dialect.appendLockHint( lockOptions, "tab1" );
@ -457,7 +457,7 @@ public void testAppendLockHintWrite() {
@Test
@TestForIssue(jiraKey = "HHH-9635")
public void testAppendLockHintWriteWithNoTimeOut() {
final String expectedLockHint = "tab1 with (updlock, rowlock, nowait)";
final String expectedLockHint = "tab1 with (updlock, holdlock, rowlock, nowait)";
LockOptions lockOptions = new LockOptions( LockMode.WRITE );
lockOptions.setTimeOut( LockOptions.NO_WAIT );
@ -470,7 +470,7 @@ public void testAppendLockHintWriteWithNoTimeOut() {
@Test
@TestForIssue(jiraKey = "HHH-9635")
public void testAppendLockHintUpgradeNoWait() {
final String expectedLockHint = "tab1 with (updlock, rowlock, nowait)";
final String expectedLockHint = "tab1 with (updlock, holdlock, rowlock, nowait)";
LockOptions lockOptions = new LockOptions( LockMode.UPGRADE_NOWAIT );
String lockHint = dialect.appendLockHint( lockOptions, "tab1" );
@ -481,7 +481,7 @@ public void testAppendLockHintUpgradeNoWait() {
@Test
@TestForIssue(jiraKey = "HHH-9635")
public void testAppendLockHintUpgradeNoWaitNoTimeout() {
final String expectedLockHint = "tab1 with (updlock, rowlock, nowait)";
final String expectedLockHint = "tab1 with (updlock, holdlock, rowlock, nowait)";
LockOptions lockOptions = new LockOptions( LockMode.UPGRADE_NOWAIT );
lockOptions.setTimeOut( LockOptions.NO_WAIT );
@ -493,7 +493,7 @@ public void testAppendLockHintUpgradeNoWaitNoTimeout() {
@Test
@TestForIssue(jiraKey = "HHH-9635")
public void testAppendLockHintUpgrade() {
final String expectedLockHint = "tab1 with (updlock, rowlock)";
final String expectedLockHint = "tab1 with (updlock, holdlock, rowlock)";
LockOptions lockOptions = new LockOptions( LockMode.UPGRADE );
String lockHint = dialect.appendLockHint( lockOptions, "tab1" );
@ -504,7 +504,7 @@ public void testAppendLockHintUpgrade() {
@Test
@TestForIssue(jiraKey = "HHH-9635")
public void testAppendLockHintUpgradeNoTimeout() {
final String expectedLockHint = "tab1 with (updlock, rowlock, nowait)";
final String expectedLockHint = "tab1 with (updlock, holdlock, rowlock, nowait)";
LockOptions lockOptions = new LockOptions( LockMode.UPGRADE );
lockOptions.setTimeOut( LockOptions.NO_WAIT );
@ -516,7 +516,7 @@ public void testAppendLockHintUpgradeNoTimeout() {
@Test
@TestForIssue(jiraKey = "HHH-9635")
public void testAppendLockHintPessimisticWrite() {
final String expectedLockHint = "tab1 with (updlock, rowlock)";
final String expectedLockHint = "tab1 with (updlock, holdlock, rowlock)";
LockOptions lockOptions = new LockOptions( LockMode.UPGRADE );
String lockHint = dialect.appendLockHint( lockOptions, "tab1" );
@ -527,7 +527,7 @@ public void testAppendLockHintPessimisticWrite() {
@Test
@TestForIssue(jiraKey = "HHH-9635")
public void testAppendLockHintPessimisticWriteNoTimeOut() {
final String expectedLockHint = "tab1 with (updlock, rowlock, nowait)";
final String expectedLockHint = "tab1 with (updlock, holdlock, rowlock, nowait)";
LockOptions lockOptions = new LockOptions( LockMode.UPGRADE );
lockOptions.setTimeOut( LockOptions.NO_WAIT );

View File

@ -13,6 +13,7 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.LockTimeoutException;
@ -22,9 +23,12 @@
import javax.persistence.Query;
import javax.persistence.QueryTimeoutException;
import org.hibernate.Session;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.HSQLDialect;
import org.hibernate.dialect.Oracle10gDialect;
import org.hibernate.dialect.PostgreSQL81Dialect;
import org.hibernate.dialect.SQLServerDialect;
import org.hibernate.dialect.SybaseASE15Dialect;
import org.hibernate.jpa.AvailableSettings;
import org.hibernate.jpa.QueryHints;
@ -40,6 +44,7 @@
import org.jboss.logging.Logger;
import static org.hibernate.testing.transaction.TransactionUtil.doInJPA;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@ -108,6 +113,7 @@ public void testFindWithPessimisticWriteLockTimeoutException() {
}
catch (LockTimeoutException lte) {
// Proper exception thrown for dialect supporting lock timeouts when an immediate timeout is set.
lte.getCause();
}
catch (PessimisticLockException pe) {
fail( "Find with immediate timeout should have thrown LockTimeoutException." );
@ -134,6 +140,100 @@ public void testFindWithPessimisticWriteLockTimeoutException() {
em2.getTransaction().commit();
em2.close();
}
@Test
@RequiresDialectFeature( value = DialectChecks.SupportSkipLocked.class )
public void testUpdateWithPessimisticReadLockSkipLocked() {
Lock lock = new Lock();
lock.setName( "name" );
doInJPA( this::entityManagerFactory, entityManager -> {
entityManager.persist( lock );
} );
EntityManager em2 = createIsolatedEntityManager();
em2.getTransaction().begin();
try {
Map<String, Object> properties = new HashMap<>();
properties.put( org.hibernate.cfg.AvailableSettings.JPA_LOCK_TIMEOUT, -2L );
em2.find( Lock.class, lock.getId(), LockModeType.PESSIMISTIC_READ, properties );
try {
doInJPA( this::entityManagerFactory, entityManager -> {
entityManager.createNativeQuery( updateStatement() )
.setParameter( "name", "changed" )
.setParameter( "id", lock.getId() )
.executeUpdate();
} );
fail("Should throw LockTimeoutException");
}
catch (LockTimeoutException expected) {
}
}
finally {
em2.getTransaction().commit();
}
doInJPA( this::entityManagerFactory, entityManager -> {
Lock _lock = entityManager.merge( lock );
entityManager.remove( _lock );
} );
}
@Test
@RequiresDialectFeature(value = DialectChecks.SupportsLockTimeouts.class)
public void testUpdateWithPessimisticReadLockWithoutNoWait() {
Lock lock = new Lock();
lock.setName( "name" );
doInJPA( this::entityManagerFactory, entityManager -> {
entityManager.persist( lock );
} );
EntityManager em2 = createIsolatedEntityManager();
em2.getTransaction().begin();
try {
em2.find( Lock.class, lock.getId(), LockModeType.PESSIMISTIC_READ );
AtomicBoolean failureExpected = new AtomicBoolean();
try {
doInJPA( this::entityManagerFactory, entityManager -> {
try {
entityManager.createNativeQuery( updateStatement() )
.setParameter( "name", "changed" )
.setParameter( "id", lock.getId() )
.executeUpdate();
}
catch (LockTimeoutException | PessimisticLockException expected) {
failureExpected.set( true );
}
} );
}
catch (Exception e) {
if ( !failureExpected.get() ) {
fail( "Should throw LockTimeoutException or PessimisticLockException" );
}
}
}
finally {
em2.getTransaction().commit();
}
doInJPA( this::entityManagerFactory, entityManager -> {
Lock _lock = entityManager.merge( lock );
entityManager.remove( _lock );
} );
}
protected String updateStatement() {
if( SQLServerDialect.class.isAssignableFrom( Dialect.getDialect().getClass() ) ) {
return "UPDATE Lock_ WITH(NOWAIT) SET name = :name where id = :id";
}
return "UPDATE Lock_ SET name = :name where id = :id";
}
@Test
public void testLockRead() throws Exception {

View File

@ -18,7 +18,7 @@ public class SQLServer2005LockHintsTest extends AbstractLockHintTest {
public static final Dialect DIALECT = new SQLServer2005Dialect();
protected String getLockHintUsed() {
return "with (updlock, rowlock, nowait)";
return "with (updlock, holdlock, rowlock, nowait)";
}
protected Dialect getDialectUnderTest() {

View File

@ -41,7 +41,7 @@ public void setId(Long id) {
this.id = id;
}
@Column(name="`value`")
@Column(name="a_value")
public String getValue() {
return value;
}

View File

@ -0,0 +1,193 @@
package org.hibernate.test.locking;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.Session;
import org.hibernate.dialect.Oracle8iDialect;
import org.hibernate.dialect.PostgreSQL95Dialect;
import org.hibernate.dialect.SQLServer2005Dialect;
import org.hibernate.query.Query;
import org.hibernate.testing.RequiresDialect;
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
import org.junit.Test;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* @author Vlad Mihalcea
*/
public abstract class AbstractSkipLockedTest
extends BaseNonConfigCoreFunctionalTestCase {
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[] { A.class, BatchJob.class };
}
@Test
@RequiresDialect({ SQLServer2005Dialect.class })
public void testSQLServerSkipLocked() {
doInHibernate( this::sessionFactory, session -> {
for ( long i = 1; i <= 10; i++ ) {
BatchJob batchJob = new BatchJob();
batchJob.setId( i );
session.persist( batchJob );
}
} );
doInHibernate( this::sessionFactory, session -> {
List<BatchJob> firstFive = nextFiveBatchJobs( session );
assertEquals( 5, firstFive.size() );
assertTrue( firstFive.stream().map( BatchJob::getId ).collect( Collectors.toList() )
.containsAll( Arrays.asList( 1L, 2L, 3L, 4L, 5L ) ) );
executeSync( () -> {
doInHibernate( this::sessionFactory, _session -> {
List<BatchJob> nextFive = nextFiveBatchJobs( _session );
assertEquals( 5, nextFive.size() );
assertTrue( nextFive.stream().map( BatchJob::getId ).collect( Collectors.toList() )
.containsAll( Arrays.asList( 6L, 7L, 8L, 9L, 10L ) ) );
} );
} );
} );
}
@Test
@RequiresDialect({ PostgreSQL95Dialect.class })
public void testPostgreSQLSkipLocked() {
doInHibernate( this::sessionFactory, session -> {
for ( long i = 1; i <= 10; i++ ) {
BatchJob batchJob = new BatchJob();
batchJob.setId( i );
session.persist( batchJob );
}
} );
doInHibernate( this::sessionFactory, session -> {
List<BatchJob> firstFive = nextFiveBatchJobs( session );
assertEquals( 5, firstFive.size() );
assertTrue( firstFive.stream().map( BatchJob::getId ).collect( Collectors.toList() )
.containsAll( Arrays.asList( 1L, 2L, 3L, 4L, 5L ) ) );
executeSync( () -> {
doInHibernate( this::sessionFactory, _session -> {
List<BatchJob> nextFive = nextFiveBatchJobs( _session );
assertEquals( 5, nextFive.size() );
if ( lockMode() == LockMode.PESSIMISTIC_READ ) {
assertTrue( nextFive.stream().map( BatchJob::getId ).collect( Collectors.toList() )
.containsAll( Arrays.asList( 1L, 2L, 3L, 4L, 5L ) ) );
}
else {
assertTrue( nextFive.stream().map( BatchJob::getId ).collect( Collectors.toList() )
.containsAll( Arrays.asList( 6L, 7L, 8L, 9L, 10L ) ) );
}
} );
} );
} );
}
@Test
@RequiresDialect({ Oracle8iDialect.class })
public void testOracleSkipLocked() {
doInHibernate( this::sessionFactory, session -> {
for ( long i = 1; i <= 10; i++ ) {
BatchJob batchJob = new BatchJob();
batchJob.setId( i );
session.persist( batchJob );
}
} );
doInHibernate( this::sessionFactory, session -> {
List<BatchJob> firstFive = nextFiveBatchJobs( session );
assertEquals( 5, firstFive.size() );
assertTrue( firstFive.stream().map( BatchJob::getId ).collect( Collectors.toList() )
.containsAll( Arrays.asList( 1L, 2L, 3L, 4L, 5L ) ) );
executeSync( () -> {
doInHibernate( this::sessionFactory, _session -> {
List<BatchJob> nextFive = nextFiveBatchJobs( _session );
assertEquals( 0, nextFive.size() );
nextFive = nextFiveBatchJobs( _session, 10 );
assertTrue( nextFive.stream().map( BatchJob::getId ).collect( Collectors.toList() )
.containsAll( Arrays.asList( 6L, 7L, 8L, 9L, 10L ) ) );
} );
} );
} );
}
private List<BatchJob> nextFiveBatchJobs(Session session) {
return nextFiveBatchJobs( session, 5 );
}
@SuppressWarnings("unchecked")
private List<BatchJob> nextFiveBatchJobs(Session session, Integer maxResult) {
Query query = session.createQuery(
"select j from BatchJob j", BatchJob.class )
.setMaxResults( maxResult )
.unwrap( Query.class );
applySkipLocked(query);
return query.list();
}
protected void applySkipLocked(Query query) {
query.setLockOptions(
new LockOptions( lockMode() )
.setTimeOut( LockOptions.SKIP_LOCKED )
);
}
protected abstract LockMode lockMode();
@Entity(name = "BatchJob")
public static class BatchJob {
@Id
private Long id;
private boolean processed;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public boolean isProcessed() {
return processed;
}
public void setProcessed(boolean processed) {
this.processed = processed;
}
}
}

View File

@ -8,13 +8,10 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.PessimisticLockException;
import org.hibernate.Session;
import org.hibernate.dialect.MySQLDialect;
import org.hibernate.dialect.SQLServerDialect;
import org.hibernate.dialect.SybaseASE15Dialect;
import org.hibernate.exception.GenericJDBCException;
@ -22,8 +19,6 @@
import org.hibernate.testing.SkipForDialect;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.async.Executable;
import org.hibernate.testing.async.TimedExecutor;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Test;
@ -174,7 +169,9 @@ private void nowAttemptToUpdateRow() {
_session.doWork( connection -> {
try {
connection.setNetworkTimeout( Executors.newSingleThreadExecutor(), 1000);
} catch (Throwable ignore) {}
} catch (Throwable ignore) {
ignore.fillInStackTrace();
}
} );
try {
// load with write lock to deal with databases that block (wait indefinitely) direct attempts
@ -184,8 +181,10 @@ private void nowAttemptToUpdateRow() {
id,
new LockOptions( LockMode.PESSIMISTIC_WRITE ).setTimeOut( LockOptions.NO_WAIT )
);
it.setValue( "changed" );
_session.flush();
_session.createNativeQuery( updateStatement() )
.setParameter( "value", "changed" )
.setParameter( "id", it.getId() )
.executeUpdate();
fail( "Pessimistic lock not obtained/held" );
}
catch ( Exception e ) {
@ -203,4 +202,11 @@ private void nowAttemptToUpdateRow() {
} );
} );
}
protected String updateStatement() {
if( SQLServerDialect.class.isAssignableFrom( DIALECT.getClass() ) ) {
return "UPDATE T_LOCK_A WITH(NOWAIT) SET a_value = :value where id = :id";
}
return "UPDATE T_LOCK_A SET a_value = :value where id = :id";
}
}

View File

@ -0,0 +1,16 @@
package org.hibernate.test.locking;
import org.hibernate.LockMode;
/**
* @author Vlad Mihalcea
*/
public class PessimisticReadSkipLockedTest
extends AbstractSkipLockedTest {
@Override
protected LockMode lockMode() {
return LockMode.PESSIMISTIC_READ;
}
}

View File

@ -0,0 +1,15 @@
package org.hibernate.test.locking;
import org.hibernate.LockMode;
/**
* @author Vlad Mihalcea
*/
public class PessimisticWriteSkipLockedTest
extends AbstractSkipLockedTest {
@Override
protected LockMode lockMode() {
return LockMode.PESSIMISTIC_WRITE;
}
}

View File

@ -0,0 +1,25 @@
package org.hibernate.test.locking;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.query.Query;
/**
* @author Vlad Mihalcea
*/
public class UpgradeSkipLockedTest
extends AbstractSkipLockedTest {
@Override
protected void applySkipLocked(Query query) {
query.setLockOptions(
new LockOptions( lockMode() ).setFollowOnLocking( false )
);
}
@Override
protected LockMode lockMode() {
return LockMode.UPGRADE_SKIPLOCKED;
}
}

View File

@ -223,4 +223,10 @@ public boolean isMatch(Dialect dialect) {
return dialect.supportsRowValueConstructorSyntaxInInList();
}
}
public static class SupportSkipLocked implements DialectCheck {
public boolean isMatch(Dialect dialect) {
return dialect.supportsSkipLocked();
}
}
}

View File

@ -91,7 +91,7 @@ protected void executeSync(Runnable callable) {
Thread.currentThread().interrupt();
}
catch (ExecutionException e) {
e.printStackTrace();
throw new RuntimeException( e );
}
}
}