Shorter lifespan for offline session cache entries in memory

Closes #26810

Co-authored-by: Thomas Darimont <thomas.darimont@googlemail.com>
Co-authored-by: Martin Kanis <mkanis@redhat.com>

Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
Signed-off-by: Martin Kanis <mkanis@redhat.com>
This commit is contained in:
Thomas Darimont 2023-10-25 12:17:35 +02:00 committed by Alexander Schwartz
parent d3ae075a33
commit 93fc6a6c54
7 changed files with 167 additions and 14 deletions

View file

@ -160,6 +160,13 @@ The old behavior to preload them at startup is now deprecated, as pre-loading th
For more details, check the For more details, check the
link:{upgradingguide_link}[{upgradingguide_name}]. 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 = 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. When enabling metrics for {project_name}'s embedded caches, the metrics now use labels for the cache manager and the cache names.

View file

@ -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. 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. 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=<lifespan-in-seconds>
----
Similarly for offline client sessions:
[source,bash]
----
--spi-user-sessions-infinispan-offline-client-session-cache-entry-lifespan-override=<lifespan-in-seconds>
----

View file

@ -118,6 +118,10 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
protected final boolean loadOfflineSessionsFromDatabase; protected final boolean loadOfflineSessionsFromDatabase;
protected final SessionFunction offlineSessionCacheEntryLifespanAdjuster;
protected final SessionFunction offlineClientSessionCacheEntryLifespanAdjuster;
public InfinispanUserSessionProvider(KeycloakSession session, public InfinispanUserSessionProvider(KeycloakSession session,
RemoteCacheInvoker remoteCacheInvoker, RemoteCacheInvoker remoteCacheInvoker,
CrossDCLastSessionRefreshStore lastSessionRefreshStore, CrossDCLastSessionRefreshStore lastSessionRefreshStore,
@ -128,7 +132,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionCache, Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionCache,
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache, Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache,
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionCache, Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionCache,
boolean loadOfflineSessionsFromDatabase) { boolean loadOfflineSessionsFromDatabase,
SessionFunction<UserSessionEntity> offlineSessionCacheEntryLifespanAdjuster,
SessionFunction<AuthenticatedClientSessionEntity> offlineClientSessionCacheEntryLifespanAdjuster) {
this.session = session; this.session = session;
this.sessionCache = sessionCache; this.sessionCache = sessionCache;
@ -137,9 +143,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
this.offlineClientSessionCache = offlineClientSessionCache; this.offlineClientSessionCache = offlineClientSessionCache;
this.sessionTx = new InfinispanChangelogBasedTransaction<>(session, sessionCache, remoteCacheInvoker, SessionTimeouts::getUserSessionLifespanMs, SessionTimeouts::getUserSessionMaxIdleMs); 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.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); this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
@ -149,6 +155,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
this.remoteCacheInvoker = remoteCacheInvoker; this.remoteCacheInvoker = remoteCacheInvoker;
this.keyGenerator = keyGenerator; this.keyGenerator = keyGenerator;
this.loadOfflineSessionsFromDatabase = loadOfflineSessionsFromDatabase; this.loadOfflineSessionsFromDatabase = loadOfflineSessionsFromDatabase;
this.offlineSessionCacheEntryLifespanAdjuster = offlineSessionCacheEntryLifespanAdjuster;
this.offlineClientSessionCacheEntryLifespanAdjuster = offlineClientSessionCacheEntryLifespanAdjuster;
session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx); session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx);
session.getTransactionManager().enlistAfterCompletion(sessionTx); session.getTransactionManager().enlistAfterCompletion(sessionTx);
@ -917,7 +925,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
boolean importWithExpiration = sessionsById.size() == 1; boolean importWithExpiration = sessionsById.size() == 1;
if (importWithExpiration) { if (importWithExpiration) {
importSessionsWithExpiration(sessionsById, cache, importSessionsWithExpiration(sessionsById, cache,
offline ? SessionTimeouts::getOfflineSessionLifespanMs : SessionTimeouts::getUserSessionLifespanMs, offline ? offlineSessionCacheEntryLifespanAdjuster : SessionTimeouts::getUserSessionLifespanMs,
offline ? SessionTimeouts::getOfflineSessionMaxIdleMs : SessionTimeouts::getUserSessionMaxIdleMs); offline ? SessionTimeouts::getOfflineSessionMaxIdleMs : SessionTimeouts::getUserSessionMaxIdleMs);
} else { } else {
Retry.executeWithBackoff((int iteration) -> { Retry.executeWithBackoff((int iteration) -> {
@ -934,7 +942,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
if (importWithExpiration) { if (importWithExpiration) {
importSessionsWithExpiration(sessionsByIdForTransport, remoteCache, importSessionsWithExpiration(sessionsByIdForTransport, remoteCache,
offline ? SessionTimeouts::getOfflineSessionLifespanMs : SessionTimeouts::getUserSessionLifespanMs, offline ? offlineSessionCacheEntryLifespanAdjuster : SessionTimeouts::getUserSessionLifespanMs,
offline ? SessionTimeouts::getOfflineSessionMaxIdleMs : SessionTimeouts::getUserSessionMaxIdleMs); offline ? SessionTimeouts::getOfflineSessionMaxIdleMs : SessionTimeouts::getUserSessionMaxIdleMs);
} else { } else {
Retry.executeWithBackoff((int iteration) -> { Retry.executeWithBackoff((int iteration) -> {
@ -961,7 +969,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
if (importWithExpiration) { if (importWithExpiration) {
importSessionsWithExpiration(clientSessionsById, clientSessCache, importSessionsWithExpiration(clientSessionsById, clientSessCache,
offline ? SessionTimeouts::getOfflineClientSessionLifespanMs : SessionTimeouts::getClientSessionLifespanMs, offline ? offlineClientSessionCacheEntryLifespanAdjuster : SessionTimeouts::getClientSessionLifespanMs,
offline ? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs); offline ? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs);
} else { } else {
Retry.executeWithBackoff((int iteration) -> { Retry.executeWithBackoff((int iteration) -> {
@ -978,7 +986,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
if (importWithExpiration) { if (importWithExpiration) {
importSessionsWithExpiration(sessionsByIdForTransport, remoteCacheClientSessions, importSessionsWithExpiration(sessionsByIdForTransport, remoteCacheClientSessions,
offline ? SessionTimeouts::getOfflineClientSessionLifespanMs : SessionTimeouts::getClientSessionLifespanMs, offline ? offlineClientSessionCacheEntryLifespanAdjuster : SessionTimeouts::getClientSessionLifespanMs,
offline ? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs); offline ? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs);
} else { } else {
Retry.executeWithBackoff((int iteration) -> { Retry.executeWithBackoff((int iteration) -> {
@ -1096,7 +1104,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
if (checkExpiration) { if (checkExpiration) {
SessionFunction<AuthenticatedClientSessionEntity> lifespanChecker = offline SessionFunction<AuthenticatedClientSessionEntity> lifespanChecker = offline
? SessionTimeouts::getOfflineClientSessionLifespanMs : SessionTimeouts::getClientSessionLifespanMs; ? offlineClientSessionCacheEntryLifespanAdjuster : SessionTimeouts::getClientSessionLifespanMs;
SessionFunction<AuthenticatedClientSessionEntity> idleTimeoutChecker = offline SessionFunction<AuthenticatedClientSessionEntity> idleTimeoutChecker = offline
? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs; ? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs;
if (idleTimeoutChecker.apply(sessionToImportInto.getRealm(), clientSession.getClient(), entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG if (idleTimeoutChecker.apply(sessionToImportInto.getRealm(), clientSession.getClient(), entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG

View file

@ -27,6 +27,7 @@ import org.keycloak.common.Profile;
import org.keycloak.common.util.Environment; import org.keycloak.common.util.Environment;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask; import org.keycloak.models.KeycloakSessionTask;
@ -61,13 +62,18 @@ import org.keycloak.models.utils.PostMigrationEvent;
import org.keycloak.models.utils.ResetTimeOffsetEvent; import org.keycloak.models.utils.ResetTimeOffsetEvent;
import org.keycloak.provider.ProviderEvent; import org.keycloak.provider.ProviderEvent;
import org.keycloak.provider.ProviderEventListener; import org.keycloak.provider.ProviderEventListener;
import org.keycloak.provider.ServerInfoAwareProviderFactory;
import java.io.Serializable; import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProviderFactory.PROVIDER_PRIORITY; 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); private static final Logger log = Logger.getLogger(InfinispanUserSessionProviderFactory.class);
@ -81,6 +87,10 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
private boolean preloadOfflineSessionsFromDatabase; private boolean preloadOfflineSessionsFromDatabase;
private long offlineSessionCacheEntryLifespanOverride;
private long offlineClientSessionCacheEntryLifespanOverride;
private Config.Scope config; private Config.Scope config;
private RemoteCacheInvoker remoteCacheInvoker; private RemoteCacheInvoker remoteCacheInvoker;
@ -97,8 +107,21 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache = connections.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME); Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache = connections.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME);
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME); Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME);
return new InfinispanUserSessionProvider(session, remoteCacheInvoker, lastSessionRefreshStore, offlineLastSessionRefreshStore, return new InfinispanUserSessionProvider(
persisterLastSessionRefreshStore, keyGenerator, cache, offlineSessionsCache, clientSessionCache, offlineClientSessionsCache, !preloadOfflineSessionsFromDatabase); session,
remoteCacheInvoker,
lastSessionRefreshStore,
offlineLastSessionRefreshStore,
persisterLastSessionRefreshStore,
keyGenerator,
cache,
offlineSessionsCache,
clientSessionCache,
offlineClientSessionsCache,
!preloadOfflineSessionsFromDatabase,
this::deriveOfflineSessionCacheEntryLifespanMs,
this::deriveOfflineClientSessionCacheEntryLifespanOverrideMs
);
} }
@Override @Override
@ -108,6 +131,9 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
if (preloadOfflineSessionsFromDatabase && !Profile.isFeatureEnabled(Profile.Feature.OFFLINE_SESSION_PRELOADING)) { 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."); 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 @Override
@ -280,7 +306,7 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME); Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME);
RemoteCache offlineSessionsRemoteCache = checkRemoteCache(session, offlineSessionsCache, (RealmModel realm) -> { RemoteCache offlineSessionsRemoteCache = checkRemoteCache(session, offlineSessionsCache, (RealmModel realm) -> {
return Time.toMillis(realm.getOfflineSessionIdleTimeout()); return Time.toMillis(realm.getOfflineSessionIdleTimeout());
}, SessionTimeouts::getOfflineSessionLifespanMs, SessionTimeouts::getOfflineSessionMaxIdleMs); }, this::deriveOfflineSessionCacheEntryLifespanMs, SessionTimeouts::getOfflineSessionMaxIdleMs);
if (offlineSessionsRemoteCache != null) { if (offlineSessionsRemoteCache != null) {
offlineLastSessionRefreshStore = new CrossDCLastSessionRefreshStoreFactory().createAndInit(session, offlineSessionsCache, true); offlineLastSessionRefreshStore = new CrossDCLastSessionRefreshStoreFactory().createAndInit(session, offlineSessionsCache, true);
@ -289,7 +315,7 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME); Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME);
checkRemoteCache(session, offlineClientSessionsCache, (RealmModel realm) -> { checkRemoteCache(session, offlineClientSessionsCache, (RealmModel realm) -> {
return Time.toMillis(realm.getOfflineSessionIdleTimeout()); return Time.toMillis(realm.getOfflineSessionIdleTimeout());
}, SessionTimeouts::getOfflineClientSessionLifespanMs, SessionTimeouts::getOfflineClientSessionMaxIdleMs); }, this::deriveOfflineClientSessionCacheEntryLifespanOverrideMs, SessionTimeouts::getOfflineClientSessionMaxIdleMs);
} }
private <K, V extends SessionEntity> RemoteCache 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,
@ -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) { private void loadSessionsFromRemoteCaches(KeycloakSession session) {
for (String cacheName : remoteCacheInvoker.getRemoteCacheNames()) { for (String cacheName : remoteCacheInvoker.getRemoteCacheNames()) {
@ -362,5 +424,14 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
public int order() { public int order() {
return PROVIDER_PRIORITY; return PROVIDER_PRIORITY;
} }
@Override
public Map<String, String> getOperationalInfo() {
Map<String, String> info = new HashMap<>();
info.put("preloadOfflineSessionsFromDatabase", Boolean.toString(preloadOfflineSessionsFromDatabase));
info.put("offlineSessionCacheEntryLifespanOverride", Long.toString(offlineSessionCacheEntryLifespanOverride));
info.put("offlineClientSessionCacheEntryLifespanOverride", Long.toString(offlineClientSessionCacheEntryLifespanOverride));
return info;
}
} }

View file

@ -18,6 +18,8 @@ package org.keycloak.testsuite.model.parameters;
import org.junit.runner.Description; import org.junit.runner.Description;
import org.junit.runners.model.Statement; 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.Config;
import org.keycloak.testsuite.model.KeycloakModelParameters; import org.keycloak.testsuite.model.KeycloakModelParameters;
import org.keycloak.testsuite.model.HotRodServerRule; import org.keycloak.testsuite.model.HotRodServerRule;
@ -54,7 +56,11 @@ public class CrossDCInfinispan extends KeycloakModelParameters {
.config("nodeName", "node-" + NODE_COUNTER.get()) .config("nodeName", "node-" + NODE_COUNTER.get())
.config("siteName", siteName(NODE_COUNTER.get())) .config("siteName", siteName(NODE_COUNTER.get()))
.config("remoteStorePort", siteName(NODE_COUNTER.get()).equals("site-2") ? "11333" : "11222") .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");
} }
} }

View file

@ -102,6 +102,8 @@ public class Infinispan extends KeycloakModelParameters {
.spi(UserSessionSpi.NAME) .spi(UserSessionSpi.NAME)
.provider(InfinispanUserSessionProviderFactory.PROVIDER_ID) .provider(InfinispanUserSessionProviderFactory.PROVIDER_ID)
.config("sessionPreloadStalledTimeoutInSeconds", "10") .config("sessionPreloadStalledTimeoutInSeconds", "10")
.config("offlineSessionCacheEntryLifespanOverride", "43200")
.config("offlineClientSessionCacheEntryLifespanOverride", "43200")
; ;
} }

View file

@ -18,7 +18,9 @@
package org.keycloak.testsuite.model.session; package org.keycloak.testsuite.model.session;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.infinispan.AdvancedCache;
import org.infinispan.Cache; import org.infinispan.Cache;
import org.infinispan.context.Flag;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Assume; import org.junit.Assume;
import org.junit.Test; import org.junit.Test;
@ -35,7 +37,9 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider; import org.keycloak.models.UserProvider;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider; import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.UserSessionSpi;
import org.keycloak.models.session.UserSessionPersisterProvider; 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.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStoreFactory;
import org.keycloak.models.utils.ResetTimeOffsetEvent; import org.keycloak.models.utils.ResetTimeOffsetEvent;
import org.keycloak.services.managers.UserSessionManager; 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<String> createOfflineSessionIncludeClientSessions(KeycloakSession session, UserSessionModel private static Set<String> createOfflineSessionIncludeClientSessions(KeycloakSession session, UserSessionModel
userSession) { userSession) {
Set<String> offlineSessions = new HashSet<>(); Set<String> offlineSessions = new HashSet<>();