Merge pull request #4563 from hmlnarik/KEYCLOAK-5656-Transport-factory-should-not-limit-to-a-single-DC-3

KEYCLOAK-5656 Use standard infinispan remote-store
This commit is contained in:
Hynek Mlnařík 2017-10-17 09:39:01 +02:00 committed by GitHub
commit fe76b2428b
23 changed files with 211 additions and 473 deletions

View file

@ -116,65 +116,63 @@ Keycloak servers setup
<cache-container name="keycloak" jndi-name="infinispan/Keycloak" module="org.keycloak.keycloak-model-infinispan"> <cache-container name="keycloak" jndi-name="infinispan/Keycloak" module="org.keycloak.keycloak-model-infinispan">
``` ```
3.3) Add the `store` under `work` cache: 3.3) Add the `remote-store` under `work` cache:
```xml ```xml
<replicated-cache name="work" mode="SYNC"> <replicated-cache name="work" mode="SYNC">
<store class="org.keycloak.models.sessions.infinispan.remotestore.KeycloakRemoteStoreConfigurationBuilder" passivation="false" fetch-state="false" purge="false" preload="false" shared="true"> <remote-store cache="work" remote-servers="remote-cache" passivation="false" fetch-state="false" purge="false" preload="false" shared="true">
<property name="rawValues">true</property> <property name="rawValues">true</property>
<property name="marshaller">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property> <property name="marshaller">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>
<property name="remoteCacheName">work</property> </remote-store>
<property name="sessionCache">false</property>
</store>
</replicated-cache> </replicated-cache>
``` ```
3.5) Add the `store` like this under `sessions` cache: 3.5) Add the `remote-store` like this under `sessions` cache:
```xml ```xml
<distributed-cache name="sessions" mode="SYNC" owners="1"> <distributed-cache name="sessions" mode="SYNC" owners="1">
<store class="org.keycloak.models.sessions.infinispan.remotestore.KeycloakRemoteStoreConfigurationBuilder" passivation="false" fetch-state="false" purge="false" preload="false" shared="true"> <remote-store cache="sessions" remote-servers="remote-cache" passivation="false" fetch-state="false" purge="false" preload="false" shared="true">
<property name="remoteCacheName">sessions</property> <property name="rawValues">true</property>
<property name="useConfigTemplateFromCache">work</property> <property name="marshaller">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>
<property name="sessionCache">true</property> </remote-store>
</store>
</distributed-cache> </distributed-cache>
``` ```
3.6) Same for `offlineSessions` and `loginFailures` caches (The only difference from `sessions` cache is, that `remoteCacheName` property value are different: 3.6) Same for `offlineSessions`, `loginFailures`, and `actionTokens` caches (the only difference from `sessions` cache is that `cache` property value are different):
```xml ```xml
<distributed-cache name="offlineSessions" mode="SYNC" owners="1"> <distributed-cache name="offlineSessions" mode="SYNC" owners="1">
<store class="org.keycloak.models.sessions.infinispan.remotestore.KeycloakRemoteStoreConfigurationBuilder" passivation="false" fetch-state="false" purge="false" preload="false" shared="true"> <remote-store cache="offlineSessions" remote-servers="remote-cache" passivation="false" fetch-state="false" purge="false" preload="false" shared="true">
<property name="remoteCacheName">offlineSessions</property> <property name="rawValues">true</property>
<property name="useConfigTemplateFromCache">work</property> <property name="marshaller">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>
<property name="sessionCache">true</property> </remote-store>
</store>
</distributed-cache> </distributed-cache>
<distributed-cache name="loginFailures" mode="SYNC" owners="1"> <distributed-cache name="loginFailures" mode="SYNC" owners="1">
<store class="org.keycloak.models.sessions.infinispan.remotestore.KeycloakRemoteStoreConfigurationBuilder" passivation="false" fetch-state="false" purge="false" preload="false" shared="true"> <remote-store cache="loginFailures" remote-servers="remote-cache" passivation="false" fetch-state="false" purge="false" preload="false" shared="true">
<property name="remoteCacheName">loginFailures</property> <property name="rawValues">true</property>
<property name="useConfigTemplateFromCache">work</property> <property name="marshaller">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>
<property name="sessionCache">true</property> </remote-store>
</store>
</distributed-cache> </distributed-cache>
```
3.7) The configuration of `actionTokens` cache have different `remoteCacheName`, `sessionCache` and the `preload` attribute:
```xml
<distributed-cache name="actionTokens" mode="SYNC" owners="2"> <distributed-cache name="actionTokens" mode="SYNC" owners="2">
<eviction max-entries="-1" strategy="NONE"/> <eviction max-entries="-1" strategy="NONE"/>
<expiration max-idle="-1" interval="300000"/> <expiration max-idle="-1" interval="300000"/>
<store class="org.keycloak.models.sessions.infinispan.remotestore.KeycloakRemoteStoreConfigurationBuilder" passivation="false" fetch-state="false" purge="false" preload="true" shared="true"> <remote-store cache="actionTokens" remote-servers="remote-cache" passivation="false" fetch-state="false" purge="false" preload="true" shared="true">
<property name="remoteCacheName">actionTokens</property> <property name="rawValues">true</property>
<property name="useConfigTemplateFromCache">work</property> <property name="marshaller">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>
<property name="sessionCache">false</property> </remote-store>
</store>
</distributed-cache> </distributed-cache>
``` ```
3.7) Add outbound socket binding for the remote store into `socket-binding-group` configuration:
```xml
<outbound-socket-binding name="remote-cache">
<remote-destination host="${remote.cache.host:localhost}" port="${remote.cache.port:11222}"/>
</outbound-socket-binding>
```
3.8) The configuration of distributed cache `authenticationSessions` and other caches is left unchanged. 3.8) The configuration of distributed cache `authenticationSessions` and other caches is left unchanged.
3.9) Optionally enable DEBUG logging under `logging` subsystem: 3.9) Optionally enable DEBUG logging under `logging` subsystem:

View file

@ -25,7 +25,7 @@ import org.infinispan.manager.EmbeddedCacheManager;
*/ */
public class DefaultInfinispanConnectionProvider implements InfinispanConnectionProvider { public class DefaultInfinispanConnectionProvider implements InfinispanConnectionProvider {
private EmbeddedCacheManager cacheManager; private final EmbeddedCacheManager cacheManager;
private final String siteName; private final String siteName;
private final String nodeName; private final String nodeName;

View file

@ -41,9 +41,9 @@ import org.keycloak.Config;
import org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory; import org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.sessions.infinispan.remotestore.KeycloakRemoteStoreConfigurationBuilder;
import javax.naming.InitialContext; import javax.naming.InitialContext;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -157,7 +157,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
this.nodeName = generateNodeName(); this.nodeName = generateNodeName();
} }
logger.debugv("Using container managed Infinispan cache container, lookup={1}", cacheContainerLookup); logger.debugv("Using container managed Infinispan cache container, lookup={0}", cacheContainerLookup);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Failed to retrieve cache container", e); throw new RuntimeException("Failed to retrieve cache container", e);
} }
@ -354,8 +354,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
builder.persistence() builder.persistence()
.passivation(false) .passivation(false)
.addStore(KeycloakRemoteStoreConfigurationBuilder.class) .addStore(RemoteStoreConfigurationBuilder.class)
.sessionCache(sessionCache)
.fetchPersistentState(false) .fetchPersistentState(false)
.ignoreModifications(false) .ignoreModifications(false)
.purgeOnStartup(false) .purgeOnStartup(false)
@ -382,8 +381,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
builder.persistence() builder.persistence()
.passivation(false) .passivation(false)
.addStore(KeycloakRemoteStoreConfigurationBuilder.class) .addStore(RemoteStoreConfigurationBuilder.class)
.sessionCache(false)
.fetchPersistentState(false) .fetchPersistentState(false)
.ignoreModifications(false) .ignoreModifications(false)
.purgeOnStartup(false) .purgeOnStartup(false)

View file

@ -26,13 +26,31 @@ import org.infinispan.context.Flag;
*/ */
public class CacheDecorators { public class CacheDecorators {
/**
* Adds {@link Flag#CACHE_MODE_LOCAL} flag to the cache.
* @param cache
* @return Cache with the flag applied.
*/
public static <K, V> AdvancedCache<K, V> localCache(Cache<K, V> cache) { public static <K, V> AdvancedCache<K, V> localCache(Cache<K, V> cache) {
return cache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL); return cache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL);
} }
/**
* Adds {@link Flag#SKIP_CACHE_LOAD} and {@link Flag#SKIP_CACHE_STORE} flags to the cache.
* @param cache
* @return Cache with the flags applied.
*/
public static <K, V> AdvancedCache<K, V> skipCacheLoaders(Cache<K, V> cache) { public static <K, V> AdvancedCache<K, V> skipCacheLoaders(Cache<K, V> cache) {
return cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE); return cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE);
} }
/**
* Adds {@link Flag#SKIP_CACHE_STORE} flag to the cache.
* @param cache
* @return Cache with the flags applied.
*/
public static <K, V> AdvancedCache<K, V> skipCacheStore(Cache<K, V> cache) {
return cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE);
}
} }

View file

@ -303,8 +303,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache); RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache);
if (remoteCache != null) { if (remoteCache != null) {
UserSessionEntity remoteSessionEntity = (UserSessionEntity) remoteCache.get(id); SessionEntityWrapper<UserSessionEntity> remoteSessionEntityWrapper = (SessionEntityWrapper<UserSessionEntity>) remoteCache.get(id);
if (remoteSessionEntity != null) { if (remoteSessionEntityWrapper != null) {
UserSessionEntity remoteSessionEntity = remoteSessionEntityWrapper.getEntity();
log.debugf("getUserSessionWithPredicate(%s): remote cache contains session entity %s", id, remoteSessionEntity); log.debugf("getUserSessionWithPredicate(%s): remote cache contains session entity %s", id, remoteSessionEntity);
UserSessionModel remoteSessionAdapter = wrap(realm, remoteSessionEntity, offline); UserSessionModel remoteSessionAdapter = wrap(realm, remoteSessionEntity, offline);
@ -399,7 +400,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
FuturesHelper futures = new FuturesHelper(); FuturesHelper futures = new FuturesHelper();
// Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account) // Each cluster node cleanups just local sessions, which are those owned by itself (+ few more taking l1 cache into account)
Cache<String, SessionEntityWrapper<UserSessionEntity>> localCache = CacheDecorators.localCache(sessionCache); Cache<String, SessionEntityWrapper<UserSessionEntity>> localCache = CacheDecorators.localCache(sessionCache);
Cache<String, SessionEntityWrapper<UserSessionEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache); Cache<String, SessionEntityWrapper<UserSessionEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);

View file

@ -39,6 +39,7 @@ import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.events.AbstractUserSessionClusterListener; import org.keycloak.models.sessions.infinispan.events.AbstractUserSessionClusterListener;
import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent; import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent;
@ -204,7 +205,7 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
InfinispanConnectionProvider ispn = session.getProvider(InfinispanConnectionProvider.class); InfinispanConnectionProvider ispn = session.getProvider(InfinispanConnectionProvider.class);
Cache sessionsCache = ispn.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionsCache = ispn.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
boolean sessionsRemoteCache = checkRemoteCache(session, sessionsCache, (RealmModel realm) -> { boolean sessionsRemoteCache = checkRemoteCache(session, sessionsCache, (RealmModel realm) -> {
return realm.getSsoSessionIdleTimeout() * 1000; return realm.getSsoSessionIdleTimeout() * 1000;
}); });
@ -214,7 +215,7 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
} }
Cache offlineSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME); Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
boolean offlineSessionsRemoteCache = checkRemoteCache(session, offlineSessionsCache, (RealmModel realm) -> { boolean offlineSessionsRemoteCache = checkRemoteCache(session, offlineSessionsCache, (RealmModel realm) -> {
return realm.getOfflineSessionIdleTimeout() * 1000; return realm.getOfflineSessionIdleTimeout() * 1000;
}); });
@ -223,13 +224,13 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
offlineLastSessionRefreshStore = new LastSessionRefreshStoreFactory().createAndInit(session, offlineSessionsCache, true); offlineLastSessionRefreshStore = new LastSessionRefreshStoreFactory().createAndInit(session, offlineSessionsCache, true);
} }
Cache loginFailuresCache = ispn.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME); Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailuresCache = ispn.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME);
boolean loginFailuresRemoteCache = checkRemoteCache(session, loginFailuresCache, (RealmModel realm) -> { boolean loginFailuresRemoteCache = checkRemoteCache(session, loginFailuresCache, (RealmModel realm) -> {
return realm.getMaxDeltaTimeSeconds() * 1000; return realm.getMaxDeltaTimeSeconds() * 1000;
}); });
} }
private boolean checkRemoteCache(KeycloakSession session, Cache ispnCache, RemoteCacheInvoker.MaxIdleTimeLoader maxIdleLoader) { private <K, V extends SessionEntity> boolean checkRemoteCache(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> ispnCache, RemoteCacheInvoker.MaxIdleTimeLoader maxIdleLoader) {
Set<RemoteStore> remoteStores = InfinispanUtil.getRemoteStores(ispnCache); Set<RemoteStore> remoteStores = InfinispanUtil.getRemoteStores(ispnCache);
if (remoteStores.isEmpty()) { if (remoteStores.isEmpty()) {
@ -238,7 +239,7 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
} else { } else {
log.infof("Remote store configured for cache '%s'", ispnCache.getName()); log.infof("Remote store configured for cache '%s'", ispnCache.getName());
RemoteCache remoteCache = remoteStores.iterator().next().getRemoteCache(); RemoteCache<K, SessionEntityWrapper<V>> remoteCache = (RemoteCache) remoteStores.iterator().next().getRemoteCache();
remoteCacheInvoker.addRemoteCache(ispnCache.getName(), remoteCache, maxIdleLoader); remoteCacheInvoker.addRemoteCache(ispnCache.getName(), remoteCache, maxIdleLoader);

View file

@ -27,6 +27,7 @@ import org.jboss.logging.Logger;
import org.keycloak.models.AbstractKeycloakTransaction; import org.keycloak.models.AbstractKeycloakTransaction;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.sessions.infinispan.CacheDecorators;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker; import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
@ -172,17 +173,17 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
switch (operation) { switch (operation) {
case REMOVE: case REMOVE:
// Just remove it // Just remove it
cache CacheDecorators.skipCacheStore(cache)
.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES) .getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES)
.remove(key); .remove(key);
break; break;
case ADD: case ADD:
cache CacheDecorators.skipCacheStore(cache)
.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES) .getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES)
.put(key, sessionWrapper, task.getLifespanMs(), TimeUnit.MILLISECONDS); .put(key, sessionWrapper, task.getLifespanMs(), TimeUnit.MILLISECONDS);
break; break;
case ADD_IF_ABSENT: case ADD_IF_ABSENT:
SessionEntityWrapper<V> existing = cache.putIfAbsent(key, sessionWrapper); SessionEntityWrapper<V> existing = CacheDecorators.skipCacheStore(cache).putIfAbsent(key, sessionWrapper);
if (existing != null) { if (existing != null) {
logger.debugf("Existing entity in cache for key: %s . Will update it", key); logger.debugf("Existing entity in cache for key: %s . Will update it", key);
@ -210,7 +211,7 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
SessionEntityWrapper<V> newVersionEntity = generateNewVersionAndWrapEntity(session, oldVersionEntity.getLocalMetadata()); SessionEntityWrapper<V> newVersionEntity = generateNewVersionAndWrapEntity(session, oldVersionEntity.getLocalMetadata());
// Atomic cluster-aware replace // Atomic cluster-aware replace
replaced = cache.replace(key, oldVersionEntity, newVersionEntity); replaced = CacheDecorators.skipCacheStore(cache).replace(key, oldVersionEntity, newVersionEntity);
// Replace fail. Need to load latest entity from cache, apply updates again and try to replace in cache again // Replace fail. Need to load latest entity from cache, apply updates again and try to replace in cache again
if (!replaced) { if (!replaced) {

View file

@ -29,6 +29,7 @@ import org.infinispan.commons.marshall.Externalizer;
import org.infinispan.commons.marshall.MarshallUtil; import org.infinispan.commons.marshall.MarshallUtil;
import org.infinispan.commons.marshall.SerializeWith; import org.infinispan.commons.marshall.SerializeWith;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.jboss.logging.Logger;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -36,11 +37,12 @@ import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
@SerializeWith(SessionEntityWrapper.ExternalizerImpl.class) @SerializeWith(SessionEntityWrapper.ExternalizerImpl.class)
public class SessionEntityWrapper<S extends SessionEntity> { public class SessionEntityWrapper<S extends SessionEntity> {
private static final Logger log = Logger.getLogger(SessionEntityWrapper.class);
private UUID version; private UUID version;
private final S entity; private final S entity;
private final Map<String, String> localMetadata; private final Map<String, String> localMetadata;
protected SessionEntityWrapper(UUID version, Map<String, String> localMetadata, S entity) { protected SessionEntityWrapper(UUID version, Map<String, String> localMetadata, S entity) {
if (version == null) { if (version == null) {
throw new IllegalArgumentException("Version UUID can't be null"); throw new IllegalArgumentException("Version UUID can't be null");
@ -52,13 +54,34 @@ public class SessionEntityWrapper<S extends SessionEntity> {
} }
public SessionEntityWrapper(Map<String, String> localMetadata, S entity) { public SessionEntityWrapper(Map<String, String> localMetadata, S entity) {
this(UUID.randomUUID(),localMetadata, entity); this(UUID.randomUUID(), localMetadata, entity);
} }
public SessionEntityWrapper(S entity) { public SessionEntityWrapper(S entity) {
this(new ConcurrentHashMap<>(), entity); this(new ConcurrentHashMap<>(), entity);
} }
private SessionEntityWrapper(S entity, boolean forTransport) {
if (! forTransport) {
throw new IllegalArgumentException("This constructor is only for transport entities");
}
this.version = null;
this.localMetadata = null;
this.entity = entity;
}
public static <S extends SessionEntity> SessionEntityWrapper<S> forTransport(S entity) {
return new SessionEntityWrapper<>(entity, true);
}
public SessionEntityWrapper<S> forTransport() {
return new SessionEntityWrapper<>(this.entity, true);
}
private boolean isForTransport() {
return this.version == null;
}
public UUID getVersion() { public UUID getVersion() {
return version; return version;
@ -68,16 +91,21 @@ public class SessionEntityWrapper<S extends SessionEntity> {
this.version = version; this.version = version;
} }
public S getEntity() { public S getEntity() {
return entity; return entity;
} }
public String getLocalMetadataNote(String key) { public String getLocalMetadataNote(String key) {
if (isForTransport()) {
throw new IllegalStateException("This entity is only intended for transport");
}
return localMetadata.get(key); return localMetadata.get(key);
} }
public void putLocalMetadataNote(String key, String value) { public void putLocalMetadataNote(String key, String value) {
if (isForTransport()) {
throw new IllegalStateException("This entity is only intended for transport");
}
localMetadata.put(key, value); localMetadata.put(key, value);
} }
@ -87,6 +115,9 @@ public class SessionEntityWrapper<S extends SessionEntity> {
} }
public void putLocalMetadataNoteInt(String key, int value) { public void putLocalMetadataNoteInt(String key, int value) {
if (isForTransport()) {
throw new IllegalStateException("This entity is only intended for transport");
}
localMetadata.put(key, String.valueOf(value)); localMetadata.put(key, String.valueOf(value));
} }
@ -122,31 +153,45 @@ public class SessionEntityWrapper<S extends SessionEntity> {
public static class ExternalizerImpl implements Externalizer<SessionEntityWrapper> { public static class ExternalizerImpl implements Externalizer<SessionEntityWrapper> {
private static final int VERSION_1 = 1;
@Override @Override
public void writeObject(ObjectOutput output, SessionEntityWrapper obj) throws IOException { public void writeObject(ObjectOutput output, SessionEntityWrapper obj) throws IOException {
MarshallUtil.marshallUUID(obj.version, output, false); output.write(VERSION_1);
final boolean forTransport = obj.isForTransport();
output.writeBoolean(forTransport);
if (! forTransport) {
output.writeLong(obj.getVersion().getMostSignificantBits());
output.writeLong(obj.getVersion().getLeastSignificantBits());
MarshallUtil.marshallMap(obj.localMetadata, output); MarshallUtil.marshallMap(obj.localMetadata, output);
output.writeObject(obj.getEntity()); }
output.writeObject(obj.entity);
} }
@Override @Override
public SessionEntityWrapper readObject(ObjectInput input) throws IOException, ClassNotFoundException { public SessionEntityWrapper readObject(ObjectInput input) throws IOException, ClassNotFoundException {
UUID objVersion = MarshallUtil.unmarshallUUID(input, false); byte version = input.readByte();
Map<String, String> localMetadata = MarshallUtil.unmarshallMap(input, new MarshallUtil.MapBuilder<String, String, Map<String, String>>() { if (version != VERSION_1) {
throw new IOException("Invalid version: " + version);
@Override
public Map<String, String> build(int size) {
return new ConcurrentHashMap<>(size);
} }
final boolean forTransport = input.readBoolean();
}); if (forTransport) {
final SessionEntity entity = (SessionEntity) input.readObject();
SessionEntity entity = (SessionEntity) input.readObject(); log.debugf("Loaded entity from remote store: %s", entity);
return new SessionEntityWrapper(entity);
return new SessionEntityWrapper<>(objVersion, localMetadata, entity); } else {
UUID sessionVersion = new UUID(input.readLong(), input.readLong());
ConcurrentHashMap<String, String> map = MarshallUtil.unmarshallMap(input, (size) -> new ConcurrentHashMap<>(size));
final SessionEntity entity = (SessionEntity) input.readObject();
log.debugf("Found entity locally: %s", entity);
return new SessionEntityWrapper(sessionVersion, map, entity);
}
} }
} }

View file

@ -28,6 +28,7 @@ import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
@ -43,11 +44,11 @@ public class LastSessionRefreshListener implements ClusterListener {
private final boolean offline; private final boolean offline;
private final KeycloakSessionFactory sessionFactory; private final KeycloakSessionFactory sessionFactory;
private final Cache<String, SessionEntityWrapper> cache; private final Cache<String, SessionEntityWrapper<UserSessionEntity>> cache;
private final boolean distributed; private final boolean distributed;
private final String myAddress; private final String myAddress;
public LastSessionRefreshListener(KeycloakSession session, Cache<String, SessionEntityWrapper> cache, boolean offline) { public LastSessionRefreshListener(KeycloakSession session, Cache<String, SessionEntityWrapper<UserSessionEntity>> cache, boolean offline) {
this.sessionFactory = session.getKeycloakSessionFactory(); this.sessionFactory = session.getKeycloakSessionFactory();
this.cache = cache; this.cache = cache;
this.offline = offline; this.offline = offline;

View file

@ -22,6 +22,7 @@ import org.keycloak.cluster.ClusterProvider;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.timer.TimerProvider; import org.keycloak.timer.TimerProvider;
/** /**
@ -39,12 +40,12 @@ public class LastSessionRefreshStoreFactory {
public static final int DEFAULT_MAX_COUNT = 100; public static final int DEFAULT_MAX_COUNT = 100;
public LastSessionRefreshStore createAndInit(KeycloakSession kcSession, Cache<String, SessionEntityWrapper> cache, boolean offline) { public LastSessionRefreshStore createAndInit(KeycloakSession kcSession, Cache<String, SessionEntityWrapper<UserSessionEntity>> cache, boolean offline) {
return createAndInit(kcSession, cache, DEFAULT_TIMER_INTERVAL_MS, DEFAULT_MAX_INTERVAL_BETWEEN_MESSAGES_SECONDS, DEFAULT_MAX_COUNT, offline); return createAndInit(kcSession, cache, DEFAULT_TIMER_INTERVAL_MS, DEFAULT_MAX_INTERVAL_BETWEEN_MESSAGES_SECONDS, DEFAULT_MAX_COUNT, offline);
} }
public LastSessionRefreshStore createAndInit(KeycloakSession kcSession, Cache<String, SessionEntityWrapper> cache, long timerIntervalMs, int maxIntervalBetweenMessagesSeconds, int maxCount, boolean offline) { public LastSessionRefreshStore createAndInit(KeycloakSession kcSession, Cache<String, SessionEntityWrapper<UserSessionEntity>> cache, long timerIntervalMs, int maxIntervalBetweenMessagesSeconds, int maxCount, boolean offline) {
String eventKey = offline ? "lastSessionRefreshes-offline" : "lastSessionRefreshes"; String eventKey = offline ? "lastSessionRefreshes-offline" : "lastSessionRefreshes";
LastSessionRefreshStore store = createStoreInstance(maxIntervalBetweenMessagesSeconds, maxCount, eventKey); LastSessionRefreshStore store = createStoreInstance(maxIntervalBetweenMessagesSeconds, maxCount, eventKey);

View file

@ -1,176 +0,0 @@
/*
* Copyright 2016 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.sessions.infinispan.remotestore;
import java.util.concurrent.Executor;
import org.infinispan.commons.CacheException;
import org.infinispan.commons.configuration.ConfiguredBy;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.cache.PersistenceConfigurationBuilder;
import org.infinispan.configuration.cache.StoreConfiguration;
import org.infinispan.filter.KeyFilter;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.marshall.core.MarshalledEntry;
import org.infinispan.metadata.InternalMetadata;
import org.infinispan.persistence.InitializationContextImpl;
import org.infinispan.persistence.remote.RemoteStore;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfiguration;
import org.infinispan.persistence.spi.InitializationContext;
import org.infinispan.persistence.spi.PersistenceException;
import org.jboss.logging.Logger;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@ConfiguredBy(KeycloakRemoteStoreConfiguration.class)
public class KeycloakRemoteStore extends RemoteStore {
protected static final Logger logger = Logger.getLogger(KeycloakRemoteStore.class);
private String remoteCacheName;
@Override
public void start() throws PersistenceException {
this.remoteCacheName = getConfiguration().remoteCacheName();
Boolean sessionCache = getConfiguration().sessionCache();
String cacheTemplateName = getConfiguration().useConfigTemplateFromCache();
if (cacheTemplateName != null) {
logger.debugf("Will override configuration of cache '%s' from template of cache '%s'", ctx.getCache().getName(), cacheTemplateName);
// Just to ensure that dependent cache is started and it's configuration fully loaded
EmbeddedCacheManager cacheManager = ctx.getCache().getCacheManager();
cacheManager.getCache(cacheTemplateName, true);
KeycloakRemoteStoreConfiguration templateConfig = (KeycloakRemoteStoreConfiguration) cacheManager.getCacheConfiguration(cacheTemplateName).persistence().stores().stream()
.filter((StoreConfiguration storeConfig) -> storeConfig instanceof KeycloakRemoteStoreConfiguration)
.findFirst()
.orElseThrow(() -> new CacheException("Unable to find remoteStore on cache '" + cacheTemplateName + "."));
// We have template configuration, so create new configuration from it. Override just remoteCacheName and sessionsCache (not pretty, but works for now)
PersistenceConfigurationBuilder readPersistenceBuilder = new ConfigurationBuilder().read(ctx.getCache().getCacheConfiguration()).persistence();
KeycloakRemoteStoreConfigurationBuilder configBuilder = new KeycloakRemoteStoreConfigurationBuilder(readPersistenceBuilder);
configBuilder.read(templateConfig);
// Rather log this to clearly show in the log that this might be a configuration mistake (Note that it can be expected for some cases)
if (!this.remoteCacheName.equals(ctx.getCache().getName())) {
logger.warnf("Cache name and remoteCache name are different - maybe it's expected. Cache name '%s', remoteCache name '%s'.", ctx.getCache().getName(), this.remoteCacheName);
}
configBuilder.remoteCacheName(this.remoteCacheName);
configBuilder.sessionCache(sessionCache);
RemoteStoreConfiguration newCfg1 = configBuilder.create();
KeycloakRemoteStoreConfiguration newCfg = new KeycloakRemoteStoreConfiguration(newCfg1);
InitializationContext ctx = new InitializationContextImpl(newCfg, this.ctx.getCache(), this.ctx.getMarshaller(), this.ctx.getTimeService(),
this.ctx.getByteBufferFactory(), this.ctx.getMarshalledEntryFactory());
init(ctx);
} else {
logger.debugf("Skip overriding configuration from template for cache '%s'", ctx.getCache().getName());
}
logger.debugf("Using configuration for remote cache '%s': %s", remoteCacheName, getConfiguration().toString());
super.start();
if (getRemoteCache() == null) {
String cacheName = getConfiguration().remoteCacheName();
throw new CacheException("Remote cache '" + cacheName + "' is not available.");
}
}
@Override
public MarshalledEntry load(Object key) throws PersistenceException {
if (!getConfiguration().sessionCache()) {
return super.load(key);
}
logger.debugf("Calling load: '%s' for remote cache '%s'", key, remoteCacheName);
MarshalledEntry entry = super.load(key);
if (entry == null) {
return null;
}
// wrap remote entity
SessionEntity entity = (SessionEntity) entry.getValue();
SessionEntityWrapper entityWrapper = new SessionEntityWrapper(entity);
MarshalledEntry wrappedEntry = marshalledEntry(entry.getKey(), entityWrapper, entry.getMetadata());
logger.debugf("Found entry in load: %s", wrappedEntry.toString());
return wrappedEntry;
}
// Don't do anything. Iterate over remoteCache.keySet() can have big performance impact. We handle bulk load by ourselves if needed.
@Override
public void process(KeyFilter filter, CacheLoaderTask task, Executor executor, boolean fetchValue, boolean fetchMetadata) {
if (!getConfiguration().sessionCache()) {
super.process(filter, task, executor, fetchValue, fetchMetadata);
return;
}
logger.debugf("Skip calling process with filter '%s' on cache '%s'", filter, remoteCacheName);
// super.process(filter, task, executor, fetchValue, fetchMetadata);
}
// Don't do anything. Writes handled by KC itself as we need more flexibility
@Override
public void write(MarshalledEntry entry) throws PersistenceException {
if (!getConfiguration().sessionCache()) {
super.write(entry);
return;
}
}
@Override
public boolean delete(Object key) throws PersistenceException {
if (!getConfiguration().sessionCache()) {
return super.delete(key);
}
logger.debugf("Calling delete for key '%s' on cache '%s'", key, remoteCacheName);
// Optimization - we don't need to know the previous value.
// TODO: For some usecases (bulk removal of user sessions), it may be better for performance to call removeAsync and wait for all futures to be finished
getRemoteCache().remove(key);
return true;
}
protected MarshalledEntry marshalledEntry(Object key, Object value, InternalMetadata metadata) {
return ctx.getMarshalledEntryFactory().newMarshalledEntry(key, value, metadata);
}
@Override
public KeycloakRemoteStoreConfiguration getConfiguration() {
return (KeycloakRemoteStoreConfiguration) super.getConfiguration();
}
}

View file

@ -1,55 +0,0 @@
/*
* Copyright 2016 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.sessions.infinispan.remotestore;
import org.infinispan.commons.configuration.BuiltBy;
import org.infinispan.commons.configuration.ConfigurationFor;
import org.infinispan.commons.configuration.attributes.Attribute;
import org.infinispan.commons.configuration.attributes.AttributeDefinition;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfiguration;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@BuiltBy(KeycloakRemoteStoreConfigurationBuilder.class)
@ConfigurationFor(KeycloakRemoteStore.class)
public class KeycloakRemoteStoreConfiguration extends RemoteStoreConfiguration {
static final AttributeDefinition<String> USE_CONFIG_TEMPLATE_FROM_CACHE = AttributeDefinition.builder("useConfigTemplateFromCache", null, String.class).immutable().build();
static final AttributeDefinition<Boolean> SESSION_CACHE = AttributeDefinition.builder("sessionCache", null, Boolean.class).immutable().build();
private final Attribute<String> useConfigTemplateFromCache;
private final Attribute<Boolean> sessionCache;
public KeycloakRemoteStoreConfiguration(RemoteStoreConfiguration other) {
super(other.attributes(), other.async(), other.singletonStore(), other.asyncExecutorFactory(), other.connectionPool());
useConfigTemplateFromCache = attributes.attribute(USE_CONFIG_TEMPLATE_FROM_CACHE.name());
sessionCache = attributes.attribute(SESSION_CACHE.name());
}
public String useConfigTemplateFromCache() {
return useConfigTemplateFromCache.get();
}
public Boolean sessionCache() {
return sessionCache.get()==null ? false : sessionCache.get();
}
}

View file

@ -1,78 +0,0 @@
/*
* Copyright 2016 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.sessions.infinispan.remotestore;
import java.lang.reflect.Field;
import java.util.Map;
import org.infinispan.commons.CacheConfigurationException;
import org.infinispan.commons.configuration.attributes.Attribute;
import org.infinispan.commons.configuration.attributes.AttributeDefinition;
import org.infinispan.commons.configuration.attributes.AttributeSet;
import org.infinispan.configuration.cache.PersistenceConfigurationBuilder;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfiguration;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
import org.keycloak.common.util.reflections.Reflections;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class KeycloakRemoteStoreConfigurationBuilder extends RemoteStoreConfigurationBuilder {
public KeycloakRemoteStoreConfigurationBuilder(PersistenceConfigurationBuilder builder) {
super(builder);
// No better way to add new attribute definition to superclass :/
try {
Field f = Reflections.findDeclaredField(AttributeSet.class, "attributes");
f.setAccessible(true);
Map<String, Attribute<? extends Object>> attributesInternal = (Map<String, Attribute<? extends Object>>) f.get(this.attributes);
AttributeDefinition<String> def = KeycloakRemoteStoreConfiguration.USE_CONFIG_TEMPLATE_FROM_CACHE;
Attribute<String> attribute = def.toAttribute();
attributesInternal.put(def.name(), attribute);
AttributeDefinition<Boolean> defBool = KeycloakRemoteStoreConfiguration.SESSION_CACHE;
Attribute<Boolean> attributeBool = defBool.toAttribute();
attributesInternal.put(defBool.name(), attributeBool);
} catch (IllegalAccessException iae) {
throw new CacheConfigurationException(iae);
}
}
@Override
public KeycloakRemoteStoreConfiguration create() {
RemoteStoreConfiguration cfg = super.create();
KeycloakRemoteStoreConfiguration cfg2 = new KeycloakRemoteStoreConfiguration(cfg);
return cfg2;
}
public KeycloakRemoteStoreConfigurationBuilder useConfigTemplateFromCache(String useConfigTemplateFromCache) {
attributes.attribute(KeycloakRemoteStoreConfiguration.USE_CONFIG_TEMPLATE_FROM_CACHE).set(useConfigTemplateFromCache);
return this;
}
public KeycloakRemoteStoreConfigurationBuilder sessionCache(Boolean sessionCache) {
attributes.attribute(KeycloakRemoteStoreConfiguration.SESSION_CACHE).set(sessionCache);
return this;
}
}

View file

@ -82,23 +82,22 @@ public class RemoteCacheInvoker {
} }
private <K, V extends SessionEntity> void runOnRemoteCache(RemoteCache<K, V> remoteCache, long maxIdleMs, K key, SessionUpdateTask<V> task, SessionEntityWrapper<V> sessionWrapper) { private <K, V extends SessionEntity> void runOnRemoteCache(RemoteCache<K, SessionEntityWrapper<V>> remoteCache, long maxIdleMs, K key, SessionUpdateTask<V> task, SessionEntityWrapper<V> sessionWrapper) {
V session = sessionWrapper.getEntity(); final V session = sessionWrapper.getEntity();
SessionUpdateTask.CacheOperation operation = task.getOperation(session); SessionUpdateTask.CacheOperation operation = task.getOperation(session);
switch (operation) { switch (operation) {
case REMOVE: case REMOVE:
// REMOVE already handled at remote cache store level remoteCache.remove(key);
//remoteCache.remove(key);
break; break;
case ADD: case ADD:
remoteCache.put(key, session, task.getLifespanMs(), TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS); remoteCache.put(key, sessionWrapper.forTransport(), task.getLifespanMs(), TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS);
break; break;
case ADD_IF_ABSENT: case ADD_IF_ABSENT:
final int currentTime = Time.currentTime(); final int currentTime = Time.currentTime();
SessionEntity existing = remoteCache SessionEntityWrapper<V> existing = remoteCache
.withFlags(Flag.FORCE_RETURN_VALUE) .withFlags(Flag.FORCE_RETURN_VALUE)
.putIfAbsent(key, session, -1, TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS); .putIfAbsent(key, sessionWrapper.forTransport(), -1, TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS);
if (existing != null) { if (existing != null) {
logger.debugf("Existing entity in remote cache for key: %s . Will update it", key); logger.debugf("Existing entity in remote cache for key: %s . Will update it", key);
@ -116,23 +115,24 @@ public class RemoteCacheInvoker {
} }
private <K, V extends SessionEntity> void replace(RemoteCache<K, V> remoteCache, long lifespanMs, long maxIdleMs, K key, SessionUpdateTask<V> task) { private <K, V extends SessionEntity> void replace(RemoteCache<K, SessionEntityWrapper<V>> remoteCache, long lifespanMs, long maxIdleMs, K key, SessionUpdateTask<V> task) {
boolean replaced = false; boolean replaced = false;
while (!replaced) { while (!replaced) {
VersionedValue<V> versioned = remoteCache.getVersioned(key); VersionedValue<SessionEntityWrapper<V>> versioned = remoteCache.getVersioned(key);
if (versioned == null) { if (versioned == null) {
logger.warnf("Not found entity to replace for key '%s'", key); logger.warnf("Not found entity to replace for key '%s'", key);
return; return;
} }
V session = versioned.getValue(); SessionEntityWrapper<V> sessionWrapper = versioned.getValue();
final V session = sessionWrapper.getEntity();
// Run task on the remote session // Run task on the remote session
task.runUpdate(session); task.runUpdate(session);
logger.debugf("Before replaceWithVersion. Entity to write version %d: %s", versioned.getVersion(), session); logger.debugf("Before replaceWithVersion. Entity to write version %d: %s", versioned.getVersion(), session);
replaced = remoteCache.replaceWithVersion(key, session, versioned.getVersion(), lifespanMs, TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS); replaced = remoteCache.replaceWithVersion(key, SessionEntityWrapper.forTransport(session), versioned.getVersion(), lifespanMs, TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS);
if (!replaced) { if (!replaced) {
logger.debugf("Failed to replace entity '%s' version %d. Will retry again", key, versioned.getVersion()); logger.debugf("Failed to replace entity '%s' version %d. Will retry again", key, versioned.getVersion());

View file

@ -47,7 +47,7 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
protected static final Logger logger = Logger.getLogger(RemoteCacheSessionListener.class); protected static final Logger logger = Logger.getLogger(RemoteCacheSessionListener.class);
private Cache<K, SessionEntityWrapper<V>> cache; private Cache<K, SessionEntityWrapper<V>> cache;
private RemoteCache<K, V> remoteCache; private RemoteCache<K, SessionEntityWrapper<V>> remoteCache;
private boolean distributed; private boolean distributed;
private String myAddress; private String myAddress;
private ClientListenerExecutorDecorator<K> executor; private ClientListenerExecutorDecorator<K> executor;
@ -57,7 +57,7 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
} }
protected void init(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> cache, RemoteCache<K, V> remoteCache) { protected void init(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> cache, RemoteCache<K, SessionEntityWrapper<V>> remoteCache) {
this.cache = cache; this.cache = cache;
this.remoteCache = remoteCache; this.remoteCache = remoteCache;
@ -113,10 +113,10 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
replaceRetries++; replaceRetries++;
SessionEntityWrapper<V> localEntityWrapper = cache.get(key); SessionEntityWrapper<V> localEntityWrapper = cache.get(key);
VersionedValue<V> remoteSessionVersioned = remoteCache.getVersioned(key); VersionedValue<SessionEntityWrapper<V>> remoteSessionVersioned = remoteCache.getVersioned(key);
// Probably already removed // Probably already removed
if (remoteSessionVersioned == null) { if (remoteSessionVersioned == null || remoteSessionVersioned.getValue() == null) {
logger.debugf("Entity '%s' not present in remoteCache. Ignoring replace", logger.debugf("Entity '%s' not present in remoteCache. Ignoring replace",
key.toString()); key.toString());
return; return;
@ -134,7 +134,7 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
sleepInterval = sleepInterval << 1; sleepInterval = sleepInterval << 1;
} }
} }
SessionEntity remoteSession = remoteSessionVersioned.getValue(); SessionEntity remoteSession = remoteSessionVersioned.getValue().getEntity();
logger.debugf("Read session entity from the remote cache: %s . replaceRetries=%d", remoteSession.toString(), replaceRetries); logger.debugf("Read session entity from the remote cache: %s . replaceRetries=%d", remoteSession.toString(), replaceRetries);
@ -201,7 +201,7 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
} }
public static <K, V extends SessionEntity> RemoteCacheSessionListener createListener(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> cache, RemoteCache<K, V> remoteCache) { public static <K, V extends SessionEntity> RemoteCacheSessionListener createListener(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> cache, RemoteCache<K, SessionEntityWrapper<V>> remoteCache) {
/*boolean isCoordinator = InfinispanUtil.isCoordinator(cache); /*boolean isCoordinator = InfinispanUtil.isCoordinator(cache);
// Just cluster coordinator will fetch userSessions from remote cache. // Just cluster coordinator will fetch userSessions from remote cache.

View file

@ -29,7 +29,6 @@ import org.jboss.logging.Logger;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.initializer.BaseCacheInitializer; import org.keycloak.models.sessions.infinispan.initializer.BaseCacheInitializer;
import org.keycloak.models.sessions.infinispan.initializer.OfflinePersistentUserSessionLoader; import org.keycloak.models.sessions.infinispan.initializer.OfflinePersistentUserSessionLoader;
import org.keycloak.models.sessions.infinispan.initializer.SessionLoader; import org.keycloak.models.sessions.infinispan.initializer.SessionLoader;
@ -116,9 +115,7 @@ public class RemoteCacheSessionsLoader implements SessionLoader {
for (Map.Entry<byte[], byte[]> entry : remoteObjects.entrySet()) { for (Map.Entry<byte[], byte[]> entry : remoteObjects.entrySet()) {
try { try {
Object key = marshaller.objectFromByteBuffer(entry.getKey()); Object key = marshaller.objectFromByteBuffer(entry.getKey());
SessionEntity entity = (SessionEntity) marshaller.objectFromByteBuffer(entry.getValue()); SessionEntityWrapper entityWrapper = (SessionEntityWrapper) marshaller.objectFromByteBuffer(entry.getValue());
SessionEntityWrapper entityWrapper = new SessionEntityWrapper(entity);
decoratedCache.putAsync(key, entityWrapper); decoratedCache.putAsync(key, entityWrapper);
} catch (Exception e) { } catch (Exception e) {

View file

@ -39,8 +39,8 @@ import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.remotestore.KeycloakRemoteStoreConfigurationBuilder;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
/** /**
* Test concurrency for remoteStore (backed by HotRod RemoteCaches) against external JDG. Especially tests "replaceWithVersion" contract. * Test concurrency for remoteStore (backed by HotRod RemoteCaches) against external JDG. Especially tests "replaceWithVersion" contract.
@ -207,7 +207,7 @@ public class ConcurrencyJDGSessionsCacheTest {
private static EmbeddedCacheManager createManager(int threadId) { private static EmbeddedCacheManager createManager(int threadId) {
return new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.SESSION_CACHE_NAME, KeycloakRemoteStoreConfigurationBuilder.class); return new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.SESSION_CACHE_NAME, RemoteStoreConfigurationBuilder.class);
} }

View file

@ -19,11 +19,12 @@ package org.keycloak.cluster.infinispan;
import org.infinispan.configuration.cache.Configuration; import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder; import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.cache.StoreConfigurationBuilder;
import org.infinispan.configuration.global.GlobalConfigurationBuilder; import org.infinispan.configuration.global.GlobalConfigurationBuilder;
import org.infinispan.manager.DefaultCacheManager; import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager; import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.persistence.remote.configuration.ExhaustedAction; import org.infinispan.persistence.remote.configuration.ExhaustedAction;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder; import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationChildBuilder;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -31,7 +32,7 @@ import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationB
class TestCacheManagerFactory { class TestCacheManagerFactory {
<T extends RemoteStoreConfigurationBuilder> EmbeddedCacheManager createManager(int threadId, String cacheName, Class<T> builderClass) { <T extends StoreConfigurationBuilder<?, T> & RemoteStoreConfigurationChildBuilder<T>> EmbeddedCacheManager createManager(int threadId, String cacheName, Class<T> builderClass) {
System.setProperty("java.net.preferIPv4Stack", "true"); System.setProperty("java.net.preferIPv4Stack", "true");
System.setProperty("jgroups.tcp.port", "53715"); System.setProperty("jgroups.tcp.port", "53715");
GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder(); GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
@ -57,7 +58,7 @@ class TestCacheManagerFactory {
} }
private <T extends RemoteStoreConfigurationBuilder> Configuration getCacheBackedByRemoteStore(int threadId, String cacheName, Class<T> builderClass) { private <T extends StoreConfigurationBuilder<?, T> & RemoteStoreConfigurationChildBuilder<T>> Configuration getCacheBackedByRemoteStore(int threadId, String cacheName, Class<T> builderClass) {
ConfigurationBuilder cacheConfigBuilder = new ConfigurationBuilder(); ConfigurationBuilder cacheConfigBuilder = new ConfigurationBuilder();
String host = "localhost"; String host = "localhost";

View file

@ -44,8 +44,8 @@ public class DefaultExecutorsProviderFactory implements ExecutorsProviderFactory
protected static final Logger logger = Logger.getLogger(DefaultExecutorsProviderFactory.class); protected static final Logger logger = Logger.getLogger(DefaultExecutorsProviderFactory.class);
private int DEFAULT_MIN_THREADS = 4; private static final int DEFAULT_MIN_THREADS = 4;
private int DEFAULT_MAX_THREADS = 16; private static final int DEFAULT_MAX_THREADS = 16;
private static final String MANAGED_EXECUTORS_SERVICE_JNDI_PREFIX = "java:jboss/ee/concurrency/executor/"; private static final String MANAGED_EXECUTORS_SERVICE_JNDI_PREFIX = "java:jboss/ee/concurrency/executor/";

View file

@ -8,105 +8,88 @@ echo *** Update jgoups subsystem ***
echo *** Update infinispan subsystem *** echo *** Update infinispan subsystem ***
/subsystem=infinispan/cache-container=keycloak:write-attribute(name=module, value=org.keycloak.keycloak-model-infinispan) /subsystem=infinispan/cache-container=keycloak:write-attribute(name=module, value=org.keycloak.keycloak-model-infinispan)
echo ** Add remote socket binding to infinispan server **
/socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=remote-cache:add(host=${remote.cache.host:localhost}, port=${remote.cache.port:11222})
echo ** Update replicated-cache work element ** echo ** Update replicated-cache work element **
/subsystem=infinispan/cache-container=keycloak/replicated-cache=work/store=custom:add( \ /subsystem=infinispan/cache-container=keycloak/replicated-cache=work/store=remote:add( \
class=org.keycloak.models.sessions.infinispan.remotestore.KeycloakRemoteStoreConfigurationBuilder, \
passivation=false, \ passivation=false, \
fetch-state=false, \ fetch-state=false, \
purge=false, \ purge=false, \
preload=false, \ preload=false, \
shared=true \ shared=true, \
) remote-servers=["remote-cache"], \
cache=work, \
/subsystem=infinispan/cache-container=keycloak/replicated-cache=work/store=custom:write-attribute( \ properties={ \
name=properties, value={ \
rawValues=true, \ rawValues=true, \
marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \ marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory \
remoteCacheName=work, \
sessionCache=false \
} \ } \
) )
/subsystem=infinispan/cache-container=keycloak/replicated-cache=work:write-attribute(name=statistics-enabled,value=true) /subsystem=infinispan/cache-container=keycloak/replicated-cache=work:write-attribute(name=statistics-enabled,value=true)
echo ** Update distributed-cache sessions element ** echo ** Update distributed-cache sessions element **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=custom:add( \ /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=remote:add( \
class=org.keycloak.models.sessions.infinispan.remotestore.KeycloakRemoteStoreConfigurationBuilder, \
passivation=false, \ passivation=false, \
fetch-state=false, \ fetch-state=false, \
purge=false, \ purge=false, \
preload=false, \ preload=false, \
shared=true \ shared=true, \
) remote-servers=["remote-cache"], \
cache=sessions, \
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=custom:write-attribute( \ properties={ \
name=properties, value={ \ rawValues=true, \
remoteCacheName=sessions, \ marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory \
useConfigTemplateFromCache=work, \
sessionCache=true \
} \ } \
) )
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:write-attribute(name=statistics-enabled,value=true) /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:write-attribute(name=statistics-enabled,value=true)
echo ** Update distributed-cache offlineSessions element ** echo ** Update distributed-cache offlineSessions element **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions/store=custom:add( \ /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions/store=remote:add( \
class=org.keycloak.models.sessions.infinispan.remotestore.KeycloakRemoteStoreConfigurationBuilder, \
passivation=false, \ passivation=false, \
fetch-state=false, \ fetch-state=false, \
purge=false, \ purge=false, \
preload=false, \ preload=false, \
shared=true \ shared=true, \
) remote-servers=["remote-cache"], \
cache=offlineSessions, \
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions/store=custom:write-attribute( \ properties={ \
name=properties, value={ \ rawValues=true, \
remoteCacheName=offlineSessions, \ marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory \
useConfigTemplateFromCache=work, \
sessionCache=true \
} \ } \
) )
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:write-attribute(name=statistics-enabled,value=true) /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:write-attribute(name=statistics-enabled,value=true)
echo ** Update distributed-cache loginFailures element ** echo ** Update distributed-cache loginFailures element **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures/store=custom:add( \ /subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures/store=remote:add( \
class=org.keycloak.models.sessions.infinispan.remotestore.KeycloakRemoteStoreConfigurationBuilder, \
passivation=false, \ passivation=false, \
fetch-state=false, \ fetch-state=false, \
purge=false, \ purge=false, \
preload=false, \ preload=false, \
shared=true \ shared=true, \
) remote-servers=["remote-cache"], \
cache=loginFailures, \
/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures/store=custom:write-attribute( \ properties={ \
name=properties, value={ \ rawValues=true, \
remoteCacheName=loginFailures, \ marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory \
useConfigTemplateFromCache=work, \
sessionCache=true \
} \ } \
) )
/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:write-attribute(name=statistics-enabled,value=true) /subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:write-attribute(name=statistics-enabled,value=true)
echo ** Update distributed-cache actionTokens element ** echo ** Update distributed-cache actionTokens element **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/store=custom:add( \ /subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/store=remote:add( \
class=org.keycloak.models.sessions.infinispan.remotestore.KeycloakRemoteStoreConfigurationBuilder, \
passivation=false, \ passivation=false, \
fetch-state=false, \ fetch-state=false, \
purge=false, \ purge=false, \
preload=true, \ preload=false, \
shared=true \ shared=true, \
) cache=actionTokens, \
remote-servers=["remote-cache"], \
/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/store=custom:write-attribute( \ properties={ \
name=properties, value={ \ rawValues=true, \
remoteCacheName=actionTokens, \ marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory \
useConfigTemplateFromCache=work, \
sessionCache=false \
} \ } \
) )
/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens:write-attribute(name=statistics-enabled,value=true) /subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens:write-attribute(name=statistics-enabled,value=true)
echo ** Update distributed-cache authenticationSessions element ** echo ** Update distributed-cache authenticationSessions element **

View file

@ -35,6 +35,7 @@ import org.infinispan.remoting.transport.Transport;
import org.jgroups.JChannel; import org.jgroups.JChannel;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import org.keycloak.testsuite.rest.representation.JGroupsStats; import org.keycloak.testsuite.rest.representation.JGroupsStats;
@ -136,11 +137,11 @@ public class TestCacheResource {
if (remoteCache == null) { if (remoteCache == null) {
return -1; return -1;
} else { } else {
UserSessionEntity userSession = (UserSessionEntity) remoteCache.get(userSessionId); SessionEntityWrapper<UserSessionEntity> userSession = (SessionEntityWrapper<UserSessionEntity>) remoteCache.get(userSessionId);
if (userSession == null) { if (userSession == null) {
return -1; return -1;
} else { } else {
return userSession.getLastSessionRefresh(); return userSession.getEntity().getLastSessionRefresh();
} }
} }
} }

View file

@ -222,10 +222,10 @@ public class BruteForceCrossDCTest extends AbstractAdminCrossDCTest {
log.infof("%s: dc0User1=%d, dc0user2=%d, dc1user1=%d, dc1user2=%d, dc0CacheSize=%d, dc1CacheSize=%d", prefixMessage, dc0user1, dc0user2, dc1user1, dc1user2, dc0CacheSize, dc1CacheSize); log.infof("%s: dc0User1=%d, dc0user2=%d, dc1user1=%d, dc1user2=%d, dc0CacheSize=%d, dc1CacheSize=%d", prefixMessage, dc0user1, dc0user2, dc1user1, dc1user2, dc0CacheSize, dc1CacheSize);
Assert.assertEquals(dc0user1, expectedUser1); Assert.assertEquals(expectedUser1, dc0user1);
Assert.assertEquals(dc0user2, expectedUser2); Assert.assertEquals(expectedUser2, dc0user2);
Assert.assertEquals(dc1user1, expectedUser1); Assert.assertEquals(expectedUser1, dc1user1);
Assert.assertEquals(dc1user2, expectedUser2); Assert.assertEquals(expectedUser2, dc1user2);
Assert.assertEquals(expectedCacheSize, dc0CacheSize); Assert.assertEquals(expectedCacheSize, dc0CacheSize);
Assert.assertEquals(expectedCacheSize, dc1CacheSize); Assert.assertEquals(expectedCacheSize, dc1CacheSize);

View file

@ -34,6 +34,7 @@ import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshStore; import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshStore;
import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshStoreFactory; import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshStoreFactory;
import org.keycloak.models.sessions.infinispan.changes.sessions.SessionData; import org.keycloak.models.sessions.infinispan.changes.sessions.SessionData;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.Retry; import org.keycloak.testsuite.Retry;
@ -168,7 +169,7 @@ public class LastSessionRefreshUnitTest extends AbstractKeycloakTest {
}; };
Cache<String, SessionEntityWrapper> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
return factory.createAndInit(session, cache, timerIntervalMs, maxIntervalBetweenMessagesSeconds, 10, false); return factory.createAndInit(session, cache, timerIntervalMs, maxIntervalBetweenMessagesSeconds, 10, false);
} }