Disable peristent user session batching

Closes #41662

Signed-off-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com>
Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
Co-authored-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com>
Co-authored-by: Alexander Schwartz <aschwart@redhat.com>
This commit is contained in:
Pedro Ruivo 2025-09-01 15:33:21 +01:00 committed by GitHub
parent af96183788
commit 935caa97ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 62 additions and 48 deletions

View File

@ -64,6 +64,11 @@ When using the `+/realms/{realm-name}/broker/{provider_alias}/token+` endpoint f
When using GitHub as an IDP, you can now enable JSON responses to leverage the token refresh for this endpoint.
=== Persistent User Session Batching Disabled
The batching of persistent user session updates has been turned off by default because it negatively impacts performance with some database vendors, which offset the benefits with other database vendors.
It can be enabled using the CLI option `--spi-user-sessions--infinispan--use-batches=true`, but users are encouraged to load test their environment to verify performance improvements.
== Required field in User Session note mapper
The name of the session note is now shown as a required field in the Admin UI.

View File

@ -35,7 +35,6 @@ import org.keycloak.infinispan.util.InfinispanUtils;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionProvider;
@ -59,10 +58,13 @@ import org.keycloak.models.utils.ResetTimeOffsetEvent;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.provider.ProviderEvent;
import org.keycloak.provider.ProviderEventListener;
import org.keycloak.provider.ServerInfoAwareProviderFactory;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME;
public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory<UserSessionProvider>, ServerInfoAwareProviderFactory, EnvironmentDependentProviderFactory {
private static final Logger log = Logger.getLogger(InfinispanUserSessionProviderFactory.class);
@ -77,20 +79,18 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
public static final String CONFIG_USE_CACHES = "useCaches";
private static final boolean DEFAULT_USE_CACHES = true;
public static final String CONFIG_USE_BATCHES = "useBatches";
private static final boolean DEFAULT_USE_BATCHES = true;
private static final boolean DEFAULT_USE_BATCHES = false;
private long offlineSessionCacheEntryLifespanOverride;
private long offlineClientSessionCacheEntryLifespanOverride;
private Config.Scope config;
private PersisterLastSessionRefreshStore persisterLastSessionRefreshStore;
private InfinispanKeyGenerator keyGenerator;
SerializeExecutionsByKey<String> serializerSession = new SerializeExecutionsByKey<>();
SerializeExecutionsByKey<String> serializerOfflineSession = new SerializeExecutionsByKey<>();
SerializeExecutionsByKey<UUID> serializerClientSession = new SerializeExecutionsByKey<>();
SerializeExecutionsByKey<UUID> serializerOfflineClientSession = new SerializeExecutionsByKey<>();
final SerializeExecutionsByKey<String> serializerSession = new SerializeExecutionsByKey<>();
final SerializeExecutionsByKey<String> serializerOfflineSession = new SerializeExecutionsByKey<>();
final SerializeExecutionsByKey<UUID> serializerClientSession = new SerializeExecutionsByKey<>();
final SerializeExecutionsByKey<UUID> serializerOfflineClientSession = new SerializeExecutionsByKey<>();
ArrayBlockingQueue<PersistentUpdate> asyncQueuePersistentUpdate;
private PersistentSessionsWorker persistentSessionsWorker;
private int maxBatchSize;
@ -106,10 +106,10 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
if (useCaches) {
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
cache = connections.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME);
clientSessionCache = connections.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME);
offlineClientSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME);
cache = connections.getCache(USER_SESSION_CACHE_NAME);
offlineSessionsCache = connections.getCache(OFFLINE_USER_SESSION_CACHE_NAME);
clientSessionCache = connections.getCache(CLIENT_SESSION_CACHE_NAME);
offlineClientSessionsCache = connections.getCache(OFFLINE_CLIENT_SESSION_CACHE_NAME);
}
if (MultiSiteUtils.isPersistentSessionsEnabled()) {
@ -146,7 +146,6 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
@Override
public void init(Config.Scope config) {
this.config = config;
offlineSessionCacheEntryLifespanOverride = config.getInt(CONFIG_OFFLINE_SESSION_CACHE_ENTRY_LIFESPAN_OVERRIDE, -1);
if (offlineSessionCacheEntryLifespanOverride != -1) {
// to be removed in KC 27
@ -168,11 +167,8 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
@Override
public void postInit(final KeycloakSessionFactory factory) {
factory.register(new ProviderEventListener() {
@Override
public void onEvent(ProviderEvent event) {
if (event instanceof PostMigrationEvent) {
factory.register(event -> {
if (event instanceof PostMigrationEvent) {
if (!useCaches) {
keyGenerator = new InfinispanKeyGenerator() {
@Override
@ -191,22 +187,20 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
});
}
} else if (event instanceof UserModel.UserRemovedEvent) {
UserModel.UserRemovedEvent userRemovedEvent = (UserModel.UserRemovedEvent) event;
} else if (event instanceof UserModel.UserRemovedEvent userRemovedEvent) {
UserSessionProvider provider1 = userRemovedEvent.getKeycloakSession().getProvider(UserSessionProvider.class, getId());
if (provider1 instanceof InfinispanUserSessionProvider) {
((InfinispanUserSessionProvider) provider1).onUserRemoved(userRemovedEvent.getRealm(), userRemovedEvent.getUser());
} else if (provider1 instanceof PersistentUserSessionProvider) {
((PersistentUserSessionProvider) provider1).onUserRemoved(userRemovedEvent.getRealm(), userRemovedEvent.getUser());
} else {
throw new IllegalStateException("Unknown provider type: " + provider1.getClass());
}
UserSessionProvider provider1 = userRemovedEvent.getKeycloakSession().getProvider(UserSessionProvider.class, getId());
if (provider1 instanceof InfinispanUserSessionProvider) {
((InfinispanUserSessionProvider) provider1).onUserRemoved(userRemovedEvent.getRealm(), userRemovedEvent.getUser());
} else if (provider1 instanceof PersistentUserSessionProvider) {
((PersistentUserSessionProvider) provider1).onUserRemoved(userRemovedEvent.getRealm(), userRemovedEvent.getUser());
} else {
throw new IllegalStateException("Unknown provider type: " + provider1.getClass());
}
} else if (event instanceof ResetTimeOffsetEvent) {
if (persisterLastSessionRefreshStore != null) {
persisterLastSessionRefreshStore.reset();
}
} else if (event instanceof ResetTimeOffsetEvent) {
if (persisterLastSessionRefreshStore != null) {
persisterLastSessionRefreshStore.reset();
}
}
});
@ -219,13 +213,9 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
}
public void initializePersisterLastSessionRefreshStore(final KeycloakSessionFactory sessionFactory) {
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
// Initialize persister for periodically doing bulk DB updates of lastSessionRefresh timestamps of refreshed sessions
persisterLastSessionRefreshStore = new PersisterLastSessionRefreshStoreFactory().createAndInit(session, true);
}
KeycloakModelUtils.runJobInTransaction(sessionFactory, session -> {
// Initialize persister for periodically doing bulk DB updates of lastSessionRefresh timestamps of refreshed sessions
persisterLastSessionRefreshStore = new PersisterLastSessionRefreshStoreFactory().createAndInit(session, true);
});
}

View File

@ -966,6 +966,8 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
userSessionsPerformer.applyChangesSynchronously(s);
clientSessionPerformer.applyChangesSynchronously(s);
});
userSessionsPerformer.clear();
clientSessionPerformer.clear();
}
}

View File

@ -113,17 +113,20 @@ public class JpaChangesPerformer<K, V extends SessionEntity> implements SessionC
exceptions.forEach(ex::addSuppressed);
throw ex;
}
changes.clear();
clear();
}
}
public void applyChangesSynchronously(KeycloakSession session) {
if (!changes.isEmpty()) {
changes.forEach(persistentUpdate -> persistentUpdate.perform(session));
changes.clear();
}
}
public void clear() {
changes.clear();
}
private void processClientSessionUpdate(KeycloakSession innerSession, Map.Entry<K, SessionUpdatesList<V>> entry, MergedUpdate<V> merged) {
SessionUpdatesList<V> sessionUpdates = entry.getValue();
SessionEntityWrapper<V> sessionWrapper = sessionUpdates.getEntityWrapper();
@ -348,7 +351,7 @@ public class JpaChangesPerformer<K, V extends SessionEntity> implements SessionC
@Override
public void setNotes(Map<String, String> notes) {
clientSessionModel.getNotes().keySet().forEach(clientSessionModel::removeNote);
notes.forEach((k, v) -> clientSessionModel.setNote(k, v));
notes.forEach(clientSessionModel::setNote);
}
@Override
@ -647,7 +650,7 @@ public class JpaChangesPerformer<K, V extends SessionEntity> implements SessionC
@Override
public void setNotes(Map<String, String> notes) {
userSessionModel.getNotes().keySet().forEach(userSessionModel::removeNote);
notes.forEach((k, v) -> userSessionModel.setNote(k, v));
notes.forEach(userSessionModel::setNote);
}
@Override

View File

@ -20,8 +20,10 @@ package org.keycloak.models.sessions.infinispan.changes;
import org.infinispan.Cache;
import org.infinispan.commons.util.concurrent.CompletionStages;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Retry;
import org.keycloak.models.AbstractKeycloakTransaction;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.sessions.infinispan.SessionFunction;
@ -145,8 +147,18 @@ abstract public class PersistentSessionsChangelogBasedTransaction<K, V extends S
changesPerformers.add(new JpaChangesPerformer<>(cacheName, null) {
@Override
public void applyChanges() {
KeycloakModelUtils.runJobInTransaction(kcSession.getKeycloakSessionFactory(),
super::applyChangesSynchronously);
Retry.executeWithBackoff(
iteration -> KeycloakModelUtils.runJobInTransaction(kcSession.getKeycloakSessionFactory(), super::applyChangesSynchronously),
(iteration, t) -> {
if (t instanceof ModelDuplicateException ex) {
// duplicate exceptions are unlikely to succeed on a retry,
throw ex;
} else if (iteration > 20) {
// never retry more than 20 times
throw new RuntimeException("Maximum number of retries reached", t);
}
}, PersistentSessionsWorker.UPDATE_TIMEOUT, PersistentSessionsWorker.UPDATE_BASE_INTERVAL_MILLIS);
clear();
}
});
}

View File

@ -43,6 +43,8 @@ import java.util.concurrent.TimeUnit;
*/
public class PersistentSessionsWorker {
private static final Logger LOG = Logger.getLogger(PersistentSessionsWorker.class);
public static final Duration UPDATE_TIMEOUT = Duration.of(10, ChronoUnit.SECONDS);
public static final int UPDATE_BASE_INTERVAL_MILLIS = 0;
private final KeycloakSessionFactory factory;
private final ArrayBlockingQueue<PersistentUpdate> asyncQueuePersistentUpdate;
@ -150,7 +152,7 @@ public class PersistentSessionsWorker {
}
}
},
Duration.of(10, ChronoUnit.SECONDS), 0);
UPDATE_TIMEOUT, UPDATE_BASE_INTERVAL_MILLIS);
} catch (RuntimeException ex) {
tracing.error(ex);
batch.forEach(o -> o.fail(ex));