From 93fc6a6c543c30cd304d2145e51f252037f31b0a Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Wed, 25 Oct 2023 12:17:35 +0200 Subject: [PATCH] Shorter lifespan for offline session cache entries in memory Closes #26810 Co-authored-by: Thomas Darimont Co-authored-by: Martin Kanis Signed-off-by: Thomas Darimont Signed-off-by: Martin Kanis --- .../release_notes/topics/24_0_0.adoc | 7 ++ .../server_admin/topics/sessions/offline.adoc | 14 ++++ .../InfinispanUserSessionProvider.java | 24 ++++-- .../InfinispanUserSessionProviderFactory.java | 81 +++++++++++++++++-- .../model/parameters/CrossDCInfinispan.java | 8 +- .../model/parameters/Infinispan.java | 2 + .../UserSessionProviderOfflineModelTest.java | 45 +++++++++++ 7 files changed, 167 insertions(+), 14 deletions(-) diff --git a/docs/documentation/release_notes/topics/24_0_0.adoc b/docs/documentation/release_notes/topics/24_0_0.adoc index dd346533c1..3ad5763ce7 100644 --- a/docs/documentation/release_notes/topics/24_0_0.adoc +++ b/docs/documentation/release_notes/topics/24_0_0.adoc @@ -160,6 +160,13 @@ The old behavior to preload them at startup is now deprecated, as pre-loading th For more details, check the link:{upgradingguide_link}[{upgradingguide_name}]. += Configuration option for offline session lifespan override in memory + +To reduce memory requirements, we introduced a configuration option to shorten lifespan for offline sessions imported into the Infinispan caches. Currently, the offline session lifespan override is disabled by default. + +For more details, check the +link:{adminguide_link}#_offline-access[{adminguide_name}]. + = Infinispan metrics use labels for cache manager and cache names When enabling metrics for {project_name}'s embedded caches, the metrics now use labels for the cache manager and the cache names. diff --git a/docs/documentation/server_admin/topics/sessions/offline.adoc b/docs/documentation/server_admin/topics/sessions/offline.adoc index b280cc5d61..df6e2757d5 100644 --- a/docs/documentation/server_admin/topics/sessions/offline.adoc +++ b/docs/documentation/server_admin/topics/sessions/offline.adoc @@ -21,3 +21,17 @@ Users can view and revoke offline tokens that {project_name} grants them in the To issue an offline token, users must have the role mapping for the realm-level `offline_access` role. Clients must also have that role in their scope. Clients must add an `offline_access` client scope as an `Optional client scope` to the role, which is done by default. Clients can request an offline token by adding the parameter `scope=offline_access` when sending their authorization request to {project_name}. The {project_name} OIDC client adapter automatically adds this parameter when you use it to access your application's secured URL (such as, $$http://localhost:8080/customer-portal/secured?scope=offline_access$$). The Direct Access Grant and Service Accounts support offline tokens if you include `scope=offline_access` in the authentication request body. + +Offline sessions are besides the Infinispan caches stored also in the database. Whenever the {project_name} server is restarted or an offline session is evicted from the Infinispan cache, it is still available in the database. Any following attempt to access the offline session will load the session from the database, and also import it to the Infinispan cache. To reduce memory requirements, we introduced a configuration option to shorten lifespan for imported offline sessions. Such sessions will be evicted from the Infinispan caches after the specified lifespan, but still available in the database. This will lower memory consumption, especially for deployments with a large number of offline sessions. Currently, the offline session lifespan override is disabled by default. To specify the lifespan override for offline user sessions, start {project_name} server with the following parameter: + +[source,bash] +---- +--spi-user-sessions-infinispan-offline-session-cache-entry-lifespan-override= +---- + +Similarly for offline client sessions: + +[source,bash] +---- +--spi-user-sessions-infinispan-offline-client-session-cache-entry-lifespan-override= +---- diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java index 379d1dcd9e..c1ce04521a 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java @@ -118,6 +118,10 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { protected final boolean loadOfflineSessionsFromDatabase; + protected final SessionFunction offlineSessionCacheEntryLifespanAdjuster; + + protected final SessionFunction offlineClientSessionCacheEntryLifespanAdjuster; + public InfinispanUserSessionProvider(KeycloakSession session, RemoteCacheInvoker remoteCacheInvoker, CrossDCLastSessionRefreshStore lastSessionRefreshStore, @@ -128,7 +132,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { Cache> offlineSessionCache, Cache> clientSessionCache, Cache> offlineClientSessionCache, - boolean loadOfflineSessionsFromDatabase) { + boolean loadOfflineSessionsFromDatabase, + SessionFunction offlineSessionCacheEntryLifespanAdjuster, + SessionFunction offlineClientSessionCacheEntryLifespanAdjuster) { this.session = session; this.sessionCache = sessionCache; @@ -137,9 +143,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { this.offlineClientSessionCache = offlineClientSessionCache; this.sessionTx = new InfinispanChangelogBasedTransaction<>(session, sessionCache, remoteCacheInvoker, SessionTimeouts::getUserSessionLifespanMs, SessionTimeouts::getUserSessionMaxIdleMs); - this.offlineSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineSessionCache, remoteCacheInvoker, SessionTimeouts::getOfflineSessionLifespanMs, SessionTimeouts::getOfflineSessionMaxIdleMs); + this.offlineSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineSessionCache, remoteCacheInvoker, offlineSessionCacheEntryLifespanAdjuster, 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.offlineClientSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineClientSessionCache, remoteCacheInvoker, offlineClientSessionCacheEntryLifespanAdjuster, SessionTimeouts::getOfflineClientSessionMaxIdleMs); this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session); @@ -149,6 +155,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { this.remoteCacheInvoker = remoteCacheInvoker; this.keyGenerator = keyGenerator; this.loadOfflineSessionsFromDatabase = loadOfflineSessionsFromDatabase; + this.offlineSessionCacheEntryLifespanAdjuster = offlineSessionCacheEntryLifespanAdjuster; + this.offlineClientSessionCacheEntryLifespanAdjuster = offlineClientSessionCacheEntryLifespanAdjuster; session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx); session.getTransactionManager().enlistAfterCompletion(sessionTx); @@ -917,7 +925,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { boolean importWithExpiration = sessionsById.size() == 1; if (importWithExpiration) { importSessionsWithExpiration(sessionsById, cache, - offline ? SessionTimeouts::getOfflineSessionLifespanMs : SessionTimeouts::getUserSessionLifespanMs, + offline ? offlineSessionCacheEntryLifespanAdjuster : SessionTimeouts::getUserSessionLifespanMs, offline ? SessionTimeouts::getOfflineSessionMaxIdleMs : SessionTimeouts::getUserSessionMaxIdleMs); } else { Retry.executeWithBackoff((int iteration) -> { @@ -934,7 +942,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { if (importWithExpiration) { importSessionsWithExpiration(sessionsByIdForTransport, remoteCache, - offline ? SessionTimeouts::getOfflineSessionLifespanMs : SessionTimeouts::getUserSessionLifespanMs, + offline ? offlineSessionCacheEntryLifespanAdjuster : SessionTimeouts::getUserSessionLifespanMs, offline ? SessionTimeouts::getOfflineSessionMaxIdleMs : SessionTimeouts::getUserSessionMaxIdleMs); } else { Retry.executeWithBackoff((int iteration) -> { @@ -961,7 +969,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { if (importWithExpiration) { importSessionsWithExpiration(clientSessionsById, clientSessCache, - offline ? SessionTimeouts::getOfflineClientSessionLifespanMs : SessionTimeouts::getClientSessionLifespanMs, + offline ? offlineClientSessionCacheEntryLifespanAdjuster : SessionTimeouts::getClientSessionLifespanMs, offline ? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs); } else { Retry.executeWithBackoff((int iteration) -> { @@ -978,7 +986,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { if (importWithExpiration) { importSessionsWithExpiration(sessionsByIdForTransport, remoteCacheClientSessions, - offline ? SessionTimeouts::getOfflineClientSessionLifespanMs : SessionTimeouts::getClientSessionLifespanMs, + offline ? offlineClientSessionCacheEntryLifespanAdjuster : SessionTimeouts::getClientSessionLifespanMs, offline ? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs); } else { Retry.executeWithBackoff((int iteration) -> { @@ -1096,7 +1104,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { if (checkExpiration) { SessionFunction lifespanChecker = offline - ? SessionTimeouts::getOfflineClientSessionLifespanMs : SessionTimeouts::getClientSessionLifespanMs; + ? offlineClientSessionCacheEntryLifespanAdjuster : SessionTimeouts::getClientSessionLifespanMs; SessionFunction idleTimeoutChecker = offline ? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs; if (idleTimeoutChecker.apply(sessionToImportInto.getRealm(), clientSession.getClient(), entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java index 86af8891c4..f3735878cb 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java @@ -27,6 +27,7 @@ import org.keycloak.common.Profile; import org.keycloak.common.util.Environment; import org.keycloak.common.util.Time; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionTask; @@ -61,13 +62,18 @@ import org.keycloak.models.utils.PostMigrationEvent; import org.keycloak.models.utils.ResetTimeOffsetEvent; import org.keycloak.provider.ProviderEvent; import org.keycloak.provider.ProviderEventListener; +import org.keycloak.provider.ServerInfoAwareProviderFactory; import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.concurrent.TimeUnit; + import static org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProviderFactory.PROVIDER_PRIORITY; -public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory { +public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory, ServerInfoAwareProviderFactory { private static final Logger log = Logger.getLogger(InfinispanUserSessionProviderFactory.class); @@ -81,6 +87,10 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider private boolean preloadOfflineSessionsFromDatabase; + private long offlineSessionCacheEntryLifespanOverride; + + private long offlineClientSessionCacheEntryLifespanOverride; + private Config.Scope config; private RemoteCacheInvoker remoteCacheInvoker; @@ -97,8 +107,21 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider Cache> clientSessionCache = connections.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME); Cache> offlineClientSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME); - return new InfinispanUserSessionProvider(session, remoteCacheInvoker, lastSessionRefreshStore, offlineLastSessionRefreshStore, - persisterLastSessionRefreshStore, keyGenerator, cache, offlineSessionsCache, clientSessionCache, offlineClientSessionsCache, !preloadOfflineSessionsFromDatabase); + return new InfinispanUserSessionProvider( + session, + remoteCacheInvoker, + lastSessionRefreshStore, + offlineLastSessionRefreshStore, + persisterLastSessionRefreshStore, + keyGenerator, + cache, + offlineSessionsCache, + clientSessionCache, + offlineClientSessionsCache, + !preloadOfflineSessionsFromDatabase, + this::deriveOfflineSessionCacheEntryLifespanMs, + this::deriveOfflineClientSessionCacheEntryLifespanOverrideMs + ); } @Override @@ -108,6 +131,9 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider if (preloadOfflineSessionsFromDatabase && !Profile.isFeatureEnabled(Profile.Feature.OFFLINE_SESSION_PRELOADING)) { throw new RuntimeException("The deprecated offline session preloading feature is disabled in this configuration. Read the migration guide to learn more."); } + + offlineSessionCacheEntryLifespanOverride = config.getInt("offlineSessionCacheEntryLifespanOverride", -1); + offlineClientSessionCacheEntryLifespanOverride = config.getInt("offlineClientSessionCacheEntryLifespanOverride", -1); } @Override @@ -280,7 +306,7 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider Cache> offlineSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME); RemoteCache offlineSessionsRemoteCache = checkRemoteCache(session, offlineSessionsCache, (RealmModel realm) -> { return Time.toMillis(realm.getOfflineSessionIdleTimeout()); - }, SessionTimeouts::getOfflineSessionLifespanMs, SessionTimeouts::getOfflineSessionMaxIdleMs); + }, this::deriveOfflineSessionCacheEntryLifespanMs, SessionTimeouts::getOfflineSessionMaxIdleMs); if (offlineSessionsRemoteCache != null) { offlineLastSessionRefreshStore = new CrossDCLastSessionRefreshStoreFactory().createAndInit(session, offlineSessionsCache, true); @@ -289,7 +315,7 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider Cache> offlineClientSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME); checkRemoteCache(session, offlineClientSessionsCache, (RealmModel realm) -> { return Time.toMillis(realm.getOfflineSessionIdleTimeout()); - }, SessionTimeouts::getOfflineClientSessionLifespanMs, SessionTimeouts::getOfflineClientSessionMaxIdleMs); + }, this::deriveOfflineClientSessionCacheEntryLifespanOverrideMs, SessionTimeouts::getOfflineClientSessionMaxIdleMs); } private RemoteCache checkRemoteCache(KeycloakSession session, Cache> ispnCache, RemoteCacheInvoker.MaxIdleTimeLoader maxIdleLoader, @@ -316,6 +342,42 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider } } + protected Long deriveOfflineSessionCacheEntryLifespanMs(RealmModel realm, ClientModel client, UserSessionEntity entity) { + + long configuredOfflineSessionLifespan = SessionTimeouts.getOfflineSessionLifespanMs(realm, client, entity); + + if (offlineSessionCacheEntryLifespanOverride == -1) { + // override not configured -> take the value from realm settings + return configuredOfflineSessionLifespan; + } + + if (configuredOfflineSessionLifespan == -1) { + // "Offline Session Max Limited" is "off" + return TimeUnit.SECONDS.toMillis(offlineSessionCacheEntryLifespanOverride); + } + + // both values are configured, Offline Session Max could be smaller than the override, so we use the minimum of both + return Math.min(TimeUnit.SECONDS.toMillis(offlineSessionCacheEntryLifespanOverride), configuredOfflineSessionLifespan); + } + + protected Long deriveOfflineClientSessionCacheEntryLifespanOverrideMs(RealmModel realm, ClientModel client, AuthenticatedClientSessionEntity entity) { + + long configuredOfflineClientSessionLifespan = SessionTimeouts.getOfflineClientSessionLifespanMs(realm, client, entity); + + if (offlineClientSessionCacheEntryLifespanOverride == -1) { + // override not configured -> take the value from realm settings + return configuredOfflineClientSessionLifespan; + } + + if (configuredOfflineClientSessionLifespan == -1) { + // "Offline Session Max Limited" is "off" + return TimeUnit.SECONDS.toMillis(offlineClientSessionCacheEntryLifespanOverride); + } + + // both values are configured, Offline Session Max could be smaller than the override, so we use the minimum of both + return Math.min(TimeUnit.SECONDS.toMillis(offlineClientSessionCacheEntryLifespanOverride), configuredOfflineClientSessionLifespan); + } + private void loadSessionsFromRemoteCaches(KeycloakSession session) { for (String cacheName : remoteCacheInvoker.getRemoteCacheNames()) { @@ -362,5 +424,14 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider public int order() { return PROVIDER_PRIORITY; } + + @Override + public Map getOperationalInfo() { + Map info = new HashMap<>(); + info.put("preloadOfflineSessionsFromDatabase", Boolean.toString(preloadOfflineSessionsFromDatabase)); + info.put("offlineSessionCacheEntryLifespanOverride", Long.toString(offlineSessionCacheEntryLifespanOverride)); + info.put("offlineClientSessionCacheEntryLifespanOverride", Long.toString(offlineClientSessionCacheEntryLifespanOverride)); + return info; + } } diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/CrossDCInfinispan.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/CrossDCInfinispan.java index 98f02c3fc8..23e0e69202 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/CrossDCInfinispan.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/CrossDCInfinispan.java @@ -18,6 +18,8 @@ package org.keycloak.testsuite.model.parameters; import org.junit.runner.Description; import org.junit.runners.model.Statement; +import org.keycloak.models.UserSessionSpi; +import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory; import org.keycloak.testsuite.model.Config; import org.keycloak.testsuite.model.KeycloakModelParameters; import org.keycloak.testsuite.model.HotRodServerRule; @@ -54,7 +56,11 @@ public class CrossDCInfinispan extends KeycloakModelParameters { .config("nodeName", "node-" + NODE_COUNTER.get()) .config("siteName", siteName(NODE_COUNTER.get())) .config("remoteStorePort", siteName(NODE_COUNTER.get()).equals("site-2") ? "11333" : "11222") - .config("jgroupsUdpMcastAddr", mcastAddr(NODE_COUNTER.get())); + .config("jgroupsUdpMcastAddr", mcastAddr(NODE_COUNTER.get())) + .spi(UserSessionSpi.NAME) + .provider(InfinispanUserSessionProviderFactory.PROVIDER_ID) + .config("offlineSessionCacheEntryLifespanOverride", "43200") + .config("offlineClientSessionCacheEntryLifespanOverride", "43200"); } } 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 cc15ca41a8..0d60c90e94 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 @@ -102,6 +102,8 @@ public class Infinispan extends KeycloakModelParameters { .spi(UserSessionSpi.NAME) .provider(InfinispanUserSessionProviderFactory.PROVIDER_ID) .config("sessionPreloadStalledTimeoutInSeconds", "10") + .config("offlineSessionCacheEntryLifespanOverride", "43200") + .config("offlineClientSessionCacheEntryLifespanOverride", "43200") ; } 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 bbb6c3774f..48f42523e5 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 @@ -18,7 +18,9 @@ package org.keycloak.testsuite.model.session; import org.hamcrest.Matchers; +import org.infinispan.AdvancedCache; import org.infinispan.Cache; +import org.infinispan.context.Flag; import org.junit.Assert; import org.junit.Assume; import org.junit.Test; @@ -35,7 +37,9 @@ import org.keycloak.models.UserModel; import org.keycloak.models.UserProvider; import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.UserSessionSpi; import org.keycloak.models.session.UserSessionPersisterProvider; +import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory; import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStoreFactory; import org.keycloak.models.utils.ResetTimeOffsetEvent; import org.keycloak.services.managers.UserSessionManager; @@ -470,6 +474,47 @@ public class UserSessionProviderOfflineModelTest extends KeycloakModelTest { }); } + @Test + public void testOfflineSessionLifespanOverride() { + // skip the test for CrossDC or when offline session preloading is enabled + Assume.assumeFalse(Objects.equals(CONFIG.scope("userSessions.infinispan").get("preloadOfflineSessionsFromDatabase"), "true") || + Objects.equals(CONFIG.scope("connectionsInfinispan.default").get("remoteStoreEnabled"), "true")); + + createOfflineSessions("user1", 2, new LinkedList<>(), new LinkedList<>()); + + reinitializeKeycloakSessionFactory(); + + withRealm(realmId, (session, realm) -> { + InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class); + + // skip remote cache load as we are only interested in embedded caches + AdvancedCache offlineUSCache = provider.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME).getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD); + AdvancedCache offlineCSCache = provider.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME).getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD); + + Assert.assertEquals(0, offlineUSCache.size()); + Assert.assertEquals(0, offlineCSCache.size()); + + // lazy load offline user sessions from DB => this should also import user and client sessions to the caches + Assert.assertEquals(2, session.sessions().getOfflineUserSessionsStream(realm, session.users().getUserByUsername(realm, "user1")).count()); + + // check sessions were imported to the caches + Assert.assertEquals(2, offlineUSCache.size()); + Assert.assertEquals(4, offlineCSCache.size()); + + // lifespan override set to 12h (43200s) + setTimeOffset(44000); + + // check sessions were evicted from the caches + Assert.assertEquals(0, offlineUSCache.size()); + Assert.assertEquals(0, offlineCSCache.size()); + + // sessions should still be in the DB + Assert.assertEquals(2, session.sessions().getOfflineUserSessionsStream(realm, session.users().getUserByUsername(realm, "user1")).count()); + + return null; + }); + } + private static Set createOfflineSessionIncludeClientSessions(KeycloakSession session, UserSessionModel userSession) { Set offlineSessions = new HashSet<>();