From 6fa62e47dbd65ef583fec1c9411611b4b162a3d4 Mon Sep 17 00:00:00 2001 From: Michal Hajas Date: Wed, 1 Feb 2023 16:24:07 +0100 Subject: [PATCH] Leverage HotRod client provided transaction Closes #13280 --- .../map/storage/hotRod/HotRodMapStorage.java | 17 +-- .../hotRod/HotRodMapStorageProvider.java | 33 +++++- .../HotRodMapStorageProviderFactory.java | 56 +++++++--- .../SingleUseObjectHotRodMapStorage.java | 14 +-- ...efaultHotRodConnectionProviderFactory.java | 13 ++- .../AllAreasHotRodTransactionsWrapper.java | 59 ++++++++++ .../HotRodRemoteTransactionWrapper.java | 105 ++++++++++++++++++ .../HotRodTransactionManagerLookup.java | 51 +++++++++ .../NoActionHotRodTransactionWrapper.java | 101 +++++++++++++++++ .../config/admin-events-cache-config.xml | 2 + .../config/auth-events-cache-config.xml | 2 + .../config/auth-sessions-cache-config.xml | 4 +- .../resources/config/authz-cache-config.xml | 2 + .../config/client-scopes-cache-config.xml | 4 +- .../resources/config/clients-cache-config.xml | 4 +- .../resources/config/groups-cache-config.xml | 4 +- .../resources/config/locks-cache-config.xml | 2 + .../resources/config/realms-cache-config.xml | 4 +- .../resources/config/roles-cache-config.xml | 4 +- .../single-use-objects-cache-config.xml | 4 +- .../user-login-failures-cache-config.xml | 4 +- .../config/user-sessions-cache-config.xml | 4 +- .../resources/config/users-cache-config.xml | 4 +- .../jpa/JpaMapStorageProviderFactory.java | 7 ++ .../java/org/keycloak/models/RealmSpi.java | 4 +- .../testsuite/admin/ComponentsTest.java | 28 +++++ .../concurrency/ConcurrentLoginTest.java | 8 +- .../SingleUseObjectModelTest.java | 11 +- 28 files changed, 508 insertions(+), 47 deletions(-) create mode 100644 model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/AllAreasHotRodTransactionsWrapper.java create mode 100644 model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/HotRodRemoteTransactionWrapper.java create mode 100644 model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/HotRodTransactionManagerLookup.java create mode 100644 model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/NoActionHotRodTransactionWrapper.java 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 85daded7b3..d9253b6560 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 @@ -40,6 +40,8 @@ import org.keycloak.models.map.storage.chm.ConcurrentHashMapCrudOperations; import org.keycloak.models.map.storage.chm.ConcurrentHashMapKeycloakTransaction; import org.keycloak.models.map.storage.chm.MapFieldPredicates; import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder; +import org.keycloak.models.map.storage.hotRod.transaction.NoActionHotRodTransactionWrapper; +import org.keycloak.models.map.storage.hotRod.transaction.AllAreasHotRodTransactionsWrapper; import org.keycloak.storage.SearchableModelField; import java.util.Map; @@ -64,14 +66,16 @@ public class HotRodMapStorage delegateProducer; protected final DeepCloner cloner; protected boolean isExpirableEntity; + private final AllAreasHotRodTransactionsWrapper txWrapper; - public HotRodMapStorage(RemoteCache remoteCache, StringKeyConverter keyConverter, HotRodEntityDescriptor storedEntityDescriptor, DeepCloner cloner) { + public HotRodMapStorage(RemoteCache remoteCache, StringKeyConverter keyConverter, HotRodEntityDescriptor storedEntityDescriptor, DeepCloner cloner, AllAreasHotRodTransactionsWrapper txWrapper) { this.remoteCache = remoteCache; this.keyConverter = keyConverter; this.storedEntityDescriptor = storedEntityDescriptor; this.cloner = cloner; this.delegateProducer = storedEntityDescriptor.getHotRodDelegateProvider(); this.isExpirableEntity = ExpirableEntity.class.isAssignableFrom(ModelEntityUtil.getEntityType(storedEntityDescriptor.getModelTypeClass())); + this.txWrapper = txWrapper; } @Override @@ -228,13 +232,10 @@ public class HotRodMapStorage createTransaction(KeycloakSession session) { - MapKeycloakTransaction sessionTransaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class); - - if (sessionTransaction == null) { - sessionTransaction = createTransactionInternal(session); - session.setAttribute("map-transaction-" + hashCode(), sessionTransaction); - } - return sessionTransaction; + // Here we return transaction that has no action because the returned transaction is enlisted to different + // phase than we need. Instead of tx returned by this method txWrapper is enlisted and executes all changes + // performed by the returned transaction. + return new NoActionHotRodTransactionWrapper<>((ConcurrentHashMapKeycloakTransaction) txWrapper.getOrCreateTxForModel(storedEntityDescriptor.getModelTypeClass(), () -> createTransactionInternal(session))); } protected MapKeycloakTransaction createTransactionInternal(KeycloakSession session) { diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProvider.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProvider.java index 42e2b6f737..cb101fede1 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProvider.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProvider.java @@ -17,25 +17,54 @@ package org.keycloak.models.map.storage.hotRod; +import org.infinispan.client.hotrod.RemoteCache; import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.MapStorageProvider; import org.keycloak.models.map.storage.MapStorageProviderFactory; +import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDescriptor; +import org.keycloak.models.map.storage.hotRod.connections.HotRodConnectionProvider; +import org.keycloak.models.map.storage.hotRod.transaction.HotRodRemoteTransactionWrapper; +import org.keycloak.models.map.storage.hotRod.transaction.AllAreasHotRodTransactionsWrapper; public class HotRodMapStorageProvider implements MapStorageProvider { private final KeycloakSession session; private final HotRodMapStorageProviderFactory factory; + private final String hotRodConfigurationIdentifier; + private final boolean jtaEnabled; - public HotRodMapStorageProvider(KeycloakSession session, HotRodMapStorageProviderFactory factory) { + public HotRodMapStorageProvider(KeycloakSession session, HotRodMapStorageProviderFactory factory, String hotRodConfigurationIdentifier, boolean jtaEnabled) { this.session = session; this.factory = factory; + this.hotRodConfigurationIdentifier = hotRodConfigurationIdentifier; + this.jtaEnabled = jtaEnabled; } @Override public MapStorage getStorage(Class modelType, MapStorageProviderFactory.Flag... flags) { - return (MapStorage) factory.getHotRodStorage(session, modelType, flags); + // Check if HotRod transaction was already initialized for this configuration within this session + AllAreasHotRodTransactionsWrapper txWrapper = session.getAttribute(this.hotRodConfigurationIdentifier, AllAreasHotRodTransactionsWrapper.class); + if (txWrapper == null) { + // If not create new AllAreasHotRodTransactionsWrapper and put it into session, so it is created only once + txWrapper = new AllAreasHotRodTransactionsWrapper(); + session.setAttribute(this.hotRodConfigurationIdentifier, txWrapper); + + // Enlist the wrapper into prepare phase so it is executed before HotRod client provided transaction + session.getTransactionManager().enlistPrepare(txWrapper); + + if (!jtaEnabled) { + // If there is no JTA transaction enabled control HotRod client provided transaction manually using + // HotRodRemoteTransactionWrapper + HotRodConnectionProvider connectionProvider = session.getProvider(HotRodConnectionProvider.class); + HotRodEntityDescriptor entityDescriptor = factory.getEntityDescriptor(modelType); + RemoteCache remoteCache = connectionProvider.getRemoteCache(entityDescriptor.getCacheName()); + session.getTransactionManager().enlist(new HotRodRemoteTransactionWrapper(remoteCache.getTransactionManager())); + } + } + + return (MapStorage) factory.getHotRodStorage(session, modelType, txWrapper, flags); } @Override diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java index 34d232fc5c..26604a36e2 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java @@ -94,7 +94,9 @@ import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodOTPPolicyEntity import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodRequiredActionProviderEntityDelegate; import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodRequiredCredentialEntityDelegate; import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodWebAuthnPolicyEntityDelegate; +import org.keycloak.models.map.storage.hotRod.singleUseObject.HotRodSingleUseObjectEntity; import org.keycloak.models.map.storage.hotRod.singleUseObject.HotRodSingleUseObjectEntityDelegate; +import org.keycloak.models.map.storage.hotRod.transaction.AllAreasHotRodTransactionsWrapper; import org.keycloak.models.map.storage.hotRod.user.HotRodUserConsentEntityDelegate; import org.keycloak.models.map.storage.hotRod.user.HotRodUserCredentialEntityDelegate; import org.keycloak.models.map.storage.hotRod.user.HotRodUserEntityDelegate; @@ -111,14 +113,20 @@ import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity; import org.keycloak.models.map.userSession.MapUserSessionEntity; import org.keycloak.provider.EnvironmentDependentProviderFactory; import org.keycloak.storage.SearchableModelField; +import org.keycloak.transaction.JtaTransactionManagerLookup; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory, MapStorageProviderFactory, EnvironmentDependentProviderFactory { public static final String PROVIDER_ID = "hotrod"; - private final Map, HotRodMapStorage> storages = new ConcurrentHashMap<>(); + private static final String SESSION_TX_PREFIX = "hotrod-map-tx-"; + private static final AtomicInteger ENUMERATOR = new AtomicInteger(0); + private final String sessionProviderKey; + private final String hotRodConfigurationIdentifier; + + private boolean jtaEnabled; private static final Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> clientSessionPredicates = MapFieldPredicates.basePredicates(HotRodAuthenticatedClientSessionEntity.ID); @@ -171,44 +179,65 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory .build(); + public HotRodMapStorageProviderFactory() { + int index = ENUMERATOR.getAndIncrement(); + // this identifier is used to create HotRodMapProvider only once per session per factory instance + this.sessionProviderKey = HotRodMapStorageProviderFactory.class.getName() + "-" + PROVIDER_ID + "-" + index; + + // When there are more HotRod configurations available in Keycloak (for example, global/realm1/realm2 etc.) + // there will be more instances of this factory created where each holds one configuration. + // The following identifier can be used to uniquely identify instance of this factory. + // This can be later used, for example, to store provider/transaction instances inside session + // attributes without collisions between several configurations + this.hotRodConfigurationIdentifier = SESSION_TX_PREFIX + index; + } + @Override public MapStorageProvider create(KeycloakSession session) { - return new HotRodMapStorageProvider(session, this); + HotRodMapStorageProvider provider = session.getAttribute(this.sessionProviderKey, HotRodMapStorageProvider.class); + if (provider == null) { + provider = new HotRodMapStorageProvider(session, this, this.hotRodConfigurationIdentifier, this.jtaEnabled); + session.setAttribute(this.sessionProviderKey, provider); + } + return provider; } public HotRodEntityDescriptor getEntityDescriptor(Class c) { return AutogeneratedHotRodDescriptors.ENTITY_DESCRIPTOR_MAP.get(c); } - public & AbstractEntity, M> HotRodMapStorage getHotRodStorage(KeycloakSession session, Class modelType, MapStorageProviderFactory.Flag... flags) { + public & AbstractEntity, M> HotRodMapStorage getHotRodStorage(KeycloakSession session, Class modelType, AllAreasHotRodTransactionsWrapper txWrapper, MapStorageProviderFactory.Flag... flags) { // We need to preload client session store before we load user session store to avoid recursive update of storages map - if (modelType == UserSessionModel.class) getHotRodStorage(session, AuthenticatedClientSessionModel.class, flags); + if (modelType == UserSessionModel.class) getHotRodStorage(session, AuthenticatedClientSessionModel.class, txWrapper, flags); - return storages.computeIfAbsent(modelType, c -> createHotRodStorage(session, modelType, flags)); + return createHotRodStorage(session, modelType, txWrapper, flags); } - private & AbstractEntity, M> HotRodMapStorage createHotRodStorage(KeycloakSession session, Class modelType, MapStorageProviderFactory.Flag... flags) { + private & AbstractEntity, M> HotRodMapStorage createHotRodStorage(KeycloakSession session, Class modelType, AllAreasHotRodTransactionsWrapper txWrapper, MapStorageProviderFactory.Flag... flags) { HotRodConnectionProvider connectionProvider = session.getProvider(HotRodConnectionProvider.class); HotRodEntityDescriptor entityDescriptor = (HotRodEntityDescriptor) getEntityDescriptor(modelType); if (modelType == SingleUseObjectValueModel.class) { - return new SingleUseObjectHotRodMapStorage(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConverter.StringKey.INSTANCE, entityDescriptor, CLONER); + return (HotRodMapStorage) new SingleUseObjectHotRodMapStorage(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConverter.StringKey.INSTANCE, (HotRodEntityDescriptor) getEntityDescriptor(modelType), CLONER, txWrapper); } if (modelType == AuthenticatedClientSessionModel.class) { return new HotRodMapStorage(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConverter.StringKey.INSTANCE, entityDescriptor, - CLONER) { + CLONER, txWrapper) { @Override protected MapKeycloakTransaction createTransactionInternal(KeycloakSession session) { return new ConcurrentHashMapKeycloakTransaction(this, keyConverter, cloner, clientSessionPredicates); } }; } if (modelType == UserSessionModel.class) { - HotRodMapStorage clientSessionStore = getHotRodStorage(session, AuthenticatedClientSessionModel.class); + // Precompute client session storage to avoid recursive initialization + HotRodMapStorage clientSessionStore = getHotRodStorage(session, AuthenticatedClientSessionModel.class, txWrapper); + clientSessionStore.createTransaction(session); + return new HotRodMapStorage(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConverter.StringKey.INSTANCE, entityDescriptor, - CLONER) { + CLONER, txWrapper) { @Override protected MapKeycloakTransaction createTransactionInternal(KeycloakSession session) { Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> fieldPredicates = MapFieldPredicates.getPredicates((Class) storedEntityDescriptor.getModelTypeClass()); @@ -216,7 +245,7 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory } }; } - return new HotRodMapStorage<>(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConverter.StringKey.INSTANCE, entityDescriptor, CLONER); + return new HotRodMapStorage<>(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConverter.StringKey.INSTANCE, entityDescriptor, CLONER, txWrapper); } @Override @@ -226,7 +255,8 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory @Override public void postInit(KeycloakSessionFactory factory) { - + JtaTransactionManagerLookup jtaLookup = (JtaTransactionManagerLookup) factory.getProviderFactory(JtaTransactionManagerLookup.class); + jtaEnabled = jtaLookup != null && jtaLookup.getTransactionManager() != null; } @Override diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/SingleUseObjectHotRodMapStorage.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/SingleUseObjectHotRodMapStorage.java index 9cb2f80f6d..98ce842a83 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/SingleUseObjectHotRodMapStorage.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/SingleUseObjectHotRodMapStorage.java @@ -20,21 +20,19 @@ package org.keycloak.models.map.storage.hotRod; import org.infinispan.client.hotrod.RemoteCache; import org.keycloak.models.SingleUseObjectValueModel; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.StringKeyConverter; +import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder; import org.keycloak.models.map.storage.chm.SingleUseObjectKeycloakTransaction; -import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.QueryParameters; import org.keycloak.models.map.storage.chm.MapFieldPredicates; import org.keycloak.models.map.storage.chm.SingleUseObjectModelCriteriaBuilder; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; -import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity; -import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDelegate; import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDescriptor; import org.keycloak.models.map.storage.hotRod.singleUseObject.HotRodSingleUseObjectEntity; import org.keycloak.models.map.storage.hotRod.singleUseObject.HotRodSingleUseObjectEntityDelegate; +import org.keycloak.models.map.storage.hotRod.transaction.AllAreasHotRodTransactionsWrapper; import org.keycloak.storage.SearchableModelField; import java.util.Map; @@ -44,7 +42,7 @@ import java.util.stream.Stream; /** * @author Martin Kanis */ -public class SingleUseObjectHotRodMapStorage & AbstractEntity, M> +public class SingleUseObjectHotRodMapStorage extends HotRodMapStorage { private final StringKeyConverter keyConverter; @@ -53,8 +51,8 @@ public class SingleUseObjectHotRodMapStorage remoteCache, StringKeyConverter keyConverter, HotRodEntityDescriptor storedEntityDescriptor, - DeepCloner cloner) { - super(remoteCache, keyConverter, storedEntityDescriptor, cloner); + DeepCloner cloner, AllAreasHotRodTransactionsWrapper txWrapper) { + super(remoteCache, keyConverter, storedEntityDescriptor, cloner, txWrapper); this.keyConverter = keyConverter; this.storedEntityDescriptor = storedEntityDescriptor; this.cloner = cloner; @@ -62,7 +60,7 @@ public class SingleUseObjectHotRodMapStorage createTransactionInternal(KeycloakSession session) { - Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> fieldPredicates = + Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> fieldPredicates = MapFieldPredicates.getPredicates((Class) storedEntityDescriptor.getModelTypeClass()); return new SingleUseObjectKeycloakTransaction(this, keyConverter, cloner, fieldPredicates); } diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/connections/DefaultHotRodConnectionProviderFactory.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/connections/DefaultHotRodConnectionProviderFactory.java index 70ff5d0657..b16d9079df 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/connections/DefaultHotRodConnectionProviderFactory.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/connections/DefaultHotRodConnectionProviderFactory.java @@ -22,7 +22,9 @@ import org.infinispan.client.hotrod.RemoteCacheManagerAdmin; import org.infinispan.client.hotrod.configuration.ClientIntelligence; import org.infinispan.client.hotrod.configuration.ConfigurationBuilder; import org.infinispan.client.hotrod.configuration.NearCacheMode; +import org.infinispan.client.hotrod.configuration.TransactionMode; import org.infinispan.commons.marshall.ProtoStreamMarshaller; +import org.infinispan.commons.tx.lookup.TransactionManagerLookup; import org.infinispan.protostream.GeneratedSchema; import org.infinispan.query.remote.client.ProtobufMetadataManagerConstants; import org.jboss.logging.Logger; @@ -33,6 +35,7 @@ import org.keycloak.models.map.storage.hotRod.locking.HotRodLocksUtils; import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDescriptor; import org.keycloak.models.map.storage.hotRod.common.CommonPrimitivesProtoSchemaInitializer; import org.keycloak.models.map.storage.hotRod.common.HotRodVersionUtils; +import org.keycloak.models.map.storage.hotRod.transaction.HotRodTransactionManagerLookup; import org.keycloak.provider.EnvironmentDependentProviderFactory; import java.net.URI; @@ -63,12 +66,14 @@ public class DefaultHotRodConnectionProviderFactory implements HotRodConnectionP private volatile RemoteCacheManager remoteCacheManager; + private TransactionManagerLookup transactionManagerLookup; + @Override public HotRodConnectionProvider create(KeycloakSession session) { if (remoteCacheManager == null) { synchronized (this) { if (remoteCacheManager == null) { - lazyInit(); + lazyInit(session); } } } @@ -98,8 +103,10 @@ public class DefaultHotRodConnectionProviderFactory implements HotRodConnectionP this.config = config; } - public void lazyInit() { + public void lazyInit(KeycloakSession session) { LOG.debugf("Initializing HotRod client connection to Infinispan server."); + transactionManagerLookup = new HotRodTransactionManagerLookup(session); + ConfigurationBuilder remoteBuilder = new ConfigurationBuilder(); remoteBuilder.addServer() .host(config.get("host", "localhost")) @@ -265,6 +272,8 @@ public class DefaultHotRodConnectionProviderFactory implements HotRodConnectionP LOG.debugf("Configuring cache %s", cacheName); builder.remoteCache(cacheName) .configurationURI(getCacheConfigUri(cacheName)) + .transactionMode(TransactionMode.FULL_XA) + .transactionManagerLookup(transactionManagerLookup) .nearCacheMode(config.scope(cacheName).getBoolean("nearCacheEnabled", config.getBoolean("nearCacheEnabled", true)) ? NearCacheMode.INVALIDATED : NearCacheMode.DISABLED) .nearCacheMaxEntries(config.scope(cacheName).getInt("nearCacheMaxEntries", config.getInt("nearCacheMaxEntries", 10000))) .nearCacheUseBloomFilter(config.scope(cacheName).getBoolean("nearCacheBloomFilter", config.getBoolean("nearCacheBloomFilter", false))); diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/AllAreasHotRodTransactionsWrapper.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/AllAreasHotRodTransactionsWrapper.java new file mode 100644 index 0000000000..20b3ff6344 --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/AllAreasHotRodTransactionsWrapper.java @@ -0,0 +1,59 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.map.storage.hotRod.transaction; + +import org.keycloak.models.AbstractKeycloakTransaction; +import org.keycloak.models.map.storage.MapKeycloakTransaction; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +/** + * This wrapper encapsulates transactions from all areas. This is needed because we need to control when the changes + * from each area are applied to make sure it is performed before the HotRod client provided transaction is committed. + */ +public class AllAreasHotRodTransactionsWrapper extends AbstractKeycloakTransaction { + + private final Map, MapKeycloakTransaction> MapKeycloakTransactionsMap = new ConcurrentHashMap<>(); + + public MapKeycloakTransaction getOrCreateTxForModel(Class modelType, Supplier> supplier) { + MapKeycloakTransaction tx = MapKeycloakTransactionsMap.computeIfAbsent(modelType, t -> supplier.get()); + if (!tx.isActive()) { + tx.begin(); + } + + return tx; + } + + @Override + protected void commitImpl() { + MapKeycloakTransactionsMap.values().forEach(MapKeycloakTransaction::commit); + } + + @Override + protected void rollbackImpl() { + MapKeycloakTransactionsMap.values().forEach(MapKeycloakTransaction::rollback); + } + + @Override + public void setRollbackOnly() { + super.setRollbackOnly(); + MapKeycloakTransactionsMap.values().forEach(MapKeycloakTransaction::setRollbackOnly); + } +} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/HotRodRemoteTransactionWrapper.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/HotRodRemoteTransactionWrapper.java new file mode 100644 index 0000000000..d7c0c8a4e9 --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/HotRodRemoteTransactionWrapper.java @@ -0,0 +1,105 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.map.storage.hotRod.transaction; + +import org.keycloak.models.KeycloakTransaction; + +import javax.transaction.HeuristicMixedException; +import javax.transaction.HeuristicRollbackException; +import javax.transaction.NotSupportedException; +import javax.transaction.RollbackException; +import javax.transaction.Status; +import javax.transaction.SystemException; +import javax.transaction.TransactionManager; + +/** + * When no JTA transaction is present in the runtime this wrapper is used + * to enlist HotRod client provided transaction to our + * {@link KeycloakTransactionManager}. If JTA transaction is present this should + * not be used. + */ +public class HotRodRemoteTransactionWrapper implements KeycloakTransaction { + + private final TransactionManager transactionManager; + + public HotRodRemoteTransactionWrapper(TransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + @Override + public void begin() { + try { + if (transactionManager.getStatus() == Status.STATUS_NO_TRANSACTION) { + transactionManager.begin(); + } + } catch (NotSupportedException | SystemException e) { + throw new RuntimeException(e); + } + } + + @Override + public void commit() { + try { + if (transactionManager.getStatus() == Status.STATUS_ACTIVE) { + transactionManager.commit(); + } + } catch (HeuristicRollbackException | SystemException | HeuristicMixedException | RollbackException e) { + throw new RuntimeException(e); + } + } + + @Override + public void rollback() { + try { + if (transactionManager.getStatus() == Status.STATUS_ACTIVE) { + transactionManager.rollback(); + } + } catch (SystemException e) { + throw new RuntimeException(e); + } + } + + @Override + public void setRollbackOnly() { + try { + if (transactionManager.getStatus() == Status.STATUS_ACTIVE) { + transactionManager.setRollbackOnly(); + } + } catch (SystemException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean getRollbackOnly() { + try { + return transactionManager.getStatus() == Status.STATUS_MARKED_ROLLBACK; + } catch (SystemException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean isActive() { + try { + return transactionManager.getStatus() == Status.STATUS_ACTIVE; + } catch (SystemException e) { + throw new RuntimeException(e); + } + } +} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/HotRodTransactionManagerLookup.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/HotRodTransactionManagerLookup.java new file mode 100644 index 0000000000..e51d83582c --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/HotRodTransactionManagerLookup.java @@ -0,0 +1,51 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.map.storage.hotRod.transaction; + +import org.infinispan.client.hotrod.transaction.manager.RemoteTransactionManager; +import org.infinispan.commons.tx.lookup.TransactionManagerLookup; +import org.keycloak.models.KeycloakSession; +import org.keycloak.transaction.JtaTransactionManagerLookup; + +import javax.transaction.TransactionManager; + +/** + * HotRod client provides its own {@link org.infinispan.client.hotrod.transaction.lookup.GenericTransactionManagerLookup} + * that is able to locate variety of JTA transaction implementation present + * in the runtime. We need to make sure we use JTA only when it is detected + * by other parts of Keycloak (such as {@link org.keycloak.models.KeycloakTransactionManager}), + * therefore we implemented this custom TransactionManagerLookup that locates + * JTA transaction using {@link JtaTransactionManagerLookup} provider + * + */ +public class HotRodTransactionManagerLookup implements TransactionManagerLookup { + + private final TransactionManager transactionManager; + + public HotRodTransactionManagerLookup(KeycloakSession session) { + JtaTransactionManagerLookup jtaLookup = session.getProvider(JtaTransactionManagerLookup.class); + TransactionManager txManager = jtaLookup != null ? jtaLookup.getTransactionManager() : null; + transactionManager = txManager != null ? txManager : RemoteTransactionManager.getInstance(); + } + + @Override + public TransactionManager getTransactionManager() throws Exception { + return transactionManager; + } + +} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/NoActionHotRodTransactionWrapper.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/NoActionHotRodTransactionWrapper.java new file mode 100644 index 0000000000..3fe28dae8e --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/NoActionHotRodTransactionWrapper.java @@ -0,0 +1,101 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.map.storage.hotRod.transaction; + +import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.models.map.common.UpdatableEntity; +import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.QueryParameters; +import org.keycloak.models.map.storage.chm.ConcurrentHashMapKeycloakTransaction; + +import java.util.stream.Stream; + +/** + * This is used to return ConcurrentHashMapTransaction (used for operating + * RemoteCache) functionality to providers but not enlist actualTx the way + * we need: in prepare phase. + */ +public class NoActionHotRodTransactionWrapper implements MapKeycloakTransaction { + + + private final ConcurrentHashMapKeycloakTransaction actualTx; + + public NoActionHotRodTransactionWrapper(ConcurrentHashMapKeycloakTransaction actualTx) { + this.actualTx = actualTx; + } + + @Override + public V create(V value) { + return actualTx.create(value); + } + + @Override + public V read(String key) { + return actualTx.read(key); + } + + @Override + public Stream read(QueryParameters queryParameters) { + return actualTx.read(queryParameters); + } + + @Override + public long getCount(QueryParameters queryParameters) { + return actualTx.getCount(queryParameters); + } + + @Override + public boolean delete(String key) { + return actualTx.delete(key); + } + + @Override + public long delete(QueryParameters queryParameters) { + return actualTx.delete(queryParameters); + } + + @Override + public void begin() { + // Does nothing + } + + @Override + public void commit() { + // Does nothing + } + + @Override + public void rollback() { + // Does nothing + } + + @Override + public void setRollbackOnly() { + actualTx.setRollbackOnly(); + } + + @Override + public boolean getRollbackOnly() { + return actualTx.getRollbackOnly(); + } + + @Override + public boolean isActive() { + return actualTx.isActive(); + } +} diff --git a/model/map-hot-rod/src/main/resources/config/admin-events-cache-config.xml b/model/map-hot-rod/src/main/resources/config/admin-events-cache-config.xml index 381157a422..67e8d5f95c 100644 --- a/model/map-hot-rod/src/main/resources/config/admin-events-cache-config.xml +++ b/model/map-hot-rod/src/main/resources/config/admin-events-cache-config.xml @@ -16,6 +16,8 @@ --> + + kc.HotRodAdminEventEntity diff --git a/model/map-hot-rod/src/main/resources/config/auth-events-cache-config.xml b/model/map-hot-rod/src/main/resources/config/auth-events-cache-config.xml index 07ac54ecc7..575b4001ac 100644 --- a/model/map-hot-rod/src/main/resources/config/auth-events-cache-config.xml +++ b/model/map-hot-rod/src/main/resources/config/auth-events-cache-config.xml @@ -16,6 +16,8 @@ --> + + kc.HotRodAuthEventEntity diff --git a/model/map-hot-rod/src/main/resources/config/auth-sessions-cache-config.xml b/model/map-hot-rod/src/main/resources/config/auth-sessions-cache-config.xml index ab111cf737..0cc8a1bcd5 100644 --- a/model/map-hot-rod/src/main/resources/config/auth-sessions-cache-config.xml +++ b/model/map-hot-rod/src/main/resources/config/auth-sessions-cache-config.xml @@ -15,10 +15,12 @@ ~ limitations under the License. --> + + + kc.HotRodRootAuthenticationSessionEntity - diff --git a/model/map-hot-rod/src/main/resources/config/authz-cache-config.xml b/model/map-hot-rod/src/main/resources/config/authz-cache-config.xml index 41b721d6d2..21280da555 100644 --- a/model/map-hot-rod/src/main/resources/config/authz-cache-config.xml +++ b/model/map-hot-rod/src/main/resources/config/authz-cache-config.xml @@ -16,6 +16,8 @@ --> + + kc.HotRodResourceServerEntity diff --git a/model/map-hot-rod/src/main/resources/config/client-scopes-cache-config.xml b/model/map-hot-rod/src/main/resources/config/client-scopes-cache-config.xml index f42f3734a7..8e1868f340 100644 --- a/model/map-hot-rod/src/main/resources/config/client-scopes-cache-config.xml +++ b/model/map-hot-rod/src/main/resources/config/client-scopes-cache-config.xml @@ -15,10 +15,12 @@ ~ limitations under the License. --> + + + kc.HotRodClientScopeEntity - diff --git a/model/map-hot-rod/src/main/resources/config/clients-cache-config.xml b/model/map-hot-rod/src/main/resources/config/clients-cache-config.xml index 5b1ccd4795..435c828551 100644 --- a/model/map-hot-rod/src/main/resources/config/clients-cache-config.xml +++ b/model/map-hot-rod/src/main/resources/config/clients-cache-config.xml @@ -15,10 +15,12 @@ ~ limitations under the License. --> + + + kc.HotRodClientEntity - diff --git a/model/map-hot-rod/src/main/resources/config/groups-cache-config.xml b/model/map-hot-rod/src/main/resources/config/groups-cache-config.xml index ef55b626d3..2833945deb 100644 --- a/model/map-hot-rod/src/main/resources/config/groups-cache-config.xml +++ b/model/map-hot-rod/src/main/resources/config/groups-cache-config.xml @@ -15,10 +15,12 @@ ~ limitations under the License. --> + + + kc.HotRodGroupEntity - diff --git a/model/map-hot-rod/src/main/resources/config/locks-cache-config.xml b/model/map-hot-rod/src/main/resources/config/locks-cache-config.xml index 786d83b238..4ee6c091cd 100644 --- a/model/map-hot-rod/src/main/resources/config/locks-cache-config.xml +++ b/model/map-hot-rod/src/main/resources/config/locks-cache-config.xml @@ -15,5 +15,7 @@ ~ limitations under the License. --> + + diff --git a/model/map-hot-rod/src/main/resources/config/realms-cache-config.xml b/model/map-hot-rod/src/main/resources/config/realms-cache-config.xml index 4bb88c7d32..5657701949 100644 --- a/model/map-hot-rod/src/main/resources/config/realms-cache-config.xml +++ b/model/map-hot-rod/src/main/resources/config/realms-cache-config.xml @@ -15,12 +15,14 @@ ~ limitations under the License. --> + + + kc.HotRodRealmEntity - diff --git a/model/map-hot-rod/src/main/resources/config/roles-cache-config.xml b/model/map-hot-rod/src/main/resources/config/roles-cache-config.xml index 62b20d4bcd..5851e7dbdd 100644 --- a/model/map-hot-rod/src/main/resources/config/roles-cache-config.xml +++ b/model/map-hot-rod/src/main/resources/config/roles-cache-config.xml @@ -15,11 +15,13 @@ ~ limitations under the License. --> + + + kc.HotRodRoleEntity - diff --git a/model/map-hot-rod/src/main/resources/config/single-use-objects-cache-config.xml b/model/map-hot-rod/src/main/resources/config/single-use-objects-cache-config.xml index 0b8fdc17b2..2358951d7b 100644 --- a/model/map-hot-rod/src/main/resources/config/single-use-objects-cache-config.xml +++ b/model/map-hot-rod/src/main/resources/config/single-use-objects-cache-config.xml @@ -15,12 +15,14 @@ ~ limitations under the License. --> + + + kc.HotRodSingleUseObjectEntity - diff --git a/model/map-hot-rod/src/main/resources/config/user-login-failures-cache-config.xml b/model/map-hot-rod/src/main/resources/config/user-login-failures-cache-config.xml index 0517ac64f2..ece065c8b6 100644 --- a/model/map-hot-rod/src/main/resources/config/user-login-failures-cache-config.xml +++ b/model/map-hot-rod/src/main/resources/config/user-login-failures-cache-config.xml @@ -15,11 +15,13 @@ ~ limitations under the License. --> + + + kc.HotRodUserLoginFailureEntity - diff --git a/model/map-hot-rod/src/main/resources/config/user-sessions-cache-config.xml b/model/map-hot-rod/src/main/resources/config/user-sessions-cache-config.xml index 8a512b2792..b6d24590bb 100644 --- a/model/map-hot-rod/src/main/resources/config/user-sessions-cache-config.xml +++ b/model/map-hot-rod/src/main/resources/config/user-sessions-cache-config.xml @@ -15,12 +15,14 @@ ~ limitations under the License. --> + + + kc.HotRodUserSessionEntity kc.HotRodAuthenticatedClientSessionEntity - diff --git a/model/map-hot-rod/src/main/resources/config/users-cache-config.xml b/model/map-hot-rod/src/main/resources/config/users-cache-config.xml index 956e0ac5f4..9cfe474c31 100644 --- a/model/map-hot-rod/src/main/resources/config/users-cache-config.xml +++ b/model/map-hot-rod/src/main/resources/config/users-cache-config.xml @@ -15,11 +15,13 @@ ~ limitations under the License. --> + + + kc.HotRodUserEntity - diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java index 039837f3fc..6b7b11b8f5 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java @@ -259,7 +259,14 @@ public class JpaMapStorageProviderFactory implements public JpaMapStorageProviderFactory() { int index = ENUMERATOR.getAndIncrement(); + // this identifier is used to create HotRodMapProvider only once per session per factory instance this.sessionProviderKey = PROVIDER_ID + "-" + index; + + // When there are more JPA configurations available in Keycloak (for example, global/realm1/realm2 etc.) + // there will be more instances of this factory created where each holds one configuration. + // The following identifier can be used to uniquely identify instance of this factory. + // This can be later used, for example, to store provider/transaction instances inside session + // attributes without collisions between several configurations this.sessionTxKey = SESSION_TX_PREFIX + index; } diff --git a/server-spi-private/src/main/java/org/keycloak/models/RealmSpi.java b/server-spi-private/src/main/java/org/keycloak/models/RealmSpi.java index fe04875684..620ce71ea3 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/RealmSpi.java +++ b/server-spi-private/src/main/java/org/keycloak/models/RealmSpi.java @@ -26,6 +26,8 @@ import org.keycloak.provider.Spi; */ public class RealmSpi implements Spi { + public static final String NAME = "realm"; + @Override public boolean isInternal() { return true; @@ -33,7 +35,7 @@ public class RealmSpi implements Spi { @Override public String getName() { - return "realm"; + return NAME; } @Override diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ComponentsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ComponentsTest.java index 351aed8add..a51b8b5204 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ComponentsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ComponentsTest.java @@ -17,12 +17,18 @@ package org.keycloak.testsuite.admin; +import org.junit.Assume; +import org.keycloak.Config; import org.keycloak.admin.client.resource.ComponentResource; import org.junit.Before; import org.junit.Test; import org.keycloak.admin.client.resource.ComponentsResource; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.models.RealmSpi; +import org.keycloak.models.map.common.AbstractMapProviderFactory; +import org.keycloak.models.map.realm.MapRealmProviderFactory; +import org.keycloak.models.map.storage.hotRod.HotRodMapStorageProviderFactory; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.idm.*; import org.keycloak.testsuite.components.TestProvider; @@ -39,6 +45,7 @@ import java.util.function.BiConsumer; import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.hamcrest.Matchers; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.*; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; @@ -50,8 +57,19 @@ public class ComponentsTest extends AbstractAdminTest { private ComponentsResource components; + private String realmProvider; + @Before public void before() throws Exception { + // realmProvider is used only to prevent tests from running in certain configs, should be removed once GHI #15410 is resolved. + realmProvider = testingClient.server().fetch(session -> Config.getProvider(RealmSpi.NAME), String.class); + if (realmProvider.equals(MapRealmProviderFactory.PROVIDER_ID)) { + // append the storage provider in case of map + String mapStorageProvider = testingClient.server().fetch(session -> Config.scope(RealmSpi.NAME, + MapRealmProviderFactory.PROVIDER_ID, AbstractMapProviderFactory.CONFIG_STORAGE).get("provider"), String.class); + if (mapStorageProvider != null) realmProvider = realmProvider + "-" + mapStorageProvider; + } + components = adminClient.realm(REALM_NAME).components(); } @@ -81,6 +99,11 @@ public class ComponentsTest extends AbstractAdminTest { @Test public void testConcurrencyWithoutChildren() throws InterruptedException { + // remove this restriction once GHI #15410 is resolved. + Assume.assumeThat("Test does not run with HotRod after HotRod client transaction was enabled. This will be removed with pessimistic locking introduction.", + realmProvider, + not(equalTo(MapRealmProviderFactory.PROVIDER_ID + "-" + HotRodMapStorageProviderFactory.PROVIDER_ID))); + testConcurrency((s, i) -> s.submit(new CreateAndDeleteComponent(s, i))); // Data consistency is not guaranteed with concurrent access to entities in map store. @@ -91,6 +114,11 @@ public class ComponentsTest extends AbstractAdminTest { @Test public void testConcurrencyWithChildren() throws InterruptedException { + // remove this restriction once GHI #15410 is resolved. + Assume.assumeThat("Test does not run with HotRod after HotRod client transaction was enabled. This will be removed with pessimistic locking introduction.", + realmProvider, + not(equalTo(MapRealmProviderFactory.PROVIDER_ID + "-" + HotRodMapStorageProviderFactory.PROVIDER_ID))); + testConcurrency((s, i) -> s.submit(new CreateAndDeleteComponentWithFlatChildren(s, i))); // Data consistency is not guaranteed with concurrent access to entities in map store. diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java index 6d5440a484..a1c9bcf47d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java @@ -53,7 +53,7 @@ import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.jose.jws.JWSInput; import org.keycloak.models.UserSessionSpi; import org.keycloak.models.map.common.AbstractMapProviderFactory; -import org.keycloak.models.map.storage.MapStorageProviderFactory; +import org.keycloak.models.map.storage.hotRod.HotRodMapStorageProviderFactory; import org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory; import org.keycloak.models.map.userSession.MapUserSessionProviderFactory; import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory; @@ -76,6 +76,7 @@ import org.apache.http.impl.client.BasicCookieStore; import org.hamcrest.Matchers; import org.keycloak.util.JsonSerialization; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_SSL_REQUIRED; @@ -173,6 +174,11 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest { @Test public void concurrentLoginSingleUserSingleClient() throws Throwable { + // remove this restriction once GHI #15410 is resolved. + Assume.assumeThat("Test does not run with HotRod after HotRod client transaction was enabled. This will be removed with pessimistic locking introduction.", + userSessionProvider, + not(equalTo(MapUserSessionProviderFactory.PROVIDER_ID + "-" + HotRodMapStorageProviderFactory.PROVIDER_ID))); + log.info("*********************************************"); long start = System.currentTimeMillis(); diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/singleUseObject/SingleUseObjectModelTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/singleUseObject/SingleUseObjectModelTest.java index cfa16ec9da..59eaaea177 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/singleUseObject/SingleUseObjectModelTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/singleUseObject/SingleUseObjectModelTest.java @@ -104,9 +104,13 @@ public class SingleUseObjectModelTest extends KeycloakModelTest { Map notes = singleUseObjectProvider.get(key.serializeKey()); Assert.assertNotNull(notes); Assert.assertEquals("bar", notes.get("foo")); + }); - setTimeOffset(70); + setTimeOffset(70); + inComittedTransaction(session -> { + SingleUseObjectProvider singleUseObjectProvider = session.getProvider(SingleUseObjectProvider.class); + Map notes = singleUseObjectProvider.get(key.serializeKey()); notes = singleUseObjectProvider.get(key.serializeKey()); Assert.assertNull(notes); }); @@ -155,9 +159,12 @@ public class SingleUseObjectModelTest extends KeycloakModelTest { SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class); Map actualNotes = singleUseStore.get(key); assertThat(actualNotes, Matchers.anEmptyMap()); + }); - setTimeOffset(70); + setTimeOffset(70); + inComittedTransaction(session -> { + SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class); Assert.assertNull(singleUseStore.get(key)); }); }