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:
parent
d3ae075a33
commit
93fc6a6c54
7 changed files with 167 additions and 14 deletions
|
@ -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.
|
||||||
|
|
|
@ -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>
|
||||||
|
----
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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<>();
|
||||||
|
|
Loading…
Reference in a new issue