Leverage HotRod client provided transaction

Closes #13280
This commit is contained in:
Michal Hajas 2023-02-01 16:24:07 +01:00
parent d3ba2ecbed
commit 6fa62e47db
28 changed files with 508 additions and 47 deletions

View file

@ -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<K, E extends AbstractHotRodEntity, V extends Abstr
private final Function<E, V> delegateProducer;
protected final DeepCloner cloner;
protected boolean isExpirableEntity;
private final AllAreasHotRodTransactionsWrapper txWrapper;
public HotRodMapStorage(RemoteCache<K, E> remoteCache, StringKeyConverter<K> keyConverter, HotRodEntityDescriptor<E, V> storedEntityDescriptor, DeepCloner cloner) {
public HotRodMapStorage(RemoteCache<K, E> remoteCache, StringKeyConverter<K> keyConverter, HotRodEntityDescriptor<E, V> 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<K, E extends AbstractHotRodEntity, V extends Abstr
@Override
public MapKeycloakTransaction<V, M> createTransaction(KeycloakSession session) {
MapKeycloakTransaction<V, M> 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<K, V, M>) txWrapper.getOrCreateTxForModel(storedEntityDescriptor.getModelTypeClass(), () -> createTransactionInternal(session)));
}
protected MapKeycloakTransaction<V, M> createTransactionInternal(KeycloakSession session) {

View file

@ -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 <V extends AbstractEntity, M> MapStorage<V, M> getStorage(Class<M> modelType, MapStorageProviderFactory.Flag... flags) {
return (MapStorage<V, M>) 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<Object, Object> remoteCache = connectionProvider.getRemoteCache(entityDescriptor.getCacheName());
session.getTransactionManager().enlist(new HotRodRemoteTransactionWrapper(remoteCache.getTransactionManager()));
}
}
return (MapStorage<V, M>) factory.getHotRodStorage(session, modelType, txWrapper, flags);
}
@Override

View file

@ -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<MapStorageProvider>, MapStorageProviderFactory, EnvironmentDependentProviderFactory {
public static final String PROVIDER_ID = "hotrod";
private final Map<Class<?>, 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<SearchableModelField<AuthenticatedClientSessionModel>, MapModelCriteriaBuilder.UpdatePredicatesFunc<Object, AbstractEntity, AuthenticatedClientSessionModel>> 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 <E extends AbstractHotRodEntity, V extends HotRodEntityDelegate<E> & AbstractEntity, M> HotRodMapStorage<String, E, V, M> getHotRodStorage(KeycloakSession session, Class<M> modelType, MapStorageProviderFactory.Flag... flags) {
public <E extends AbstractHotRodEntity, V extends HotRodEntityDelegate<E> & AbstractEntity, M> HotRodMapStorage<String, E, V, M> getHotRodStorage(KeycloakSession session, Class<M> 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 <E extends AbstractHotRodEntity, V extends HotRodEntityDelegate<E> & AbstractEntity, M> HotRodMapStorage<String, E, V, M> createHotRodStorage(KeycloakSession session, Class<M> modelType, MapStorageProviderFactory.Flag... flags) {
private <E extends AbstractHotRodEntity, V extends HotRodEntityDelegate<E> & AbstractEntity, M> HotRodMapStorage<String, E, V, M> createHotRodStorage(KeycloakSession session, Class<M> modelType, AllAreasHotRodTransactionsWrapper txWrapper, MapStorageProviderFactory.Flag... flags) {
HotRodConnectionProvider connectionProvider = session.getProvider(HotRodConnectionProvider.class);
HotRodEntityDescriptor<E, V> entityDescriptor = (HotRodEntityDescriptor<E, V>) 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<HotRodSingleUseObjectEntity, HotRodSingleUseObjectEntityDelegate>) 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<SearchableModelField<? super UserSessionModel>, MapModelCriteriaBuilder.UpdatePredicatesFunc<String, MapUserSessionEntity, UserSessionModel>> fieldPredicates = MapFieldPredicates.getPredicates((Class<UserSessionModel>) 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

View file

@ -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 <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class SingleUseObjectHotRodMapStorage<K, E extends AbstractHotRodEntity, V extends HotRodEntityDelegate<E> & AbstractEntity, M>
public class SingleUseObjectHotRodMapStorage
extends HotRodMapStorage<String, HotRodSingleUseObjectEntity, HotRodSingleUseObjectEntityDelegate, SingleUseObjectValueModel> {
private final StringKeyConverter<String> keyConverter;
@ -53,8 +51,8 @@ public class SingleUseObjectHotRodMapStorage<K, E extends AbstractHotRodEntity,
public SingleUseObjectHotRodMapStorage(RemoteCache<String, HotRodSingleUseObjectEntity> remoteCache, StringKeyConverter<String> keyConverter,
HotRodEntityDescriptor<HotRodSingleUseObjectEntity, HotRodSingleUseObjectEntityDelegate> 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<K, E extends AbstractHotRodEntity,
@Override
protected MapKeycloakTransaction<HotRodSingleUseObjectEntityDelegate, SingleUseObjectValueModel> createTransactionInternal(KeycloakSession session) {
Map<SearchableModelField<? super SingleUseObjectValueModel>, MapModelCriteriaBuilder.UpdatePredicatesFunc<K, HotRodSingleUseObjectEntityDelegate, SingleUseObjectValueModel>> fieldPredicates =
Map<SearchableModelField<? super SingleUseObjectValueModel>, MapModelCriteriaBuilder.UpdatePredicatesFunc<String, HotRodSingleUseObjectEntityDelegate, SingleUseObjectValueModel>> fieldPredicates =
MapFieldPredicates.getPredicates((Class<SingleUseObjectValueModel>) storedEntityDescriptor.getModelTypeClass());
return new SingleUseObjectKeycloakTransaction(this, keyConverter, cloner, fieldPredicates);
}

View file

@ -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)));

View file

@ -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<Class<?>, MapKeycloakTransaction<?, ?>> MapKeycloakTransactionsMap = new ConcurrentHashMap<>();
public MapKeycloakTransaction<?, ?> getOrCreateTxForModel(Class<?> modelType, Supplier<MapKeycloakTransaction<?,?>> 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);
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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<K, V extends AbstractEntity & UpdatableEntity, M> implements MapKeycloakTransaction<V, M> {
private final ConcurrentHashMapKeycloakTransaction<K, V, M> actualTx;
public NoActionHotRodTransactionWrapper(ConcurrentHashMapKeycloakTransaction<K, V, M> 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<V> read(QueryParameters<M> queryParameters) {
return actualTx.read(queryParameters);
}
@Override
public long getCount(QueryParameters<M> queryParameters) {
return actualTx.getCount(queryParameters);
}
@Override
public boolean delete(String key) {
return actualTx.delete(key);
}
@Override
public long delete(QueryParameters<M> 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();
}
}

View file

@ -16,6 +16,8 @@
-->
<distributed-cache name="admin-events" mode="SYNC">
<encoding media-type="application/x-protostream"/>
<locking isolation="REPEATABLE_READ" />
<transaction mode="FULL_XA"/>
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodAdminEventEntity</indexed-entity>

View file

@ -16,6 +16,8 @@
-->
<distributed-cache name="auth-events" mode="SYNC">
<encoding media-type="application/x-protostream"/>
<locking isolation="REPEATABLE_READ" />
<transaction mode="FULL_XA"/>
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodAuthEventEntity</indexed-entity>

View file

@ -15,10 +15,12 @@
~ limitations under the License.
-->
<distributed-cache name="auth-sessions" mode="SYNC">
<encoding media-type="application/x-protostream"/>
<locking isolation="REPEATABLE_READ" />
<transaction mode="FULL_XA"/>
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodRootAuthenticationSessionEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>

View file

@ -16,6 +16,8 @@
-->
<distributed-cache name="authz" mode="SYNC">
<encoding media-type="application/x-protostream"/>
<locking isolation="REPEATABLE_READ" />
<transaction mode="FULL_XA"/>
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodResourceServerEntity</indexed-entity>

View file

@ -15,10 +15,12 @@
~ limitations under the License.
-->
<distributed-cache name="client-scopes" mode="SYNC">
<encoding media-type="application/x-protostream"/>
<locking isolation="REPEATABLE_READ" />
<transaction mode="FULL_XA"/>
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodClientScopeEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>

View file

@ -15,10 +15,12 @@
~ limitations under the License.
-->
<distributed-cache name="clients" mode="SYNC">
<encoding media-type="application/x-protostream"/>
<locking isolation="REPEATABLE_READ" />
<transaction mode="FULL_XA"/>
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodClientEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>

View file

@ -15,10 +15,12 @@
~ limitations under the License.
-->
<distributed-cache name="groups" mode="SYNC">
<encoding media-type="application/x-protostream"/>
<locking isolation="REPEATABLE_READ" />
<transaction mode="FULL_XA"/>
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodGroupEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>

View file

@ -15,5 +15,7 @@
~ limitations under the License.
-->
<replicated-cache name="locks" mode="SYNC">
<locking isolation="REPEATABLE_READ" />
<transaction mode="NON_XA"/>
<encoding media-type="text/plain"/>
</replicated-cache>

View file

@ -15,12 +15,14 @@
~ limitations under the License.
-->
<distributed-cache name="realms" mode="SYNC">
<encoding media-type="application/x-protostream"/>
<locking isolation="REPEATABLE_READ" />
<transaction mode="FULL_XA"/>
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodRealmEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>

View file

@ -15,11 +15,13 @@
~ limitations under the License.
-->
<distributed-cache name="roles" mode="SYNC">
<encoding media-type="application/x-protostream"/>
<locking isolation="REPEATABLE_READ" />
<transaction mode="FULL_XA"/>
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodRoleEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>

View file

@ -15,12 +15,14 @@
~ limitations under the License.
-->
<distributed-cache name="single-use-objects" mode="SYNC">
<encoding media-type="application/x-protostream"/>
<locking isolation="REPEATABLE_READ" />
<transaction mode="FULL_XA"/>
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodSingleUseObjectEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>

View file

@ -15,11 +15,13 @@
~ limitations under the License.
-->
<distributed-cache name="user-login-failures" mode="SYNC">
<encoding media-type="application/x-protostream"/>
<locking isolation="REPEATABLE_READ" />
<transaction mode="FULL_XA"/>
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodUserLoginFailureEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>

View file

@ -15,12 +15,14 @@
~ limitations under the License.
-->
<distributed-cache name="user-sessions" mode="SYNC">
<encoding media-type="application/x-protostream"/>
<locking isolation="REPEATABLE_READ" />
<transaction mode="FULL_XA"/>
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodUserSessionEntity</indexed-entity>
<indexed-entity>kc.HotRodAuthenticatedClientSessionEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>

View file

@ -15,11 +15,13 @@
~ limitations under the License.
-->
<distributed-cache name="users" mode="SYNC">
<encoding media-type="application/x-protostream"/>
<locking isolation="REPEATABLE_READ" />
<transaction mode="FULL_XA"/>
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodUserEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>

View file

@ -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;
}

View file

@ -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

View file

@ -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.

View file

@ -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();

View file

@ -104,9 +104,13 @@ public class SingleUseObjectModelTest extends KeycloakModelTest {
Map<String, String> 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<String, String> 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<String, String> 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));
});
}