KEYCLOAK-4066 TimeoutException in cluster environment in ClearExpiredSessions
This commit is contained in:
parent
811d532b48
commit
7098daaf72
7 changed files with 206 additions and 10 deletions
|
@ -164,9 +164,16 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
|
||||||
throw new RuntimeException("Invalid value for sessionsMode");
|
throw new RuntimeException("Invalid value for sessionsMode");
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionConfigBuilder.clustering().hash()
|
int l1Lifespan = config.getInt("l1Lifespan", 600000);
|
||||||
.numOwners(config.getInt("sessionsOwners", 2))
|
boolean l1Enabled = l1Lifespan > 0;
|
||||||
.numSegments(config.getInt("sessionsSegments", 60)).build();
|
sessionConfigBuilder.clustering()
|
||||||
|
.hash()
|
||||||
|
.numOwners(config.getInt("sessionsOwners", 2))
|
||||||
|
.numSegments(config.getInt("sessionsSegments", 60))
|
||||||
|
.l1()
|
||||||
|
.enabled(l1Enabled)
|
||||||
|
.lifespan(l1Lifespan)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
Configuration sessionCacheConfiguration = sessionConfigBuilder.build();
|
Configuration sessionCacheConfiguration = sessionConfigBuilder.build();
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.models.sessions.infinispan;
|
||||||
|
|
||||||
import org.infinispan.Cache;
|
import org.infinispan.Cache;
|
||||||
import org.infinispan.CacheStream;
|
import org.infinispan.CacheStream;
|
||||||
|
import org.infinispan.context.Flag;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.ClientInitialAccessModel;
|
import org.keycloak.models.ClientInitialAccessModel;
|
||||||
|
@ -291,6 +292,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeExpired(RealmModel realm) {
|
public void removeExpired(RealmModel realm) {
|
||||||
|
log.debugf("Removing expired sessions");
|
||||||
removeExpiredUserSessions(realm);
|
removeExpiredUserSessions(realm);
|
||||||
removeExpiredClientSessions(realm);
|
removeExpiredClientSessions(realm);
|
||||||
removeExpiredOfflineUserSessions(realm);
|
removeExpiredOfflineUserSessions(realm);
|
||||||
|
@ -302,9 +304,13 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
||||||
int expired = Time.currentTime() - realm.getSsoSessionMaxLifespan();
|
int expired = Time.currentTime() - realm.getSsoSessionMaxLifespan();
|
||||||
int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout();
|
int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout();
|
||||||
|
|
||||||
Iterator<Map.Entry<String, SessionEntity>> itr = sessionCache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).expired(expired, expiredRefresh)).iterator();
|
// Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
|
||||||
|
Iterator<Map.Entry<String, SessionEntity>> itr = sessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
|
||||||
|
.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).expired(expired, expiredRefresh)).iterator();
|
||||||
|
|
||||||
|
int counter = 0;
|
||||||
while (itr.hasNext()) {
|
while (itr.hasNext()) {
|
||||||
|
counter++;
|
||||||
UserSessionEntity entity = (UserSessionEntity) itr.next().getValue();
|
UserSessionEntity entity = (UserSessionEntity) itr.next().getValue();
|
||||||
tx.remove(sessionCache, entity.getId());
|
tx.remove(sessionCache, entity.getId());
|
||||||
|
|
||||||
|
@ -314,23 +320,38 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.debugf("Removed %d expired user sessions for realm '%s'", counter, realm.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeExpiredClientSessions(RealmModel realm) {
|
private void removeExpiredClientSessions(RealmModel realm) {
|
||||||
int expiredDettachedClientSession = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm);
|
int expiredDettachedClientSession = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm);
|
||||||
|
|
||||||
Iterator<Map.Entry<String, SessionEntity>> itr = sessionCache.entrySet().stream().filter(ClientSessionPredicate.create(realm.getId()).expiredRefresh(expiredDettachedClientSession).requireNullUserSession()).iterator();
|
// Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
|
||||||
|
Iterator<Map.Entry<String, SessionEntity>> itr = sessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
|
||||||
|
.entrySet().stream().filter(ClientSessionPredicate.create(realm.getId()).expiredRefresh(expiredDettachedClientSession).requireNullUserSession()).iterator();
|
||||||
|
|
||||||
|
int counter = 0;
|
||||||
while (itr.hasNext()) {
|
while (itr.hasNext()) {
|
||||||
|
counter++;
|
||||||
tx.remove(sessionCache, itr.next().getKey());
|
tx.remove(sessionCache, itr.next().getKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.debugf("Removed %d expired client sessions for realm '%s'", counter, realm.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeExpiredOfflineUserSessions(RealmModel realm) {
|
private void removeExpiredOfflineUserSessions(RealmModel realm) {
|
||||||
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
|
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
|
||||||
int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout();
|
int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout();
|
||||||
|
|
||||||
Iterator<Map.Entry<String, SessionEntity>> itr = offlineSessionCache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).expired(null, expiredOffline)).iterator();
|
// Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
|
||||||
|
UserSessionPredicate predicate = UserSessionPredicate.create(realm.getId()).expired(null, expiredOffline);
|
||||||
|
Iterator<Map.Entry<String, SessionEntity>> itr = offlineSessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
|
||||||
|
.entrySet().stream().filter(predicate).iterator();
|
||||||
|
|
||||||
|
int counter = 0;
|
||||||
while (itr.hasNext()) {
|
while (itr.hasNext()) {
|
||||||
|
counter++;
|
||||||
UserSessionEntity entity = (UserSessionEntity) itr.next().getValue();
|
UserSessionEntity entity = (UserSessionEntity) itr.next().getValue();
|
||||||
tx.remove(offlineSessionCache, entity.getId());
|
tx.remove(offlineSessionCache, entity.getId());
|
||||||
|
|
||||||
|
@ -340,22 +361,32 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
||||||
tx.remove(offlineSessionCache, clientSessionId);
|
tx.remove(offlineSessionCache, clientSessionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.debugf("Removed %d expired offline user sessions for realm '%s'", counter, realm.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeExpiredOfflineClientSessions(RealmModel realm) {
|
private void removeExpiredOfflineClientSessions(RealmModel realm) {
|
||||||
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
|
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
|
||||||
int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout();
|
int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout();
|
||||||
|
|
||||||
Iterator<String> itr = offlineSessionCache.entrySet().stream().filter(ClientSessionPredicate.create(realm.getId()).expiredRefresh(expiredOffline)).map(Mappers.sessionId()).iterator();
|
// Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
|
||||||
|
Iterator<String> itr = offlineSessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
|
||||||
|
.entrySet().stream().filter(ClientSessionPredicate.create(realm.getId()).expiredRefresh(expiredOffline)).map(Mappers.sessionId()).iterator();
|
||||||
|
|
||||||
|
int counter = 0;
|
||||||
while (itr.hasNext()) {
|
while (itr.hasNext()) {
|
||||||
|
counter++;
|
||||||
String sessionId = itr.next();
|
String sessionId = itr.next();
|
||||||
tx.remove(offlineSessionCache, sessionId);
|
tx.remove(offlineSessionCache, sessionId);
|
||||||
persister.removeClientSession(sessionId, true);
|
persister.removeClientSession(sessionId, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.debugf("Removed %d expired offline client sessions for realm '%s'", counter, realm.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeExpiredClientInitialAccess(RealmModel realm) {
|
private void removeExpiredClientInitialAccess(RealmModel realm) {
|
||||||
Iterator<String> itr = sessionCache.entrySet().stream().filter(ClientInitialAccessPredicate.create(realm.getId()).expired(Time.currentTime())).map(Mappers.sessionId()).iterator();
|
Iterator<String> itr = sessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
|
||||||
|
.entrySet().stream().filter(ClientInitialAccessPredicate.create(realm.getId()).expired(Time.currentTime())).map(Mappers.sessionId()).iterator();
|
||||||
while (itr.hasNext()) {
|
while (itr.hasNext()) {
|
||||||
tx.remove(sessionCache, itr.next());
|
tx.remove(sessionCache, itr.next());
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ public class SessionInitializerWorker implements DistributedCallable<String, Ser
|
||||||
|
|
||||||
KeycloakSessionFactory sessionFactory = workCache.getAdvancedCache().getComponentRegistry().getComponent(KeycloakSessionFactory.class);
|
KeycloakSessionFactory sessionFactory = workCache.getAdvancedCache().getComponentRegistry().getComponent(KeycloakSessionFactory.class);
|
||||||
if (sessionFactory == null) {
|
if (sessionFactory == null) {
|
||||||
log.warnf("KeycloakSessionFactory not yet set in cache. Worker skipped");
|
log.debugf("KeycloakSessionFactory not yet set in cache. Worker skipped");
|
||||||
return InfinispanUserSessionInitializer.WorkerResult.create(segment, false);
|
return InfinispanUserSessionInitializer.WorkerResult.create(segment, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.testsuite.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.testsuite.KeycloakServer;
|
||||||
|
import org.keycloak.testsuite.rule.KeycloakRule;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run test with shared MySQL DB and in cluster:
|
||||||
|
*
|
||||||
|
* -Dkeycloak.connectionsJpa.url=jdbc:mysql://localhost/keycloak -Dkeycloak.connectionsJpa.driver=com.mysql.jdbc.Driver -Dkeycloak.connectionsJpa.user=keycloak
|
||||||
|
* -Dkeycloak.connectionsJpa.password=keycloak -Dkeycloak.connectionsInfinispan.clustered=true
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
@Ignore
|
||||||
|
public class ClusterSessionCleanerTest {
|
||||||
|
|
||||||
|
protected static final Logger logger = Logger.getLogger(ClusterSessionCleanerTest.class);
|
||||||
|
|
||||||
|
private static final String REALM_NAME = "test";
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static KeycloakRule server1 = new KeycloakRule();
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static KeycloakRule server2 = new KeycloakRule() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configureServer(KeycloakServer server) {
|
||||||
|
server.getConfig().setPort(8082);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void importRealm() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void removeTestRealms() {
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testClusterPeriodicSessionCleanups() throws Exception {
|
||||||
|
// Add some userSessions on server1
|
||||||
|
KeycloakSession session1 = server1.startSession();
|
||||||
|
RealmModel realm1 = session1.realms().getRealmByName(REALM_NAME);
|
||||||
|
UserModel user1 = session1.users().getUserByUsername("test-user@localhost", realm1);
|
||||||
|
for (int i=0 ; i<15 ; i++) {
|
||||||
|
session1.sessions().createUserSession(realm1, user1, user1.getUsername(), "127.0.0.1", "form", true, null, null);
|
||||||
|
}
|
||||||
|
session1 = commit(server1, session1);
|
||||||
|
|
||||||
|
// Add some userSessions on server2
|
||||||
|
KeycloakSession session2 = server2.startSession();
|
||||||
|
RealmModel realm2 = session2.realms().getRealmByName(REALM_NAME);
|
||||||
|
UserModel user2 = session2.users().getUserByUsername("test-user@localhost", realm2);
|
||||||
|
// Check we are really in cluster (same user ids)
|
||||||
|
Assert.assertEquals(user2.getId(), user1.getId());
|
||||||
|
|
||||||
|
for (int i=0 ; i<15 ; i++) {
|
||||||
|
session2.sessions().createUserSession(realm2, user2, user2.getUsername(), "127.0.0.1", "form", true, null, null);
|
||||||
|
}
|
||||||
|
session2 = commit(server2, session2);
|
||||||
|
|
||||||
|
// Assert sessions on both nodes
|
||||||
|
List<UserSessionModel> sessions1 = getSessions(session1);
|
||||||
|
List<UserSessionModel> sessions2 = getSessions(session2);
|
||||||
|
Assert.assertEquals(30, sessions1.size());
|
||||||
|
Assert.assertEquals(30, sessions2.size());
|
||||||
|
logger.info("Before offset: sessions1 : " + sessions1.size());
|
||||||
|
logger.info("Before offset: sessions2 : " + sessions2.size());
|
||||||
|
|
||||||
|
|
||||||
|
// set Time offset and run periodic cleaner on server1
|
||||||
|
Time.setOffset(999999);
|
||||||
|
realm1 = session1.realms().getRealmByName(REALM_NAME);
|
||||||
|
session1.sessions().removeExpired(realm1);
|
||||||
|
session1 = commit(server1, session1);
|
||||||
|
|
||||||
|
// Ensure some sessions still there
|
||||||
|
sessions1 = getSessions(session1);
|
||||||
|
sessions2 = getSessions(session2);
|
||||||
|
logger.info("After server1 periodic clean: sessions1 : " + sessions1.size());
|
||||||
|
logger.info("After server1 periodic clean: sessions2 : " + sessions2.size());
|
||||||
|
|
||||||
|
|
||||||
|
// Run periodic cleaner on server2
|
||||||
|
realm2 = session2.realms().getRealmByName(REALM_NAME);
|
||||||
|
session2.sessions().removeExpired(realm2);
|
||||||
|
session2 = commit(server1, session2);
|
||||||
|
|
||||||
|
// Ensure there are no sessions on server1 or server2
|
||||||
|
sessions1 = getSessions(session1);
|
||||||
|
sessions2 = getSessions(session2);
|
||||||
|
Assert.assertTrue(sessions1.isEmpty());
|
||||||
|
Assert.assertTrue(sessions2.isEmpty());
|
||||||
|
logger.info("After both periodic cleans: sessions1 : " + sessions1.size());
|
||||||
|
logger.info("After both periodic cleans: sessions2 : " + sessions2.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<UserSessionModel> getSessions(KeycloakSession session) {
|
||||||
|
RealmModel realm = session.realms().getRealmByName(REALM_NAME);
|
||||||
|
UserModel user = session.users().getUserByUsername("test-user@localhost", realm);
|
||||||
|
return session.sessions().getUserSessions(realm, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeycloakSession commit(KeycloakRule rule, KeycloakSession session) throws Exception {
|
||||||
|
session.getTransactionManager().commit();
|
||||||
|
session.close();
|
||||||
|
return rule.startSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -223,4 +223,18 @@ public abstract class AbstractOfflineCacheCommand extends AbstractCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static class SizeLocalCommand extends AbstractOfflineCacheCommand {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "sizeLocal";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doRunCacheCommand(KeycloakSession session, Cache<String, SessionEntity> cache) {
|
||||||
|
log.info("Size local: " + cache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL).size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ public class TestsuiteCLI {
|
||||||
AbstractOfflineCacheCommand.GetCommand.class,
|
AbstractOfflineCacheCommand.GetCommand.class,
|
||||||
AbstractOfflineCacheCommand.GetMultipleCommand.class,
|
AbstractOfflineCacheCommand.GetMultipleCommand.class,
|
||||||
AbstractOfflineCacheCommand.GetLocalCommand.class,
|
AbstractOfflineCacheCommand.GetLocalCommand.class,
|
||||||
|
AbstractOfflineCacheCommand.SizeLocalCommand.class,
|
||||||
AbstractOfflineCacheCommand.RemoveCommand.class,
|
AbstractOfflineCacheCommand.RemoveCommand.class,
|
||||||
AbstractOfflineCacheCommand.SizeCommand.class,
|
AbstractOfflineCacheCommand.SizeCommand.class,
|
||||||
AbstractOfflineCacheCommand.ListCommand.class,
|
AbstractOfflineCacheCommand.ListCommand.class,
|
||||||
|
|
|
@ -97,7 +97,8 @@
|
||||||
"default": {
|
"default": {
|
||||||
"clustered": "${keycloak.connectionsInfinispan.clustered:false}",
|
"clustered": "${keycloak.connectionsInfinispan.clustered:false}",
|
||||||
"async": "${keycloak.connectionsInfinispan.async:false}",
|
"async": "${keycloak.connectionsInfinispan.async:false}",
|
||||||
"sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}",
|
"sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:1}",
|
||||||
|
"l1Lifespan": "${keycloak.connectionsInfinispan.l1Lifespan:600000}",
|
||||||
"remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:false}",
|
"remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:false}",
|
||||||
"remoteStoreHost": "${keycloak.connectionsInfinispan.remoteStoreHost:localhost}",
|
"remoteStoreHost": "${keycloak.connectionsInfinispan.remoteStoreHost:localhost}",
|
||||||
"remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}"
|
"remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}"
|
||||||
|
|
Loading…
Reference in a new issue