KEYCLOAK-16755 ClearExpiredUserSessions optimization. Rely on infinispan expiration rather than Keycloak own background task.

This commit is contained in:
mposolda 2021-01-19 20:06:03 +01:00 committed by Marek Posolda
parent 6da396821a
commit f4b5942c6c
39 changed files with 1046 additions and 405 deletions

View file

@ -65,7 +65,7 @@ public class Time {
* @param time Time in seconds since the epoch * @param time Time in seconds since the epoch
* @return Time in milliseconds * @return Time in milliseconds
*/ */
public static long toMillis(int time) { public static long toMillis(long time) {
return time * 1000L; return time * 1000L;
} }

View file

@ -19,9 +19,6 @@ package org.keycloak.cluster.infinispan;
import org.infinispan.Cache; import org.infinispan.Cache;
import org.infinispan.client.hotrod.exceptions.HotRodClientException; import org.infinispan.client.hotrod.exceptions.HotRodClientException;
import org.infinispan.commons.marshall.Externalizer;
import org.infinispan.commons.marshall.MarshallUtil;
import org.infinispan.commons.marshall.SerializeWith;
import org.infinispan.manager.EmbeddedCacheManager; import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.notifications.Listener; import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachemanagerlistener.annotation.ViewChanged; import org.infinispan.notifications.cachemanagerlistener.annotation.ViewChanged;
@ -39,26 +36,16 @@ import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.connections.infinispan.TopologyInfo; import org.keycloak.connections.infinispan.TopologyInfo;
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.stream.RootAuthenticationSessionPredicate;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Serializable; import java.io.Serializable;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -149,7 +136,8 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory
try { try {
V result; V result;
if (taskTimeoutInSeconds > 0) { if (taskTimeoutInSeconds > 0) {
result = (V) crossDCAwareCacheFactory.getCache().putIfAbsent(key, value, taskTimeoutInSeconds, TimeUnit.SECONDS); long lifespanMs = InfinispanUtil.toHotrodTimeMs(crossDCAwareCacheFactory.getCache(), Time.toMillis(taskTimeoutInSeconds));
result = (V) crossDCAwareCacheFactory.getCache().putIfAbsent(key, value, lifespanMs, TimeUnit.MILLISECONDS);
} else { } else {
result = (V) crossDCAwareCacheFactory.getCache().putIfAbsent(key, value); result = (V) crossDCAwareCacheFactory.getCache().putIfAbsent(key, value);
} }

View file

@ -185,11 +185,11 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
String jgroupsUdpMcastAddr = config.get("jgroupsUdpMcastAddr", System.getProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR)); String jgroupsUdpMcastAddr = config.get("jgroupsUdpMcastAddr", System.getProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR));
configureTransport(gcb, topologyInfo.getMyNodeName(), topologyInfo.getMySiteName(), jgroupsUdpMcastAddr); configureTransport(gcb, topologyInfo.getMyNodeName(), topologyInfo.getMySiteName(), jgroupsUdpMcastAddr);
gcb.jmx() gcb.jmx()
.jmxDomain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + topologyInfo.getMyNodeName()); .domain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + topologyInfo.getMyNodeName()).enable();
} else {
gcb.jmx().domain(InfinispanConnectionProvider.JMX_DOMAIN).enable();
} }
gcb.jmx().domain(InfinispanConnectionProvider.JMX_DOMAIN).enable();
// For Infinispan 10, we go with the JBoss marshalling. // For Infinispan 10, we go with the JBoss marshalling.
// TODO: This should be replaced later with the marshalling recommended by infinispan. Probably protostream. // TODO: This should be replaced later with the marshalling recommended by infinispan. Probably protostream.
// See https://infinispan.org/docs/stable/titles/developing/developing.html#marshalling for the details // See https://infinispan.org/docs/stable/titles/developing/developing.html#marshalling for the details

View file

@ -47,14 +47,12 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
private final InfinispanUserSessionProvider provider; private final InfinispanUserSessionProvider provider;
private AuthenticatedClientSessionEntity entity; private AuthenticatedClientSessionEntity entity;
private final ClientModel client; private final ClientModel client;
private final InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx;
private final InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx; private final InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx;
private UserSessionModel userSession; private UserSessionModel userSession;
private boolean offline; private boolean offline;
public AuthenticatedClientSessionAdapter(KeycloakSession kcSession, InfinispanUserSessionProvider provider, public AuthenticatedClientSessionAdapter(KeycloakSession kcSession, InfinispanUserSessionProvider provider,
AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionModel userSession, AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionModel userSession,
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx,
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx, boolean offline) { InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx, boolean offline) {
if (userSession == null) { if (userSession == null) {
throw new NullPointerException("userSession must not be null"); throw new NullPointerException("userSession must not be null");
@ -65,15 +63,10 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
this.entity = entity; this.entity = entity;
this.userSession = userSession; this.userSession = userSession;
this.client = client; this.client = client;
this.userSessionUpdateTx = userSessionUpdateTx;
this.clientSessionUpdateTx = clientSessionUpdateTx; this.clientSessionUpdateTx = clientSessionUpdateTx;
this.offline = offline; this.offline = offline;
} }
private void update(UserSessionUpdateTask task) {
userSessionUpdateTx.addTask(userSession.getId(), task);
}
private void update(ClientSessionUpdateTask task) { private void update(ClientSessionUpdateTask task) {
clientSessionUpdateTx.addTask(entity.getId(), task); clientSessionUpdateTx.addTask(entity.getId(), task);
} }

View file

@ -20,6 +20,7 @@ package org.keycloak.models.sessions.infinispan;
import org.keycloak.cluster.ClusterProvider; import org.keycloak.cluster.ClusterProvider;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.infinispan.Cache; import org.infinispan.Cache;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
@ -39,7 +40,6 @@ import org.keycloak.models.utils.RealmInfoUtil;
import org.keycloak.sessions.AuthenticationSessionCompoundId; import org.keycloak.sessions.AuthenticationSessionCompoundId;
import org.keycloak.sessions.AuthenticationSessionProvider; import org.keycloak.sessions.AuthenticationSessionProvider;
import org.keycloak.sessions.RootAuthenticationSessionModel; import org.keycloak.sessions.RootAuthenticationSessionModel;
import org.infinispan.AdvancedCache;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -80,7 +80,8 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
entity.setRealmId(realm.getId()); entity.setRealmId(realm.getId());
entity.setTimestamp(Time.currentTime()); entity.setTimestamp(Time.currentTime());
tx.put(cache, id, entity); int expirationSeconds = RealmInfoUtil.getDettachedClientSessionLifespan(realm);
tx.put(cache, id, entity, expirationSeconds, TimeUnit.SECONDS);
return wrap(realm, entity); return wrap(realm, entity);
} }
@ -94,28 +95,17 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
private RootAuthenticationSessionEntity getRootAuthenticationSessionEntity(String authSessionId) { private RootAuthenticationSessionEntity getRootAuthenticationSessionEntity(String authSessionId) {
// Chance created in this transaction // Chance created in this transaction
RootAuthenticationSessionEntity entity = tx.get(cache, authSessionId); RootAuthenticationSessionEntity entity = tx.get(cache, authSessionId);
if (entity == null) {
entity = cache.get(authSessionId);
}
return entity; return entity;
} }
@Override
public void removeAllExpired() {
// Rely on expiration of cache entries provided by infinispan. Nothing needed here
}
@Override @Override
public void removeExpired(RealmModel realm) { public void removeExpired(RealmModel realm) {
log.debugf("Removing expired sessions"); // Rely on expiration of cache entries provided by infinispan. Nothing needed here
int expired = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm);
final AdvancedCache<String, RootAuthenticationSessionEntity> localCache = CacheDecorators.localCache(cache);
int localCacheSizePre = localCache.size();
// Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
localCache.entrySet()
.removeIf(RootAuthenticationSessionPredicate.create(realm.getId()).expired(expired));
log.debugf("Removed %d expired authentication sessions for realm '%s'", localCache.size() - localCacheSizePre, realm.getName());
} }
@Override @Override

View file

@ -25,9 +25,11 @@ import java.util.function.Supplier;
import org.infinispan.client.hotrod.exceptions.HotRodClientException; import org.infinispan.client.hotrod.exceptions.HotRodClientException;
import org.infinispan.commons.api.BasicCache; import org.infinispan.commons.api.BasicCache;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.models.CodeToTokenStoreProvider; import org.keycloak.models.CodeToTokenStoreProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity; import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -51,7 +53,8 @@ public class InfinispanCodeToTokenStoreProvider implements CodeToTokenStoreProvi
try { try {
BasicCache<UUID, ActionTokenValueEntity> cache = codeCache.get(); BasicCache<UUID, ActionTokenValueEntity> cache = codeCache.get();
cache.put(codeId, tokenValue, lifespanSeconds, TimeUnit.SECONDS); long lifespanMs = InfinispanUtil.toHotrodTimeMs(cache, Time.toMillis(lifespanSeconds));
cache.put(codeId, tokenValue, lifespanMs, TimeUnit.MILLISECONDS);
} catch (HotRodClientException re) { } catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened. // No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {

View file

@ -112,7 +112,7 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
@Override @Override
public String toString() { public String toString() {
return String.format("CacheTaskWithValue: Operation 'put' for key %s, lifespan %d TimeUnit %s", key, lifespan, lifespanUnit.toString()); return String.format("CacheTaskWithValue: Operation 'put' for key %s, lifespan %d TimeUnit %s", key, lifespan, lifespanUnit);
} }
}); });
} }
@ -142,7 +142,7 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
} }
} }
public <K, V> void replace(Cache<K, V> cache, K key, V value) { public <K, V> void replace(Cache<K, V> cache, K key, V value, long lifespan, TimeUnit lifespanUnit) {
log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REPLACE, key); log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REPLACE, key);
Object taskKey = getTaskKey(cache, key); Object taskKey = getTaskKey(cache, key);
@ -155,12 +155,12 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
tasks.put(taskKey, new CacheTaskWithValue<V>(value) { tasks.put(taskKey, new CacheTaskWithValue<V>(value) {
@Override @Override
public void execute() { public void execute() {
decorateCache(cache).replace(key, value); decorateCache(cache).replace(key, value, lifespan, lifespanUnit);
} }
@Override @Override
public String toString() { public String toString() {
return String.format("CacheTaskWithValue: Operation 'replace' for key %s", key); return String.format("CacheTaskWithValue: Operation 'replace' for key %s, lifespan %d TimeUnit %s", key, lifespan, lifespanUnit);
} }
}); });
@ -208,7 +208,6 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
if (current instanceof CacheTaskWithValue) { if (current instanceof CacheTaskWithValue) {
return ((CacheTaskWithValue<V>) current).getValue(); return ((CacheTaskWithValue<V>) current).getValue();
} }
return null;
} }
// Should we have per-transaction cache for lookups? // Should we have per-transaction cache for lookups?

View file

@ -23,9 +23,11 @@ import java.util.function.Supplier;
import org.infinispan.client.hotrod.exceptions.HotRodClientException; import org.infinispan.client.hotrod.exceptions.HotRodClientException;
import org.infinispan.commons.api.BasicCache; import org.infinispan.commons.api.BasicCache;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.SingleUseTokenStoreProvider; import org.keycloak.models.SingleUseTokenStoreProvider;
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity; import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
/** /**
* TODO: Check if Boolean can be used as single-use cache argument instead of ActionTokenValueEntity. With respect to other single-use cache usecases like "Revoke Refresh Token" . * TODO: Check if Boolean can be used as single-use cache argument instead of ActionTokenValueEntity. With respect to other single-use cache usecases like "Revoke Refresh Token" .
@ -55,7 +57,8 @@ public class InfinispanSingleUseTokenStoreProvider implements SingleUseTokenStor
try { try {
BasicCache<String, ActionTokenValueEntity> cache = tokenCache.get(); BasicCache<String, ActionTokenValueEntity> cache = tokenCache.get();
ActionTokenValueEntity existing = cache.putIfAbsent(tokenId, tokenValue, lifespanInSeconds, TimeUnit.SECONDS); long lifespanMs = InfinispanUtil.toHotrodTimeMs(cache, Time.toMillis(lifespanInSeconds));
ActionTokenValueEntity existing = cache.putIfAbsent(tokenId, tokenValue, lifespanMs, TimeUnit.MILLISECONDS);
return existing == null; return existing == null;
} catch (HotRodClientException re) { } catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened. // No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.

View file

@ -20,16 +20,17 @@ package org.keycloak.models.sessions.infinispan;
import java.util.Collections; import java.util.Collections;
import java.util.Map; import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.infinispan.client.hotrod.exceptions.HotRodClientException; import org.infinispan.client.hotrod.exceptions.HotRodClientException;
import org.infinispan.commons.api.BasicCache; import org.infinispan.commons.api.BasicCache;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.TokenRevocationStoreProvider; import org.keycloak.models.TokenRevocationStoreProvider;
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity; import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -57,7 +58,8 @@ public class InfinispanTokenRevocationStoreProvider implements TokenRevocationSt
try { try {
BasicCache<String, ActionTokenValueEntity> cache = tokenCache.get(); BasicCache<String, ActionTokenValueEntity> cache = tokenCache.get();
cache.put(tokenId, tokenValue, lifespanSeconds + 1, TimeUnit.SECONDS); long lifespanMs = InfinispanUtil.toHotrodTimeMs(cache, Time.toMillis(lifespanSeconds + 1));
cache.put(tokenId, tokenValue, lifespanMs, TimeUnit.MILLISECONDS);
} catch (HotRodClientException re) { } catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened. // No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {

View file

@ -53,7 +53,6 @@ import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent; import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent;
import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent; import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent;
import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction; import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction;
import org.keycloak.models.sessions.infinispan.stream.AuthenticatedClientSessionPredicate;
import org.keycloak.models.sessions.infinispan.stream.Comparators; import org.keycloak.models.sessions.infinispan.stream.Comparators;
import org.keycloak.models.sessions.infinispan.stream.Mappers; import org.keycloak.models.sessions.infinispan.stream.Mappers;
import org.keycloak.models.sessions.infinispan.stream.SessionPredicate; import org.keycloak.models.sessions.infinispan.stream.SessionPredicate;
@ -62,7 +61,7 @@ import org.keycloak.models.sessions.infinispan.stream.UserSessionPredicate;
import org.keycloak.models.sessions.infinispan.util.FuturesHelper; import org.keycloak.models.sessions.infinispan.util.FuturesHelper;
import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator; import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import org.keycloak.models.utils.SessionTimeoutHelper; import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
import java.io.Serializable; import java.io.Serializable;
import java.util.Collection; import java.util.Collection;
@ -132,12 +131,12 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
this.offlineClientSessionCache = offlineClientSessionCache; this.offlineClientSessionCache = offlineClientSessionCache;
this.loginFailureCache = loginFailureCache; this.loginFailureCache = loginFailureCache;
this.sessionTx = new InfinispanChangelogBasedTransaction<>(session, sessionCache, remoteCacheInvoker); this.sessionTx = new InfinispanChangelogBasedTransaction<>(session, sessionCache, remoteCacheInvoker, SessionTimeouts::getUserSessionLifespanMs, SessionTimeouts::getUserSessionMaxIdleMs);
this.offlineSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineSessionCache, remoteCacheInvoker); this.offlineSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineSessionCache, remoteCacheInvoker, SessionTimeouts::getOfflineSessionLifespanMs, SessionTimeouts::getOfflineSessionMaxIdleMs);
this.clientSessionTx = new InfinispanChangelogBasedTransaction<>(session, clientSessionCache, remoteCacheInvoker); this.clientSessionTx = new InfinispanChangelogBasedTransaction<>(session, clientSessionCache, remoteCacheInvoker, SessionTimeouts::getClientSessionLifespanMs, SessionTimeouts::getClientSessionMaxIdleMs);
this.offlineClientSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineClientSessionCache, remoteCacheInvoker); this.offlineClientSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineClientSessionCache, remoteCacheInvoker, SessionTimeouts::getOfflineClientSessionLifespanMs, SessionTimeouts::getOfflineClientSessionMaxIdleMs);
this.loginFailuresTx = new InfinispanChangelogBasedTransaction<>(session, loginFailureCache, remoteCacheInvoker); this.loginFailuresTx = new InfinispanChangelogBasedTransaction<>(session, loginFailureCache, remoteCacheInvoker, SessionTimeouts::getLoginFailuresLifespanMs, SessionTimeouts::getLoginFailuresMaxIdleMs);
this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session); this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
@ -192,7 +191,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(false); InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(false);
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(false); InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(false);
AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(session, this, entity, client, userSession, userSessionUpdateTx, clientSessionUpdateTx, false); AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(session, this, entity, client, userSession, clientSessionUpdateTx, false);
// For now, the clientSession is considered transient in case that userSession was transient // For now, the clientSession is considered transient in case that userSession was transient
UserSessionModel.SessionPersistenceState persistenceState = (userSession instanceof UserSessionAdapter && ((UserSessionAdapter) userSession).getPersistenceState() != null) ? UserSessionModel.SessionPersistenceState persistenceState = (userSession instanceof UserSessionAdapter && ((UserSessionAdapter) userSession).getPersistenceState() != null) ?
@ -463,146 +462,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
} }
} }
public void removeAllExpired() {
// Rely on expiration of cache entries provided by infinispan. Just expire entries from persister is needed
// TODO: Avoid iteration over all realms here (Details in the KEYCLOAK-16802)
session.realms().getRealmsStream().forEach(this::removeExpired);
}
@Override @Override
public void removeExpired(RealmModel realm) { public void removeExpired(RealmModel realm) {
log.debugf("Removing expired sessions"); // Rely on expiration of cache entries provided by infinispan. Nothing needed here besides calling persister
removeExpiredUserSessions(realm);
removeExpiredOfflineUserSessions(realm);
session.getProvider(UserSessionPersisterProvider.class).removeExpired(realm); session.getProvider(UserSessionPersisterProvider.class).removeExpired(realm);
} }
private void removeExpiredUserSessions(RealmModel realm) {
int expired = Time.currentTime() - realm.getSsoSessionMaxLifespan();
int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout() - SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
int expiredRememberMe = Time.currentTime() - (realm.getSsoSessionMaxLifespanRememberMe() > 0 ? realm.getSsoSessionMaxLifespanRememberMe() : realm.getSsoSessionMaxLifespan());
int expiredRefreshRememberMe = Time.currentTime() - (realm.getSsoSessionIdleTimeoutRememberMe() > 0 ? realm.getSsoSessionIdleTimeoutRememberMe() : realm.getSsoSessionIdleTimeout()) -
SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
FuturesHelper futures = new FuturesHelper();
// 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>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
final AtomicInteger userSessionsSize = new AtomicInteger();
final AtomicInteger clientSessionsSize = new AtomicInteger();
// Ignore remoteStore for stream iteration. But we will invoke remoteStore for userSession removal propagate
localCacheStoreIgnore
.entrySet()
.stream()
.filter(UserSessionPredicate.create(realm.getId()).expired(expired, expiredRefresh, expiredRememberMe, expiredRefreshRememberMe))
.map(Mappers.userSessionEntity())
.forEach(new Consumer<UserSessionEntity>() {
@Override
public void accept(UserSessionEntity userSessionEntity) {
userSessionsSize.incrementAndGet();
Future future = sessionCache.removeAsync(userSessionEntity.getId());
futures.addTask(future);
userSessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> {
clientSessionsSize.incrementAndGet();
Future f = clientSessionCache.removeAsync(clientSessionId);
futures.addTask(f);
});
}
});
// Removing detached clientSessions. Ignore remoteStore for stream iteration. But we will invoke remoteStore for clientSession removal propagate
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> localClientSessionCache = CacheDecorators.localCache(clientSessionCache);
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> localClientSessionCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localClientSessionCache);
localClientSessionCacheStoreIgnore
.entrySet()
.stream()
.filter(AuthenticatedClientSessionPredicate.create(realm.getId()).expired(Math.min(expired, expiredRememberMe)))
.map(Mappers.clientSessionEntity())
.forEach(new Consumer<AuthenticatedClientSessionEntity>() {
@Override
public void accept(AuthenticatedClientSessionEntity clientSessionEntity) {
clientSessionsSize.incrementAndGet();
Future future = clientSessionCache.removeAsync(clientSessionEntity.getId());
futures.addTask(future);
}
});
futures.waitForAllToFinish();
log.debugf("Removed %d expired user sessions and %d expired client sessions for realm '%s'", userSessionsSize.get(),
clientSessionsSize.get(), realm.getName());
}
private void removeExpiredOfflineUserSessions(RealmModel realm) {
int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout() - SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
// Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
Cache<String, SessionEntityWrapper<UserSessionEntity>> localCache = CacheDecorators.localCache(offlineSessionCache);
UserSessionPredicate predicate = UserSessionPredicate.create(realm.getId()).expired(null, expiredOffline);
FuturesHelper futures = new FuturesHelper();
Cache<String, SessionEntityWrapper<UserSessionEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
final AtomicInteger userSessionsSize = new AtomicInteger();
final AtomicInteger clientSessionsSize = new AtomicInteger();
// Ignore remoteStore for stream iteration. But we will invoke remoteStore for userSession removal propagate
localCacheStoreIgnore
.entrySet()
.stream()
.filter(predicate)
.map(Mappers.userSessionEntity())
.forEach(new Consumer<UserSessionEntity>() {
@Override
public void accept(UserSessionEntity userSessionEntity) {
userSessionsSize.incrementAndGet();
Future future = offlineSessionCache.removeAsync(userSessionEntity.getId());
futures.addTask(future);
userSessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> {
clientSessionsSize.incrementAndGet();
Future f = offlineClientSessionCache.removeAsync(clientSessionId);
futures.addTask(f);
});
}
});
// Removing detached clientSessions. Ignore remoteStore for stream iteration. But we will invoke remoteStore for clientSession removal propagate
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> localClientSessionCache = CacheDecorators.localCache(offlineClientSessionCache);
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> localClientSessionCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localClientSessionCache);
localClientSessionCacheStoreIgnore
.entrySet()
.stream()
.filter(AuthenticatedClientSessionPredicate.create(realm.getId()).expired(expiredOffline))
.map(Mappers.clientSessionEntity())
.forEach(new Consumer<AuthenticatedClientSessionEntity>() {
@Override
public void accept(AuthenticatedClientSessionEntity clientSessionEntity) {
clientSessionsSize.incrementAndGet();
Future future = offlineClientSessionCache.removeAsync(clientSessionEntity.getId());
futures.addTask(future);
}
});
futures.waitForAllToFinish();
log.debugf("Removed %d expired offline user sessions and %d expired offline client sessions for realm '%s'",
userSessionsSize.get(), clientSessionsSize.get(), realm.getName());
}
@Override @Override
public void removeUserSessions(RealmModel realm) { public void removeUserSessions(RealmModel realm) {
// Don't send message to all DCs, just to all cluster nodes in current DC. The remoteCache will notify client listeners for removed userSessions. // Don't send message to all DCs, just to all cluster nodes in current DC. The remoteCache will notify client listeners for removed userSessions.
@ -794,7 +666,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
AuthenticatedClientSessionAdapter wrap(UserSessionModel userSession, ClientModel client, AuthenticatedClientSessionEntity entity, boolean offline) { AuthenticatedClientSessionAdapter wrap(UserSessionModel userSession, ClientModel client, AuthenticatedClientSessionEntity entity, boolean offline) {
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(offline); InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(offline);
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(offline); InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(offline);
return entity != null ? new AuthenticatedClientSessionAdapter(session,this, entity, client, userSession, userSessionUpdateTx, clientSessionUpdateTx, offline) : null; return entity != null ? new AuthenticatedClientSessionAdapter(session,this, entity, client, userSession, clientSessionUpdateTx, offline) : null;
} }
UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) { UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
@ -1041,7 +913,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(clientSession.getClient().getId(), clientSessionId); SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(clientSession.getClient().getId(), clientSessionId);
userSessionUpdateTx.addTask(sessionToImportInto.getId(), registerClientSessionTask); userSessionUpdateTx.addTask(sessionToImportInto.getId(), registerClientSessionTask);
return new AuthenticatedClientSessionAdapter(session,this, entity, clientSession.getClient(), sessionToImportInto, userSessionUpdateTx, clientSessionUpdateTx, offline); return new AuthenticatedClientSessionAdapter(session,this, entity, clientSession.getClient(), sessionToImportInto, clientSessionUpdateTx, offline);
} }

View file

@ -24,6 +24,7 @@ import org.jboss.logging.Logger;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.cluster.ClusterProvider; import org.keycloak.cluster.ClusterProvider;
import org.keycloak.common.util.Environment; import org.keycloak.common.util.Environment;
import org.keycloak.common.util.Time;
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.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
@ -56,6 +57,7 @@ import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheSessionLis
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheSessionsLoader; import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheSessionsLoader;
import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator; import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.PostMigrationEvent; import org.keycloak.models.utils.PostMigrationEvent;
import org.keycloak.models.utils.ResetTimeOffsetEvent; import org.keycloak.models.utils.ResetTimeOffsetEvent;
@ -65,6 +67,7 @@ import org.keycloak.provider.ProviderEventListener;
import java.io.Serializable; import java.io.Serializable;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.function.BiFunction;
public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory { public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory {
@ -245,47 +248,48 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
InfinispanConnectionProvider ispn = session.getProvider(InfinispanConnectionProvider.class); InfinispanConnectionProvider ispn = session.getProvider(InfinispanConnectionProvider.class);
Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionsCache = ispn.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME); Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionsCache = ispn.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
boolean sessionsRemoteCache = checkRemoteCache(session, sessionsCache, (RealmModel realm) -> { RemoteCache sessionsRemoteCache = checkRemoteCache(session, sessionsCache, (RealmModel realm) -> {
// We won't write to the remoteCache during token refresh, so the timeout needs to be longer. // We won't write to the remoteCache during token refresh, so the timeout needs to be longer.
return realm.getSsoSessionMaxLifespan() * 1000; return Time.toMillis(realm.getSsoSessionMaxLifespan());
}); }, SessionTimeouts::getUserSessionLifespanMs, SessionTimeouts::getUserSessionMaxIdleMs);
if (sessionsRemoteCache) { if (sessionsRemoteCache != null) {
lastSessionRefreshStore = new CrossDCLastSessionRefreshStoreFactory().createAndInit(session, sessionsCache, false); lastSessionRefreshStore = new CrossDCLastSessionRefreshStoreFactory().createAndInit(session, sessionsCache, false);
} }
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionsCache = ispn.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME); Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionsCache = ispn.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME);
checkRemoteCache(session, clientSessionsCache, (RealmModel realm) -> { checkRemoteCache(session, clientSessionsCache, (RealmModel realm) -> {
// We won't write to the remoteCache during token refresh, so the timeout needs to be longer. // We won't write to the remoteCache during token refresh, so the timeout needs to be longer.
return realm.getSsoSessionMaxLifespan() * 1000; return Time.toMillis(realm.getSsoSessionMaxLifespan());
}); }, SessionTimeouts::getClientSessionLifespanMs, SessionTimeouts::getClientSessionMaxIdleMs);
Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME); Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME);
boolean offlineSessionsRemoteCache = checkRemoteCache(session, offlineSessionsCache, (RealmModel realm) -> { RemoteCache offlineSessionsRemoteCache = checkRemoteCache(session, offlineSessionsCache, (RealmModel realm) -> {
return realm.getOfflineSessionIdleTimeout() * 1000; return Time.toMillis(realm.getOfflineSessionIdleTimeout());
}); }, SessionTimeouts::getOfflineSessionLifespanMs, SessionTimeouts::getOfflineSessionMaxIdleMs);
if (offlineSessionsRemoteCache) { if (offlineSessionsRemoteCache != null) {
offlineLastSessionRefreshStore = new CrossDCLastSessionRefreshStoreFactory().createAndInit(session, offlineSessionsCache, true); offlineLastSessionRefreshStore = new CrossDCLastSessionRefreshStoreFactory().createAndInit(session, offlineSessionsCache, true);
} }
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME); Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME);
checkRemoteCache(session, offlineClientSessionsCache, (RealmModel realm) -> { checkRemoteCache(session, offlineClientSessionsCache, (RealmModel realm) -> {
return realm.getOfflineSessionIdleTimeout() * 1000; return Time.toMillis(realm.getOfflineSessionIdleTimeout());
}); }, SessionTimeouts::getOfflineClientSessionLifespanMs, SessionTimeouts::getOfflineClientSessionMaxIdleMs);
Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailuresCache = ispn.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME); Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailuresCache = ispn.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME);
checkRemoteCache(session, loginFailuresCache, (RealmModel realm) -> { checkRemoteCache(session, loginFailuresCache, (RealmModel realm) -> {
return realm.getMaxDeltaTimeSeconds() * 1000; return Time.toMillis(realm.getMaxDeltaTimeSeconds());
}); }, SessionTimeouts::getLoginFailuresLifespanMs, SessionTimeouts::getLoginFailuresMaxIdleMs);
} }
private <K, V extends SessionEntity> boolean checkRemoteCache(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> ispnCache, RemoteCacheInvoker.MaxIdleTimeLoader maxIdleLoader) { private <K, V extends SessionEntity> RemoteCache checkRemoteCache(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> ispnCache, RemoteCacheInvoker.MaxIdleTimeLoader maxIdleLoader,
BiFunction<RealmModel, V, Long> lifespanMsLoader, BiFunction<RealmModel, V, Long> maxIdleTimeMsLoader) {
Set<RemoteStore> remoteStores = InfinispanUtil.getRemoteStores(ispnCache); Set<RemoteStore> remoteStores = InfinispanUtil.getRemoteStores(ispnCache);
if (remoteStores.isEmpty()) { if (remoteStores.isEmpty()) {
log.debugf("No remote store configured for cache '%s'", ispnCache.getName()); log.debugf("No remote store configured for cache '%s'", ispnCache.getName());
return false; return null;
} else { } else {
log.infof("Remote store configured for cache '%s'", ispnCache.getName()); log.infof("Remote store configured for cache '%s'", ispnCache.getName());
@ -297,9 +301,9 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
remoteCacheInvoker.addRemoteCache(ispnCache.getName(), remoteCache, maxIdleLoader); remoteCacheInvoker.addRemoteCache(ispnCache.getName(), remoteCache, maxIdleLoader);
RemoteCacheSessionListener hotrodListener = RemoteCacheSessionListener.createListener(session, ispnCache, remoteCache); RemoteCacheSessionListener hotrodListener = RemoteCacheSessionListener.createListener(session, ispnCache, remoteCache, lifespanMsLoader, maxIdleTimeMsLoader);
remoteCache.addClientListener(hotrodListener); remoteCache.addClientListener(hotrodListener);
return true; return remoteCache;
} }
} }

View file

@ -19,6 +19,7 @@ package org.keycloak.models.sessions.infinispan;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.infinispan.Cache; import org.infinispan.Cache;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
@ -27,6 +28,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity; import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity; import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity;
import org.keycloak.models.utils.RealmInfoUtil;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel; import org.keycloak.sessions.RootAuthenticationSessionModel;
@ -52,7 +54,8 @@ public class RootAuthenticationSessionAdapter implements RootAuthenticationSessi
} }
void update() { void update() {
provider.tx.replace(cache, entity.getId(), entity); int expirationSeconds = RealmInfoUtil.getDettachedClientSessionLifespan(realm);
provider.tx.replace(cache, entity.getId(), entity, expirationSeconds, TimeUnit.SECONDS);
} }

View file

@ -20,6 +20,7 @@ package org.keycloak.models.sessions.infinispan.changes;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import org.infinispan.Cache; import org.infinispan.Cache;
import org.infinispan.context.Flag; import org.infinispan.context.Flag;
@ -47,11 +48,17 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
private final Map<K, SessionUpdatesList<V>> updates = new HashMap<>(); private final Map<K, SessionUpdatesList<V>> updates = new HashMap<>();
public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, Cache<K, SessionEntityWrapper<V>> cache, RemoteCacheInvoker remoteCacheInvoker) { private final BiFunction<RealmModel, V, Long> lifespanMsLoader;
private final BiFunction<RealmModel, V, Long> maxIdleTimeMsLoader;
public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, Cache<K, SessionEntityWrapper<V>> cache, RemoteCacheInvoker remoteCacheInvoker,
BiFunction<RealmModel, V, Long> lifespanMsLoader, BiFunction<RealmModel, V, Long> maxIdleTimeMsLoader) {
this.kcSession = kcSession; this.kcSession = kcSession;
this.cacheName = cache.getName(); this.cacheName = cache.getName();
this.cache = cache; this.cache = cache;
this.remoteCacheInvoker = remoteCacheInvoker; this.remoteCacheInvoker = remoteCacheInvoker;
this.lifespanMsLoader = lifespanMsLoader;
this.maxIdleTimeMsLoader = maxIdleTimeMsLoader;
} }
@ -155,7 +162,10 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
RealmModel realm = sessionUpdates.getRealm(); RealmModel realm = sessionUpdates.getRealm();
MergedUpdate<V> merged = MergedUpdate.computeUpdate(sessionUpdates.getUpdateTasks(), sessionWrapper); long lifespanMs = lifespanMsLoader.apply(realm, sessionWrapper.getEntity());
long maxIdleTimeMs = maxIdleTimeMsLoader.apply(realm, sessionWrapper.getEntity());
MergedUpdate<V> merged = MergedUpdate.computeUpdate(sessionUpdates.getUpdateTasks(), sessionWrapper, lifespanMs, maxIdleTimeMs);
if (merged != null) { if (merged != null) {
// Now run the operation in our cluster // Now run the operation in our cluster
@ -185,21 +195,25 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
case ADD: case ADD:
CacheDecorators.skipCacheStore(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, task.getMaxIdleTimeMs(), TimeUnit.MILLISECONDS);
logger.tracef("Added entity '%s' to the cache '%s' . Lifespan: %d ms, MaxIdle: %d ms", key, cache.getName(), task.getLifespanMs(), task.getMaxIdleTimeMs());
break; break;
case ADD_IF_ABSENT: case ADD_IF_ABSENT:
SessionEntityWrapper<V> existing = CacheDecorators.skipCacheStore(cache).putIfAbsent(key, sessionWrapper); SessionEntityWrapper<V> existing = CacheDecorators.skipCacheStore(cache).putIfAbsent(key, sessionWrapper, task.getLifespanMs(), TimeUnit.MILLISECONDS, task.getMaxIdleTimeMs(), TimeUnit.MILLISECONDS);
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);
// Apply updates on the existing entity and replace it // Apply updates on the existing entity and replace it
task.runUpdate(existing.getEntity()); task.runUpdate(existing.getEntity());
replace(key, task, existing); replace(key, task, existing, task.getLifespanMs(), task.getMaxIdleTimeMs());
} else {
logger.tracef("Add_if_absent successfully called for entity '%s' to the cache '%s' . Lifespan: %d ms, MaxIdle: %d ms", key, cache.getName(), task.getLifespanMs(), task.getMaxIdleTimeMs());
} }
break; break;
case REPLACE: case REPLACE:
replace(key, task, sessionWrapper); replace(key, task, sessionWrapper, task.getLifespanMs(), task.getMaxIdleTimeMs());
break; break;
default: default:
throw new IllegalStateException("Unsupported state " + operation); throw new IllegalStateException("Unsupported state " + operation);
@ -208,7 +222,7 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
} }
private void replace(K key, MergedUpdate<V> task, SessionEntityWrapper<V> oldVersionEntity) { private void replace(K key, MergedUpdate<V> task, SessionEntityWrapper<V> oldVersionEntity, long lifespanMs, long maxIdleTimeMs) {
boolean replaced = false; boolean replaced = false;
int iteration = 0; int iteration = 0;
V session = oldVersionEntity.getEntity(); V session = oldVersionEntity.getEntity();
@ -219,7 +233,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 = CacheDecorators.skipCacheStore(cache).replace(key, oldVersionEntity, newVersionEntity); replaced = CacheDecorators.skipCacheStore(cache).replace(key, oldVersionEntity, newVersionEntity, lifespanMs, TimeUnit.MILLISECONDS, maxIdleTimeMs, TimeUnit.MILLISECONDS);
// 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) {
@ -239,7 +253,7 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
task.runUpdate(session); task.runUpdate(session);
} else { } else {
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.tracef("Replace SUCCESS for entity: %s . old version: %s, new version: %s", key, oldVersionEntity.getVersion(), newVersionEntity.getVersion()); logger.tracef("Replace SUCCESS for entity: %s . old version: %s, new version: %s, Lifespan: %d ms, MaxIdle: %d ms", key, oldVersionEntity.getVersion(), newVersionEntity.getVersion(), task.getLifespanMs(), task.getMaxIdleTimeMs());
} }
} }
} }

View file

@ -20,21 +20,29 @@ package org.keycloak.models.sessions.infinispan.changes;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import org.jboss.logging.Logger;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
class MergedUpdate<S extends SessionEntity> implements SessionUpdateTask<S> { public class MergedUpdate<S extends SessionEntity> implements SessionUpdateTask<S> {
private List<SessionUpdateTask<S>> childUpdates = new LinkedList<>(); private static final Logger logger = Logger.getLogger(MergedUpdate.class);
private final List<SessionUpdateTask<S>> childUpdates = new LinkedList<>();
private CacheOperation operation; private CacheOperation operation;
private CrossDCMessageStatus crossDCMessageStatus; private CrossDCMessageStatus crossDCMessageStatus;
private final long lifespanMs;
private final long maxIdleTimeMs;
private MergedUpdate(CacheOperation operation, CrossDCMessageStatus crossDCMessageStatus) { private MergedUpdate(CacheOperation operation, CrossDCMessageStatus crossDCMessageStatus, long lifespanMs, long maxIdleTimeMs) {
this.operation = operation; this.operation = operation;
this.crossDCMessageStatus = crossDCMessageStatus; this.crossDCMessageStatus = crossDCMessageStatus;
this.lifespanMs = lifespanMs;
this.maxIdleTimeMs = maxIdleTimeMs;
} }
@Override @Override
@ -54,8 +62,16 @@ class MergedUpdate<S extends SessionEntity> implements SessionUpdateTask<S> {
return crossDCMessageStatus; return crossDCMessageStatus;
} }
public long getLifespanMs() {
return lifespanMs;
}
public static <S extends SessionEntity> MergedUpdate<S> computeUpdate(List<SessionUpdateTask<S>> childUpdates, SessionEntityWrapper<S> sessionWrapper) { public long getMaxIdleTimeMs() {
return maxIdleTimeMs;
}
public static <S extends SessionEntity> MergedUpdate<S> computeUpdate(List<SessionUpdateTask<S>> childUpdates, SessionEntityWrapper<S> sessionWrapper, long lifespanMs, long maxIdleTimeMs) {
if (childUpdates == null || childUpdates.isEmpty()) { if (childUpdates == null || childUpdates.isEmpty()) {
return null; return null;
} }
@ -64,14 +80,21 @@ class MergedUpdate<S extends SessionEntity> implements SessionUpdateTask<S> {
S session = sessionWrapper.getEntity(); S session = sessionWrapper.getEntity();
for (SessionUpdateTask<S> child : childUpdates) { for (SessionUpdateTask<S> child : childUpdates) {
if (result == null) { if (result == null) {
result = new MergedUpdate<>(child.getOperation(session), child.getCrossDCMessageStatus(sessionWrapper)); CacheOperation operation = child.getOperation(session);
if (lifespanMs == SessionTimeouts.ENTRY_EXPIRED_FLAG || maxIdleTimeMs == SessionTimeouts.ENTRY_EXPIRED_FLAG) {
operation = CacheOperation.REMOVE;
logger.tracef("Entry '%s' is expired. Will remove it from the cache", sessionWrapper);
}
result = new MergedUpdate<>(operation, child.getCrossDCMessageStatus(sessionWrapper), lifespanMs, maxIdleTimeMs);
result.childUpdates.add(child); result.childUpdates.add(child);
} else { } else {
// Merge the operations. REMOVE is special case as other operations are not needed then. // Merge the operations. REMOVE is special case as other operations are not needed then.
CacheOperation mergedOp = result.getOperation(session).merge(child.getOperation(session), session); CacheOperation mergedOp = result.getOperation(session).merge(child.getOperation(session), session);
if (mergedOp == CacheOperation.REMOVE) { if (mergedOp == CacheOperation.REMOVE) {
result = new MergedUpdate<>(child.getOperation(session), child.getCrossDCMessageStatus(sessionWrapper)); result = new MergedUpdate<>(child.getOperation(session), child.getCrossDCMessageStatus(sessionWrapper), lifespanMs, maxIdleTimeMs);
result.childUpdates.add(child); result.childUpdates.add(child);
return result; return result;
} }

View file

@ -30,11 +30,6 @@ public interface SessionUpdateTask<S extends SessionEntity> {
CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<S> sessionWrapper); CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<S> sessionWrapper);
default long getLifespanMs() {
return -1;
}
enum CacheOperation { enum CacheOperation {
ADD, ADD,

View file

@ -32,6 +32,7 @@ import org.jboss.logging.Logger;
import org.keycloak.connections.infinispan.TopologyInfo; import org.keycloak.connections.infinispan.TopologyInfo;
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.changes.MergedUpdate;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask; import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
@ -57,7 +58,7 @@ public class RemoteCacheInvoker {
} }
public <K, V extends SessionEntity> void runTask(KeycloakSession kcSession, RealmModel realm, String cacheName, K key, SessionUpdateTask<V> task, SessionEntityWrapper<V> sessionWrapper) { public <K, V extends SessionEntity> void runTask(KeycloakSession kcSession, RealmModel realm, String cacheName, K key, MergedUpdate<V> task, SessionEntityWrapper<V> sessionWrapper) {
RemoteCacheContext context = remoteCaches.get(cacheName); RemoteCacheContext context = remoteCaches.get(cacheName);
if (context == null) { if (context == null) {
return; return;
@ -104,7 +105,7 @@ public class RemoteCacheInvoker {
} }
private <K, V extends SessionEntity> void runOnRemoteCache(TopologyInfo topology, RemoteCache<K, SessionEntityWrapper<V>> remoteCache, long maxIdleMs, K key, SessionUpdateTask<V> task, SessionEntityWrapper<V> sessionWrapper) { private <K, V extends SessionEntity> void runOnRemoteCache(TopologyInfo topology, RemoteCache<K, SessionEntityWrapper<V>> remoteCache, long maxIdleMs, K key, MergedUpdate<V> task, SessionEntityWrapper<V> sessionWrapper) {
final V session = sessionWrapper.getEntity(); final V session = sessionWrapper.getEntity();
SessionUpdateTask.CacheOperation operation = task.getOperation(session); SessionUpdateTask.CacheOperation operation = task.getOperation(session);
@ -113,12 +114,14 @@ public class RemoteCacheInvoker {
remoteCache.remove(key); remoteCache.remove(key);
break; break;
case ADD: case ADD:
remoteCache.put(key, sessionWrapper.forTransport(), task.getLifespanMs(), TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS); remoteCache.put(key, sessionWrapper.forTransport(),
InfinispanUtil.toHotrodTimeMs(remoteCache, task.getLifespanMs()), TimeUnit.MILLISECONDS,
InfinispanUtil.toHotrodTimeMs(remoteCache, maxIdleMs), TimeUnit.MILLISECONDS);
break; break;
case ADD_IF_ABSENT: case ADD_IF_ABSENT:
SessionEntityWrapper<V> existing = remoteCache SessionEntityWrapper<V> existing = remoteCache
.withFlags(Flag.FORCE_RETURN_VALUE) .withFlags(Flag.FORCE_RETURN_VALUE)
.putIfAbsent(key, sessionWrapper.forTransport(), -1, TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS); .putIfAbsent(key, sessionWrapper.forTransport(), -1, TimeUnit.MILLISECONDS, InfinispanUtil.toHotrodTimeMs(remoteCache, 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);
@ -135,6 +138,10 @@ public class RemoteCacheInvoker {
private <K, V extends SessionEntity> void replace(TopologyInfo topology, RemoteCache<K, SessionEntityWrapper<V>> remoteCache, long lifespanMs, long maxIdleMs, K key, SessionUpdateTask<V> task) { private <K, V extends SessionEntity> void replace(TopologyInfo topology, RemoteCache<K, SessionEntityWrapper<V>> remoteCache, long lifespanMs, long maxIdleMs, K key, SessionUpdateTask<V> task) {
// Adjust based on the hotrod protocol
lifespanMs = InfinispanUtil.toHotrodTimeMs(remoteCache, lifespanMs);
maxIdleMs = InfinispanUtil.toHotrodTimeMs(remoteCache, maxIdleMs);
boolean replaced = false; boolean replaced = false;
int replaceIteration = 0; int replaceIteration = 0;
while (!replaced && replaceIteration < InfinispanUtil.MAXIMUM_REPLACE_RETRIES) { while (!replaced && replaceIteration < InfinispanUtil.MAXIMUM_REPLACE_RETRIES) {

View file

@ -32,12 +32,20 @@ import org.jboss.logging.Logger;
import org.keycloak.connections.infinispan.TopologyInfo; import org.keycloak.connections.infinispan.TopologyInfo;
import org.keycloak.executors.ExecutorsProvider; import org.keycloak.executors.ExecutorsProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
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.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import java.util.Random; import java.util.Random;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import org.infinispan.client.hotrod.VersionedValue; import org.infinispan.client.hotrod.VersionedValue;
import org.keycloak.models.utils.KeycloakModelUtils;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -53,18 +61,26 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
private RemoteCache<K, SessionEntityWrapper<V>> remoteCache; private RemoteCache<K, SessionEntityWrapper<V>> remoteCache;
private TopologyInfo topologyInfo; private TopologyInfo topologyInfo;
private ClientListenerExecutorDecorator<K> executor; private ClientListenerExecutorDecorator<K> executor;
private BiFunction<RealmModel, V, Long> lifespanMsLoader;
private BiFunction<RealmModel, V, Long> maxIdleTimeMsLoader;
private KeycloakSessionFactory sessionFactory;
protected RemoteCacheSessionListener() { protected RemoteCacheSessionListener() {
} }
protected void init(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> cache, RemoteCache<K, SessionEntityWrapper<V>> remoteCache) { protected void init(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> cache, RemoteCache<K, SessionEntityWrapper<V>> remoteCache,
BiFunction<RealmModel, V, Long> lifespanMsLoader, BiFunction<RealmModel, V, Long> maxIdleTimeMsLoader) {
this.cache = cache; this.cache = cache;
this.remoteCache = remoteCache; this.remoteCache = remoteCache;
this.topologyInfo = InfinispanUtil.getTopologyInfo(session); this.topologyInfo = InfinispanUtil.getTopologyInfo(session);
this.lifespanMsLoader = lifespanMsLoader;
this.maxIdleTimeMsLoader = maxIdleTimeMsLoader;
this.sessionFactory = session.getKeycloakSessionFactory();
ExecutorService executor = session.getProvider(ExecutorsProvider.class).getExecutor("client-listener-" + cache.getName()); ExecutorService executor = session.getProvider(ExecutorsProvider.class).getExecutor("client-listener-" + cache.getName());
this.executor = new ClientListenerExecutorDecorator<>(executor); this.executor = new ClientListenerExecutorDecorator<>(executor);
} }
@ -107,8 +123,7 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
// Maybe can happen under some circumstances that remoteCache doesn't yet contain the value sent in the event (maybe just theoretically...) // Maybe can happen under some circumstances that remoteCache doesn't yet contain the value sent in the event (maybe just theoretically...)
if (remoteSessionVersioned == null || remoteSessionVersioned.getValue() == null) { if (remoteSessionVersioned == null || remoteSessionVersioned.getValue() == null) {
logger.debugf("Entity '%s' not present in remoteCache. Ignoring create", logger.debugf("Entity '%s' not present in remoteCache. Ignoring create", key);
key.toString());
return; return;
} }
@ -116,36 +131,46 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
V remoteSession = remoteSessionVersioned.getValue().getEntity(); V remoteSession = remoteSessionVersioned.getValue().getEntity();
SessionEntityWrapper<V> newWrapper = new SessionEntityWrapper<>(remoteSession); SessionEntityWrapper<V> newWrapper = new SessionEntityWrapper<>(remoteSession);
logger.debugf("Read session entity wrapper from the remote cache: %s", remoteSession.toString()); logger.debugf("Read session entity wrapper from the remote cache: %s", remoteSession);
// Using putIfAbsent. Theoretic possibility that entity was already put to cache by someone else KeycloakModelUtils.runJobInTransaction(sessionFactory, (session -> {
cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
.putIfAbsent(key, newWrapper); RealmModel realm = session.realms().getRealm(newWrapper.getEntity().getRealmId());
long lifespanMs = lifespanMsLoader.apply(realm, newWrapper.getEntity());
long maxIdleTimeMs = maxIdleTimeMsLoader.apply(realm, newWrapper.getEntity());
logger.tracef("Calling putIfAbsent for entity '%s' in the cache '%s' . lifespan: %d ms, maxIdleTime: %d ms", key, remoteCache.getName(), lifespanMs, maxIdleTimeMs);
// Using putIfAbsent. Theoretic possibility that entity was already put to cache by someone else
cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
.putIfAbsent(key, newWrapper, lifespanMs, TimeUnit.MILLISECONDS, maxIdleTimeMs, TimeUnit.MILLISECONDS);
}));
} }
protected void replaceRemoteEntityInCache(K key, long eventVersion) { protected void replaceRemoteEntityInCache(K key, long eventVersion) {
// TODO can be optimized and remoteSession sent in the event itself? // TODO can be optimized and remoteSession sent in the event itself?
boolean replaced = false; AtomicBoolean replaced = new AtomicBoolean(false);
int replaceRetries = 0; int replaceRetries = 0;
int sleepInterval = 25; int sleepInterval = 25;
do { do {
replaceRetries++; replaceRetries++;
SessionEntityWrapper<V> localEntityWrapper = cache.get(key); SessionEntityWrapper<V> localEntityWrapper = cache.get(key);
VersionedValue<SessionEntityWrapper<V>> remoteSessionVersioned = remoteCache.getWithMetadata(key); VersionedValue<SessionEntityWrapper<V>> remoteSessionVersioned = remoteCache.getWithMetadata(key);
// Probably already removed // Probably already removed
if (remoteSessionVersioned == null || remoteSessionVersioned.getValue() == 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);
return; return;
} }
if (remoteSessionVersioned.getVersion() < eventVersion) { if (remoteSessionVersioned.getVersion() < eventVersion) {
try { try {
logger.debugf("Got replace remote entity event prematurely for entity '%s', will try again. Event version: %d, got: %d", logger.debugf("Got replace remote entity event prematurely for entity '%s', will try again. Event version: %d, got: %d",
key.toString(), eventVersion, remoteSessionVersioned == null ? -1 : remoteSessionVersioned.getVersion()); key, eventVersion, remoteSessionVersioned == null ? -1 : remoteSessionVersioned.getVersion());
Thread.sleep(new Random().nextInt(sleepInterval)); // using exponential backoff Thread.sleep(new Random().nextInt(sleepInterval)); // using exponential backoff
continue; continue;
} catch (InterruptedException ex) { } catch (InterruptedException ex) {
@ -156,18 +181,26 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
} }
SessionEntity remoteSession = remoteSessionVersioned.getValue().getEntity(); 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, replaceRetries);
SessionEntityWrapper<V> sessionWrapper = remoteSession.mergeRemoteEntityWithLocalEntity(localEntityWrapper); SessionEntityWrapper<V> sessionWrapper = remoteSession.mergeRemoteEntityWithLocalEntity(localEntityWrapper);
// We received event from remoteCache, so we won't update it back KeycloakModelUtils.runJobInTransaction(sessionFactory, (session -> {
replaced = cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
.replace(key, localEntityWrapper, sessionWrapper);
if (! replaced) { RealmModel realm = session.realms().getRealm(sessionWrapper.getEntity().getRealmId());
logger.debugf("Did not succeed in merging sessions, will try again: %s", remoteSession.toString()); long lifespanMs = lifespanMsLoader.apply(realm, sessionWrapper.getEntity());
long maxIdleTimeMs = maxIdleTimeMsLoader.apply(realm, sessionWrapper.getEntity());
// We received event from remoteCache, so we won't update it back
replaced.set(cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
.replace(key, localEntityWrapper, sessionWrapper, lifespanMs, TimeUnit.MILLISECONDS, maxIdleTimeMs, TimeUnit.MILLISECONDS));
}));
if (! replaced.get()) {
logger.debugf("Did not succeed in merging sessions, will try again: %s", remoteSession);
} }
} while (replaceRetries < MAXIMUM_REPLACE_RETRIES && ! replaced); } while (replaceRetries < MAXIMUM_REPLACE_RETRIES && ! replaced.get());
} }
@ -203,7 +236,7 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
result = topologyInfo.amIOwner(cache, key); result = topologyInfo.amIOwner(cache, key);
} }
logger.debugf("Received event from remote store. Event '%s', key '%s', skip '%b'", type.toString(), key, !result); logger.debugf("Received event from remote store. Event '%s', key '%s', skip '%b'", type, key, !result);
return result; return result;
} }
@ -220,7 +253,8 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
} }
public static <K, V extends SessionEntity> RemoteCacheSessionListener createListener(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> cache, RemoteCache<K, SessionEntityWrapper<V>> remoteCache) { public static <K, V extends SessionEntity> RemoteCacheSessionListener createListener(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> cache, RemoteCache<K, SessionEntityWrapper<V>> remoteCache,
BiFunction<RealmModel, V, Long> lifespanMsLoader, BiFunction<RealmModel, V, Long> maxIdleTimeMsLoader) {
/*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.
@ -235,7 +269,7 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
}*/ }*/
RemoteCacheSessionListener<K, V> listener = new RemoteCacheSessionListener<>(); RemoteCacheSessionListener<K, V> listener = new RemoteCacheSessionListener<>();
listener.init(session, cache, remoteCache); listener.init(session, cache, remoteCache, lifespanMsLoader, maxIdleTimeMsLoader);
return listener; return listener;
} }

View file

@ -18,12 +18,16 @@
package org.keycloak.models.sessions.infinispan.util; package org.keycloak.models.sessions.infinispan.util;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.infinispan.Cache; import org.infinispan.Cache;
import org.infinispan.client.hotrod.ProtocolVersion;
import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.commons.api.BasicCache;
import org.infinispan.persistence.manager.PersistenceManager; import org.infinispan.persistence.manager.PersistenceManager;
import org.infinispan.persistence.remote.RemoteStore; import org.infinispan.persistence.remote.RemoteStore;
import org.infinispan.remoting.transport.Transport; import org.infinispan.remoting.transport.Transport;
import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.connections.infinispan.TopologyInfo; import org.keycloak.connections.infinispan.TopologyInfo;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -66,4 +70,26 @@ public class InfinispanUtil {
return transport == null || transport.isCoordinator(); return transport == null || transport.isCoordinator();
} }
/**
* Convert the given value to the proper value, which can be used when calling operations for the infinispan remoteCache.
*
* Infinispan HotRod protocol of versions older than 3.0 uses the "lifespan" or "maxIdle" as the normal expiration time when the value is 30 days or less.
* However for the bigger values, it assumes that the value is unix timestamp.
*
* @param ispnCache
* @param lifespanOrigMs
* @return
*/
public static long toHotrodTimeMs(BasicCache ispnCache, long lifespanOrigMs) {
if (ispnCache instanceof RemoteCache && lifespanOrigMs > 2592000000L) {
RemoteCache remoteCache = (RemoteCache) ispnCache;
ProtocolVersion protocolVersion = remoteCache.getRemoteCacheManager().getConfiguration().version();
if (ProtocolVersion.PROTOCOL_VERSION_30.compareTo(protocolVersion) > 0) {
return Time.currentTimeMillis() + lifespanOrigMs;
}
}
return lifespanOrigMs;
}
} }

View file

@ -0,0 +1,293 @@
/*
* Copyright 2020 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.util;
import org.keycloak.common.util.Time;
import org.keycloak.models.RealmModel;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.utils.SessionTimeoutHelper;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SessionTimeouts {
/**
* This indicates that entry is already expired and should be removed from the cache
*/
public static final long ENTRY_EXPIRED_FLAG = -2l;
/**
* This is used just if timeouts are not set on the realm (usually happens just during tests when realm is created manually with the model API)
*/
public static final int MINIMAL_EXPIRATION_SEC = 300;
/**
* Get the maximum lifespan, which this userSession can remain in the infinispan cache.
* Returned value will be used as "lifespan" when calling put/replace operation in the infinispan cache for this entity
*
* @param realm
* @param userSessionEntity
* @return
*/
public static long getUserSessionLifespanMs(RealmModel realm, UserSessionEntity userSessionEntity) {
int timeSinceSessionStart = Time.currentTime() - userSessionEntity.getStarted();
int sessionMaxLifespan = Math.max(realm.getSsoSessionMaxLifespan(), MINIMAL_EXPIRATION_SEC);
if (userSessionEntity.isRememberMe()) {
sessionMaxLifespan = Math.max(realm.getSsoSessionMaxLifespanRememberMe(), sessionMaxLifespan);
}
long timeToExpire = sessionMaxLifespan - timeSinceSessionStart;
// Indication that entry should be expired
if (timeToExpire <=0) {
return ENTRY_EXPIRED_FLAG;
}
return Time.toMillis(timeToExpire);
}
/**
* Get the maximum idle time for this userSession.
* Returned value will be used when as "maxIdleTime" when calling put/replace operation in the infinispan cache for this entity
*
* @param realm
* @param userSessionEntity
* @return
*/
public static long getUserSessionMaxIdleMs(RealmModel realm, UserSessionEntity userSessionEntity) {
int timeSinceLastRefresh = Time.currentTime() - userSessionEntity.getLastSessionRefresh();
int sessionIdleMs = Math.max(realm.getSsoSessionIdleTimeout(), MINIMAL_EXPIRATION_SEC);
if (userSessionEntity.isRememberMe()) {
sessionIdleMs = Math.max(realm.getSsoSessionIdleTimeoutRememberMe(), sessionIdleMs);
}
long maxIdleTime = sessionIdleMs - timeSinceLastRefresh + SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
// Indication that entry should be expired
if (maxIdleTime <=0) {
return ENTRY_EXPIRED_FLAG;
}
return Time.toMillis(maxIdleTime);
}
/**
* Get the maximum lifespan, which this clientSession can remain in the infinispan cache.
* Returned value will be used as "lifespan" when calling put/replace operation in the infinispan cache for this entity
*
* @param realm
* @param clientSessionEntity
* @return
*/
public static long getClientSessionLifespanMs(RealmModel realm, AuthenticatedClientSessionEntity clientSessionEntity) {
int timeSinceTimestampUpdate = Time.currentTime() - clientSessionEntity.getTimestamp();
int sessionMaxLifespan = Math.max(realm.getSsoSessionMaxLifespan(), realm.getSsoSessionMaxLifespanRememberMe());
// clientSession max lifespan has preference if set
if (realm.getClientSessionMaxLifespan() > 0) {
sessionMaxLifespan = realm.getClientSessionMaxLifespan();
}
sessionMaxLifespan = Math.max(sessionMaxLifespan, MINIMAL_EXPIRATION_SEC);
long timeToExpire = sessionMaxLifespan - timeSinceTimestampUpdate;
// Indication that entry should be expired
if (timeToExpire <=0) {
return ENTRY_EXPIRED_FLAG;
}
return Time.toMillis(timeToExpire);
}
/**
* Get the maxIdle, which this clientSession will use.
* Returned value will be used as "maxIdle" when calling put/replace operation in the infinispan cache for this entity
*
* @param realm
* @param clientSessionEntity
* @return
*/
public static long getClientSessionMaxIdleMs(RealmModel realm, AuthenticatedClientSessionEntity clientSessionEntity) {
int timeSinceTimestampUpdate = Time.currentTime() - clientSessionEntity.getTimestamp();
int sessionIdleTimeout = Math.max(realm.getSsoSessionIdleTimeout(), realm.getSsoSessionIdleTimeoutRememberMe());
// clientSession idle timeout has preference if set
if (realm.getClientSessionIdleTimeout() > 0) {
sessionIdleTimeout = realm.getClientSessionIdleTimeout();
}
sessionIdleTimeout = Math.max(sessionIdleTimeout, MINIMAL_EXPIRATION_SEC);
long timeToExpire = sessionIdleTimeout - timeSinceTimestampUpdate + SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
// Indication that entry should be expired
if (timeToExpire <=0) {
return ENTRY_EXPIRED_FLAG;
}
return Time.toMillis(timeToExpire);
}
/**
* Get the maximum lifespan, which this offline userSession can remain in the infinispan cache.
* Returned value will be used as "lifespan" when calling put/replace operation in the infinispan cache for this entity
*
* @param realm
* @param userSessionEntity
* @return
*/
public static long getOfflineSessionLifespanMs(RealmModel realm, UserSessionEntity userSessionEntity) {
// By default, this is disabled, so offlineSessions have just "maxIdle"
if (!realm.isOfflineSessionMaxLifespanEnabled()) return -1l;
int timeSinceSessionStart = Time.currentTime() - userSessionEntity.getStarted();
int sessionMaxLifespan = Math.max(realm.getOfflineSessionMaxLifespan(), MINIMAL_EXPIRATION_SEC);
long timeToExpire = sessionMaxLifespan - timeSinceSessionStart;
// Indication that entry should be expired
if (timeToExpire <=0) {
return ENTRY_EXPIRED_FLAG;
}
return Time.toMillis(timeToExpire);
}
/**
* Get the maximum idle time for this offline userSession.
* Returned value will be used when as "maxIdleTime" when calling put/replace operation in the infinispan cache for this entity
*
* @param realm
* @param userSessionEntity
* @return
*/
public static long getOfflineSessionMaxIdleMs(RealmModel realm, UserSessionEntity userSessionEntity) {
int timeSinceLastRefresh = Time.currentTime() - userSessionEntity.getLastSessionRefresh();
int sessionIdle = Math.max(realm.getOfflineSessionIdleTimeout(), MINIMAL_EXPIRATION_SEC);
long maxIdleTime = sessionIdle - timeSinceLastRefresh + SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
// Indication that entry should be expired
if (maxIdleTime <=0) {
return ENTRY_EXPIRED_FLAG;
}
return Time.toMillis(maxIdleTime);
}
/**
* Get the maximum lifespan, which this offline clientSession can remain in the infinispan cache.
* Returned value will be used as "lifespan" when calling put/replace operation in the infinispan cache for this entity
*
* @param realm
* @param authenticatedClientSessionEntity
* @return
*/
public static long getOfflineClientSessionLifespanMs(RealmModel realm, AuthenticatedClientSessionEntity authenticatedClientSessionEntity) {
// By default, this is disabled, so offlineSessions have just "maxIdle"
if (!realm.isOfflineSessionMaxLifespanEnabled() && realm.getClientOfflineSessionMaxLifespan() <= 0) return -1l;
int timeSinceTimestamp = Time.currentTime() - authenticatedClientSessionEntity.getTimestamp();
int sessionMaxLifespan = Math.max(realm.getOfflineSessionMaxLifespan(), MINIMAL_EXPIRATION_SEC);
// clientSession max lifespan has preference if set
if (realm.getClientOfflineSessionMaxLifespan() > 0) {
sessionMaxLifespan = realm.getClientOfflineSessionMaxLifespan();
}
long timeToExpire = sessionMaxLifespan - timeSinceTimestamp;
// Indication that entry should be expired
if (timeToExpire <=0) {
return ENTRY_EXPIRED_FLAG;
}
return Time.toMillis(timeToExpire);
}
/**
* Get the maxIdle, which this offline clientSession will use.
* Returned value will be used as "maxIdle" when calling put/replace operation in the infinispan cache for this entity
*
* @param realm
* @param authenticatedClientSessionEntity
* @return
*/
public static long getOfflineClientSessionMaxIdleMs(RealmModel realm, AuthenticatedClientSessionEntity authenticatedClientSessionEntity) {
int timeSinceLastRefresh = Time.currentTime() - authenticatedClientSessionEntity.getTimestamp();
int sessionIdle = Math.max(realm.getOfflineSessionIdleTimeout(), MINIMAL_EXPIRATION_SEC);
// clientSession idle timeout has preference if set
if (realm.getClientOfflineSessionIdleTimeout() > 0) {
sessionIdle = realm.getClientOfflineSessionIdleTimeout();
}
long maxIdleTime = sessionIdle - timeSinceLastRefresh + SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
// Indication that entry should be expired
if (maxIdleTime <=0) {
return ENTRY_EXPIRED_FLAG;
}
return Time.toMillis(maxIdleTime);
}
/**
* Not using lifespan for detached login failure (backwards compatibility with the background cleaner threads, which were used for cleanup of detached login failures)
*
* @param realm
* @param loginFailureEntity
* @return
*/
public static long getLoginFailuresLifespanMs(RealmModel realm, LoginFailureEntity loginFailureEntity) {
return -1l;
}
/**
* Not using maxIdle for detached login failure (backwards compatibility with the background cleaner threads, which were used for cleanup of detached login failures)
*
* @param realm
* @param loginFailureEntity
* @return
*/
public static long getLoginFailuresMaxIdleMs(RealmModel realm, LoginFailureEntity loginFailureEntity) {
return -1l;
}
}

View file

@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import org.infinispan.Cache; import org.infinispan.Cache;
@ -154,6 +155,10 @@ public class ConcurrencyJDGCacheReplaceTest {
// Create caches, listeners and finally worker threads // Create caches, listeners and finally worker threads
remoteCache1 = InfinispanUtil.getRemoteCache(cache1); remoteCache1 = InfinispanUtil.getRemoteCache(cache1);
remoteCache2 = InfinispanUtil.getRemoteCache(cache2); remoteCache2 = InfinispanUtil.getRemoteCache(cache2);
// Manual test of lifespans
testLifespans();
Thread worker1 = createWorker(cache1, 1); Thread worker1 = createWorker(cache1, 1);
Thread worker2 = createWorker(cache2, 2); Thread worker2 = createWorker(cache2, 2);
@ -375,6 +380,30 @@ public class ConcurrencyJDGCacheReplaceTest {
private enum ReplaceStatus { private enum ReplaceStatus {
REPLACED, NOT_REPLACED, ERROR REPLACED, NOT_REPLACED, ERROR
} }
private static void testLifespans() throws Exception {
long l1 = InfinispanUtil.toHotrodTimeMs(remoteCache1, 5000);
long l2 = InfinispanUtil.toHotrodTimeMs(remoteCache2, 2592000000L);
long l3 = InfinispanUtil.toHotrodTimeMs(remoteCache2, 2592000001L);
//long l4 = InfinispanUtil.getLifespanMs(remoteCache1, Time.currentTimeMillis() + 5000);
remoteCache1.put("k1", "v1", l1, TimeUnit.MILLISECONDS);
remoteCache1.put("k2", "v2", l2, TimeUnit.MILLISECONDS);
remoteCache1.put("k3", "v3", l3, TimeUnit.MILLISECONDS);
remoteCache1.put("k4", "v4", Time.currentTimeMillis() + 5000, TimeUnit.MILLISECONDS);
System.out.println("l1=" + l1 + ", l2=" + l2 + ", l3=" + l3);
System.out.println("k1=" + remoteCache1.get("k1") + ", k2=" + remoteCache1.get("k2") + ", k3=" + remoteCache1.get("k3") + ", k4=" + remoteCache1.get("k4"));
Thread.sleep(4000);
System.out.println("k1=" + remoteCache1.get("k1") + ", k2=" + remoteCache1.get("k2") + ", k3=" + remoteCache1.get("k3") + ", k4=" + remoteCache1.get("k4"));
Thread.sleep(2000);
System.out.println("k1=" + remoteCache1.get("k1") + ", k2=" + remoteCache1.get("k2") + ", k3=" + remoteCache1.get("k3") + ", k4=" + remoteCache1.get("k4"));
}
/* /*
// Worker, which operates on "classic" cache and rely on operations delegated to the second cache // Worker, which operates on "classic" cache and rely on operations delegated to the second cache
private static class CacheWorker extends Thread { private static class CacheWorker extends Thread {

View file

@ -131,6 +131,11 @@ public class MapRootAuthenticationSessionProvider implements AuthenticationSessi
tx.delete(UUID.fromString(authenticationSession.getId())); tx.delete(UUID.fromString(authenticationSession.getId()));
} }
@Override
public void removeAllExpired() {
session.realms().getRealmsStream().forEach(this::removeExpired);
}
@Override @Override
public void removeExpired(RealmModel realm) { public void removeExpired(RealmModel realm) {
Objects.requireNonNull(realm, "The provided realm can't be null!"); Objects.requireNonNull(realm, "The provided realm can't be null!");

View file

@ -138,6 +138,11 @@ public interface UserSessionProvider extends Provider {
void removeUserSession(RealmModel realm, UserSessionModel session); void removeUserSession(RealmModel realm, UserSessionModel session);
void removeUserSessions(RealmModel realm, UserModel user); void removeUserSessions(RealmModel realm, UserModel user);
/**
* Remove expired user sessions and client sessions in all the realms
*/
void removeAllExpired();
/** /**
* Removes expired user sessions owned by this realm from this provider. * Removes expired user sessions owned by this realm from this provider.
* If this `UserSessionProvider` uses `UserSessionPersister`, the removal of the expired * If this `UserSessionProvider` uses `UserSessionPersister`, the removal of the expired

View file

@ -18,6 +18,7 @@
package org.keycloak.sessions; package org.keycloak.sessions;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
@ -71,6 +72,11 @@ public interface AuthenticationSessionProvider extends Provider {
*/ */
void removeRootAuthenticationSession(RealmModel realm, RootAuthenticationSessionModel authenticationSession); void removeRootAuthenticationSession(RealmModel realm, RootAuthenticationSessionModel authenticationSession);
/**
* Remove expired authentication sessions in all the realms
*/
void removeAllExpired();
/** /**
* Removes all expired root authentication sessions for the given realm. * Removes all expired root authentication sessions for the given realm.
* @param realm {@code RealmModel} Can't be {@code null}. * @param realm {@code RealmModel} Can't be {@code null}.

View file

@ -20,7 +20,6 @@ package org.keycloak.services.scheduled;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
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.UserSessionProvider;
import org.keycloak.timer.ScheduledTask; import org.keycloak.timer.ScheduledTask;
/** /**
@ -36,11 +35,8 @@ public class ClearExpiredUserSessions implements ScheduledTask {
public void run(KeycloakSession session) { public void run(KeycloakSession session) {
long currentTimeMillis = Time.currentTimeMillis(); long currentTimeMillis = Time.currentTimeMillis();
UserSessionProvider sessions = session.sessions(); session.authenticationSessions().removeAllExpired();
session.realms().getRealmsStream().forEach(realm -> { session.sessions().removeAllExpired();
sessions.removeExpired(realm);
session.authenticationSessions().removeExpired(realm);
});
long took = Time.currentTimeMillis() - currentTimeMillis; long took = Time.currentTimeMillis() - currentTimeMillis;
logger.debugf("ClearExpiredUserSessions finished in %d ms", took); logger.debugf("ClearExpiredUserSessions finished in %d ms", took);

View file

@ -0,0 +1,94 @@
/*
* Copyright 2020 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.testsuite.model.infinispan;
import org.infinispan.commons.time.TimeService;
import org.infinispan.factories.GlobalComponentRegistry;
import org.infinispan.factories.impl.BasicComponentRegistry;
import org.infinispan.factories.impl.ComponentRef;
import org.infinispan.manager.EmbeddedCacheManager;
import org.jboss.logging.Logger;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanTestUtil {
protected static final Logger logger = Logger.getLogger(InfinispanTestUtil.class);
private static TimeService origTimeService = null;
/**
* Set Keycloak test TimeService to infinispan cacheManager. This will cause that infinispan will be aware of Keycloak Time offset, which is useful
* for testing that infinispan entries are expired after moving Keycloak time forward with {@link org.keycloak.common.util.Time#setOffset} .
*/
public static void setTestingTimeService(KeycloakSession session) {
// Testing timeService already set. This shouldn't happen if this utility is properly used
if (origTimeService != null) {
throw new IllegalStateException("Calling setTestingTimeService when testing TimeService was already set");
}
logger.info("Will set KeycloakIspnTimeService to the infinispan cacheManager");
InfinispanConnectionProvider ispnProvider = session.getProvider(InfinispanConnectionProvider.class);
EmbeddedCacheManager cacheManager = ispnProvider.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).getCacheManager();
origTimeService = replaceComponent(cacheManager, TimeService.class, new KeycloakTestTimeService(), true);
}
public static void revertTimeService(KeycloakSession session) {
// Testing timeService not set. This shouldn't happen if this utility is properly used
if (origTimeService == null) {
throw new IllegalStateException("Calling revertTimeService when testing TimeService was not set");
}
logger.info("Revert set KeycloakIspnTimeService to the infinispan cacheManager");
InfinispanConnectionProvider ispnProvider = session.getProvider(InfinispanConnectionProvider.class);
EmbeddedCacheManager cacheManager = ispnProvider.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).getCacheManager();
replaceComponent(cacheManager, TimeService.class, origTimeService, true);
origTimeService = null;
}
/**
* Forked from org.infinispan.test.TestingUtil class
*
* Replaces a component in a running cache manager (global component registry).
*
* @param cacheMgr cache in which to replace component
* @param componentType component type of which to replace
* @param replacementComponent new instance
* @param rewire if true, ComponentRegistry.rewire() is called after replacing.
*
* @return the original component that was replaced
*/
private static <T> T replaceComponent(EmbeddedCacheManager cacheMgr, Class<T> componentType, T replacementComponent, boolean rewire) {
GlobalComponentRegistry cr = cacheMgr.getGlobalComponentRegistry();
BasicComponentRegistry bcr = cr.getComponent(BasicComponentRegistry.class);
ComponentRef<T> old = bcr.getComponent(componentType);
bcr.replaceComponent(componentType.getName(), replacementComponent, true);
if (rewire) {
cr.rewire();
cr.rewireNamedRegistries();
}
return old != null ? old.wired() : null;
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright 2020 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.testsuite.model.infinispan;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import org.infinispan.util.EmbeddedTimeService;
import org.keycloak.common.util.Time;
/**
* Infinispan TimeService, which delegates to Keycloak Time.currentTime to figure current time. Useful for testing purposes.
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class KeycloakTestTimeService extends EmbeddedTimeService {
private long getCurrentTimeMillis() {
return Time.currentTimeMillis();
}
@Override
public long wallClockTime() {
return getCurrentTimeMillis();
}
@Override
public long time() {
return TimeUnit.MILLISECONDS.toNanos(getCurrentTimeMillis());
}
@Override
public Instant instant() {
return Instant.ofEpochMilli(getCurrentTimeMillis());
}
}

View file

@ -66,6 +66,7 @@ import org.keycloak.testsuite.events.TestEventsListenerProvider;
import org.keycloak.testsuite.federation.DummyUserFederationProviderFactory; import org.keycloak.testsuite.federation.DummyUserFederationProviderFactory;
import org.keycloak.testsuite.forms.PassThroughAuthenticator; import org.keycloak.testsuite.forms.PassThroughAuthenticator;
import org.keycloak.testsuite.forms.PassThroughClientAuthenticator; import org.keycloak.testsuite.forms.PassThroughClientAuthenticator;
import org.keycloak.testsuite.model.infinispan.InfinispanTestUtil;
import org.keycloak.testsuite.rest.representation.AuthenticatorState; import org.keycloak.testsuite.rest.representation.AuthenticatorState;
import org.keycloak.testsuite.rest.resource.TestCacheResource; import org.keycloak.testsuite.rest.resource.TestCacheResource;
import org.keycloak.testsuite.rest.resource.TestJavascriptResource; import org.keycloak.testsuite.rest.resource.TestJavascriptResource;
@ -181,6 +182,22 @@ public class TestingResourceProvider implements RealmResourceProvider {
return Response.noContent().build(); return Response.noContent().build();
} }
@POST
@Path("/set-testing-infinispan-time-service")
@Produces(MediaType.APPLICATION_JSON)
public Response setTestingInfinispanTimeService() {
InfinispanTestUtil.setTestingTimeService(session);
return Response.noContent().build();
}
@POST
@Path("/revert-testing-infinispan-time-service")
@Produces(MediaType.APPLICATION_JSON)
public Response revertTestingInfinispanTimeService() {
InfinispanTestUtil.revertTimeService(session);
return Response.noContent().build();
}
@GET @GET
@Path("/get-client-sessions-count") @Path("/get-client-sessions-count")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)

View file

@ -107,6 +107,13 @@ public class TestCacheResource {
cache.remove(id); cache.remove(id);
} }
@POST
@Path("/process-expiration")
@Produces(MediaType.APPLICATION_JSON)
public void processExpiration() {
cache.getAdvancedCache().getExpirationManager().processExpiration();
}
@GET @GET
@Path("/jgroups-stats") @Path("/jgroups-stats")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)

View file

@ -39,6 +39,7 @@
<module name="org.keycloak.keycloak-model-jpa"/> <module name="org.keycloak.keycloak-model-jpa"/>
<module name="org.keycloak.keycloak-ldap-federation"/> <module name="org.keycloak.keycloak-ldap-federation"/>
<module name="org.infinispan"/> <module name="org.infinispan"/>
<module name="org.infinispan.commons"/>
<module name="org.infinispan.client.hotrod"/> <module name="org.infinispan.client.hotrod"/>
<module name="org.jgroups"/> <module name="org.jgroups"/>
<module name="org.jboss.logging"/> <module name="org.jboss.logging"/>

View file

@ -66,6 +66,15 @@ public interface TestingCacheResource {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
void removeKey(@PathParam("id") String id); void removeKey(@PathParam("id") String id);
/**
* Enforce calling of the expiration on the particular infinispan cache. This will immediately expire the expired cache entries, so that they won't be available in the cache.
* Without calling this, expired entries would be removed by the infinispan expiration (probably by infinispan periodic background cleaner task)
*/
@POST
@Path("/process-expiration")
@Produces(MediaType.APPLICATION_JSON)
void processExpiration();
@GET @GET
@Path("/jgroups-stats") @Path("/jgroups-stats")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)

View file

@ -196,6 +196,20 @@ public interface TestingResource {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
void removeExpired(@QueryParam("realm") final String realm); void removeExpired(@QueryParam("realm") final String realm);
/**
* Will set {@link org.keycloak.testsuite.model.infinispan.KeycloakTestTimeService} to the infinispan CacheManager before the test.
* This will allow infinispan expiration to be aware of Keycloak {@link org.keycloak.common.util.Time#setOffset}
*/
@POST
@Path("/set-testing-infinispan-time-service")
@Produces(MediaType.APPLICATION_JSON)
void setTestingInfinispanTimeService();
@POST
@Path("/revert-testing-infinispan-time-service")
@Produces(MediaType.APPLICATION_JSON)
void revertTestingInfinispanTimeService();
@GET @GET
@Path("/get-client-sessions-count") @Path("/get-client-sessions-count")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)

View file

@ -37,6 +37,8 @@ import org.keycloak.testsuite.arquillian.CrossDCTestEnricher;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import org.keycloak.testsuite.arquillian.annotation.InitialDcState; import org.keycloak.testsuite.arquillian.annotation.InitialDcState;
import static org.keycloak.testsuite.arquillian.CrossDCTestEnricher.forAllBackendNodesStream;
/** /**
* Abstract cross-data-centre test that defines primitives for handling cross-DC setup. * Abstract cross-data-centre test that defines primitives for handling cross-DC setup.
@ -221,4 +223,26 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
super.resetTimeOffset(); super.resetTimeOffset();
setTimeOffsetOnAllStartedContainers(0); setTimeOffsetOnAllStartedContainers(0);
} }
protected void setInfinispanTestTimeServiceOnAllStartedAuthServers() {
forAllBackendNodesStream()
.filter(ContainerInfo::isStarted)
.forEach(this::setInfinispanTestTimeServiceonAuthServer);
}
private void setInfinispanTestTimeServiceonAuthServer(ContainerInfo backendAuthServer) {
log.infof("Set Infinispan Test Time Service for backend server %s", backendAuthServer.getQualifier());
getTestingClientFor(backendAuthServer).testing().setTestingInfinispanTimeService();
}
protected void revertInfinispanTestTimeServiceOnAllStartedAuthServers() {
forAllBackendNodesStream()
.filter(ContainerInfo::isStarted)
.forEach(this::revertInfinispanTestTimeServiceonAuthServer);
}
private void revertInfinispanTestTimeServiceonAuthServer(ContainerInfo backendAuthServer) {
log.infof("Revert Infinispan Test Time Service for backend server %s", backendAuthServer.getQualifier());
getTestingClientFor(backendAuthServer).testing().revertTestingInfinispanTimeService();
}
} }

View file

@ -124,115 +124,127 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
@JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME) InfinispanStatistics clientSessionCacheDc2Stats @JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME) InfinispanStatistics clientSessionCacheDc2Stats
) { ) {
// Set the infinispan testTimeService on all started auth servers
setInfinispanTestTimeServiceOnAllStartedAuthServers();
// Ensure to remove all current sessions and offline sessions
setTimeOffset(10000000);
getTestingClientForStartedNodeInDc(0).testing("test").removeExpired("test");
setTimeOffset(0);
sessionCacheDc1Stats.reset();
sessionCacheDc2Stats.reset();
clientSessionCacheDc1Stats.reset();
clientSessionCacheDc2Stats.reset();
// Disable DC2 on loadbalancer
disableDcOnLoadBalancer(DC.SECOND);
// Get statistics
AtomicLong sessionStoresDc1 = new AtomicLong(getStores(sessionCacheDc1Stats));
AtomicLong sessionStoresDc2 = new AtomicLong(getStores(sessionCacheDc2Stats));
AtomicLong clientSessionStoresDc1 = new AtomicLong(getStores(clientSessionCacheDc1Stats));
AtomicLong clientSessionStoresDc2 = new AtomicLong(getStores(clientSessionCacheDc2Stats));
AtomicInteger lsrDc1 = new AtomicInteger(-1);
AtomicInteger lsrDc2 = new AtomicInteger(-1);
// Login
OAuthClient.AuthorizationEndpointResponse response1 = oauth.doLogin("test-user@localhost", "password");
String code = response1.getCode();
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
Assert.assertNotNull(tokenResponse.getAccessToken());
String sessionId = oauth.verifyToken(tokenResponse.getAccessToken()).getSessionState();
String refreshToken1 = tokenResponse.getRefreshToken();
// Assert statistics - sessions created on both DCs and created on remoteCaches too
assertStatistics("After session created", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
lsrDc1, lsrDc2, true, true, true, false);
// Set time offset
setTimeOffset(100);
// refresh token on DC1
tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
String refreshToken3 = tokenResponse.getRefreshToken();
Assert.assertNotNull(refreshToken3);
// Assert statistics - sessions updated on both DC1 and DC2. RemoteCaches not updated
assertStatistics("After refresh at time 100", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
lsrDc1, lsrDc2, true, true, false, false);
// Set time offset
setTimeOffset(110);
// refresh token on DC1
tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
String refreshToken2 = tokenResponse.getRefreshToken();
Assert.assertNotNull(refreshToken2);
// Assert statistics - sessions updated just on DC1.
// Update of DC2 is postponed (It's just 10 seconds since last message). RemoteCaches not updated
assertStatistics("After refresh at time 110", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
lsrDc1, lsrDc2, true, false, false, false);
// 31 minutes after "100". Session should be still valid and not yet expired (RefreshToken will be invalid due the expiration on the JWT itself. Hence not testing refresh here)
setTimeOffset(1960);
boolean sessionValid = getTestingClientForStartedNodeInDc(1).server("test").fetch((KeycloakSession session) -> {
RealmModel realm = session.realms().getRealmByName("test");
UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
return AuthenticationManager.isSessionValid(realm, userSession);
}, Boolean.class);
Assert.assertTrue(sessionValid);
getTestingClientForStartedNodeInDc(1).testing("test").removeExpired("test");
// Assert statistics - nothing was updated. No refresh happened and nothing was cleared during "removeExpired"
assertStatistics("After checking valid at time 1960", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
lsrDc1, lsrDc2, false, false, false, false);
// 35 minutes after "100". Session not valid and will be expired by the cleaner
setTimeOffset(2200);
sessionValid = getTestingClientForStartedNodeInDc(1).server("test").fetch((KeycloakSession session) -> {
RealmModel realm = session.realms().getRealmByName("test");
UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
return AuthenticationManager.isSessionValid(realm, userSession);
}, Boolean.class);
Assert.assertFalse(sessionValid);
getTestingClientForStartedNodeInDc(1).testing("test").removeExpired("test");
// Session should be removed on both DCs
try { try {
getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId, false); // Ensure to remove all current sessions and offline sessions
Assert.fail("It wasn't expected to find the session " + sessionId); setTimeOffset(10000000);
} catch (NotFoundException nfe) { getTestingClientForStartedNodeInDc(0).testing("test").removeExpired("test");
// Expected getTestingClientForStartedNodeInDc(1).testing("test").removeExpired("test");
} setTimeOffset(0);
try {
getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId, false); sessionCacheDc1Stats.reset();
Assert.fail("It wasn't expected to find the session " + sessionId); sessionCacheDc2Stats.reset();
} catch (NotFoundException nfe) { clientSessionCacheDc1Stats.reset();
// Expected clientSessionCacheDc2Stats.reset();
// Disable DC2 on loadbalancer
disableDcOnLoadBalancer(DC.SECOND);
// Get statistics
AtomicLong sessionStoresDc1 = new AtomicLong(getStores(sessionCacheDc1Stats));
AtomicLong sessionStoresDc2 = new AtomicLong(getStores(sessionCacheDc2Stats));
AtomicLong clientSessionStoresDc1 = new AtomicLong(getStores(clientSessionCacheDc1Stats));
AtomicLong clientSessionStoresDc2 = new AtomicLong(getStores(clientSessionCacheDc2Stats));
AtomicInteger lsrDc1 = new AtomicInteger(-1);
AtomicInteger lsrDc2 = new AtomicInteger(-1);
// Login
OAuthClient.AuthorizationEndpointResponse response1 = oauth.doLogin("test-user@localhost", "password");
String code = response1.getCode();
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
Assert.assertNotNull(tokenResponse.getAccessToken());
String sessionId = oauth.verifyToken(tokenResponse.getAccessToken()).getSessionState();
String refreshToken1 = tokenResponse.getRefreshToken();
// Assert statistics - sessions created on both DCs and created on remoteCaches too
assertStatistics("After session created", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
lsrDc1, lsrDc2, true, true, true, false);
// Set time offset
setTimeOffset(100);
// refresh token on DC1
tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
String refreshToken2 = tokenResponse.getRefreshToken();
Assert.assertNotNull(refreshToken2);
// Assert statistics - sessions updated on both DC1 and DC2. RemoteCaches not updated
assertStatistics("After refresh at time 100", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
lsrDc1, lsrDc2, true, true, false, false);
// Set time offset
setTimeOffset(110);
// refresh token on DC1
tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
String refreshToken3 = tokenResponse.getRefreshToken();
Assert.assertNotNull(refreshToken3);
// Assert statistics - sessions updated just on DC1.
// Update of DC2 is postponed (It's just 10 seconds since last message). RemoteCaches not updated
assertStatistics("After refresh at time 110", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
lsrDc1, lsrDc2, true, false, false, false);
// 31 minutes after "100". Session should be still valid and not yet expired (RefreshToken will be invalid due the expiration on the JWT itself. Hence not testing refresh here)
setTimeOffset(1960);
boolean sessionValid = getTestingClientForStartedNodeInDc(1).server("test").fetch((KeycloakSession session) -> {
RealmModel realm = session.realms().getRealmByName("test");
UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
return AuthenticationManager.isSessionValid(realm, userSession);
}, Boolean.class);
Assert.assertTrue(sessionValid);
getTestingClientForStartedNodeInDc(1).testing("test").removeExpired("test");
// Assert statistics - nothing was updated. No refresh happened and nothing was cleared during "removeExpired"
assertStatistics("After checking valid at time 1960", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
lsrDc1, lsrDc2, false, false, false, false);
// 35 minutes after "100". Session not valid and will be expired by the cleaner
setTimeOffset(2200);
sessionValid = getTestingClientForStartedNodeInDc(1).server("test").fetch((KeycloakSession session) -> {
RealmModel realm = session.realms().getRealmByName("test");
UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
return AuthenticationManager.isSessionValid(realm, userSession);
}, Boolean.class);
Assert.assertFalse(sessionValid);
// 2000 seconds after the previous. This should ensure that session would be expired from the cache due the invalid maxIdle.
// Previous read at time 2200 "refreshed" the maxIdle in the infinispan cache. This shouldn't happen in reality as an attempt to call refreshToken request on invalid session does backchannelLogout
setTimeOffset(4200);
getTestingClientForStartedNodeInDc(1).testing("test").removeExpired("test");
// Session should be removed on both DCs
try {
getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId, false);
Assert.fail("It wasn't expected to find the session " + sessionId);
} catch (NotFoundException nfe) {
// Expected
}
try {
getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId, false);
Assert.fail("It wasn't expected to find the session " + sessionId);
} catch (NotFoundException nfe) {
// Expected
}
} finally {
// Revert time service
revertInfinispanTestTimeServiceOnAllStartedAuthServers();
} }
} }
@ -248,6 +260,7 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
// Ensure to remove all current sessions and offline sessions // Ensure to remove all current sessions and offline sessions
setTimeOffset(10000000); setTimeOffset(10000000);
getTestingClientForStartedNodeInDc(0).testing("test").removeExpired("test"); getTestingClientForStartedNodeInDc(0).testing("test").removeExpired("test");
getTestingClientForStartedNodeInDc(1).testing("test").removeExpired("test");
setTimeOffset(0); setTimeOffset(0);
sessionCacheDc1Stats.reset(); sessionCacheDc1Stats.reset();

View file

@ -27,6 +27,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import javax.ws.rs.NotFoundException; import javax.ws.rs.NotFoundException;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
@ -99,12 +100,30 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
.build(); .build();
RealmRepresentation realmRep = RealmBuilder.create() RealmRepresentation realmRep = RealmBuilder.create()
.ssoSessionIdleTimeout(300)
.ssoSessionMaxLifespan(600)
.offlineSessionIdleTimeout(900)
.name(REALM_NAME) .name(REALM_NAME)
.user(user) .user(user)
.client(client) .client(client)
.build(); .build();
adminClient.realms().create(realmRep); adminClient.realms().create(realmRep);
setInfinispanTestTimeServiceOnAllStartedAuthServers();
}
@After
public void afterTest() {
// Expire all sessions
setTimeOffset(1500);
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).processExpiration();
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME).processExpiration();
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME).processExpiration();
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME).processExpiration();
resetTimeOffset();
revertInfinispanTestTimeServiceOnAllStartedAuthServers();
} }
@ -195,7 +214,7 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
int remoteSessions2 = (Integer) cacheDc2Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES); int remoteSessions2 = (Integer) cacheDc2Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
// Needs to use "received_messages" on Infinispan 9.2.4.Final. Stats for "sent_messages" is always null // Needs to use "received_messages" on Infinispan 9.2.4.Final. Stats for "sent_messages" is always null
long messagesCount = (Long) channelStatisticsCrossDc.getSingleStatistics(InfinispanStatistics.Constants.STAT_CHANNEL_RECEIVED_MESSAGES); long messagesCount = (Long) channelStatisticsCrossDc.getSingleStatistics(InfinispanStatistics.Constants.STAT_CHANNEL_RECEIVED_MESSAGES);
log.infof(messagePrefix + ": sessions1: %d, sessions2: %d, remoteSessions1: %d, remoteSessions2: %d, sentMessages: %d", sessions1, sessions2, remoteSessions1, remoteSessions2, messagesCount); log.infof(messagePrefix + ": sessions1: %d, sessions2: %d, clientSessions1: %d, clientSessions2: %d, remoteSessions1: %d, remoteSessions2: %d, sentMessages: %d", sessions1, sessions2, clientSessions1, clientSessions2, remoteSessions1, remoteSessions2, messagesCount);
Assert.assertEquals(sessions1, sessions1Expected); Assert.assertEquals(sessions1, sessions1Expected);
Assert.assertEquals(sessions2, sessions2Expected); Assert.assertEquals(sessions2, sessions2Expected);
@ -275,8 +294,8 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
remoteSessions01 + SESSIONS_COUNT, remoteSessions02 + SESSIONS_COUNT, false); remoteSessions01 + SESSIONS_COUNT, remoteSessions02 + SESSIONS_COUNT, false);
// Set time offset // Increase offset to 10 minutes (SSO Session Idle is 5 minutes for the realm). To make sure that admin session from master realm won't expire
setTimeOffset(10000000); setTimeOffset(610);
// Assert I am not able to refresh anymore // Assert I am not able to refresh anymore
refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password"); refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password");
@ -287,7 +306,8 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
channelStatisticsCrossDc.reset(); channelStatisticsCrossDc.reset();
// Remove expired in DC0 // Remove expired in DC0
getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME); getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).processExpiration();
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME).processExpiration();
// Assert sessions removed on node1 and node2 and on remote caches. // Assert sessions removed on node1 and node2 and on remote caches.
assertStatisticsExpected("After remove expired - 2", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, assertStatisticsExpected("After remove expired - 2", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
@ -312,7 +332,7 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
channelStatisticsCrossDc.reset(); channelStatisticsCrossDc.reset();
// Remove expired in DC0 // Remove expired in DC0
getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME); getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME).processExpiration();
// Nothing yet expired. It may happen that no message sent between DCs // Nothing yet expired. It may happen that no message sent between DCs
assertStatisticsExpected("After remove expired - 1", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME, assertStatisticsExpected("After remove expired - 1", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,
@ -321,8 +341,8 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
remoteSessions01 + SESSIONS_COUNT, remoteSessions02 + SESSIONS_COUNT, false); remoteSessions01 + SESSIONS_COUNT, remoteSessions02 + SESSIONS_COUNT, false);
// Set time offset // Increase offset to 20 minutes. Client offline sessions should be expired from infinispan by that. Master sessions will still remain there.
setTimeOffset(10000000); setTimeOffset(1210);
// Assert I am not able to refresh anymore // Assert I am not able to refresh anymore
refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password"); refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password");
@ -333,7 +353,8 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
channelStatisticsCrossDc.reset(); channelStatisticsCrossDc.reset();
// Remove expired in DC0 // Remove expired in DC0
getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME); getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME).processExpiration();
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME).processExpiration();
// Assert sessions removed on node1 and node2 and on remote caches. // Assert sessions removed on node1 and node2 and on remote caches.
assertStatisticsExpected("After remove expired - 2", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME, assertStatisticsExpected("After remove expired - 2", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,
@ -555,12 +576,11 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).removeKey(userSessionId); getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).removeKey(userSessionId);
} }
// Increase offset to big value like 100 hours // Increase offset to 10 minutes (SSO Session Idle is 5 minutes for the realm). To make sure that admin session from master realm won't expire
setTimeOffset(360000); setTimeOffset(610);
// Trigger removeExpired // Trigger removeExpired
getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME); getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME).processExpiration();
getTestingClientForStartedNodeInDc(1).testing().removeExpired(REALM_NAME);
// Ensure clientSessions were removed // Ensure clientSessions were removed
assertStatisticsExpected("After remove expired", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, assertStatisticsExpected("After remove expired", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME,
@ -586,12 +606,11 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME).removeKey(userSessionId); getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME).removeKey(userSessionId);
} }
// Increase offset to big value like 10000 hours (400+ days) // Increase offset to 20 minutes. Client offline sessions should be expired from infinispan by that. Master sessions will still remain there.
setTimeOffset(36000000); setTimeOffset(1210);
// Trigger removeExpired // Trigger removeExpired
getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME); getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME).processExpiration();
getTestingClientForStartedNodeInDc(1).testing().removeExpired(REALM_NAME);
// Ensure clientSessions were removed // Ensure clientSessions were removed
assertStatisticsExpected("After remove expired", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME, assertStatisticsExpected("After remove expired", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,

View file

@ -770,23 +770,28 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
// KEYCLOAK-1037 // KEYCLOAK-1037
@Test @Test
public void loginExpiredCodeWithExplicitRemoveExpired() { public void loginExpiredCodeWithExplicitRemoveExpired() {
loginPage.open(); getTestingClient().testing().setTestingInfinispanTimeService();
setTimeOffset(5000);
// Explicitly call "removeExpired". Hence authSession won't exist, but will be restarted from the KC_RESTART
testingClient.testing().removeExpired("test");
loginPage.login("login@test.com", "password"); try {
loginPage.open();
setTimeOffset(5000);
// Explicitly call "removeExpired". Hence authSession won't exist, but will be restarted from the KC_RESTART
testingClient.testing().removeExpired("test");
//loginPage.assertCurrent(); loginPage.login("login@test.com", "password");
loginPage.assertCurrent();
Assert.assertEquals("Your login attempt timed out. Login will start from the beginning.", loginPage.getError()); //loginPage.assertCurrent();
setTimeOffset(0); loginPage.assertCurrent();
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails() Assert.assertEquals("Your login attempt timed out. Login will start from the beginning.", loginPage.getError());
.detail(Details.RESTART_AFTER_TIMEOUT, "true")
.client((String) null) events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
.assertEvent(); .detail(Details.RESTART_AFTER_TIMEOUT, "true")
.client((String) null)
.assertEvent();
} finally {
getTestingClient().testing().revertTestingInfinispanTimeService();
}
} }
@Test @Test

View file

@ -19,6 +19,7 @@ package org.keycloak.testsuite.model;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
@ -45,7 +46,8 @@ import static org.hamcrest.core.IsNull.notNullValue;
import static org.hamcrest.core.IsNull.nullValue; import static org.hamcrest.core.IsNull.nullValue;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.RoleModel; import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE; import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
/** /**
@ -54,6 +56,9 @@ import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerEx
@AuthServerContainerExclude(REMOTE) @AuthServerContainerExclude(REMOTE)
public class AuthenticationSessionProviderTest extends AbstractTestRealmKeycloakTest { public class AuthenticationSessionProviderTest extends AbstractTestRealmKeycloakTest {
@Rule
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
@Before @Before
public void before() { public void before() {
testingClient.server().run(session -> { testingClient.server().run(session -> {

View file

@ -20,6 +20,7 @@ package org.keycloak.testsuite.model;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticatedClientSessionModel;
@ -41,6 +42,7 @@ import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.ModelTest; import org.keycloak.testsuite.arquillian.annotation.ModelTest;
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
import org.keycloak.timer.TimerProvider; import org.keycloak.timer.TimerProvider;
import java.util.HashMap; import java.util.HashMap;
@ -61,6 +63,10 @@ import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.A
*/ */
@AuthServerContainerExclude(AuthServer.REMOTE) @AuthServerContainerExclude(AuthServer.REMOTE)
public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTest { public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTest {
@Rule
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
private static KeycloakSession currentSession; private static KeycloakSession currentSession;
private static RealmModel realm; private static RealmModel realm;
private static UserSessionManager sessionManager; private static UserSessionManager sessionManager;
@ -545,7 +551,7 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes
Assert.assertEquals(1, persister.getUserSessionsCount(true)); Assert.assertEquals(1, persister.getUserSessionsCount(true));
// Expire everything and assert nothing found // Expire everything and assert nothing found
Time.setOffset(6000000); Time.setOffset(7000000);
currentSession.sessions().removeExpired(realm); currentSession.sessions().removeExpired(realm);
persister.removeExpired(realm); persister.removeExpired(realm);

View file

@ -20,6 +20,7 @@ package org.keycloak.testsuite.model;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticatedClientSessionModel;
@ -57,6 +58,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -64,6 +66,9 @@ import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.A
@AuthServerContainerExclude(AuthServer.REMOTE) @AuthServerContainerExclude(AuthServer.REMOTE)
public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest { public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
@Rule
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
public static void setupRealm(KeycloakSession session){ public static void setupRealm(KeycloakSession session){
RealmModel realm = session.realms().getRealmByName("test"); RealmModel realm = session.realms().getRealmByName("test");
UserModel user1 = session.users().addUser(realm, "user1"); UserModel user1 = session.users().addUser(realm, "user1");
@ -469,6 +474,7 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
assertEquals(userSession, clientSession.getUserSession()); assertEquals(userSession, clientSession.getUserSession());
Time.setOffset(-(realm.getSsoSessionIdleTimeout() * 2)); Time.setOffset(-(realm.getSsoSessionIdleTimeout() * 2));
userSession.setLastSessionRefresh(Time.currentTime()); userSession.setLastSessionRefresh(Time.currentTime());
clientSession.setTimestamp(Time.currentTime());
validUserSessions.add(userSession.getId()); validUserSessions.add(userSession.getId());
validClientSessions.add(clientSession.getId()); validClientSessions.add(clientSession.getId());
}); });

View file

@ -0,0 +1,79 @@
/*
* Copyright 2020 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.testsuite.util;
import java.util.concurrent.atomic.AtomicInteger;
import org.jboss.logging.Logger;
import org.junit.rules.ExternalResource;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.arquillian.ContainerInfo;
import org.keycloak.testsuite.arquillian.CrossDCTestEnricher;
import static org.keycloak.testsuite.arquillian.CrossDCTestEnricher.forAllBackendNodesStream;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanTestTimeServiceRule extends ExternalResource {
private static final Logger log = Logger.getLogger(InfinispanTestTimeServiceRule.class);
private final AbstractKeycloakTest test;
public InfinispanTestTimeServiceRule(AbstractKeycloakTest test) {
this.test = test;
}
@Override
protected void before() throws Throwable {
if (!this.test.getTestContext().getSuiteContext().isAuthServerCrossDc()) {
// No cross-dc environment
test.getTestingClient().testing().setTestingInfinispanTimeService();
} else {
AtomicInteger count = new AtomicInteger(0);
// Cross-dc environment - Set on all started nodes
forAllBackendNodesStream()
.filter(ContainerInfo::isStarted)
.map(CrossDCTestEnricher.getBackendTestingClients()::get)
.forEach(testingClient -> {
testingClient.testing().setTestingInfinispanTimeService();
count.incrementAndGet();
});
//
log.infof("Totally set infinispanTimeService rule in %d servers", count.get());
}
}
@Override
protected void after() {
if (!this.test.getTestContext().getSuiteContext().isAuthServerCrossDc()) {
// No cross-dc environment
test.getTestingClient().testing().revertTestingInfinispanTimeService();
} else {
// Cross-dc environment - Revert on all started nodes
forAllBackendNodesStream()
.filter(ContainerInfo::isStarted)
.map(CrossDCTestEnricher.getBackendTestingClients()::get)
.forEach(testingClient -> testingClient.testing().revertTestingInfinispanTimeService());
}
}
}