Skip to content

Commit

Permalink
Shorter lifespan for offline session cache entries in memory
Browse files Browse the repository at this point in the history
Closes keycloak#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>
  • Loading branch information
thomasdarimont authored and ahus1 committed Feb 9, 2024
1 parent d3ae075 commit 93fc6a6
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 14 deletions.
7 changes: 7 additions & 0 deletions docs/documentation/release_notes/topics/24_0_0.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions docs/documentation/server_admin/topics/sessions/offline.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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=<lifespan-in-seconds>
----

Similarly for offline client sessions:

[source,bash]
----
--spi-user-sessions-infinispan-offline-client-session-cache-entry-lifespan-override=<lifespan-in-seconds>
----
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -128,7 +132,9 @@ public InfinispanUserSessionProvider(KeycloakSession session,
Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionCache,
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache,
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionCache,
boolean loadOfflineSessionsFromDatabase) {
boolean loadOfflineSessionsFromDatabase,
SessionFunction<UserSessionEntity> offlineSessionCacheEntryLifespanAdjuster,
SessionFunction<AuthenticatedClientSessionEntity> offlineClientSessionCacheEntryLifespanAdjuster) {
this.session = session;

this.sessionCache = sessionCache;
Expand All @@ -137,9 +143,9 @@ public InfinispanUserSessionProvider(KeycloakSession session,
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);

Expand All @@ -149,6 +155,8 @@ public InfinispanUserSessionProvider(KeycloakSession session,
this.remoteCacheInvoker = remoteCacheInvoker;
this.keyGenerator = keyGenerator;
this.loadOfflineSessionsFromDatabase = loadOfflineSessionsFromDatabase;
this.offlineSessionCacheEntryLifespanAdjuster = offlineSessionCacheEntryLifespanAdjuster;
this.offlineClientSessionCacheEntryLifespanAdjuster = offlineClientSessionCacheEntryLifespanAdjuster;

session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx);
session.getTransactionManager().enlistAfterCompletion(sessionTx);
Expand Down Expand Up @@ -917,7 +925,7 @@ public void importUserSessions(Collection<UserSessionModel> persistentUserSessio
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) -> {
Expand All @@ -934,7 +942,7 @@ public void importUserSessions(Collection<UserSessionModel> persistentUserSessio

if (importWithExpiration) {
importSessionsWithExpiration(sessionsByIdForTransport, remoteCache,
offline ? SessionTimeouts::getOfflineSessionLifespanMs : SessionTimeouts::getUserSessionLifespanMs,
offline ? offlineSessionCacheEntryLifespanAdjuster : SessionTimeouts::getUserSessionLifespanMs,
offline ? SessionTimeouts::getOfflineSessionMaxIdleMs : SessionTimeouts::getUserSessionMaxIdleMs);
} else {
Retry.executeWithBackoff((int iteration) -> {
Expand All @@ -961,7 +969,7 @@ public void importUserSessions(Collection<UserSessionModel> persistentUserSessio

if (importWithExpiration) {
importSessionsWithExpiration(clientSessionsById, clientSessCache,
offline ? SessionTimeouts::getOfflineClientSessionLifespanMs : SessionTimeouts::getClientSessionLifespanMs,
offline ? offlineClientSessionCacheEntryLifespanAdjuster : SessionTimeouts::getClientSessionLifespanMs,
offline ? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs);
} else {
Retry.executeWithBackoff((int iteration) -> {
Expand All @@ -978,7 +986,7 @@ public void importUserSessions(Collection<UserSessionModel> persistentUserSessio

if (importWithExpiration) {
importSessionsWithExpiration(sessionsByIdForTransport, remoteCacheClientSessions,
offline ? SessionTimeouts::getOfflineClientSessionLifespanMs : SessionTimeouts::getClientSessionLifespanMs,
offline ? offlineClientSessionCacheEntryLifespanAdjuster : SessionTimeouts::getClientSessionLifespanMs,
offline ? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs);
} else {
Retry.executeWithBackoff((int iteration) -> {
Expand Down Expand Up @@ -1096,7 +1104,7 @@ private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter

if (checkExpiration) {
SessionFunction<AuthenticatedClientSessionEntity> lifespanChecker = offline
? SessionTimeouts::getOfflineClientSessionLifespanMs : SessionTimeouts::getClientSessionLifespanMs;
? offlineClientSessionCacheEntryLifespanAdjuster : SessionTimeouts::getClientSessionLifespanMs;
SessionFunction<AuthenticatedClientSessionEntity> idleTimeoutChecker = offline
? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs;
if (idleTimeoutChecker.apply(sessionToImportInto.getRealm(), clientSession.getClient(), entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
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;
Expand Down Expand Up @@ -61,13 +62,18 @@
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);

Expand All @@ -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;
Expand All @@ -97,8 +107,21 @@ public InfinispanUserSessionProvider create(KeycloakSession session) {
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);

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
Expand All @@ -108,6 +131,9 @@ public void init(Config.Scope config) {
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
Expand Down Expand Up @@ -280,7 +306,7 @@ protected void checkRemoteCaches(KeycloakSession session) {
Cache<String, SessionEntityWrapper<UserSessionEntity>> 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);
Expand All @@ -289,7 +315,7 @@ protected void checkRemoteCaches(KeycloakSession session) {
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> 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 <K, V extends SessionEntity> RemoteCache checkRemoteCache(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> ispnCache, RemoteCacheInvoker.MaxIdleTimeLoader maxIdleLoader,
Expand All @@ -316,6 +342,42 @@ private <K, V extends SessionEntity> RemoteCache checkRemoteCache(KeycloakSessio
}
}

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()) {
Expand Down Expand Up @@ -362,5 +424,14 @@ public String getId() {
public int order() {
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;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

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;
Expand Down Expand Up @@ -54,7 +56,11 @@ public void updateConfig(Config cf) {
.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");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ public void updateConfig(Config cf) {
.spi(UserSessionSpi.NAME)
.provider(InfinispanUserSessionProviderFactory.PROVIDER_ID)
.config("sessionPreloadStalledTimeoutInSeconds", "10")
.config("offlineSessionCacheEntryLifespanOverride", "43200")
.config("offlineClientSessionCacheEntryLifespanOverride", "43200")
;
}

Expand Down
Loading

0 comments on commit 93fc6a6

Please sign in to comment.