From ddc870889eebfcb6972bf51c88fe77c684702576 Mon Sep 17 00:00:00 2001 From: Christian Beikov <christian.beikov@gmail.com> Date: Fri, 14 Feb 2025 16:54:50 +0100 Subject: [PATCH 1/2] HHH-9127 Add test showing forceIncrement leaves stale data in transactional cache --- .../test/cache/ForceIncrementCacheTest.java | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/cache/ForceIncrementCacheTest.java diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/cache/ForceIncrementCacheTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/cache/ForceIncrementCacheTest.java new file mode 100644 index 000000000000..82bd09edf434 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/cache/ForceIncrementCacheTest.java @@ -0,0 +1,135 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.cache; + +import jakarta.persistence.Cacheable; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.LockModeType; +import jakarta.persistence.Version; +import org.hibernate.LockMode; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@SessionFactory +@DomainModel(annotatedClasses = { + ForceIncrementCacheTest.Person.class +}) +@Jira("https://hibernate.atlassian.net/browse/HHH-9127") +public class ForceIncrementCacheTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.persist( new Person( 1L, "Marco" ) ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from Person" ).executeUpdate(); + } ); + } + + @Test + public void testOptimisticForceIncrementOnLoad(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Person entity = session.find( Person.class, 1L, LockModeType.OPTIMISTIC_FORCE_INCREMENT ); + assertThat( entity.getVersion() ).isEqualTo( 0L ); + } ); + // in a different transaction + scope.inTransaction( session -> { + Person entity = session.find( Person.class, 1L ); + assertThat( entity.getVersion() ).isEqualTo( 1L ); + } ); + } + + @Test + public void testPessimisticForceIncrementOnLoad(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Person entity = session.find( Person.class, 1L ); + assertThat( entity.getVersion() ).isEqualTo( 0L ); + } ); + scope.inTransaction( session -> { + Person entity = session.find( Person.class, 1L, LockModeType.PESSIMISTIC_FORCE_INCREMENT ); + assertThat( entity.getVersion() ).isEqualTo( 1L ); + } ); + // in a different transaction + scope.inTransaction( session -> { + Person entity = session.find( Person.class, 1L ); + assertThat( entity.getVersion() ).isEqualTo( 1L ); + } ); + } + + @Test + public void testForceIncrementOnLock(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Person entity = session.find( Person.class, 1L ); + assertThat( entity.getVersion() ).isEqualTo( 0L ); + session.lock( entity, LockMode.PESSIMISTIC_FORCE_INCREMENT ); + assertThat( entity.getVersion() ).isEqualTo( 1L ); + } ); + // in a different transaction + scope.inTransaction( session -> { + Person entity = session.find( Person.class, 1L ); + assertThat( entity.getVersion() ).isEqualTo( 1L ); + } ); + } + + @Entity(name = "Person") + @Cacheable + @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL) + public static class Person { + + @Id + private Long id; + @Version + private Long version; + private String name; + + public Person() { + } + + public Person(final long id, final String name) { + setId( id ); + setName( name ); + } + + public Long getId() { + return id; + } + + public void setId(final Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } + + } +} From 25792bce278cb520b26d267d1706db59c20c1aeb Mon Sep 17 00:00:00 2001 From: Christian Beikov <christian.beikov@gmail.com> Date: Fri, 14 Feb 2025 16:57:13 +0100 Subject: [PATCH 2/2] HHH-9127 Invoked cache update and afterUpdate for forceIncrement --- .../EntityIncrementVersionProcess.java | 6 +- ...simisticForceIncrementLockingStrategy.java | 5 +- .../org/hibernate/engine/spi/ActionQueue.java | 5 +- .../engine/spi/SessionDelegatorBaseImpl.java | 6 + .../spi/SharedSessionContractImplementor.java | 10 + .../spi/SharedSessionDelegatorBaseImpl.java | 6 + .../DefaultPostLoadEventListener.java | 5 +- .../internal/OptimisticLockHelper.java | 187 ++++++++++++++++++ .../org/hibernate/internal/SessionImpl.java | 6 + .../internal/StatelessSessionImpl.java | 35 ++-- .../loader/ast/internal/LoaderHelper.java | 5 +- .../test/cache/ForceIncrementCacheTest.java | 2 +- 12 files changed, 246 insertions(+), 32 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/internal/OptimisticLockHelper.java diff --git a/hibernate-core/src/main/java/org/hibernate/action/internal/EntityIncrementVersionProcess.java b/hibernate-core/src/main/java/org/hibernate/action/internal/EntityIncrementVersionProcess.java index d02e11574c54..fa0cef4f15b9 100644 --- a/hibernate-core/src/main/java/org/hibernate/action/internal/EntityIncrementVersionProcess.java +++ b/hibernate-core/src/main/java/org/hibernate/action/internal/EntityIncrementVersionProcess.java @@ -7,7 +7,7 @@ import org.hibernate.action.spi.BeforeTransactionCompletionProcess; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.SessionImplementor; -import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.internal.OptimisticLockHelper; /** * A {@link BeforeTransactionCompletionProcess} implementation to verify and @@ -41,8 +41,6 @@ public void doBeforeTransactionCompletion(SessionImplementor session) { return; } - final EntityPersister persister = entry.getPersister(); - final Object nextVersion = persister.forceVersionIncrement( entry.getId(), entry.getVersion(), session ); - entry.forceLocked( object, nextVersion ); + OptimisticLockHelper.forceVersionIncrement( object, entry, session.asEventSource() ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/PessimisticForceIncrementLockingStrategy.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/PessimisticForceIncrementLockingStrategy.java index 108b6633fab7..7c29ec5b2dfc 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/PessimisticForceIncrementLockingStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/PessimisticForceIncrementLockingStrategy.java @@ -8,6 +8,7 @@ import org.hibernate.LockMode; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.OptimisticLockHelper; import org.hibernate.persister.entity.EntityPersister; /** @@ -44,9 +45,7 @@ public void lock(Object id, Object version, Object object, int timeout, SharedSe throw new HibernateException( "[" + lockMode + "] not supported for non-versioned entities [" + lockable.getEntityName() + "]" ); } final EntityEntry entry = session.getPersistenceContextInternal().getEntry( object ); - final EntityPersister persister = entry.getPersister(); - final Object nextVersion = persister.forceVersionIncrement( entry.getId(), entry.getVersion(), false, session ); - entry.forceLocked( object, nextVersion ); + OptimisticLockHelper.forceVersionIncrement( object, entry, session ); } /** diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java index 6c96dca887aa..e1673860f10d 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java @@ -552,9 +552,10 @@ public void beforeTransactionCompletion() { // Execute completion actions only in transaction owner (aka parent session). if ( beforeTransactionProcesses != null ) { beforeTransactionProcesses.beforeTransactionCompletion(); - // `beforeTransactionCompletion()` can have added batch operations (e.g. to increment entity version) - session.getJdbcCoordinator().executeBatch(); } + // Make sure to always execute pending batches before the transaction completes. + // One such pending batch could be the pessimistic version increment for an entity + session.getJdbcCoordinator().executeBatch(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java index fe44d888a12a..7be4173ff575 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java @@ -30,6 +30,7 @@ import org.hibernate.SimpleNaturalIdLoadAccess; import org.hibernate.Transaction; import org.hibernate.UnknownProfileException; +import org.hibernate.action.spi.AfterTransactionCompletionProcess; import org.hibernate.cache.spi.CacheTransactionSynchronization; import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.engine.jdbc.LobCreator; @@ -1152,6 +1153,11 @@ public ActionQueue getActionQueue() { return delegate.getActionQueue(); } + @Override + public void registerProcess(AfterTransactionCompletionProcess process) { + delegate.registerProcess( process ); + } + @Override public Object instantiate(EntityPersister persister, Object id) throws HibernateException { return delegate.instantiate( persister, id ); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java index b58a56c7842a..dc6825a07888 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java @@ -17,6 +17,7 @@ import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.StatelessSession; +import org.hibernate.action.spi.AfterTransactionCompletionProcess; import org.hibernate.boot.spi.SessionFactoryOptions; import org.hibernate.dialect.Dialect; import org.hibernate.event.spi.EventSource; @@ -592,6 +593,15 @@ default boolean isStatelessSession() { */ void lock(String entityName, Object child, LockOptions lockOptions); + /** + * Registers the given process for execution after transaction completion. + * + * @param process The process to register + * @since 7.0 + */ + @Incubating + void registerProcess(AfterTransactionCompletionProcess process); + /** * Attempts to load the entity from the second-level cache. * diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java index 920ff90b6a77..26f47a861c22 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java @@ -21,6 +21,7 @@ import org.hibernate.LockOptions; import org.hibernate.SharedSessionContract; import org.hibernate.Transaction; +import org.hibernate.action.spi.AfterTransactionCompletionProcess; import org.hibernate.cache.spi.CacheTransactionSynchronization; import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.engine.jdbc.LobCreator; @@ -699,6 +700,11 @@ public void lock(String entityName, Object child, LockOptions lockOptions) { delegate.lock( entityName, child, lockOptions ); } + @Override + public void registerProcess(AfterTransactionCompletionProcess process) { + delegate.registerProcess( process ); + } + @Override public Object loadFromSecondLevelCache(EntityPersister persister, EntityKey entityKey, Object instanceToLoad, LockMode lockMode) { return delegate.loadFromSecondLevelCache( persister, entityKey, instanceToLoad, lockMode ); diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultPostLoadEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultPostLoadEventListener.java index 6035feb101f1..c9b4c8f670e6 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultPostLoadEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultPostLoadEventListener.java @@ -14,6 +14,7 @@ import org.hibernate.event.spi.EventSource; import org.hibernate.event.spi.PostLoadEvent; import org.hibernate.event.spi.PostLoadEventListener; +import org.hibernate.internal.OptimisticLockHelper; import org.hibernate.jpa.event.spi.CallbackRegistry; import org.hibernate.jpa.event.spi.CallbackRegistryConsumer; import org.hibernate.persister.entity.EntityPersister; @@ -50,9 +51,7 @@ public void onPostLoad(PostLoadEvent event) { if ( persister.isVersioned() ) { switch ( lockMode ) { case PESSIMISTIC_FORCE_INCREMENT: - final Object nextVersion = - persister.forceVersionIncrement( entry.getId(), entry.getVersion(), false, session ); - entry.forceLocked( entity, nextVersion ); + OptimisticLockHelper.forceVersionIncrement( entity, entry, session ); break; case OPTIMISTIC_FORCE_INCREMENT: session.getActionQueue().registerProcess( new EntityIncrementVersionProcess( entity ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/OptimisticLockHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/OptimisticLockHelper.java new file mode 100644 index 000000000000..2c1105959bbc --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/internal/OptimisticLockHelper.java @@ -0,0 +1,187 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.internal; + +import org.hibernate.CacheMode; +import org.hibernate.action.spi.AfterTransactionCompletionProcess; +import org.hibernate.cache.spi.access.EntityDataAccess; +import org.hibernate.cache.spi.access.SoftLock; +import org.hibernate.cache.spi.entry.CacheEntry; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.SessionEventListenerManager; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.engine.spi.Status; +import org.hibernate.event.monitor.spi.DiagnosticEvent; +import org.hibernate.event.monitor.spi.EventMonitor; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.stat.internal.StatsHelper; +import org.hibernate.stat.spi.StatisticsImplementor; + +public final class OptimisticLockHelper { + + private OptimisticLockHelper() { + //utility class, not to be constructed + } + + public static void forceVersionIncrement(Object object, EntityEntry entry, SharedSessionContractImplementor session) { + final EntityPersister persister = entry.getPersister(); + final Object previousVersion = entry.getVersion(); + SoftLock lock = null; + final Object cacheKey; + if ( persister.canWriteToCache() ) { + final EntityDataAccess cache = persister.getCacheAccessStrategy(); + cacheKey = cache.generateCacheKey( + entry.getId(), + persister, + session.getFactory(), + session.getTenantIdentifier() + ); + lock = cache.lockItem( session, cacheKey, previousVersion ); + } + else { + cacheKey = null; + } + final Object nextVersion = persister.forceVersionIncrement( entry.getId(), previousVersion, session ); + entry.forceLocked( object, nextVersion ); + if ( persister.canWriteToCache() ) { + final Object cacheEntry = updateCacheItem( + object, + previousVersion, + nextVersion, + cacheKey, + entry, + persister, + session + ); + session.registerProcess( new CacheCleanupProcess( + cacheKey, + persister, + previousVersion, + nextVersion, + lock, + cacheEntry + ) ); + } + } + + private static Object updateCacheItem(Object entity, Object previousVersion, Object nextVersion, Object ck, EntityEntry entry, EntityPersister persister, SharedSessionContractImplementor session) { + if ( isCacheInvalidationRequired( persister, session ) || entry.getStatus() != Status.MANAGED ) { + persister.getCacheAccessStrategy().remove( session, ck ); + } + else if ( session.getCacheMode().isPutEnabled() ) { + //TODO: inefficient if that cache is just going to ignore the updated state! + final CacheEntry ce = persister.buildCacheEntry( entity, entry.getLoadedState(), nextVersion, session ); + final Object cacheEntry = persister.getCacheEntryStructure().structure( ce ); + final boolean put = updateCache( persister, cacheEntry, previousVersion, nextVersion, ck, session ); + + final StatisticsImplementor statistics = session.getFactory().getStatistics(); + if ( put && statistics.isStatisticsEnabled() ) { + statistics.entityCachePut( + StatsHelper.getRootEntityRole( persister ), + persister.getCacheAccessStrategy().getRegion().getName() + ); + } + return cacheEntry; + } + return null; + } + + private static boolean updateCache(EntityPersister persister, Object cacheEntry, Object previousVersion, Object nextVersion, Object ck, SharedSessionContractImplementor session) { + final EventMonitor eventMonitor = session.getEventMonitor(); + final DiagnosticEvent cachePutEvent = eventMonitor.beginCachePutEvent(); + final EntityDataAccess cacheAccessStrategy = persister.getCacheAccessStrategy(); + boolean update = false; + try { + session.getEventListenerManager().cachePutStart(); + update = cacheAccessStrategy.update( session, ck, cacheEntry, nextVersion, previousVersion ); + return update; + } + finally { + eventMonitor.completeCachePutEvent( + cachePutEvent, + session, + cacheAccessStrategy, + persister, + update, + EventMonitor.CacheActionDescription.ENTITY_UPDATE + ); + session.getEventListenerManager().cachePutEnd(); + } + } + + private static boolean isCacheInvalidationRequired( + EntityPersister persister, + SharedSessionContractImplementor session) { + // the cache has to be invalidated when CacheMode is equal to GET or IGNORE + return persister.isCacheInvalidationRequired() + || session.getCacheMode() == CacheMode.GET + || session.getCacheMode() == CacheMode.IGNORE; + } + + private static class CacheCleanupProcess implements AfterTransactionCompletionProcess { + private final Object cacheKey; + private final EntityPersister persister; + private final Object previousVersion; + private final Object nextVersion; + private final SoftLock lock; + private final Object cacheEntry; + + private CacheCleanupProcess(Object cacheKey, EntityPersister persister, Object previousVersion, Object nextVersion, SoftLock lock, Object cacheEntry) { + this.cacheKey = cacheKey; + this.persister = persister; + this.previousVersion = previousVersion; + this.nextVersion = nextVersion; + this.lock = lock; + this.cacheEntry = cacheEntry; + } + + @Override + public void doAfterTransactionCompletion(boolean success, SharedSessionContractImplementor session) { + final EntityDataAccess cache = persister.getCacheAccessStrategy(); + if ( cacheUpdateRequired( success, persister, session ) ) { + cacheAfterUpdate( cache, cacheKey, session ); + } + else { + cache.unlockItem( session, cacheKey, lock ); + } + } + + private static boolean cacheUpdateRequired(boolean success, EntityPersister persister, SharedSessionContractImplementor session) { + return success + && !persister.isCacheInvalidationRequired() + && session.getCacheMode().isPutEnabled(); + } + + protected void cacheAfterUpdate(EntityDataAccess cache, Object ck, SharedSessionContractImplementor session) { + final SessionEventListenerManager eventListenerManager = session.getEventListenerManager(); + final EventMonitor eventMonitor = session.getEventMonitor(); + final DiagnosticEvent cachePutEvent = eventMonitor.beginCachePutEvent(); + boolean put = false; + try { + eventListenerManager.cachePutStart(); + put = cache.afterUpdate( session, ck, cacheEntry, nextVersion, previousVersion, lock ); + } + finally { + eventMonitor.completeCachePutEvent( + cachePutEvent, + session, + cache, + persister, + put, + EventMonitor.CacheActionDescription.ENTITY_AFTER_UPDATE + ); + final StatisticsImplementor statistics = session.getFactory().getStatistics(); + if ( put && statistics.isStatisticsEnabled() ) { + statistics.entityCachePut( + StatsHelper.getRootEntityRole( persister ), + cache.getRegion().getName() + ); + } + eventListenerManager.cachePutEnd(); + } + } + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java index 31ff72ea5acc..60f879686798 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -56,6 +56,7 @@ import org.hibernate.TypeMismatchException; import org.hibernate.UnknownProfileException; import org.hibernate.UnresolvableObjectException; +import org.hibernate.action.spi.AfterTransactionCompletionProcess; import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.engine.internal.PersistenceContexts; @@ -1905,6 +1906,11 @@ public ActionQueue getActionQueue() { return actionQueue; } + @Override + public void registerProcess(AfterTransactionCompletionProcess process) { + getActionQueue().registerProcess( process ); + } + @Override public PersistenceContext getPersistenceContext() { checkOpenOrWaitingForAutoClose(); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java index f28cd9176b31..4611c245f2fd 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java @@ -18,6 +18,7 @@ import org.hibernate.StatelessSession; import org.hibernate.TransientObjectException; import org.hibernate.UnresolvableObjectException; +import org.hibernate.action.spi.AfterTransactionCompletionProcess; import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata; import org.hibernate.cache.CacheException; @@ -34,7 +35,6 @@ import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.SessionImplementor; -import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.transaction.internal.jta.JtaStatusHelper; import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform; import org.hibernate.event.monitor.spi.EventMonitor; @@ -133,7 +133,7 @@ public class StatelessSessionImpl extends AbstractSharedSessionContract implemen private final LoadQueryInfluencers influencers; private final PersistenceContext temporaryPersistenceContext; private final boolean connectionProvided; - private final List<Runnable> afterCompletions = new ArrayList<>(); + private final List<AfterTransactionCompletionProcess> afterCompletions = new ArrayList<>(); private final EventListenerGroups eventListenerGroups; @@ -1260,17 +1260,17 @@ public void beforeTransactionCompletion() { @Override public void afterTransactionCompletion(boolean successful, boolean delayed) { - processAfterCompletions(); + processAfterCompletions( successful ); afterTransactionCompletionEvents( successful ); if ( shouldAutoClose() && !isClosed() ) { managedClose(); } } - private void processAfterCompletions() { - for ( Runnable completion: afterCompletions ) { + private void processAfterCompletions(boolean successful) { + for ( AfterTransactionCompletionProcess completion: afterCompletions ) { try { - completion.run(); + completion.doAfterTransactionCompletion( successful, this ); } catch (CacheException ce) { LOG.unableToReleaseCacheLock( ce ); @@ -1324,16 +1324,15 @@ public boolean isStatelessSession() { protected Object lockCacheItem(Object id, Object previousVersion, EntityPersister persister) { if ( persister.canWriteToCache() ) { - final SharedSessionContractImplementor session = getSession(); final EntityDataAccess cache = persister.getCacheAccessStrategy(); final Object ck = cache.generateCacheKey( id, persister, - session.getFactory(), - session.getTenantIdentifier() + getFactory(), + getTenantIdentifier() ); - final SoftLock lock = cache.lockItem( session, ck, previousVersion ); - afterCompletions.add( () -> cache.unlockItem( this, ck, lock ) ); + final SoftLock lock = cache.lockItem( this, ck, previousVersion ); + afterCompletions.add( (success, session) -> cache.unlockItem( session, ck, lock ) ); return ck; } else { @@ -1349,16 +1348,15 @@ protected void removeCacheItem(Object ck, EntityPersister persister) { protected Object lockCacheItem(Object key, CollectionPersister persister) { if ( persister.hasCache() ) { - final SharedSessionContractImplementor session = getSession(); final CollectionDataAccess cache = persister.getCacheAccessStrategy(); final Object ck = cache.generateCacheKey( key, persister, - session.getFactory(), - session.getTenantIdentifier() + getFactory(), + getTenantIdentifier() ); - final SoftLock lock = cache.lockItem( session, ck, null ); - afterCompletions.add( () -> cache.unlockItem( this, ck, lock ) ); + final SoftLock lock = cache.lockItem( this, ck, null ); + afterCompletions.add( (success, session) -> cache.unlockItem( this, ck, lock ) ); return ck; } else { @@ -1372,6 +1370,11 @@ protected void removeCacheItem(Object ck, CollectionPersister persister) { } } + @Override + public void registerProcess(AfterTransactionCompletionProcess process) { + afterCompletions.add( process ); + } + @Override public void lock(String entityName, Object child, LockOptions lockOptions) { final EntityPersister persister = getEntityPersister( entityName, child ); diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderHelper.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderHelper.java index 19c10db2c535..c949b10c78c1 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderHelper.java @@ -21,6 +21,7 @@ import org.hibernate.event.monitor.spi.EventMonitor; import org.hibernate.event.spi.EventSource; import org.hibernate.event.monitor.spi.DiagnosticEvent; +import org.hibernate.internal.OptimisticLockHelper; import org.hibernate.internal.build.AllowReflection; import org.hibernate.loader.LoaderLogging; import org.hibernate.metamodel.mapping.BasicValuedModelPart; @@ -105,9 +106,7 @@ public static void upgradeLock( if ( persister.isVersioned() && requestedLockMode == LockMode.PESSIMISTIC_FORCE_INCREMENT ) { // todo : should we check the current isolation mode explicitly? - final Object nextVersion = - persister.forceVersionIncrement( entry.getId(), entry.getVersion(), false, session ); - entry.forceLocked( object, nextVersion ); + OptimisticLockHelper.forceVersionIncrement( object, entry, session ); } else if ( entry.isExistsInDatabase() ) { final EventMonitor eventMonitor = session.getEventMonitor(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/cache/ForceIncrementCacheTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/cache/ForceIncrementCacheTest.java index 82bd09edf434..c5aa5240e1da 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/cache/ForceIncrementCacheTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/cache/ForceIncrementCacheTest.java @@ -1,5 +1,5 @@ /* - * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.orm.test.cache;