mirror of
https://github.com/keycloak/keycloak.git
synced 2026-02-18 18:37:54 -05:00
Revisit Infinispan session idle and lifetime settings
Some checks are pending
Weblate Sync / Trigger Weblate to pull the latest changes (push) Waiting to run
Some checks are pending
Weblate Sync / Trigger Weblate to pull the latest changes (push) Waiting to run
Closes #46421 Signed-off-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com> Co-authored-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com>
This commit is contained in:
parent
974eff0a92
commit
5096806b52
9 changed files with 119 additions and 28 deletions
|
|
@ -56,6 +56,7 @@ import org.keycloak.models.sessions.infinispan.changes.PersistentSessionUpdateTa
|
|||
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
|
||||
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
|
||||
import org.keycloak.models.sessions.infinispan.changes.Tasks;
|
||||
import org.keycloak.models.sessions.infinispan.changes.UserSessionInfinispanChangelogBasedTransaction;
|
||||
import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStore;
|
||||
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.EmbeddedClientSessionKey;
|
||||
|
|
@ -96,10 +97,10 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi
|
|||
|
||||
protected final KeycloakSession session;
|
||||
|
||||
protected final InfinispanChangelogBasedTransaction<String, UserSessionEntity> sessionTx;
|
||||
protected final InfinispanChangelogBasedTransaction<String, UserSessionEntity> offlineSessionTx;
|
||||
protected final InfinispanChangelogBasedTransaction<EmbeddedClientSessionKey, AuthenticatedClientSessionEntity> clientSessionTx;
|
||||
protected final InfinispanChangelogBasedTransaction<EmbeddedClientSessionKey, AuthenticatedClientSessionEntity> offlineClientSessionTx;
|
||||
protected final UserSessionInfinispanChangelogBasedTransaction<String, UserSessionEntity> sessionTx;
|
||||
protected final UserSessionInfinispanChangelogBasedTransaction<String, UserSessionEntity> offlineSessionTx;
|
||||
protected final UserSessionInfinispanChangelogBasedTransaction<EmbeddedClientSessionKey, AuthenticatedClientSessionEntity> clientSessionTx;
|
||||
protected final UserSessionInfinispanChangelogBasedTransaction<EmbeddedClientSessionKey, AuthenticatedClientSessionEntity> offlineClientSessionTx;
|
||||
|
||||
protected final SessionEventsSenderTransaction clusterEventsSenderTx;
|
||||
|
||||
|
|
@ -111,10 +112,10 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi
|
|||
|
||||
public InfinispanUserSessionProvider(KeycloakSession session,
|
||||
PersisterLastSessionRefreshStore persisterLastSessionRefreshStore,
|
||||
InfinispanChangelogBasedTransaction<String, UserSessionEntity> sessionTx,
|
||||
InfinispanChangelogBasedTransaction<String, UserSessionEntity> offlineSessionTx,
|
||||
InfinispanChangelogBasedTransaction<EmbeddedClientSessionKey, AuthenticatedClientSessionEntity> clientSessionTx,
|
||||
InfinispanChangelogBasedTransaction<EmbeddedClientSessionKey, AuthenticatedClientSessionEntity> offlineClientSessionTx,
|
||||
UserSessionInfinispanChangelogBasedTransaction<String, UserSessionEntity> sessionTx,
|
||||
UserSessionInfinispanChangelogBasedTransaction<String, UserSessionEntity> offlineSessionTx,
|
||||
UserSessionInfinispanChangelogBasedTransaction<EmbeddedClientSessionKey, AuthenticatedClientSessionEntity> clientSessionTx,
|
||||
UserSessionInfinispanChangelogBasedTransaction<EmbeddedClientSessionKey, AuthenticatedClientSessionEntity> offlineClientSessionTx,
|
||||
SessionFunction<UserSessionEntity> offlineSessionCacheEntryLifespanAdjuster,
|
||||
SessionFunction<AuthenticatedClientSessionEntity> offlineClientSessionCacheEntryLifespanAdjuster) {
|
||||
this.session = session;
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@ import org.keycloak.models.UserSessionProviderFactory;
|
|||
import org.keycloak.models.UserSessionSpi;
|
||||
import org.keycloak.models.sessions.infinispan.changes.CacheHolder;
|
||||
import org.keycloak.models.sessions.infinispan.changes.ClientSessionPersistentChangelogBasedTransaction;
|
||||
import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction;
|
||||
import org.keycloak.models.sessions.infinispan.changes.InfinispanChangesUtils;
|
||||
import org.keycloak.models.sessions.infinispan.changes.PersistentSessionsWorker;
|
||||
import org.keycloak.models.sessions.infinispan.changes.PersistentUpdate;
|
||||
import org.keycloak.models.sessions.infinispan.changes.UserSessionInfinispanChangelogBasedTransaction;
|
||||
import org.keycloak.models.sessions.infinispan.changes.UserSessionPersistentChangelogBasedTransaction;
|
||||
import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStore;
|
||||
import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStoreFactory;
|
||||
|
|
@ -422,10 +422,10 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
|
|||
}
|
||||
|
||||
private VolatileTransactions createVolatileTransaction(KeycloakSession session) {
|
||||
var sessionTx = new InfinispanChangelogBasedTransaction<>(session, sessionCacheHolder);
|
||||
var offlineSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineSessionCacheHolder);
|
||||
var clientSessionTx = new InfinispanChangelogBasedTransaction<>(session, clientSessionCacheHolder);
|
||||
var offlineClientSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineClientSessionCacheHolder);
|
||||
var sessionTx = new UserSessionInfinispanChangelogBasedTransaction<>(session, sessionCacheHolder);
|
||||
var offlineSessionTx = new UserSessionInfinispanChangelogBasedTransaction<>(session, offlineSessionCacheHolder);
|
||||
var clientSessionTx = new UserSessionInfinispanChangelogBasedTransaction<>(session, clientSessionCacheHolder);
|
||||
var offlineClientSessionTx = new UserSessionInfinispanChangelogBasedTransaction<>(session, offlineClientSessionCacheHolder);
|
||||
|
||||
var transactionProvider = session.getProvider(InfinispanTransactionProvider.class);
|
||||
transactionProvider.registerTransaction(sessionTx);
|
||||
|
|
@ -453,10 +453,11 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
|
|||
return new PersistentTransaction(sessionTx, clientSessionTx);
|
||||
}
|
||||
|
||||
private record VolatileTransactions(InfinispanChangelogBasedTransaction<String, UserSessionEntity> sessionTx,
|
||||
InfinispanChangelogBasedTransaction<String, UserSessionEntity> offlineSessionTx,
|
||||
InfinispanChangelogBasedTransaction<EmbeddedClientSessionKey, AuthenticatedClientSessionEntity> clientSessionTx,
|
||||
InfinispanChangelogBasedTransaction<EmbeddedClientSessionKey, AuthenticatedClientSessionEntity> offlineClientSessionTx) {}
|
||||
private record VolatileTransactions(
|
||||
UserSessionInfinispanChangelogBasedTransaction<String, UserSessionEntity> sessionTx,
|
||||
UserSessionInfinispanChangelogBasedTransaction<String, UserSessionEntity> offlineSessionTx,
|
||||
UserSessionInfinispanChangelogBasedTransaction<EmbeddedClientSessionKey, AuthenticatedClientSessionEntity> clientSessionTx,
|
||||
UserSessionInfinispanChangelogBasedTransaction<EmbeddedClientSessionKey, AuthenticatedClientSessionEntity> offlineClientSessionTx) {}
|
||||
|
||||
private record PersistentTransaction(UserSessionPersistentChangelogBasedTransaction userTx, ClientSessionPersistentChangelogBasedTransaction clientTx) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> imp
|
|||
long lifespanMs = cacheHolder.lifespanFunction().apply(realm, sessionUpdates.getClient(), sessionWrapper.getEntity());
|
||||
long maxIdleTimeMs = cacheHolder.maxIdleFunction().apply(realm, sessionUpdates.getClient(), sessionWrapper.getEntity());
|
||||
|
||||
MergedUpdate<V> merged = MergedUpdate.computeUpdate(updateTasks, sessionWrapper, lifespanMs, maxIdleTimeMs);
|
||||
MergedUpdate<V> merged = MergedUpdate.computeUpdate(updateTasks, sessionWrapper, computeLifespan(maxIdleTimeMs, lifespanMs), computeMaxIdle(maxIdleTimeMs, lifespanMs));
|
||||
|
||||
if (merged != null) {
|
||||
// Now run the operation in our cluster
|
||||
|
|
@ -216,7 +216,7 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> imp
|
|||
// exists in transaction, avoid cache operation
|
||||
return updatesList.getEntityWrapper().getEntity();
|
||||
}
|
||||
SessionEntityWrapper<V> existing = cacheHolder.cache().putIfAbsent(key, session, lifespan, TimeUnit.MILLISECONDS, maxIdle, TimeUnit.MILLISECONDS);
|
||||
SessionEntityWrapper<V> existing = cacheHolder.cache().putIfAbsent(key, session, computeLifespan(maxIdle, lifespan), TimeUnit.MILLISECONDS, computeMaxIdle(maxIdle, lifespan), TimeUnit.MILLISECONDS);
|
||||
if (existing == null) {
|
||||
// keep track of the imported session for updates
|
||||
updates.put(key, new SessionUpdatesList<>(realmModel, session));
|
||||
|
|
@ -263,7 +263,7 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> imp
|
|||
//nothing to import, already expired
|
||||
return;
|
||||
}
|
||||
var future = cacheHolder.cache().putIfAbsentAsync(key, session, lifespan, TimeUnit.MILLISECONDS, maxIdle, TimeUnit.MILLISECONDS);
|
||||
var future = cacheHolder.cache().putIfAbsentAsync(key, session, computeLifespan(maxIdle, lifespan), TimeUnit.MILLISECONDS, computeMaxIdle(maxIdle, lifespan), TimeUnit.MILLISECONDS);
|
||||
// write result into concurrent hash map because the consumer is invoked in a different thread each time.
|
||||
stage.dependsOn(future.thenAccept(existing -> allSessions.put(key, existing == null ? session : existing)));
|
||||
});
|
||||
|
|
@ -288,4 +288,12 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> imp
|
|||
// Run the update now, so reader in same transaction can see it (TODO: Rollback may not work correctly. See if it's an issue..)
|
||||
myUpdates.addAndExecute(task);
|
||||
}
|
||||
|
||||
protected long computeLifespan(long maxIdle, long lifespan) {
|
||||
return lifespan;
|
||||
}
|
||||
|
||||
protected long computeMaxIdle(long maxIdle, long lifespan) {
|
||||
return maxIdle;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ abstract public class PersistentSessionsChangelogBasedTransaction<K, V extends S
|
|||
long lifespanMs = getLifespanMsLoader(isOffline).apply(realm, sessionUpdates.getClient(), entity);
|
||||
long maxIdleTimeMs = getMaxIdleMsLoader(isOffline).apply(realm, sessionUpdates.getClient(), entity);
|
||||
|
||||
MergedUpdate<V> merged = MergedUpdate.computeUpdate(sessionUpdates.getUpdateTasks(), sessionWrapper, lifespanMs, maxIdleTimeMs);
|
||||
MergedUpdate<V> merged = MergedUpdate.computeUpdate(sessionUpdates.getUpdateTasks(), sessionWrapper, SessionTimeouts.calculateEffectiveSessionLifespan(maxIdleTimeMs, lifespanMs), SessionTimeouts.IMMORTAL_FLAG);
|
||||
|
||||
if (merged != null) {
|
||||
var c = isOffline ? offlineCacheHolder : cacheHolder;
|
||||
|
|
@ -274,7 +274,7 @@ abstract public class PersistentSessionsChangelogBasedTransaction<K, V extends S
|
|||
SessionEntityWrapper<V> existing = null;
|
||||
try {
|
||||
if (getCache(offline) != null) {
|
||||
existing = getCache(offline).putIfAbsent(key, session, lifespan, TimeUnit.MILLISECONDS, maxIdle, TimeUnit.MILLISECONDS);
|
||||
existing = getCache(offline).putIfAbsent(key, session, SessionTimeouts.calculateEffectiveSessionLifespan(maxIdle, lifespan), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
} catch (RuntimeException exception) {
|
||||
// If the import fails, the transaction can continue with the data from the database.
|
||||
|
|
@ -324,7 +324,7 @@ abstract public class PersistentSessionsChangelogBasedTransaction<K, V extends S
|
|||
//nothing to import, already expired
|
||||
return;
|
||||
}
|
||||
var future = cache.putIfAbsentAsync(key, session, lifespan, TimeUnit.MILLISECONDS, maxIdle, TimeUnit.MILLISECONDS)
|
||||
var future = cache.putIfAbsentAsync(key, session, SessionTimeouts.calculateEffectiveSessionLifespan(maxIdle, lifespan), TimeUnit.MILLISECONDS)
|
||||
.exceptionally(throwable -> {
|
||||
// If the import fails, the transaction can continue with the data from the database.
|
||||
LOG.debugf(throwable, "Failed to import session %s", session);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright 2026 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.sessions.infinispan.changes;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
|
||||
|
||||
/**
|
||||
* User session transaction implementation that optimizes Infinispan cache expiration settings.
|
||||
* <p>
|
||||
* This class overrides the parent's timeout computation to disable max-idle tracking, which is expensive for Infinispan
|
||||
* to maintain. Instead, the lifespan is adjusted to be the minimum of the original lifespan and max-idle values,
|
||||
* ensuring sessions still expire at the correct time without the overhead of idle time tracking.
|
||||
*/
|
||||
public class UserSessionInfinispanChangelogBasedTransaction<K, V extends SessionEntity> extends InfinispanChangelogBasedTransaction<K, V> {
|
||||
public UserSessionInfinispanChangelogBasedTransaction(KeycloakSession kcSession, CacheHolder<K, V> cacheHolder) {
|
||||
super(kcSession, cacheHolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long computeLifespan(long maxIdle, long lifespan) {
|
||||
return SessionTimeouts.calculateEffectiveSessionLifespan(maxIdle, lifespan);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long computeMaxIdle(long maxIdle, long lifespan) {
|
||||
return SessionTimeouts.IMMORTAL_FLAG;
|
||||
}
|
||||
}
|
||||
|
|
@ -104,7 +104,7 @@ public class AuthenticatedClientSessionUpdater extends BaseUpdater<ClientSession
|
|||
public Expiration computeExpiration() {
|
||||
long maxIdle = SessionTimeouts.getClientSessionMaxIdleMs(userSession.getRealm(), client, offline, isUserSessionRememberMe(), getTimestamp());
|
||||
long lifespan = SessionTimeouts.getClientSessionLifespanMs(userSession.getRealm(), client, offline, isUserSessionRememberMe(), getStarted(), getUserSessionStarted());
|
||||
return new Expiration(maxIdle, lifespan);
|
||||
return new Expiration(SessionTimeouts.IMMORTAL_FLAG, SessionTimeouts.calculateEffectiveSessionLifespan(maxIdle, lifespan));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ public class UserSessionUpdater extends BaseUpdater<String, RemoteUserSessionEnt
|
|||
public Expiration computeExpiration() {
|
||||
long maxIdle = SessionTimeouts.getUserSessionMaxIdleMs(realm, isOffline(), getValue().isRememberMe(), getValue().getLastSessionRefresh());
|
||||
long lifespan = SessionTimeouts.getUserSessionLifespanMs(realm, isOffline(), getValue().isRememberMe(), getValue().getStarted());
|
||||
return new Expiration(maxIdle, lifespan);
|
||||
return new Expiration(SessionTimeouts.IMMORTAL_FLAG, SessionTimeouts.calculateEffectiveSessionLifespan(maxIdle, lifespan));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ public class SessionTimeouts {
|
|||
*/
|
||||
public static final long ENTRY_EXPIRED_FLAG = -2;
|
||||
|
||||
private static final long IMMORTAL_FLAG = -1;
|
||||
public static final long IMMORTAL_FLAG = -1;
|
||||
|
||||
/**
|
||||
* Get the maximum lifespan, which this userSession can remain in the infinispan cache.
|
||||
|
|
@ -244,4 +244,32 @@ public class SessionTimeouts {
|
|||
public static long getAuthSessionMaxIdleMS(RealmModel realm, ClientModel client, RootAuthenticationSessionEntity entity) {
|
||||
return IMMORTAL_FLAG;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the effective lifespan value to use when storing user and client session entries in the Infinispan
|
||||
* cache.
|
||||
* <p>
|
||||
* This method optimizes Infinispan cache configuration by incorporating the max-idle timeout into the lifespan
|
||||
* value. Since Infinispan's max-idle implementation is expensive (requires tracking last access time and additional
|
||||
* overhead), this optimization avoids using max-idle directly and instead sets the lifespan to the minimum of
|
||||
* max-idle and lifespan values. This ensures session entries expire at the correct time without the performance
|
||||
* cost of max-idle tracking.
|
||||
*
|
||||
* @param maxIdle the maximum idle time in milliseconds, or {@link #IMMORTAL_FLAG} for no idle timeout
|
||||
* @param lifespan the maximum lifespan in milliseconds, or {@link #IMMORTAL_FLAG} for no lifespan timeout
|
||||
* @return the effective lifespan to use for the session cache entry: returns {@code lifespan} if {@code maxIdle} is
|
||||
* {@link #IMMORTAL_FLAG}, {@code maxIdle} if {@code lifespan} is {@link #IMMORTAL_FLAG}, otherwise returns
|
||||
* {@code min(maxIdle, lifespan)}
|
||||
*/
|
||||
public static long calculateEffectiveSessionLifespan(long maxIdle, long lifespan) {
|
||||
// currently, max-idle is never IMMORTAL_FLAG; the jvm should be able to remove the check.
|
||||
// keep it to be future-proof.
|
||||
if (maxIdle == IMMORTAL_FLAG) {
|
||||
return lifespan;
|
||||
}
|
||||
if (lifespan == IMMORTAL_FLAG) {
|
||||
return maxIdle;
|
||||
}
|
||||
return Math.min(maxIdle, lifespan);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ import org.keycloak.authorization.DefaultAuthorizationProviderFactory;
|
|||
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
|
||||
import org.keycloak.authorization.policy.provider.PolicySpi;
|
||||
import org.keycloak.authorization.store.StoreFactorySpi;
|
||||
import org.keycloak.cache.AlternativeLookupProviderFactory;
|
||||
import org.keycloak.cache.AlternativeLookupSPI;
|
||||
import org.keycloak.cache.LocalCacheProviderFactory;
|
||||
import org.keycloak.cache.LocalCacheSPI;
|
||||
import org.keycloak.cluster.ClusterSpi;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.profile.PropertiesProfileConfigResolver;
|
||||
|
|
@ -254,7 +258,9 @@ public abstract class KeycloakModelTest {
|
|||
UserSessionSpi.class,
|
||||
UserSpi.class,
|
||||
DatastoreSpi.class,
|
||||
CacheRemoteConfigProviderSpi.class);
|
||||
CacheRemoteConfigProviderSpi.class,
|
||||
AlternativeLookupSPI.class,
|
||||
LocalCacheSPI.class);
|
||||
|
||||
private static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = Set.of(
|
||||
ComponentFactoryProviderFactory.class,
|
||||
|
|
@ -264,7 +270,9 @@ public abstract class KeycloakModelTest {
|
|||
DeploymentStateProviderFactory.class,
|
||||
DatastoreProviderFactory.class,
|
||||
TracingProviderFactory.class,
|
||||
CacheRemoteConfigProviderFactory.class);
|
||||
CacheRemoteConfigProviderFactory.class,
|
||||
AlternativeLookupProviderFactory.class,
|
||||
LocalCacheProviderFactory.class);
|
||||
|
||||
protected static final List<KeycloakModelParameters> MODEL_PARAMETERS;
|
||||
protected static final Config CONFIG = new Config(KeycloakModelTest::useDefaultFactory);
|
||||
|
|
|
|||
Loading…
Reference in a new issue