From b98735a6a60c19344217704d9c02d875551686d2 Mon Sep 17 00:00:00 2001 From: Peter Flintholm Date: Fri, 2 Apr 2021 22:55:21 +0200 Subject: [PATCH] KEYCLOAK-18039: Optimise offline session load on startup Co-authored-by: Hynek Mlnarik --- ...ltInfinispanConnectionProviderFactory.java | 77 +++++++++++++++++++ .../OfflinePersistentUserSessionLoader.java | 20 ++--- .../OfflinePersistentWorkerContext.java | 9 +-- .../OfflinePersistentWorkerResult.java | 10 +-- .../JpaUserSessionPersisterProvider.java | 32 ++++---- .../PersistentClientSessionEntity.java | 2 +- .../session/PersistentUserSessionEntity.java | 6 +- .../DisabledUserSessionPersisterProvider.java | 2 +- .../session/UserSessionPersisterProvider.java | 9 +-- .../model/infinispan/InfinispanTestUtil.java | 42 ++-------- .../infinispan/KeycloakTestTimeService.java | 52 ------------- .../rest/TestingResourceProvider.java | 2 +- .../client/resources/TestingResource.java | 3 +- .../model/parameters/Infinispan.java | 1 + .../UserSessionPersisterProviderTest.java | 42 +++++----- .../session/UserSessionProviderModelTest.java | 2 +- .../UserSessionProviderOfflineModelTest.java | 2 +- .../cli/LoadPersistentSessionsCommand.java | 12 +-- 18 files changed, 146 insertions(+), 179 deletions(-) delete mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/model/infinispan/KeycloakTestTimeService.java diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java index fd30e0aa8956..8c5416e9c5ef 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java @@ -46,6 +46,7 @@ import org.keycloak.cluster.ClusterProvider; import org.keycloak.cluster.ManagedCacheManagerProvider; import org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory; +import org.keycloak.common.util.Time; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @@ -56,7 +57,14 @@ import org.keycloak.models.utils.PostMigrationEvent; import org.keycloak.provider.InvalidationHandler.ObjectType; import org.keycloak.provider.ProviderEvent; +import java.time.Instant; +import java.util.concurrent.atomic.AtomicReference; +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.persistence.remote.configuration.RemoteStoreConfigurationBuilder; +import org.infinispan.util.EmbeddedTimeService; import static org.keycloak.models.cache.infinispan.InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS; import static org.keycloak.models.cache.infinispan.InfinispanCacheRealmProviderFactory.REALM_INVALIDATION_EVENTS; @@ -191,6 +199,7 @@ protected void initEmbedded() { boolean clustered = config.getBoolean("clustered", false); boolean async = config.getBoolean("async", false); + boolean useKeycloakTimeService = config.getBoolean("useKeycloakTimeService", false); this.topologyInfo = new TopologyInfo(cacheManager, config, true); @@ -209,6 +218,9 @@ protected void initEmbedded() { gcb.serialization().marshaller(new JBossUserMarshaller()); cacheManager = new DefaultCacheManager(gcb.build()); + if (useKeycloakTimeService) { + setTimeServiceToKeycloakTime(cacheManager); + } containerManaged = false; logger.debug("Started embedded Infinispan cache container"); @@ -354,6 +366,25 @@ protected void initEmbedded() { cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME, true); } + /** + * Replaces the {@link TimeService} in infinispan with the one that respects Keycloak {@link Time}. + * @param cacheManager + * @return Runnable to revert replacement of the infinispan time service + */ + public static Runnable setTimeServiceToKeycloakTime(EmbeddedCacheManager cacheManager) { + TimeService previousTimeService = replaceComponent(cacheManager, TimeService.class, KEYCLOAK_TIME_SERVICE, true); + AtomicReference ref = new AtomicReference<>(previousTimeService); + return () -> { + if (ref.get() == null) { + logger.warn("Calling revert of the TimeService when testing TimeService was already reverted"); + return; + } + + logger.info("Revert set KeycloakIspnTimeService to the infinispan cacheManager"); + + replaceComponent(cacheManager, TimeService.class, ref.getAndSet(null), true); + }; + } private Configuration getRevisionCacheConfig(long maxEntries) { ConfigurationBuilder cb = createCacheConfigurationBuilder(); @@ -545,4 +576,50 @@ private void registerSystemWideListeners(KeycloakSession session) { }); } + + /** + * 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 replaceComponent(EmbeddedCacheManager cacheMgr, Class componentType, T replacementComponent, boolean rewire) { + GlobalComponentRegistry cr = cacheMgr.getGlobalComponentRegistry(); + BasicComponentRegistry bcr = cr.getComponent(BasicComponentRegistry.class); + ComponentRef old = bcr.getComponent(componentType); + bcr.replaceComponent(componentType.getName(), replacementComponent, true); + if (rewire) { + cr.rewire(); + cr.rewireNamedRegistries(); + } + return old != null ? old.wired() : null; + } + + public static final TimeService KEYCLOAK_TIME_SERVICE = new 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()); + } + }; } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflinePersistentUserSessionLoader.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflinePersistentUserSessionLoader.java index 8784f7fb0de9..9a4065c8f411 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflinePersistentUserSessionLoader.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflinePersistentUserSessionLoader.java @@ -38,7 +38,7 @@ public class OfflinePersistentUserSessionLoader implements SessionLoader, Serializable { // Placeholder String used in the searching conditions to identify very first session - private static final String FIRST_SESSION_ID = "000"; + private static final String FIRST_SESSION_ID = "00000000-0000-0000-0000-000000000000"; private static final Logger log = Logger.getLogger(OfflinePersistentUserSessionLoader.class); @@ -72,24 +72,21 @@ public OfflinePersistentLoaderContext computeLoaderContext(KeycloakSession sessi @Override public OfflinePersistentWorkerContext computeWorkerContext(OfflinePersistentLoaderContext loaderCtx, int segment, int workerId, OfflinePersistentWorkerResult previousResult) { - int lastCreatedOn; String lastSessionId; if (previousResult == null) { - lastCreatedOn = 0; lastSessionId = FIRST_SESSION_ID; } else { - lastCreatedOn = previousResult.getLastCreatedOn(); lastSessionId = previousResult.getLastSessionId(); } // We know the last loaded session. New workers iteration will start from this place - return new OfflinePersistentWorkerContext(segment, workerId, lastCreatedOn, lastSessionId); + return new OfflinePersistentWorkerContext(segment, workerId, lastSessionId); } @Override public OfflinePersistentWorkerResult createFailedWorkerResult(OfflinePersistentLoaderContext loaderContext, OfflinePersistentWorkerContext workerContext) { - return new OfflinePersistentWorkerResult(false, workerContext.getSegment(), workerContext.getWorkerId(), -1, FIRST_SESSION_ID); + return new OfflinePersistentWorkerResult(false, workerContext.getSegment(), workerContext.getWorkerId(), FIRST_SESSION_ID); } @@ -97,14 +94,14 @@ public OfflinePersistentWorkerResult createFailedWorkerResult(OfflinePersistentL public OfflinePersistentWorkerResult loadSessions(KeycloakSession session, OfflinePersistentLoaderContext loaderContext, OfflinePersistentWorkerContext ctx) { int first = ctx.getWorkerId() * sessionsPerSegment; - log.tracef("Loading sessions for segment=%d createdOn=%d lastSessionId=%s", ctx.getSegment(), ctx.getLastCreatedOn(), ctx.getLastSessionId()); + log.tracef("Loading sessions for segment=%d lastSessionId=%s", ctx.getSegment(), ctx.getLastSessionId()); UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); List sessions = persister - .loadUserSessionsStream(first, sessionsPerSegment, true, ctx.getLastCreatedOn(), ctx.getLastSessionId()) + .loadUserSessionsStream(first, sessionsPerSegment, true, ctx.getLastSessionId()) .collect(Collectors.toList()); - log.tracef("Sessions loaded from DB - segment=%d createdOn=%d lastSessionId=%s", ctx.getSegment(), ctx.getLastCreatedOn(), ctx.getLastSessionId()); + log.tracef("Sessions loaded from DB - segment=%d lastSessionId=%s", ctx.getSegment(), ctx.getLastSessionId()); UserSessionModel lastSession = null; if (!sessions.isEmpty()) { @@ -114,12 +111,11 @@ public OfflinePersistentWorkerResult loadSessions(KeycloakSession session, Offli session.sessions().importUserSessions(sessions, true); } - int lastCreatedOn = lastSession==null ? Time.currentTime() + 100000 : lastSession.getStarted(); String lastSessionId = lastSession==null ? FIRST_SESSION_ID : lastSession.getId(); - log.tracef("Sessions imported to infinispan - segment: %d, lastCreatedOn: %d, lastSessionId: %s", ctx.getSegment(), lastCreatedOn, lastSessionId); + log.tracef("Sessions imported to infinispan - segment: %d, lastSessionId: %s", ctx.getSegment(), lastSessionId); - return new OfflinePersistentWorkerResult(true, ctx.getSegment(), ctx.getWorkerId(), lastCreatedOn, lastSessionId); + return new OfflinePersistentWorkerResult(true, ctx.getSegment(), ctx.getWorkerId(), lastSessionId); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflinePersistentWorkerContext.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflinePersistentWorkerContext.java index 8d8e3f3ba372..83ea795b0ed1 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflinePersistentWorkerContext.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflinePersistentWorkerContext.java @@ -22,20 +22,13 @@ */ public class OfflinePersistentWorkerContext extends SessionLoader.WorkerContext { - private final int lastCreatedOn; private final String lastSessionId; - public OfflinePersistentWorkerContext(int segment, int workerId, int lastCreatedOn, String lastSessionId) { + public OfflinePersistentWorkerContext(int segment, int workerId, String lastSessionId) { super(segment, workerId); - this.lastCreatedOn = lastCreatedOn; this.lastSessionId = lastSessionId; } - - public int getLastCreatedOn() { - return lastCreatedOn; - } - public String getLastSessionId() { return lastSessionId; } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflinePersistentWorkerResult.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflinePersistentWorkerResult.java index 44aa2c52cd46..8e9ed33de91a 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflinePersistentWorkerResult.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflinePersistentWorkerResult.java @@ -22,22 +22,14 @@ */ public class OfflinePersistentWorkerResult extends SessionLoader.WorkerResult { - private final int lastCreatedOn; private final String lastSessionId; - public OfflinePersistentWorkerResult(boolean success, int segment, int workerId, int lastCreatedOn, String lastSessionId) { + public OfflinePersistentWorkerResult(boolean success, int segment, int workerId, String lastSessionId) { super(success, segment, workerId); - this.lastCreatedOn = lastCreatedOn; this.lastSessionId = lastSessionId; } - - public int getLastCreatedOn() { - return lastCreatedOn; - } - - public String getLastSessionId() { return lastSessionId; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java index cb390a2b6004..78c2fcb67c88 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java @@ -244,31 +244,33 @@ public void removeExpired(RealmModel realm) { @Override public Stream loadUserSessionsStream(Integer firstResult, Integer maxResults, boolean offline, - Integer lastCreatedOn, String lastUserSessionId) { + String lastUserSessionId) { String offlineStr = offlineToString(offline); - TypedQuery query = em.createNamedQuery("findUserSessions", PersistentUserSessionEntity.class); - query.setParameter("offline", offlineStr); - query.setParameter("lastCreatedOn", lastCreatedOn); - query.setParameter("lastSessionId", lastUserSessionId); + TypedQuery queryUserSessions = em.createNamedQuery("findUserSessionsOrderedById", PersistentUserSessionEntity.class); + queryUserSessions.setParameter("offline", offlineStr); + queryUserSessions.setParameter("lastSessionId", lastUserSessionId); - List result = closing(paginateQuery(query, firstResult, maxResults).getResultStream() + List userSessionAdapters = closing(paginateQuery(queryUserSessions, firstResult, maxResults).getResultStream() .map(this::toAdapter)) .filter(Objects::nonNull) .collect(Collectors.toList()); - Map sessionsById = result.stream() + Map sessionsById = userSessionAdapters.stream() .collect(Collectors.toMap(UserSessionModel::getId, Function.identity())); - Set userSessionIds = sessionsById.keySet(); - Set removedClientUUIDs = new HashSet<>(); - if (!userSessionIds.isEmpty()) { - TypedQuery query2 = em.createNamedQuery("findClientSessionsByUserSessions", PersistentClientSessionEntity.class); - query2.setParameter("userSessionIds", userSessionIds); - query2.setParameter("offline", offlineStr); - closing(query2.getResultStream()).forEach(clientSession -> { + if (!sessionsById.isEmpty()) { + String fromUserSessionId = userSessionAdapters.get(0).getId(); + String toUserSessionId = userSessionAdapters.get(userSessionAdapters.size() - 1).getId(); + + TypedQuery queryClientSessions = em.createNamedQuery("findClientSessionsOrderedById", PersistentClientSessionEntity.class); + queryClientSessions.setParameter("offline", offlineStr); + queryClientSessions.setParameter("fromSessionId", fromUserSessionId); + queryClientSessions.setParameter("toSessionId", toUserSessionId); + + closing(queryClientSessions.getResultStream()).forEach(clientSession -> { PersistentUserSessionAdapter userSession = sessionsById.get(clientSession.getUserSessionId()); PersistentAuthenticatedClientSessionAdapter clientSessAdapter = toAdapter(userSession.getRealm(), userSession, clientSession); @@ -287,7 +289,7 @@ public Stream loadUserSessionsStream(Integer firstResult, Inte onClientRemoved(clientUUID); } - return result.stream().map(UserSessionModel.class::cast); + return userSessionAdapters.stream().map(UserSessionModel.class::cast); } private PersistentUserSessionAdapter toAdapter(PersistentUserSessionEntity entity) { diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java index 5703f1840326..d8173c41ff3f 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java @@ -38,7 +38,7 @@ @NamedQuery(name="deleteClientSessionsByUserSession", query="delete from PersistentClientSessionEntity sess where sess.userSessionId = :userSessionId and sess.offline = :offline"), @NamedQuery(name="deleteExpiredClientSessions", query="delete from PersistentClientSessionEntity sess where sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.realmId = :realmId AND u.offline = :offline AND u.lastSessionRefresh < :lastSessionRefresh)"), @NamedQuery(name="findClientSessionsByUserSession", query="select sess from PersistentClientSessionEntity sess where sess.userSessionId=:userSessionId and sess.offline = :offline"), - @NamedQuery(name="findClientSessionsByUserSessions", query="select sess from PersistentClientSessionEntity sess where sess.offline = :offline and sess.userSessionId IN (:userSessionIds) order by sess.userSessionId") + @NamedQuery(name="findClientSessionsOrderedById", query="select sess from PersistentClientSessionEntity sess where sess.offline = :offline and sess.userSessionId >= :fromSessionId and sess.userSessionId <= :toSessionId order by sess.userSessionId") }) @Table(name="OFFLINE_CLIENT_SESSION") @Entity diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java index 6216a19c1859..e9f873798351 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java @@ -38,9 +38,9 @@ @NamedQuery(name="updateUserSessionLastSessionRefresh", query="update PersistentUserSessionEntity sess set lastSessionRefresh = :lastSessionRefresh where sess.realmId = :realmId" + " AND sess.offline = :offline AND sess.userSessionId IN (:userSessionIds)"), @NamedQuery(name="findUserSessionsCount", query="select count(sess) from PersistentUserSessionEntity sess where sess.offline = :offline"), - @NamedQuery(name="findUserSessions", query="select sess from PersistentUserSessionEntity sess, RealmEntity realm where realm.id = sess.realmId AND sess.offline = :offline" + - " AND (sess.createdOn > :lastCreatedOn OR (sess.createdOn = :lastCreatedOn AND sess.userSessionId > :lastSessionId))" + - " order by sess.createdOn,sess.userSessionId") + @NamedQuery(name="findUserSessionsOrderedById", query="select sess from PersistentUserSessionEntity sess, RealmEntity realm where realm.id = sess.realmId AND sess.offline = :offline" + + " AND sess.userSessionId > :lastSessionId" + + " order by sess.userSessionId") }) @Table(name="OFFLINE_USER_SESSION") diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java b/server-spi-private/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java index 3c653b04c63b..26b12a3d1d31 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java @@ -111,7 +111,7 @@ public void removeExpired(RealmModel realm) { @Override public Stream loadUserSessionsStream(Integer firstResult, Integer maxResults, boolean offline, - Integer lastCreatedOn, String lastUserSessionId) { + String lastUserSessionId) { return Stream.empty(); } diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java b/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java index 1e8b9ea8fedc..3b7e4e0b44c2 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java @@ -58,11 +58,11 @@ public interface UserSessionPersisterProvider extends Provider { /** * Called during startup. For each userSession, it loads also clientSessions - * @deprecated Use {@link #loadUserSessionsStream(Integer, Integer, boolean, Integer, String) loadUserSessionsStream} instead. + * @deprecated Use {@link #loadUserSessionsStream(Integer, Integer, boolean, String) loadUserSessionsStream} instead. */ @Deprecated default List loadUserSessions(int firstResult, int maxResults, boolean offline, int lastCreatedOn, String lastUserSessionId) { - return loadUserSessionsStream(firstResult, maxResults, offline, lastCreatedOn, lastUserSessionId).collect(Collectors.toList()); + return loadUserSessionsStream(firstResult, maxResults, offline, lastUserSessionId).collect(Collectors.toList()); } /** @@ -70,13 +70,12 @@ default List loadUserSessions(int firstResult, int maxResults, * @param firstResult {@code Integer} Index of the first desired user session. Ignored if negative or {@code null}. * @param maxResults {@code Integer} Maximum number of returned user sessions. Ignored if negative or {@code null}. * @param offline {@code boolean} Flag to include offline sessions. - * @param lastCreatedOn {@code Integer} Timestamp when the user session was created. It will return only user sessions created later. - * @param lastUserSessionId {@code String} Id of the user session. In case of equal {@code lastCreatedOn} + * @param lastUserSessionId {@code String} Id of the user session. It will return only user sessions with id's lexicographically greater than this. * it will compare the id in dictionary order and takes only those created later. * @return Stream of {@link UserSessionModel}. Never returns {@code null}. */ Stream loadUserSessionsStream(Integer firstResult, Integer maxResults, boolean offline, - Integer lastCreatedOn, String lastUserSessionId); + String lastUserSessionId); int getUserSessionsCount(boolean offline); diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/model/infinispan/InfinispanTestUtil.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/model/infinispan/InfinispanTestUtil.java index 11a3450f8c3d..3e0ff8f17eec 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/model/infinispan/InfinispanTestUtil.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/model/infinispan/InfinispanTestUtil.java @@ -18,10 +18,7 @@ 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.keycloak.connections.infinispan.DefaultInfinispanConnectionProviderFactory; import org.infinispan.manager.EmbeddedCacheManager; import org.jboss.logging.Logger; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; @@ -34,7 +31,7 @@ public class InfinispanTestUtil { protected static final Logger logger = Logger.getLogger(InfinispanTestUtil.class); - private static TimeService origTimeService = null; + private static Runnable origTimeService = null; /** * Set Keycloak test TimeService to infinispan cacheManager. This will cause that infinispan will be aware of Keycloak Time offset, which is useful @@ -50,45 +47,16 @@ public static void setTestingTimeService(KeycloakSession session) { InfinispanConnectionProvider ispnProvider = session.getProvider(InfinispanConnectionProvider.class); EmbeddedCacheManager cacheManager = ispnProvider.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).getCacheManager(); - origTimeService = replaceComponent(cacheManager, TimeService.class, new KeycloakTestTimeService(), true); + origTimeService = DefaultInfinispanConnectionProviderFactory.setTimeServiceToKeycloakTime(cacheManager); } - public static void revertTimeService(KeycloakSession session) { + public static void revertTimeService() { // 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.run(); 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 replaceComponent(EmbeddedCacheManager cacheMgr, Class componentType, T replacementComponent, boolean rewire) { - GlobalComponentRegistry cr = cacheMgr.getGlobalComponentRegistry(); - BasicComponentRegistry bcr = cr.getComponent(BasicComponentRegistry.class); - ComponentRef old = bcr.getComponent(componentType); - bcr.replaceComponent(componentType.getName(), replacementComponent, true); - if (rewire) { - cr.rewire(); - cr.rewireNamedRegistries(); - } - return old != null ? old.wired() : null; - } } diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/model/infinispan/KeycloakTestTimeService.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/model/infinispan/KeycloakTestTimeService.java deleted file mode 100644 index 475bfee7baad..000000000000 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/model/infinispan/KeycloakTestTimeService.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 Marek Posolda - */ -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()); - } -} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java index 2546034a113b..9716aaca7c4f 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java @@ -195,7 +195,7 @@ public Response setTestingInfinispanTimeService() { @Path("/revert-testing-infinispan-time-service") @Produces(MediaType.APPLICATION_JSON) public Response revertTestingInfinispanTimeService() { - InfinispanTestUtil.revertTimeService(session); + InfinispanTestUtil.revertTimeService(); return Response.noContent().build(); } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java index 2e4650fb3b14..1ad4d0e4ec56 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java @@ -38,6 +38,7 @@ import java.util.List; import java.util.Map; +import org.infinispan.commons.time.TimeService; /** * @author Marko Strukelj @@ -197,7 +198,7 @@ public List getAdminEvents(@QueryParam("realmId") Stri void removeExpired(@QueryParam("realm") final String realm); /** - * Will set {@link org.keycloak.testsuite.model.infinispan.KeycloakTestTimeService} to the infinispan CacheManager before the test. + * Will set Inifispan's {@link TimeService} that is aware of Keycloak time shifts to the infinispan {@code CacheManager} before the test. * This will allow infinispan expiration to be aware of Keycloak {@link org.keycloak.common.util.Time#setOffset} */ @POST diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Infinispan.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Infinispan.java index 895d739efed8..4ef001a6b165 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Infinispan.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Infinispan.java @@ -72,6 +72,7 @@ public void updateConfig(Config cf) { .provider("default") .config("embedded", "true") .config("clustered", "true") + .config("useKeycloakTimeService", "true") .config("nodeName", "node-" + NODE_COUNTER.incrementAndGet()); } diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionPersisterProviderTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionPersisterProviderTest.java index 79e5ca58b73d..becf41e27711 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionPersisterProviderTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionPersisterProviderTest.java @@ -55,6 +55,7 @@ import org.hamcrest.Matchers; import org.keycloak.testsuite.model.KeycloakModelTest; import org.keycloak.testsuite.model.RequireProvider; +import java.util.LinkedList; /** * @author Marek Posolda @@ -66,12 +67,14 @@ @RequireProvider(RealmProvider.class) public class UserSessionPersisterProviderTest extends KeycloakModelTest { + private static final int USER_SESSION_COUNT = 2000; private String realmId; @Override public void createEnvironment(KeycloakSession s) { RealmModel realm = s.realms().createRealm("test"); realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT); + realm.setOfflineSessionMaxLifespan(Constants.DEFAULT_OFFLINE_SESSION_MAX_LIFESPAN); realm.setDefaultRole(s.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName())); this.realmId = realm.getId(); @@ -408,49 +411,41 @@ public void testOnUserRemoved() { public void testNoSessions() { inComittedTransaction(session -> { UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); - Stream sessions = persister.loadUserSessionsStream(0, 1, true, 0, "abc"); + Stream sessions = persister.loadUserSessionsStream(0, 1, true, "00000000-0000-0000-0000-000000000000"); Assert.assertEquals(0, sessions.count()); }); } @Test public void testMoreSessions() { - AtomicReference> userSessionsAt = new AtomicReference<>(); - inComittedTransaction(session -> { RealmModel realm = session.realms().getRealm(realmId); // Create 10 userSessions - each having 1 clientSession - List userSessions = new ArrayList<>(); + List userSessionsInner = new LinkedList<>(); UserModel user = session.users().getUserByUsername(realm, "user1"); - for (int i = 0; i < 20; i++) { + for (int i = 0; i < USER_SESSION_COUNT; i++) { // Having different offsets for each session (to ensure that lastSessionRefresh is also different) Time.setOffset(i); UserSessionModel userSession = session.sessions().createUserSession(realm, user, "user1", "127.0.0.1", "form", true, null, null); createClientSession(session, realmId, realm.getClientByClientId("test-app"), userSession, "http://redirect", "state"); - userSessions.add(userSession); + userSessionsInner.add(userSession.getId()); } - userSessionsAt.set(userSessions); - }); - - inComittedTransaction(session -> { - RealmModel realm = session.realms().getRealm(realmId); - - List userSessions = userSessionsAt.get(); - - for (UserSessionModel userSession : userSessions) { - UserSessionModel userSession2 = session.sessions().getUserSession(realm, userSession.getId()); + for (String userSessionId : userSessionsInner) { + UserSessionModel userSession2 = session.sessions().getUserSession(realm, userSessionId); persistUserSession(session, userSession2, true); } - }); - inComittedTransaction(session -> { - RealmModel realm = session.realms().getRealm(realmId); + return null; + }); - List loadedSessions = loadPersistedSessionsPaginated(session, true, 2, 10, 20); + withRealm(realmId, (session, realm) -> { + final int sessionsPerPage = 3; + List loadedSessions = loadPersistedSessionsPaginated(session, true, sessionsPerPage, + USER_SESSION_COUNT / sessionsPerPage + (USER_SESSION_COUNT / sessionsPerPage == 0 ? 0 : 1), USER_SESSION_COUNT); UserModel user = session.users().getUserByUsername(realm, "user1"); ClientModel testApp = realm.getClientByClientId("test-app"); @@ -462,6 +457,7 @@ public void testMoreSessions() { assertEquals(1, loadedSession.getAuthenticatedClientSessions().size()); assertTrue(loadedSession.getAuthenticatedClientSessions().containsKey(testApp.getId())); } + return null; }); } @@ -563,12 +559,11 @@ private List loadPersistedSessionsPaginated(KeycloakSession se int pageCount = 0; boolean next = true; List result = new ArrayList<>(); - int lastCreatedOn = 0; - String lastSessionId = "abc"; + String lastSessionId = "00000000-0000-0000-0000-000000000000"; while (next) { List sess = persister - .loadUserSessionsStream(0, sessionsPerPage, offline, lastCreatedOn, lastSessionId) + .loadUserSessionsStream(0, sessionsPerPage, offline, lastSessionId) .collect(Collectors.toList()); if (sess.size() < sessionsPerPage) { @@ -582,7 +577,6 @@ private List loadPersistedSessionsPaginated(KeycloakSession se pageCount++; UserSessionModel lastSession = sess.get(sess.size() - 1); - lastCreatedOn = lastSession.getStarted(); lastSessionId = lastSession.getId(); } diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderModelTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderModelTest.java index 62f28d80d27b..5a0ec8ef22d4 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderModelTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderModelTest.java @@ -192,7 +192,7 @@ public void testExpiredClientSessions() { if (timer != null && timerTaskCtx != null) { timer.schedule(timerTaskCtx.getRunnable(), timerTaskCtx.getIntervalMillis(), PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME); - InfinispanTestUtil.revertTimeService(kcSession); + InfinispanTestUtil.revertTimeService(); } } } diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderOfflineModelTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderOfflineModelTest.java index 2a82321a3e84..a1a851b13c5f 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderOfflineModelTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderOfflineModelTest.java @@ -218,7 +218,7 @@ public void testExpired() { timer.schedule(timerTaskCtx.getRunnable(), timerTaskCtx.getIntervalMillis(), PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME); } - InfinispanTestUtil.revertTimeService(kcSession); + InfinispanTestUtil.revertTimeService(); } } diff --git a/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/LoadPersistentSessionsCommand.java b/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/LoadPersistentSessionsCommand.java index 4cb5e416d810..119e81714994 100644 --- a/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/LoadPersistentSessionsCommand.java +++ b/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/LoadPersistentSessionsCommand.java @@ -47,7 +47,6 @@ protected void doRunCommand(KeycloakSession session) { //int workersCount = 8; //int limit = 64; - AtomicInteger lastCreatedOn = new AtomicInteger(0); AtomicReference lastSessionId = new AtomicReference<>("abc"); AtomicBoolean finished = new AtomicBoolean(false); @@ -55,7 +54,7 @@ protected void doRunCommand(KeycloakSession session) { while (!finished.get()) { if (i % 16 == 0) { - log.infof("Starting iteration: %s . lastCreatedOn: %d, lastSessionId: %s", i, lastCreatedOn.get(), lastSessionId.get()); + log.infof("Starting iteration: %s . lastCreatedOn: %d, lastSessionId: %s", i, lastSessionId.get()); } i = i + workersCount; @@ -63,7 +62,7 @@ protected void doRunCommand(KeycloakSession session) { MyWorker lastWorker = null; for (int workerId = 0 ; workerId < workersCount ; workerId++) { - lastWorker = new MyWorker(workerId, lastCreatedOn.get(), lastSessionId.get(), limit, sessionFactory); + lastWorker = new MyWorker(workerId, lastSessionId.get(), limit, sessionFactory); Thread worker = new Thread(lastWorker); workers.add(worker); } @@ -85,7 +84,6 @@ protected void doRunCommand(KeycloakSession session) { finished.set(true); } else { UserSessionModel lastSession = lastWorkerSessions.get(lastWorkerSessions.size() - 1); - lastCreatedOn.set(lastSession.getStarted()); lastSessionId.set(lastSession.getId()); } @@ -104,16 +102,14 @@ public String printUsage() { private static class MyWorker implements Runnable { private final int workerId; - private final int lastCreatedOn; private final String lastSessionId; private final int limit; private final KeycloakSessionFactory sessionFactory; private List loadedSessions = new LinkedList<>(); - public MyWorker(int workerId, int lastCreatedOn, String lastSessionId, int limit, KeycloakSessionFactory sessionFactory) { + public MyWorker(int workerId, String lastSessionId, int limit, KeycloakSessionFactory sessionFactory) { this.workerId = workerId; - this.lastCreatedOn = lastCreatedOn; this.lastSessionId = lastSessionId; this.limit = limit; this.sessionFactory = sessionFactory; @@ -126,7 +122,7 @@ public void run() { UserSessionPersisterProvider persister = keycloakSession.getProvider(UserSessionPersisterProvider.class); loadedSessions = persister - .loadUserSessionsStream(offset, limit, true, lastCreatedOn, lastSessionId) + .loadUserSessionsStream(offset, limit, true, lastSessionId) .collect(Collectors.toList()); });