KEYCLOAK-16755 ClearExpiredUserSessions optimization. Rely on infinispan expiration rather than Keycloak own background task.
This commit is contained in:
parent
6da396821a
commit
f4b5942c6c
39 changed files with 1046 additions and 405 deletions
|
@ -65,7 +65,7 @@ public class Time {
|
|||
* @param time Time in seconds since the epoch
|
||||
* @return Time in milliseconds
|
||||
*/
|
||||
public static long toMillis(int time) {
|
||||
public static long toMillis(long time) {
|
||||
return time * 1000L;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,9 +19,6 @@ package org.keycloak.cluster.infinispan;
|
|||
|
||||
import org.infinispan.Cache;
|
||||
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.notifications.Listener;
|
||||
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.models.KeycloakSession;
|
||||
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.KeycloakMarshallUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInput;
|
||||
import java.io.ObjectOutput;
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
|
@ -149,7 +136,8 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory
|
|||
try {
|
||||
V result;
|
||||
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 {
|
||||
result = (V) crossDCAwareCacheFactory.getCache().putIfAbsent(key, value);
|
||||
}
|
||||
|
|
|
@ -185,11 +185,11 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
|
|||
String jgroupsUdpMcastAddr = config.get("jgroupsUdpMcastAddr", System.getProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR));
|
||||
configureTransport(gcb, topologyInfo.getMyNodeName(), topologyInfo.getMySiteName(), jgroupsUdpMcastAddr);
|
||||
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.
|
||||
// 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
|
||||
|
|
|
@ -47,14 +47,12 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
|
|||
private final InfinispanUserSessionProvider provider;
|
||||
private AuthenticatedClientSessionEntity entity;
|
||||
private final ClientModel client;
|
||||
private final InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx;
|
||||
private final InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx;
|
||||
private UserSessionModel userSession;
|
||||
private boolean offline;
|
||||
|
||||
public AuthenticatedClientSessionAdapter(KeycloakSession kcSession, InfinispanUserSessionProvider provider,
|
||||
AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionModel userSession,
|
||||
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx,
|
||||
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx, boolean offline) {
|
||||
if (userSession == null) {
|
||||
throw new NullPointerException("userSession must not be null");
|
||||
|
@ -65,15 +63,10 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
|
|||
this.entity = entity;
|
||||
this.userSession = userSession;
|
||||
this.client = client;
|
||||
this.userSessionUpdateTx = userSessionUpdateTx;
|
||||
this.clientSessionUpdateTx = clientSessionUpdateTx;
|
||||
this.offline = offline;
|
||||
}
|
||||
|
||||
private void update(UserSessionUpdateTask task) {
|
||||
userSessionUpdateTx.addTask(userSession.getId(), task);
|
||||
}
|
||||
|
||||
private void update(ClientSessionUpdateTask task) {
|
||||
clientSessionUpdateTx.addTask(entity.getId(), task);
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.models.sessions.infinispan;
|
|||
import org.keycloak.cluster.ClusterProvider;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.jboss.logging.Logger;
|
||||
|
@ -39,7 +40,6 @@ import org.keycloak.models.utils.RealmInfoUtil;
|
|||
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||
import org.keycloak.sessions.AuthenticationSessionProvider;
|
||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||
import org.infinispan.AdvancedCache;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -80,7 +80,8 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
|
|||
entity.setRealmId(realm.getId());
|
||||
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);
|
||||
}
|
||||
|
@ -94,28 +95,17 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
|
|||
private RootAuthenticationSessionEntity getRootAuthenticationSessionEntity(String authSessionId) {
|
||||
// Chance created in this transaction
|
||||
RootAuthenticationSessionEntity entity = tx.get(cache, authSessionId);
|
||||
|
||||
if (entity == null) {
|
||||
entity = cache.get(authSessionId);
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAllExpired() {
|
||||
// Rely on expiration of cache entries provided by infinispan. Nothing needed here
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeExpired(RealmModel realm) {
|
||||
log.debugf("Removing expired sessions");
|
||||
|
||||
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());
|
||||
// Rely on expiration of cache entries provided by infinispan. Nothing needed here
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -25,9 +25,11 @@ import java.util.function.Supplier;
|
|||
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
|
||||
import org.infinispan.commons.api.BasicCache;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.CodeToTokenStoreProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
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>
|
||||
|
@ -51,7 +53,8 @@ public class InfinispanCodeToTokenStoreProvider implements CodeToTokenStoreProvi
|
|||
|
||||
try {
|
||||
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) {
|
||||
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
|
||||
if (logger.isDebugEnabled()) {
|
||||
|
|
|
@ -112,7 +112,7 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
|
|||
|
||||
@Override
|
||||
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);
|
||||
|
||||
Object taskKey = getTaskKey(cache, key);
|
||||
|
@ -155,12 +155,12 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
|
|||
tasks.put(taskKey, new CacheTaskWithValue<V>(value) {
|
||||
@Override
|
||||
public void execute() {
|
||||
decorateCache(cache).replace(key, value);
|
||||
decorateCache(cache).replace(key, value, lifespan, lifespanUnit);
|
||||
}
|
||||
|
||||
@Override
|
||||
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) {
|
||||
return ((CacheTaskWithValue<V>) current).getValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Should we have per-transaction cache for lookups?
|
||||
|
|
|
@ -23,9 +23,11 @@ import java.util.function.Supplier;
|
|||
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
|
||||
import org.infinispan.commons.api.BasicCache;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.SingleUseTokenStoreProvider;
|
||||
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" .
|
||||
|
@ -55,7 +57,8 @@ public class InfinispanSingleUseTokenStoreProvider implements SingleUseTokenStor
|
|||
|
||||
try {
|
||||
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;
|
||||
} catch (HotRodClientException re) {
|
||||
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
|
||||
|
|
|
@ -20,16 +20,17 @@ package org.keycloak.models.sessions.infinispan;
|
|||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
|
||||
import org.infinispan.commons.api.BasicCache;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.TokenRevocationStoreProvider;
|
||||
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>
|
||||
|
@ -57,7 +58,8 @@ public class InfinispanTokenRevocationStoreProvider implements TokenRevocationSt
|
|||
|
||||
try {
|
||||
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) {
|
||||
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
|
||||
if (logger.isDebugEnabled()) {
|
||||
|
|
|
@ -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.RemoveUserSessionsEvent;
|
||||
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.Mappers;
|
||||
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.InfinispanKeyGenerator;
|
||||
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.util.Collection;
|
||||
|
@ -132,12 +131,12 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
this.offlineClientSessionCache = offlineClientSessionCache;
|
||||
this.loginFailureCache = loginFailureCache;
|
||||
|
||||
this.sessionTx = new InfinispanChangelogBasedTransaction<>(session, sessionCache, remoteCacheInvoker);
|
||||
this.offlineSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineSessionCache, remoteCacheInvoker);
|
||||
this.clientSessionTx = new InfinispanChangelogBasedTransaction<>(session, clientSessionCache, remoteCacheInvoker);
|
||||
this.offlineClientSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineClientSessionCache, remoteCacheInvoker);
|
||||
this.sessionTx = new InfinispanChangelogBasedTransaction<>(session, sessionCache, remoteCacheInvoker, SessionTimeouts::getUserSessionLifespanMs, SessionTimeouts::getUserSessionMaxIdleMs);
|
||||
this.offlineSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineSessionCache, remoteCacheInvoker, SessionTimeouts::getOfflineSessionLifespanMs, SessionTimeouts::getOfflineSessionMaxIdleMs);
|
||||
this.clientSessionTx = new InfinispanChangelogBasedTransaction<>(session, clientSessionCache, remoteCacheInvoker, SessionTimeouts::getClientSessionLifespanMs, SessionTimeouts::getClientSessionMaxIdleMs);
|
||||
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);
|
||||
|
||||
|
@ -192,7 +191,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
|
||||
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(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
|
||||
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
|
||||
public void removeExpired(RealmModel realm) {
|
||||
log.debugf("Removing expired sessions");
|
||||
removeExpiredUserSessions(realm);
|
||||
removeExpiredOfflineUserSessions(realm);
|
||||
// Rely on expiration of cache entries provided by infinispan. Nothing needed here besides calling persister
|
||||
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
|
||||
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.
|
||||
|
@ -794,7 +666,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
AuthenticatedClientSessionAdapter wrap(UserSessionModel userSession, ClientModel client, AuthenticatedClientSessionEntity entity, boolean offline) {
|
||||
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(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) {
|
||||
|
@ -1041,7 +913,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(clientSession.getClient().getId(), clientSessionId);
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.jboss.logging.Logger;
|
|||
import org.keycloak.Config;
|
||||
import org.keycloak.cluster.ClusterProvider;
|
||||
import org.keycloak.common.util.Environment;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
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.util.InfinispanKeyGenerator;
|
||||
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.PostMigrationEvent;
|
||||
import org.keycloak.models.utils.ResetTimeOffsetEvent;
|
||||
|
@ -65,6 +67,7 @@ import org.keycloak.provider.ProviderEventListener;
|
|||
import java.io.Serializable;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory {
|
||||
|
||||
|
@ -245,47 +248,48 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
|
|||
InfinispanConnectionProvider ispn = session.getProvider(InfinispanConnectionProvider.class);
|
||||
|
||||
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.
|
||||
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);
|
||||
}
|
||||
|
||||
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionsCache = ispn.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME);
|
||||
checkRemoteCache(session, clientSessionsCache, (RealmModel realm) -> {
|
||||
// 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);
|
||||
boolean offlineSessionsRemoteCache = checkRemoteCache(session, offlineSessionsCache, (RealmModel realm) -> {
|
||||
return realm.getOfflineSessionIdleTimeout() * 1000;
|
||||
});
|
||||
RemoteCache offlineSessionsRemoteCache = checkRemoteCache(session, offlineSessionsCache, (RealmModel realm) -> {
|
||||
return Time.toMillis(realm.getOfflineSessionIdleTimeout());
|
||||
}, SessionTimeouts::getOfflineSessionLifespanMs, SessionTimeouts::getOfflineSessionMaxIdleMs);
|
||||
|
||||
if (offlineSessionsRemoteCache) {
|
||||
if (offlineSessionsRemoteCache != null) {
|
||||
offlineLastSessionRefreshStore = new CrossDCLastSessionRefreshStoreFactory().createAndInit(session, offlineSessionsCache, true);
|
||||
}
|
||||
|
||||
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME);
|
||||
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);
|
||||
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);
|
||||
|
||||
if (remoteStores.isEmpty()) {
|
||||
log.debugf("No remote store configured for cache '%s'", ispnCache.getName());
|
||||
return false;
|
||||
return null;
|
||||
} else {
|
||||
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);
|
||||
|
||||
RemoteCacheSessionListener hotrodListener = RemoteCacheSessionListener.createListener(session, ispnCache, remoteCache);
|
||||
RemoteCacheSessionListener hotrodListener = RemoteCacheSessionListener.createListener(session, ispnCache, remoteCache, lifespanMsLoader, maxIdleTimeMsLoader);
|
||||
remoteCache.addClientListener(hotrodListener);
|
||||
return true;
|
||||
return remoteCache;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.models.sessions.infinispan;
|
|||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.keycloak.common.util.Time;
|
||||
|
@ -27,6 +28,7 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity;
|
||||
import org.keycloak.models.utils.RealmInfoUtil;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||
|
||||
|
@ -52,7 +54,8 @@ public class RootAuthenticationSessionAdapter implements RootAuthenticationSessi
|
|||
}
|
||||
|
||||
void update() {
|
||||
provider.tx.replace(cache, entity.getId(), entity);
|
||||
int expirationSeconds = RealmInfoUtil.getDettachedClientSessionLifespan(realm);
|
||||
provider.tx.replace(cache, entity.getId(), entity, expirationSeconds, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.models.sessions.infinispan.changes;
|
|||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
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<>();
|
||||
|
||||
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.cacheName = cache.getName();
|
||||
this.cache = cache;
|
||||
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();
|
||||
|
||||
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) {
|
||||
// Now run the operation in our cluster
|
||||
|
@ -185,21 +195,25 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
|
|||
case ADD:
|
||||
CacheDecorators.skipCacheStore(cache)
|
||||
.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;
|
||||
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) {
|
||||
logger.debugf("Existing entity in cache for key: %s . Will update it", key);
|
||||
|
||||
// Apply updates on the existing entity and replace it
|
||||
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;
|
||||
case REPLACE:
|
||||
replace(key, task, sessionWrapper);
|
||||
replace(key, task, sessionWrapper, task.getLifespanMs(), task.getMaxIdleTimeMs());
|
||||
break;
|
||||
default:
|
||||
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;
|
||||
int iteration = 0;
|
||||
V session = oldVersionEntity.getEntity();
|
||||
|
@ -219,7 +233,7 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
|
|||
SessionEntityWrapper<V> newVersionEntity = generateNewVersionAndWrapEntity(session, oldVersionEntity.getLocalMetadata());
|
||||
|
||||
// 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
|
||||
if (!replaced) {
|
||||
|
@ -239,7 +253,7 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
|
|||
task.runUpdate(session);
|
||||
} else {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,21 +20,29 @@ package org.keycloak.models.sessions.infinispan.changes;
|
|||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
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>
|
||||
*/
|
||||
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 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.crossDCMessageStatus = crossDCMessageStatus;
|
||||
this.lifespanMs = lifespanMs;
|
||||
this.maxIdleTimeMs = maxIdleTimeMs;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -54,8 +62,16 @@ class MergedUpdate<S extends SessionEntity> implements SessionUpdateTask<S> {
|
|||
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()) {
|
||||
return null;
|
||||
}
|
||||
|
@ -64,14 +80,21 @@ class MergedUpdate<S extends SessionEntity> implements SessionUpdateTask<S> {
|
|||
S session = sessionWrapper.getEntity();
|
||||
for (SessionUpdateTask<S> child : childUpdates) {
|
||||
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);
|
||||
} else {
|
||||
|
||||
// Merge the operations. REMOVE is special case as other operations are not needed then.
|
||||
CacheOperation mergedOp = result.getOperation(session).merge(child.getOperation(session), session);
|
||||
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);
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -30,11 +30,6 @@ public interface SessionUpdateTask<S extends SessionEntity> {
|
|||
|
||||
CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<S> sessionWrapper);
|
||||
|
||||
default long getLifespanMs() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
enum CacheOperation {
|
||||
|
||||
ADD,
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.jboss.logging.Logger;
|
|||
import org.keycloak.connections.infinispan.TopologyInfo;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
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.SessionUpdateTask;
|
||||
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);
|
||||
if (context == null) {
|
||||
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();
|
||||
SessionUpdateTask.CacheOperation operation = task.getOperation(session);
|
||||
|
||||
|
@ -113,12 +114,14 @@ public class RemoteCacheInvoker {
|
|||
remoteCache.remove(key);
|
||||
break;
|
||||
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;
|
||||
case ADD_IF_ABSENT:
|
||||
SessionEntityWrapper<V> existing = remoteCache
|
||||
.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) {
|
||||
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) {
|
||||
// Adjust based on the hotrod protocol
|
||||
lifespanMs = InfinispanUtil.toHotrodTimeMs(remoteCache, lifespanMs);
|
||||
maxIdleMs = InfinispanUtil.toHotrodTimeMs(remoteCache, maxIdleMs);
|
||||
|
||||
boolean replaced = false;
|
||||
int replaceIteration = 0;
|
||||
while (!replaced && replaceIteration < InfinispanUtil.MAXIMUM_REPLACE_RETRIES) {
|
||||
|
|
|
@ -32,12 +32,20 @@ import org.jboss.logging.Logger;
|
|||
import org.keycloak.connections.infinispan.TopologyInfo;
|
||||
import org.keycloak.executors.ExecutorsProvider;
|
||||
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.entities.SessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
|
||||
import java.util.Random;
|
||||
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.keycloak.models.utils.KeycloakModelUtils;
|
||||
|
||||
/**
|
||||
* @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 TopologyInfo topologyInfo;
|
||||
private ClientListenerExecutorDecorator<K> executor;
|
||||
private BiFunction<RealmModel, V, Long> lifespanMsLoader;
|
||||
private BiFunction<RealmModel, V, Long> maxIdleTimeMsLoader;
|
||||
private KeycloakSessionFactory sessionFactory;
|
||||
|
||||
|
||||
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.remoteCache = remoteCache;
|
||||
|
||||
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());
|
||||
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...)
|
||||
if (remoteSessionVersioned == null || remoteSessionVersioned.getValue() == null) {
|
||||
logger.debugf("Entity '%s' not present in remoteCache. Ignoring create",
|
||||
key.toString());
|
||||
logger.debugf("Entity '%s' not present in remoteCache. Ignoring create", key);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -116,36 +131,46 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
|
|||
V remoteSession = remoteSessionVersioned.getValue().getEntity();
|
||||
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
|
||||
cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
|
||||
.putIfAbsent(key, newWrapper);
|
||||
KeycloakModelUtils.runJobInTransaction(sessionFactory, (session -> {
|
||||
|
||||
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) {
|
||||
// TODO can be optimized and remoteSession sent in the event itself?
|
||||
boolean replaced = false;
|
||||
AtomicBoolean replaced = new AtomicBoolean(false);
|
||||
int replaceRetries = 0;
|
||||
int sleepInterval = 25;
|
||||
do {
|
||||
replaceRetries++;
|
||||
|
||||
|
||||
SessionEntityWrapper<V> localEntityWrapper = cache.get(key);
|
||||
VersionedValue<SessionEntityWrapper<V>> remoteSessionVersioned = remoteCache.getWithMetadata(key);
|
||||
|
||||
// Probably already removed
|
||||
if (remoteSessionVersioned == null || remoteSessionVersioned.getValue() == null) {
|
||||
logger.debugf("Entity '%s' not present in remoteCache. Ignoring replace",
|
||||
key.toString());
|
||||
key);
|
||||
return;
|
||||
}
|
||||
|
||||
if (remoteSessionVersioned.getVersion() < eventVersion) {
|
||||
try {
|
||||
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
|
||||
continue;
|
||||
} catch (InterruptedException ex) {
|
||||
|
@ -156,18 +181,26 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
|
|||
}
|
||||
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);
|
||||
|
||||
// We received event from remoteCache, so we won't update it back
|
||||
replaced = cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
|
||||
.replace(key, localEntityWrapper, sessionWrapper);
|
||||
KeycloakModelUtils.runJobInTransaction(sessionFactory, (session -> {
|
||||
|
||||
if (! replaced) {
|
||||
logger.debugf("Did not succeed in merging sessions, will try again: %s", remoteSession.toString());
|
||||
RealmModel realm = session.realms().getRealm(sessionWrapper.getEntity().getRealmId());
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
// 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<>();
|
||||
listener.init(session, cache, remoteCache);
|
||||
listener.init(session, cache, remoteCache, lifespanMsLoader, maxIdleTimeMsLoader);
|
||||
|
||||
return listener;
|
||||
}
|
||||
|
|
|
@ -18,12 +18,16 @@
|
|||
package org.keycloak.models.sessions.infinispan.util;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.infinispan.client.hotrod.ProtocolVersion;
|
||||
import org.infinispan.client.hotrod.RemoteCache;
|
||||
import org.infinispan.commons.api.BasicCache;
|
||||
import org.infinispan.persistence.manager.PersistenceManager;
|
||||
import org.infinispan.persistence.remote.RemoteStore;
|
||||
import org.infinispan.remoting.transport.Transport;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.connections.infinispan.TopologyInfo;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -66,4 +70,26 @@ public class InfinispanUtil {
|
|||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -21,6 +21,7 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
|
@ -154,6 +155,10 @@ public class ConcurrencyJDGCacheReplaceTest {
|
|||
// Create caches, listeners and finally worker threads
|
||||
remoteCache1 = InfinispanUtil.getRemoteCache(cache1);
|
||||
remoteCache2 = InfinispanUtil.getRemoteCache(cache2);
|
||||
|
||||
// Manual test of lifespans
|
||||
testLifespans();
|
||||
|
||||
Thread worker1 = createWorker(cache1, 1);
|
||||
Thread worker2 = createWorker(cache2, 2);
|
||||
|
||||
|
@ -375,6 +380,30 @@ public class ConcurrencyJDGCacheReplaceTest {
|
|||
private enum ReplaceStatus {
|
||||
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
|
||||
private static class CacheWorker extends Thread {
|
||||
|
|
|
@ -131,6 +131,11 @@ public class MapRootAuthenticationSessionProvider implements AuthenticationSessi
|
|||
tx.delete(UUID.fromString(authenticationSession.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAllExpired() {
|
||||
session.realms().getRealmsStream().forEach(this::removeExpired);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeExpired(RealmModel realm) {
|
||||
Objects.requireNonNull(realm, "The provided realm can't be null!");
|
||||
|
|
|
@ -138,6 +138,11 @@ public interface UserSessionProvider extends Provider {
|
|||
void removeUserSession(RealmModel realm, UserSessionModel session);
|
||||
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.
|
||||
* If this `UserSessionProvider` uses `UserSessionPersister`, the removal of the expired
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package org.keycloak.sessions;
|
||||
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
|
@ -71,6 +72,11 @@ public interface AuthenticationSessionProvider extends Provider {
|
|||
*/
|
||||
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.
|
||||
* @param realm {@code RealmModel} Can't be {@code null}.
|
||||
|
|
|
@ -20,7 +20,6 @@ package org.keycloak.services.scheduled;
|
|||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserSessionProvider;
|
||||
import org.keycloak.timer.ScheduledTask;
|
||||
|
||||
/**
|
||||
|
@ -36,11 +35,8 @@ public class ClearExpiredUserSessions implements ScheduledTask {
|
|||
public void run(KeycloakSession session) {
|
||||
long currentTimeMillis = Time.currentTimeMillis();
|
||||
|
||||
UserSessionProvider sessions = session.sessions();
|
||||
session.realms().getRealmsStream().forEach(realm -> {
|
||||
sessions.removeExpired(realm);
|
||||
session.authenticationSessions().removeExpired(realm);
|
||||
});
|
||||
session.authenticationSessions().removeAllExpired();
|
||||
session.sessions().removeAllExpired();
|
||||
|
||||
long took = Time.currentTimeMillis() - currentTimeMillis;
|
||||
logger.debugf("ClearExpiredUserSessions finished in %d ms", took);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -66,6 +66,7 @@ import org.keycloak.testsuite.events.TestEventsListenerProvider;
|
|||
import org.keycloak.testsuite.federation.DummyUserFederationProviderFactory;
|
||||
import org.keycloak.testsuite.forms.PassThroughAuthenticator;
|
||||
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.resource.TestCacheResource;
|
||||
import org.keycloak.testsuite.rest.resource.TestJavascriptResource;
|
||||
|
@ -181,6 +182,22 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
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
|
||||
@Path("/get-client-sessions-count")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
|
|
|
@ -107,6 +107,13 @@ public class TestCacheResource {
|
|||
cache.remove(id);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/process-expiration")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public void processExpiration() {
|
||||
cache.getAdvancedCache().getExpirationManager().processExpiration();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/jgroups-stats")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
<module name="org.keycloak.keycloak-model-jpa"/>
|
||||
<module name="org.keycloak.keycloak-ldap-federation"/>
|
||||
<module name="org.infinispan"/>
|
||||
<module name="org.infinispan.commons"/>
|
||||
<module name="org.infinispan.client.hotrod"/>
|
||||
<module name="org.jgroups"/>
|
||||
<module name="org.jboss.logging"/>
|
||||
|
|
|
@ -66,6 +66,15 @@ public interface TestingCacheResource {
|
|||
@Produces(MediaType.APPLICATION_JSON)
|
||||
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
|
||||
@Path("/jgroups-stats")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
|
|
|
@ -196,6 +196,20 @@ public interface TestingResource {
|
|||
@Produces(MediaType.APPLICATION_JSON)
|
||||
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
|
||||
@Path("/get-client-sessions-count")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
|
|
|
@ -37,6 +37,8 @@ import org.keycloak.testsuite.arquillian.CrossDCTestEnricher;
|
|||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
||||
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.
|
||||
|
@ -221,4 +223,26 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
|
|||
super.resetTimeOffset();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
) {
|
||||
// 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 {
|
||||
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
|
||||
// Ensure to remove all current sessions and offline sessions
|
||||
setTimeOffset(10000000);
|
||||
getTestingClientForStartedNodeInDc(0).testing("test").removeExpired("test");
|
||||
getTestingClientForStartedNodeInDc(1).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 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
|
||||
setTimeOffset(10000000);
|
||||
getTestingClientForStartedNodeInDc(0).testing("test").removeExpired("test");
|
||||
getTestingClientForStartedNodeInDc(1).testing("test").removeExpired("test");
|
||||
setTimeOffset(0);
|
||||
|
||||
sessionCacheDc1Stats.reset();
|
||||
|
|
|
@ -27,6 +27,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
import javax.ws.rs.NotFoundException;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
|
@ -99,12 +100,30 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
|
|||
.build();
|
||||
|
||||
RealmRepresentation realmRep = RealmBuilder.create()
|
||||
.ssoSessionIdleTimeout(300)
|
||||
.ssoSessionMaxLifespan(600)
|
||||
.offlineSessionIdleTimeout(900)
|
||||
.name(REALM_NAME)
|
||||
.user(user)
|
||||
.client(client)
|
||||
.build();
|
||||
|
||||
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);
|
||||
// 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);
|
||||
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(sessions2, sessions2Expected);
|
||||
|
@ -275,8 +294,8 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
|
|||
remoteSessions01 + SESSIONS_COUNT, remoteSessions02 + SESSIONS_COUNT, false);
|
||||
|
||||
|
||||
// Set time offset
|
||||
setTimeOffset(10000000);
|
||||
// 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(610);
|
||||
|
||||
// Assert I am not able to refresh anymore
|
||||
refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password");
|
||||
|
@ -287,7 +306,8 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
|
|||
channelStatisticsCrossDc.reset();
|
||||
|
||||
// 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.
|
||||
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();
|
||||
|
||||
// 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
|
||||
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);
|
||||
|
||||
|
||||
// Set time offset
|
||||
setTimeOffset(10000000);
|
||||
// Increase offset to 20 minutes. Client offline sessions should be expired from infinispan by that. Master sessions will still remain there.
|
||||
setTimeOffset(1210);
|
||||
|
||||
// Assert I am not able to refresh anymore
|
||||
refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password");
|
||||
|
@ -333,7 +353,8 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
|
|||
channelStatisticsCrossDc.reset();
|
||||
|
||||
// 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.
|
||||
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);
|
||||
}
|
||||
|
||||
// Increase offset to big value like 100 hours
|
||||
setTimeOffset(360000);
|
||||
// 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(610);
|
||||
|
||||
// Trigger removeExpired
|
||||
getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
|
||||
getTestingClientForStartedNodeInDc(1).testing().removeExpired(REALM_NAME);
|
||||
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME).processExpiration();
|
||||
|
||||
// Ensure clientSessions were removed
|
||||
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);
|
||||
}
|
||||
|
||||
// Increase offset to big value like 10000 hours (400+ days)
|
||||
setTimeOffset(36000000);
|
||||
// Increase offset to 20 minutes. Client offline sessions should be expired from infinispan by that. Master sessions will still remain there.
|
||||
setTimeOffset(1210);
|
||||
|
||||
// Trigger removeExpired
|
||||
getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
|
||||
getTestingClientForStartedNodeInDc(1).testing().removeExpired(REALM_NAME);
|
||||
getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME).processExpiration();
|
||||
|
||||
// Ensure clientSessions were removed
|
||||
assertStatisticsExpected("After remove expired", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,
|
||||
|
|
|
@ -770,23 +770,28 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
|
|||
// KEYCLOAK-1037
|
||||
@Test
|
||||
public void loginExpiredCodeWithExplicitRemoveExpired() {
|
||||
loginPage.open();
|
||||
setTimeOffset(5000);
|
||||
// Explicitly call "removeExpired". Hence authSession won't exist, but will be restarted from the KC_RESTART
|
||||
testingClient.testing().removeExpired("test");
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
|
||||
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.assertCurrent();
|
||||
loginPage.login("login@test.com", "password");
|
||||
|
||||
Assert.assertEquals("Your login attempt timed out. Login will start from the beginning.", loginPage.getError());
|
||||
setTimeOffset(0);
|
||||
//loginPage.assertCurrent();
|
||||
loginPage.assertCurrent();
|
||||
|
||||
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
|
||||
.detail(Details.RESTART_AFTER_TIMEOUT, "true")
|
||||
.client((String) null)
|
||||
.assertEvent();
|
||||
Assert.assertEquals("Your login attempt timed out. Login will start from the beginning.", loginPage.getError());
|
||||
|
||||
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
|
||||
.detail(Details.RESTART_AFTER_TIMEOUT, "true")
|
||||
.client((String) null)
|
||||
.assertEvent();
|
||||
} finally {
|
||||
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.testsuite.model;
|
|||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.util.Time;
|
||||
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.junit.Assert.assertThat;
|
||||
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;
|
||||
|
||||
/**
|
||||
|
@ -54,6 +56,9 @@ import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerEx
|
|||
@AuthServerContainerExclude(REMOTE)
|
||||
public class AuthenticationSessionProviderTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
@Rule
|
||||
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
testingClient.server().run(session -> {
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.testsuite.model;
|
|||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
|
@ -41,6 +42,7 @@ import org.keycloak.services.managers.UserSessionManager;
|
|||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
import org.keycloak.testsuite.arquillian.annotation.ModelTest;
|
||||
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
|
||||
import org.keycloak.timer.TimerProvider;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
@ -61,6 +63,10 @@ import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.A
|
|||
*/
|
||||
@AuthServerContainerExclude(AuthServer.REMOTE)
|
||||
public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
@Rule
|
||||
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
|
||||
|
||||
private static KeycloakSession currentSession;
|
||||
private static RealmModel realm;
|
||||
private static UserSessionManager sessionManager;
|
||||
|
@ -545,7 +551,7 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes
|
|||
Assert.assertEquals(1, persister.getUserSessionsCount(true));
|
||||
|
||||
// Expire everything and assert nothing found
|
||||
Time.setOffset(6000000);
|
||||
Time.setOffset(7000000);
|
||||
|
||||
currentSession.sessions().removeExpired(realm);
|
||||
persister.removeExpired(realm);
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.testsuite.model;
|
|||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.util.Time;
|
||||
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.assertTrue;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
||||
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
|
||||
|
||||
/**
|
||||
* @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)
|
||||
public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
@Rule
|
||||
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
|
||||
|
||||
public static void setupRealm(KeycloakSession session){
|
||||
RealmModel realm = session.realms().getRealmByName("test");
|
||||
UserModel user1 = session.users().addUser(realm, "user1");
|
||||
|
@ -469,6 +474,7 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
|
|||
assertEquals(userSession, clientSession.getUserSession());
|
||||
Time.setOffset(-(realm.getSsoSessionIdleTimeout() * 2));
|
||||
userSession.setLastSessionRefresh(Time.currentTime());
|
||||
clientSession.setTimestamp(Time.currentTime());
|
||||
validUserSessions.add(userSession.getId());
|
||||
validClientSessions.add(clientSession.getId());
|
||||
});
|
||||
|
|
|
@ -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());
|
||||
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue