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;