/hibernate-core/src/test/java/org/hibernate/test/locking/LockModeTest.java

https://github.com/sebersole/hibernate-core · Java · 306 lines · 220 code · 33 blank · 53 comment · 4 complexity · de6416ff9a86eede2576957f7591b983 MD5 · raw file

  1. /*
  2. * Hibernate, Relational Persistence for Idiomatic Java
  3. *
  4. * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
  5. * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
  6. */
  7. package org.hibernate.test.locking;
  8. import java.util.Collections;
  9. import java.util.concurrent.CountDownLatch;
  10. import javax.persistence.LockModeType;
  11. import javax.persistence.criteria.CriteriaBuilder;
  12. import javax.persistence.criteria.CriteriaQuery;
  13. import org.hibernate.LockMode;
  14. import org.hibernate.LockOptions;
  15. import org.hibernate.Session;
  16. import org.hibernate.dialect.SQLServerDialect;
  17. import org.hibernate.dialect.SybaseASE15Dialect;
  18. import org.hibernate.dialect.SybaseDialect;
  19. import org.hibernate.engine.spi.SharedSessionContractImplementor;
  20. import org.hibernate.testing.DialectChecks;
  21. import org.hibernate.testing.RequiresDialectFeature;
  22. import org.hibernate.testing.SkipForDialect;
  23. import org.hibernate.testing.TestForIssue;
  24. import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
  25. import org.hibernate.testing.transaction.TransactionUtil;
  26. import org.hibernate.testing.util.ExceptionUtil;
  27. import org.junit.Test;
  28. import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
  29. import static org.junit.Assert.assertEquals;
  30. import static org.junit.Assert.assertNotNull;
  31. import static org.junit.Assert.fail;
  32. /**
  33. * Make sure that directly specifying lock modes, even though deprecated, continues to work until removed.
  34. *
  35. * @author Steve Ebersole
  36. */
  37. @TestForIssue( jiraKey = "HHH-5275")
  38. @SkipForDialect(value=SybaseASE15Dialect.class, strictMatching=true,
  39. comment = "skip this test on Sybase ASE 15.5, but run it on 15.7, see HHH-6820")
  40. public class LockModeTest extends BaseCoreFunctionalTestCase {
  41. private Long id;
  42. private CountDownLatch endLatch = new CountDownLatch( 1 );
  43. @Override
  44. protected Class<?>[] getAnnotatedClasses() {
  45. return new Class[] { A.class };
  46. }
  47. @Override
  48. public void prepareTest() throws Exception {
  49. doInHibernate( this::sessionFactory, session -> {
  50. id = (Long) session.save( new A( "it" ) );
  51. } );
  52. }
  53. @Override
  54. protected boolean isCleanupTestDataRequired(){return true;}
  55. @Test
  56. @RequiresDialectFeature( value = DialectChecks.SupportsLockTimeouts.class )
  57. @SuppressWarnings( {"deprecation"})
  58. public void testLoading() {
  59. // open a session, begin a transaction and lock row
  60. doInHibernate( this::sessionFactory, session -> {
  61. A it = session.byId( A.class ).with( LockOptions.UPGRADE ).load( id );
  62. // make sure we got it
  63. assertNotNull( it );
  64. // that initial transaction is still active and so the lock should still be held.
  65. // Lets open another session/transaction and verify that we cannot update the row
  66. nowAttemptToUpdateRow();
  67. } );
  68. }
  69. @Test
  70. @RequiresDialectFeature( value = DialectChecks.SupportsLockTimeouts.class )
  71. public void testCriteria() {
  72. // open a session, begin a transaction and lock row
  73. doInHibernate( this::sessionFactory, session -> {
  74. CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder();
  75. CriteriaQuery<A> criteria = criteriaBuilder.createQuery( A.class );
  76. criteria.from( A.class );
  77. A it = session.createQuery( criteria ).setLockMode( LockModeType.PESSIMISTIC_WRITE ).uniqueResult();
  78. // A it = (A) session.createCriteria( A.class )
  79. // .setLockMode( LockMode.PESSIMISTIC_WRITE )
  80. // .uniqueResult();
  81. // make sure we got it
  82. assertNotNull( it );
  83. // that initial transaction is still active and so the lock should still be held.
  84. // Lets open another session/transaction and verify that we cannot update the row
  85. nowAttemptToUpdateRow();
  86. } );
  87. }
  88. @Test
  89. @RequiresDialectFeature( value = DialectChecks.SupportsLockTimeouts.class )
  90. public void testCriteriaAliasSpecific() {
  91. // open a session, begin a transaction and lock row
  92. doInHibernate( this::sessionFactory, session -> {
  93. CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder();
  94. CriteriaQuery<A> criteria = criteriaBuilder.createQuery( A.class );
  95. criteria.from( A.class );
  96. A it = session.createQuery( criteria ).setLockMode("this",LockMode.PESSIMISTIC_WRITE ).uniqueResult();
  97. // A it = (A) session.createCriteria( A.class )
  98. // .setLockMode( "this", LockMode.PESSIMISTIC_WRITE )
  99. // .uniqueResult();
  100. // make sure we got it
  101. assertNotNull( it );
  102. // that initial transaction is still active and so the lock should still be held.
  103. // Lets open another session/transaction and verify that we cannot update the row
  104. nowAttemptToUpdateRow();
  105. } );
  106. }
  107. @Test
  108. @RequiresDialectFeature( value = DialectChecks.SupportsLockTimeouts.class )
  109. public void testQuery() {
  110. // open a session, begin a transaction and lock row
  111. doInHibernate( this::sessionFactory, session -> {
  112. A it = (A) session.createQuery( "from A a" )
  113. .setLockMode( "a", LockMode.PESSIMISTIC_WRITE )
  114. .uniqueResult();
  115. // make sure we got it
  116. assertNotNull( it );
  117. // that initial transaction is still active and so the lock should still be held.
  118. // Lets open another session/transaction and verify that we cannot update the row
  119. nowAttemptToUpdateRow();
  120. } );
  121. }
  122. @Test
  123. public void testQueryUsingLockOptions() {
  124. // todo : need an association here to make sure the alias-specific lock modes are applied correctly
  125. doInHibernate( this::sessionFactory, session -> {
  126. session.createQuery( "from A a" )
  127. .setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) )
  128. .uniqueResult();
  129. session.createQuery( "from A a" )
  130. .setLockOptions( new LockOptions().setAliasSpecificLockMode( "a", LockMode.PESSIMISTIC_WRITE ) )
  131. .uniqueResult();
  132. } );
  133. }
  134. @Test
  135. @TestForIssue(jiraKey = "HHH-2735")
  136. public void testQueryLockModeNoneWithAlias() {
  137. doInHibernate( this::sessionFactory, session -> {
  138. // shouldn't throw an exception
  139. session.createQuery( "SELECT a.value FROM A a where a.id = :id" )
  140. .setLockMode( "a", LockMode.NONE )
  141. .setParameter( "id", id )
  142. .list();
  143. } );
  144. }
  145. @Test
  146. @TestForIssue(jiraKey = "HHH-2735")
  147. public void testQueryLockModePessimisticWriteWithAlias() {
  148. doInHibernate( this::sessionFactory, session -> {
  149. // shouldn't throw an exception
  150. session.createQuery( "SELECT MAX(a.id)+1 FROM A a where a.value = :value" )
  151. .setLockMode( "a", LockMode.PESSIMISTIC_WRITE )
  152. .setParameter( "value", "it" )
  153. .list();
  154. } );
  155. }
  156. @Test
  157. @TestForIssue(jiraKey = "HHH-12257")
  158. public void testRefreshLockedEntity() {
  159. doInHibernate( this::sessionFactory, session -> {
  160. A a = session.get( A.class, id, LockMode.PESSIMISTIC_READ );
  161. checkLockMode( a, LockMode.PESSIMISTIC_READ, session );
  162. session.refresh( a );
  163. checkLockMode( a, LockMode.PESSIMISTIC_READ, session );
  164. session.refresh( A.class.getName(), a );
  165. checkLockMode( a, LockMode.PESSIMISTIC_READ, session );
  166. session.refresh( a, Collections.emptyMap() );
  167. checkLockMode( a, LockMode.PESSIMISTIC_READ, session );
  168. session.refresh( a, null, Collections.emptyMap() );
  169. checkLockMode( a, LockMode.PESSIMISTIC_READ, session );
  170. } );
  171. }
  172. @Test
  173. @TestForIssue(jiraKey = "HHH-12257")
  174. public void testRefreshWithExplicitLowerLevelLockMode() {
  175. doInHibernate( this::sessionFactory, session -> {
  176. A a = session.get( A.class, id, LockMode.PESSIMISTIC_READ );
  177. checkLockMode( a, LockMode.PESSIMISTIC_READ, session );
  178. session.refresh( a, LockMode.READ );
  179. checkLockMode( a, LockMode.PESSIMISTIC_READ, session );
  180. session.refresh( a, LockModeType.READ );
  181. checkLockMode( a, LockMode.PESSIMISTIC_READ, session );
  182. session.refresh( a, LockModeType.READ, Collections.emptyMap() );
  183. checkLockMode( a, LockMode.PESSIMISTIC_READ, session );
  184. } );
  185. }
  186. @Test
  187. @TestForIssue(jiraKey = "HHH-12257")
  188. public void testRefreshWithExplicitHigherLevelLockMode() {
  189. doInHibernate( this::sessionFactory, session -> {
  190. A a = session.get( A.class, id );
  191. checkLockMode( a, LockMode.READ, session );
  192. session.refresh( a, LockMode.UPGRADE_NOWAIT );
  193. checkLockMode( a, LockMode.UPGRADE_NOWAIT, session );
  194. session.refresh( a, LockModeType.PESSIMISTIC_READ );
  195. checkLockMode( a, LockMode.PESSIMISTIC_READ, session );
  196. session.refresh( a, LockModeType.PESSIMISTIC_WRITE, Collections.emptyMap() );
  197. checkLockMode( a, LockMode.PESSIMISTIC_WRITE, session );
  198. } );
  199. }
  200. @Test
  201. @TestForIssue(jiraKey = "HHH-12257")
  202. public void testRefreshAfterUpdate() {
  203. doInHibernate( this::sessionFactory, session -> {
  204. A a = session.get( A.class, id );
  205. checkLockMode( a, LockMode.READ, session );
  206. a.setValue( "new value" );
  207. session.flush();
  208. checkLockMode( a, LockMode.WRITE, session );
  209. session.refresh( a );
  210. checkLockMode( a, LockMode.WRITE, session );
  211. } );
  212. }
  213. private void checkLockMode(Object entity, LockMode expectedLockMode, Session session) {
  214. final LockMode lockMode =
  215. ( (SharedSessionContractImplementor) session ).getPersistenceContext().getEntry( entity ).getLockMode();
  216. assertEquals( expectedLockMode, lockMode );
  217. }
  218. private void nowAttemptToUpdateRow() {
  219. // here we just need to open a new connection (database session and transaction) and make sure that
  220. // we are not allowed to acquire exclusive locks to that row and/or write to that row. That may take
  221. // one of two forms:
  222. // 1) either the get-with-lock or the update fails immediately with a sql error
  223. // 2) either the get-with-lock or the update blocks indefinitely (in real world, it would block
  224. // until the txn in the calling method completed.
  225. // To be able to cater to the second type, we run this block in a separate thread to be able to "time it out"
  226. try {
  227. executeSync( () -> {
  228. doInHibernate( this::sessionFactory, _session -> {
  229. TransactionUtil.setJdbcTimeout( _session );
  230. try {
  231. // We used to load with write lock here to deal with databases that block (wait indefinitely)
  232. // direct attempts to write a locked row.
  233. // At some point, due to a bug, the lock mode was lost when applied via lock options, leading
  234. // this code to not apply the pessimistic write lock.
  235. // See HHH-12847 + https://github.com/hibernate/hibernate-orm/commit/719e5d0c12a6ef709bee907b8b651d27b8b08a6a.
  236. // At least Sybase waits indefinitely when really applying a PESSIMISTIC_WRITE lock here (and
  237. // the NO_WAIT part is not applied by the Sybase dialect so it doesn't help).
  238. // For now going back to LockMode.NONE as it's the lock mode that has been applied for quite
  239. // some time and it seems our supported databases don't have a problem with it.
  240. A it = _session.get(
  241. A.class,
  242. id,
  243. new LockOptions( LockMode.NONE ).setTimeOut( LockOptions.NO_WAIT )
  244. );
  245. _session.createNativeQuery( updateStatement() )
  246. .setParameter( "value", "changed" )
  247. .setParameter( "id", it.getId() )
  248. .executeUpdate();
  249. fail( "Pessimistic lock not obtained/held" );
  250. }
  251. catch ( Exception e ) {
  252. if ( !ExceptionUtil.isSqlLockTimeout( e) ) {
  253. fail( "Unexpected error type testing pessimistic locking : " + e.getClass().getName() );
  254. }
  255. }
  256. } );
  257. } );
  258. }
  259. catch (Exception e) {
  260. //MariaDB throws a time out nd closes the underlying connection
  261. if( !ExceptionUtil.isConnectionClose(e)) {
  262. fail("Unknown exception thrown: " + e.getMessage());
  263. }
  264. }
  265. }
  266. protected String updateStatement() {
  267. if ( SQLServerDialect.class.isAssignableFrom( DIALECT.getClass() )
  268. || SybaseDialect.class.isAssignableFrom( DIALECT.getClass() ) ) {
  269. return "UPDATE T_LOCK_A WITH(NOWAIT) SET a_value = :value where id = :id";
  270. }
  271. return "UPDATE T_LOCK_A SET a_value = :value where id = :id";
  272. }
  273. }