diff --git a/model/map-hot-rod/pom.xml b/model/map-hot-rod/pom.xml
index a1b1b25faf..d4c1156431 100644
--- a/model/map-hot-rod/pom.xml
+++ b/model/map-hot-rod/pom.xml
@@ -62,6 +62,10 @@
${project.version}
true
+
+ jakarta.persistence
+ jakarta.persistence-api
+
diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java
index df7ee9e0dd..d2a7cf1929 100644
--- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java
+++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java
@@ -17,6 +17,7 @@
package org.keycloak.models.map.storage.hotRod;
+import org.infinispan.client.hotrod.MetadataValue;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.Search;
import org.infinispan.commons.util.CloseableIterator;
@@ -26,6 +27,7 @@ import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.models.AbstractKeycloakTransaction;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.UserSessionModel;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.common.ExpirableEntity;
@@ -50,7 +52,9 @@ import org.keycloak.models.map.storage.hotRod.transaction.NoActionHotRodTransact
import org.keycloak.storage.SearchableModelField;
import org.keycloak.utils.LockObjectsForModification;
+import javax.persistence.OptimisticLockException;
import java.time.Duration;
+import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Spliterators;
@@ -79,6 +83,7 @@ public class HotRodMapStorage, MapModelCriteriaBuilder.UpdatePredicatesFunc> fieldPredicates;
private final Long lockTimeout;
private final RemoteCache locksCache;
+ private final Map entityVersionCache = new HashMap<>();
public HotRodMapStorage(KeycloakSession session, RemoteCache remoteCache, StringKeyConverter keyConverter, HotRodEntityDescriptor storedEntityDescriptor, DeepCloner cloner, AllAreasHotRodTransactionsWrapper txWrapper, Long lockTimeout) {
this.session = session;
@@ -146,11 +151,15 @@ public class HotRodMapStorage entityWithMetadata = remoteCache.getWithMetadata(k);
+ if (entityWithMetadata == null) return null;
+
+ // store entity version
+ LOG.tracef("Entity %s read in version %s", key, entityWithMetadata.getVersion(), getShortStackTrace());
+ entityVersionCache.put(k, entityWithMetadata.getVersion());
// Create delegate that implements Map*Entity
- return delegateProducer.apply(hotRodEntity);
+ return entityWithMetadata.getValue() != null ? delegateProducer.apply(entityWithMetadata.getValue()) : null;
}
@Override
@@ -160,23 +169,37 @@ public class HotRodMapStorage 0) {
- previousValue = remoteCache.replace(key, value.getHotRodEntity(), lifespan, TimeUnit.MILLISECONDS);
+ if (!remoteCache.replaceWithVersion(key, value.getHotRodEntity(), entityVersionCache.get(key), lifespan, TimeUnit.MILLISECONDS, -1, TimeUnit.MILLISECONDS)) {
+ throw new OptimisticLockException("Entity " + key + " with version " + entityVersionCache.get(key) + " already changed by a different transaction.");
+ }
} else {
+ if (!remoteCache.removeWithVersion(key, entityVersionCache.get(key))) {
+ throw new OptimisticLockException("Entity " + key + " with version " + entityVersionCache.get(key) + " already changed by a different transaction.");
+ }
LOG.warnf("Removing entity %s from storage due to negative/zero lifespan.", key);
- previousValue = remoteCache.remove(key);
}
- return previousValue == null ? null : delegateProducer.apply(previousValue);
+
+ return delegateProducer.apply(value.getHotRodEntity());
}
}
- E previousValue = remoteCache.replace(key, value.getHotRodEntity());
- return previousValue == null ? null : delegateProducer.apply(previousValue);
+ if (!remoteCache.replaceWithVersion(key, value.getHotRodEntity(), entityVersionCache.get(key))) {
+ throw new OptimisticLockException("Entity " + key + " with version " + entityVersionCache.get(key) + " already changed by a different transaction.");
+ }
+ return delegateProducer.apply(value.getHotRodEntity());
}
@Override
public boolean delete(String key) {
K k = keyConverter.fromStringSafe(key);
+
+ Long entityVersion = entityVersionCache.get(k);
+ if (entityVersion != null) {
+ if (!remoteCache.removeWithVersion(k, entityVersion)) {
+ throw new OptimisticLockException("Entity " + key + " with version " + entityVersion + " already changed by a different transaction.");
+ }
+ return true;
+ }
return remoteCache.remove(k) != null;
}
@@ -190,20 +213,6 @@ public class HotRodMapStorage read(QueryParameters queryParameters) {
- if (LockObjectsForModification.isEnabled(session, storedEntityDescriptor.getModelTypeClass())) {
- return pessimisticQueryRead(queryParameters);
- }
-
- Query query = prepareQueryWithPrefixAndParameters(null, queryParameters);
- CloseableIterator iterator = paginateQuery(query, queryParameters.getOffset(),
- queryParameters.getLimit()).iterator();
- return closing(StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false))
- .onClose(iterator::close)
- .filter(Objects::nonNull) // see https://github.com/keycloak/keycloak/issues/9271
- .map(this.delegateProducer);
- }
-
- private Stream pessimisticQueryRead(QueryParameters queryParameters) {
DefaultModelCriteria dmc = queryParameters.getModelCriteriaBuilder();
// Optimization if the criteria contains only one id
@@ -216,17 +225,33 @@ public class HotRodMapStorage read without optimistic locking.
+ // See https://issues.redhat.com/browse/ISPN-14537
+ if (!dmc.isEmpty() && dmc.partiallyEvaluate((field, op, arg) -> field == UserSessionModel.SearchableFields.CLIENT_ID).toString().contains("__TRUE__")) {
+ Query query = prepareQueryWithPrefixAndParameters(null, queryParameters);
+ CloseableIterator iterator = paginateQuery(query, queryParameters.getOffset(),
+ queryParameters.getLimit()).iterator();
+ return closing(StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false))
+ .onClose(iterator::close)
+ .filter(Objects::nonNull) // see https://github.com/keycloak/keycloak/issues/9271
+ .map(this.delegateProducer);
+ }
+
+ // Criteria does not contain only one id, we need to read ids without locking and then read entities one by one pessimistically or optimistically
Query