Remove offline session preloading

Closes #27602

Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
This commit is contained in:
Alexander Schwartz 2024-03-06 14:57:44 +01:00 committed by Alexander Schwartz
parent 7fc2269ba5
commit 62d24216e3
19 changed files with 31 additions and 492 deletions

View file

@ -108,8 +108,6 @@ public class Profile {
CLIENT_TYPES("Client Types", Type.EXPERIMENTAL),
OFFLINE_SESSION_PRELOADING("Offline session preloading", Type.DEPRECATED),
HOSTNAME_V1("Hostname Options V1", Type.DEFAULT),
//HOSTNAME_V2("Hostname Options V2", Type.DEFAULT, 2),

View file

@ -24,12 +24,12 @@ A lot of effort went into polishing and improving the Quarkus distribution to ma
* Initial support for Cross-DC
* User-defined profiles are no longer supported but using different configuration files to achieve the same goal
* Quickstarts updated to use the new distribution
== Other improvements
=== Offline sessions lazy loaded
=== Offline sessions lazily loaded
The offline sessions are now lazily fetched from the database by default instead of preloading during the server startup.
To change the default behavior, see link:{adminguide_link}#offline-sessions-preloading[{adminguide_name}].
=== Improved User Search

View file

@ -14,7 +14,6 @@ include::topics/sessions/administering.adoc[]
include::topics/sessions/revocation.adoc[]
include::topics/sessions/timeouts.adoc[]
include::topics/sessions/offline.adoc[]
include::topics/sessions/preloading.adoc[]
include::topics/sessions/transient.adoc[]
include::topics/assembly-roles-groups.adoc[]
include::topics/authentication.adoc[]

View file

@ -1,22 +0,0 @@
[[offline-sessions-preloading]]
=== Offline sessions preloading
In addition to Infinispan caches, offline sessions are stored in a database which means they will be available even after server restart.
By default, the offline sessions are not preloaded from the database into the Infinispan caches during the server startup, because this
approach has a drawback if there are many offline sessions to be preloaded. It can significantly slow down the server startup time.
Therefore, the offline sessions are lazily fetched from the database by default.
However, {project_name} can be configured to preload the offline sessions from the database into the Infinispan caches during the server startup.
It can be achieved by setting `preloadOfflineSessionsFromDatabase` property in the `userSessions` SPI to `true`.
:tech_feature_name: Offline session preloading
:tech_feature_id: offline-session-preloading
include::../templates/deprecated.adoc[]
The following example shows how to configure offline sessions preloading.
[source,bash]
----
bin/kc.[sh|bat] start --features-enabled offline-session-preloading --spi-user-sessions-infinispan-preload-offline-sessions-from-database=true
----

View file

@ -26,3 +26,7 @@ bin/kc.[sh|bat] --spi-connections-http-client-default-max-consumed-response-size
= Removed a model module
The module `org.keycloak:keycloak-model-legacy` module was deprecated in a previous release and is removed in this release. Use the `org.keycloak:keycloak-model-storage` module instead.
= Removed offline session preloading
The old behavior to preload offline sessions at startup is now removed after it has been deprecated in the previous release.

View file

@ -39,7 +39,6 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.UserSessionSpi;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.changes.Tasks;
import org.keycloak.models.sessions.infinispan.changes.sessions.CrossDCLastSessionRefreshStore;
@ -116,8 +115,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
protected final RemoteCacheInvoker remoteCacheInvoker;
protected final InfinispanKeyGenerator keyGenerator;
protected final boolean loadOfflineSessionsFromDatabase;
protected final SessionFunction offlineSessionCacheEntryLifespanAdjuster;
protected final SessionFunction offlineClientSessionCacheEntryLifespanAdjuster;
@ -132,7 +129,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionCache,
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache,
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionCache,
boolean loadOfflineSessionsFromDatabase,
SessionFunction<UserSessionEntity> offlineSessionCacheEntryLifespanAdjuster,
SessionFunction<AuthenticatedClientSessionEntity> offlineClientSessionCacheEntryLifespanAdjuster) {
this.session = session;
@ -154,7 +150,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
this.persisterLastSessionRefreshStore = persisterLastSessionRefreshStore;
this.remoteCacheInvoker = remoteCacheInvoker;
this.keyGenerator = keyGenerator;
this.loadOfflineSessionsFromDatabase = loadOfflineSessionsFromDatabase;
this.offlineSessionCacheEntryLifespanAdjuster = offlineSessionCacheEntryLifespanAdjuster;
this.offlineClientSessionCacheEntryLifespanAdjuster = offlineClientSessionCacheEntryLifespanAdjuster;
@ -366,7 +361,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
protected Stream<UserSessionModel> getUserSessionsStream(RealmModel realm, UserSessionPredicate predicate, boolean offline) {
if (offline && loadOfflineSessionsFromDatabase) {
if (offline) {
// fetch the offline user-sessions from the persistence provider
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
@ -392,16 +387,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
Stream.empty();
}
if (predicate.getBrokerSessionId() != null) {
if (!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.");
}
// TODO add support for offline user-session lookup by brokerSessionId
// currently it is not possible to access the brokerSessionId in offline user-session in a database agnostic way
throw new ModelException("Dynamic database lookup for offline user-sessions by broker session ID is currently only supported for preloaded sessions. " +
"Set preloadOfflineSessionsFromDatabase option to \"true\" in " + UserSessionSpi.NAME + " SPI in "
+ InfinispanUserSessionProviderFactory.PROVIDER_ID + " provider to enable the lookup.");
}
throw new ModelException("For offline sessions, only lookup by userId and brokerUserId is supported");
}
Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
@ -487,7 +473,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
protected Stream<UserSessionModel> getUserSessionsStream(final RealmModel realm, ClientModel client, Integer firstResult, Integer maxResults, final boolean offline) {
if (offline && loadOfflineSessionsFromDatabase) {
if (offline) {
// fetch the actual offline user session count from the database
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
return persister.loadUserSessionsStream(realm, client, true, firstResult, maxResults);
@ -569,7 +555,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
@Override
public Map<String, Long> getActiveClientSessionStats(RealmModel realm, boolean offline) {
if (offline && loadOfflineSessionsFromDatabase) {
if (offline) {
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
return persister.getUserSessionsCountsByClients(realm, true);
}
@ -589,7 +575,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
protected long getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) {
if (offline && loadOfflineSessionsFromDatabase) {
if (offline) {
// fetch the actual offline user session count from the database
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
return persister.getUserSessionsCount(realm, client, true);
@ -822,15 +808,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return getUserSession(realm, userSessionId, true);
}
@Override
public UserSessionModel getOfflineUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId) {
if (!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.");
}
return this.getUserSessionsStream(realm, UserSessionPredicate.create(realm.getId()).brokerSessionId(brokerSessionId), true)
.findFirst().orElse(null);
}
@Override
public Stream<UserSessionModel> getOfflineUserSessionByBrokerUserIdStream(RealmModel realm, String brokerUserId) {
return getUserSessionsStream(realm, UserSessionPredicate.create(realm.getId()).brokerUserId(brokerUserId), true);
@ -866,14 +843,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
@Override
public Stream<UserSessionModel> getOfflineUserSessionsStream(RealmModel realm, UserModel user) {
if (loadOfflineSessionsFromDatabase) {
return getUserSessionsFromPersistenceProviderStream(realm, user, true);
}
return getUserSessionsStream(realm, UserSessionPredicate.create(realm.getId()).user(user.getId()), true);
}
@Override
public long getOfflineSessionsCount(RealmModel realm, ClientModel client) {
return getUserSessionsCount(realm, client, true);

View file

@ -23,7 +23,6 @@ import org.infinispan.persistence.remote.RemoteStore;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.common.Profile;
import org.keycloak.common.util.Environment;
import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
@ -39,8 +38,6 @@ import org.keycloak.models.sessions.infinispan.changes.sessions.CrossDCLastSessi
import org.keycloak.models.sessions.infinispan.changes.sessions.CrossDCLastSessionRefreshStoreFactory;
import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStore;
import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStoreFactory;
import org.keycloak.models.sessions.infinispan.initializer.CacheInitializer;
import org.keycloak.models.sessions.infinispan.initializer.DBLockBasedCacheInitializer;
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
@ -51,7 +48,6 @@ import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent;
import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent;
import org.keycloak.models.sessions.infinispan.initializer.InfinispanCacheInitializer;
import org.keycloak.models.sessions.infinispan.initializer.OfflinePersistentUserSessionLoader;
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheSessionListener;
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheSessionsLoader;
import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator;
@ -85,8 +81,6 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
public static final String REMOVE_USER_SESSIONS_EVENT = "REMOVE_USER_SESSIONS_EVENT";
private boolean preloadOfflineSessionsFromDatabase;
private long offlineSessionCacheEntryLifespanOverride;
private long offlineClientSessionCacheEntryLifespanOverride;
@ -118,7 +112,6 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
offlineSessionsCache,
clientSessionCache,
offlineClientSessionsCache,
!preloadOfflineSessionsFromDatabase,
this::deriveOfflineSessionCacheEntryLifespanMs,
this::deriveOfflineClientSessionCacheEntryLifespanOverrideMs
);
@ -127,11 +120,6 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
@Override
public void init(Config.Scope config) {
this.config = config;
preloadOfflineSessionsFromDatabase = config.getBoolean("preloadOfflineSessionsFromDatabase", false);
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);
}
@ -203,31 +191,6 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
@Override
public void run(KeycloakSession session) {
if (preloadOfflineSessionsFromDatabase) {
// only preload offline-sessions if necessary
log.debug("Start pre-loading userSessions from persistent storage");
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
Cache<String, Serializable> workCache = connections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
int defaultStateTransferTimeout = (int) (connections.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME)
.getCacheConfiguration().clustering().stateTransfer().timeout() / 1000);
InfinispanCacheInitializer ispnInitializer = new InfinispanCacheInitializer(sessionFactory, workCache,
new OfflinePersistentUserSessionLoader(sessionsPerSegment), "offlineUserSessions", sessionsPerSegment, maxErrors,
getStalledTimeoutInSeconds(defaultStateTransferTimeout));
// DB-lock to ensure that persistent sessions are loaded from DB just on one DC. The other DCs will load them from remote cache.
CacheInitializer initializer = new DBLockBasedCacheInitializer(session, ispnInitializer);
initializer.initCache();
initializer.loadSessions();
log.debug("Pre-loading userSessions from persistent storage finished");
} else {
log.debug("Skipping pre-loading of userSessions from persistent storage");
}
// Initialize persister for periodically doing bulk DB updates of lastSessionRefresh timestamps of refreshed sessions
persisterLastSessionRefreshStore = new PersisterLastSessionRefreshStoreFactory().createAndInit(session, true);
}
@ -428,7 +391,6 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
@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

@ -52,12 +52,6 @@ public abstract class BaseCacheInitializer extends CacheInitializer {
@Override
protected boolean isFinished() {
// Check if we should skipLoadingSessions. This can happen if someone else already did the task (For example in cross-dc environment, it was done by different DC)
boolean isFinishedAlready = this.sessionLoader.isFinished(this);
if (isFinishedAlready) {
return true;
}
InitializerState state = getStateFromCache();
return state != null && state.isFinished();
}
@ -122,8 +116,4 @@ public abstract class BaseCacheInitializer extends CacheInitializer {
}
}
public Cache<String, Serializable> getWorkCache() {
return workCache;
}
}

View file

@ -1,90 +0,0 @@
/*
* Copyright 2016 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.initializer;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.dblock.DBLockManager;
import org.keycloak.models.dblock.DBLockProvider;
/**
* Encapsulates preloading of sessions within the DB Lock. This DB-aware lock ensures that "startLoading" is done on single DC and the other DCs need to wait.
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DBLockBasedCacheInitializer extends CacheInitializer {
private static final Logger log = Logger.getLogger(DBLockBasedCacheInitializer.class);
private final KeycloakSession session;
private final CacheInitializer delegate;
public DBLockBasedCacheInitializer(KeycloakSession session, CacheInitializer delegate) {
this.session = session;
this.delegate = delegate;
}
@Override
public void initCache() {
delegate.initCache();
}
@Override
protected boolean isFinished() {
return delegate.isFinished();
}
@Override
protected boolean isCoordinator() {
return delegate.isCoordinator();
}
@Override
protected int getProgressIndicator() {
return delegate.getProgressIndicator();
}
@Override
protected int getStalledTimeoutInSeconds() {
return delegate.getStalledTimeoutInSeconds();
}
/**
* Just coordinator will run this. And there is DB-lock, so the delegate.startLoading() will be permitted just by the single DC
*/
@Override
protected void startLoading() {
DBLockManager dbLockManager = new DBLockManager(session);
dbLockManager.checkForcedUnlock();
DBLockProvider dbLock = dbLockManager.getDBLock();
dbLock.waitForLock(DBLockProvider.Namespace.OFFLINE_SESSIONS);
try {
if (isFinished()) {
log.infof("Task already finished when DBLock retrieved");
} else {
delegate.startLoading();
}
} finally {
dbLock.releaseLock();
}
}
}

View file

@ -1,178 +0,0 @@
/*
* Copyright 2016 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.initializer;
import org.infinispan.Cache;
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
import org.infinispan.context.Flag;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Retry;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.session.UserSessionPersisterProvider;
import java.io.Serializable;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OfflinePersistentUserSessionLoader implements SessionLoader<OfflinePersistentLoaderContext,
OfflinePersistentWorkerContext, OfflinePersistentWorkerResult>, Serializable {
// Placeholder String used in the searching conditions to identify very first session
private static final String FIRST_SESSION_ID = "00000000-0000-0000-0000-000000000000";
private static final Logger log = Logger.getLogger(OfflinePersistentUserSessionLoader.class);
// Cross-DC aware flag
public static final String PERSISTENT_SESSIONS_LOADED = "PERSISTENT_SESSIONS_LOADED";
// Just local-DC aware flag
public static final String PERSISTENT_SESSIONS_LOADED_IN_CURRENT_DC = "PERSISTENT_SESSIONS_LOADED_IN_CURRENT_DC";
private final int sessionsPerSegment;
public OfflinePersistentUserSessionLoader(int sessionsPerSegment) {
this.sessionsPerSegment = sessionsPerSegment;
}
@Override
public void init(KeycloakSession session) {
}
@Override
public OfflinePersistentLoaderContext computeLoaderContext(KeycloakSession session) {
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
int sessionsCount = persister.getUserSessionsCount(true);
return new OfflinePersistentLoaderContext(sessionsCount, sessionsPerSegment);
}
@Override
public OfflinePersistentWorkerContext computeWorkerContext(OfflinePersistentLoaderContext loaderCtx, int segment, int workerId, OfflinePersistentWorkerResult previousResult) {
String lastSessionId;
if (previousResult == null) {
lastSessionId = FIRST_SESSION_ID;
} else {
lastSessionId = previousResult.getLastSessionId();
}
// We know the last loaded session. New workers iteration will start from this place
return new OfflinePersistentWorkerContext(segment, workerId, lastSessionId);
}
@Override
public OfflinePersistentWorkerResult createFailedWorkerResult(OfflinePersistentLoaderContext loaderContext, OfflinePersistentWorkerContext workerContext) {
return new OfflinePersistentWorkerResult(false, workerContext.getSegment(), workerContext.getWorkerId(), FIRST_SESSION_ID);
}
@Override
public OfflinePersistentWorkerResult loadSessions(KeycloakSession session, OfflinePersistentLoaderContext loaderContext, OfflinePersistentWorkerContext ctx) {
int first = ctx.getWorkerId() * sessionsPerSegment;
log.tracef("Loading sessions for segment=%d lastSessionId=%s first=%d", ctx.getSegment(), ctx.getLastSessionId(), (Object) first);
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
List<UserSessionModel> sessions = persister
.loadUserSessionsStream(first, sessionsPerSegment, true, ctx.getLastSessionId())
.collect(Collectors.toList());
log.tracef("Sessions loaded from DB - segment=%d lastSessionId=%s", ctx.getSegment(), ctx.getLastSessionId());
UserSessionModel lastSession = null;
if (!sessions.isEmpty()) {
lastSession = sessions.get(sessions.size() - 1);
// Save to memory/infinispan
session.sessions().importUserSessions(sessions, true);
}
String lastSessionId = lastSession==null ? FIRST_SESSION_ID : lastSession.getId();
log.tracef("Sessions imported to infinispan - segment: %d, lastSessionId: %s", ctx.getSegment(), lastSessionId);
return new OfflinePersistentWorkerResult(true, ctx.getSegment(), ctx.getWorkerId(), lastSessionId);
}
@Override
public boolean isFinished(BaseCacheInitializer initializer) {
Cache<String, Serializable> workCache = initializer.getWorkCache();
Boolean sessionsLoaded = (Boolean) workCache.get(PERSISTENT_SESSIONS_LOADED);
if (sessionsLoaded != null && sessionsLoaded) {
log.debugf("Persistent sessions loaded already.");
return true;
} else {
log.debugf("Persistent sessions not yet loaded.");
return false;
}
}
@Override
public void afterAllSessionsLoaded(BaseCacheInitializer initializer) {
Cache<String, Serializable> workCache = initializer.getWorkCache();
// Will retry few times for the case when backup site not available in cross-dc environment.
// The site might be taken offline automatically if "take-offline" properly configured
Retry.executeWithBackoff((int iteration) -> {
try {
// Cross-DC aware flag
workCache
.getAdvancedCache().withFlags(Flag.SKIP_REMOTE_LOOKUP)
.put(PERSISTENT_SESSIONS_LOADED, true);
} catch (HotRodClientException re) {
log.warnf(re, "Failed to write flag PERSISTENT_SESSIONS_LOADED in iteration '%d' . Retrying", iteration);
// Rethrow the exception. Retry will take care of handle the exception and eventually retry the operation.
throw re;
}
}, 10, 10);
// Just local-DC aware flag
workCache
.getAdvancedCache().withFlags(Flag.SKIP_REMOTE_LOOKUP, Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE)
.put(PERSISTENT_SESSIONS_LOADED_IN_CURRENT_DC, true);
log.debugf("Persistent sessions loaded successfully!");
}
@Override
public String toString() {
return new StringBuilder("OfflinePersistentUserSessionLoader [ ")
.append("sessionsPerSegment: ").append(sessionsPerSegment)
.append(" ]")
.toString();
}
}

View file

@ -83,16 +83,6 @@ public interface SessionLoader<LOADER_CONTEXT extends SessionLoader.LoaderContex
*/
WORKER_RESULT createFailedWorkerResult(LOADER_CONTEXT loaderContext, WORKER_CONTEXT workerContext);
/**
* This will be called on nodes to check if loading is finished. It allows loader to notify that loading is finished for some reason.
*
* @param initializer
* @return
*/
boolean isFinished(BaseCacheInitializer initializer);
/**
* Callback triggered on cluster coordinator once it recognize that all sessions were successfully loaded
*

View file

@ -33,7 +33,6 @@ import org.keycloak.connections.infinispan.DefaultInfinispanConnectionProviderFa
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.sessions.infinispan.initializer.BaseCacheInitializer;
import org.keycloak.models.sessions.infinispan.initializer.OfflinePersistentUserSessionLoader;
import org.keycloak.models.sessions.infinispan.initializer.SessionLoader;
/**
@ -169,26 +168,6 @@ public class RemoteCacheSessionsLoader implements SessionLoader<RemoteCacheSessi
10);
}
@Override
public boolean isFinished(BaseCacheInitializer initializer) {
Cache<String, Serializable> workCache = initializer.getWorkCache();
// Check if persistent sessions were already loaded in this DC. This is possible just for offline sessions ATM
Boolean sessionsLoaded = (Boolean) workCache
.getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE)
.get(OfflinePersistentUserSessionLoader.PERSISTENT_SESSIONS_LOADED_IN_CURRENT_DC);
if ((cacheName.equals(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) || (cacheName.equals(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME)))
&& sessionsLoaded != null && sessionsLoaded) {
log.debugf("Sessions already loaded in current DC. Skip sessions loading from remote cache '%s'", cacheName);
return true;
} else {
log.debugf("Sessions maybe not yet loaded in current DC. Will load them from remote cache '%s'", cacheName);
return false;
}
}
@Override
public void afterAllSessionsLoaded(BaseCacheInitializer initializer) {
}

View file

@ -24,7 +24,7 @@ import org.keycloak.provider.Provider;
* one cluster node at a time.</p>
*
* <p>There are different namespaces that can be locked. The same <em>DBLockProvider</em>
* (same session in keycloack) can only be used to lock one namespace, a second
* (same session in keycloak) can only be used to lock one namespace, a second
* attempt will throw a <em>RuntimeException</em>. The <em>hasLock</em> method
* returns the local namespace locked by this provider.</p>
*
@ -43,8 +43,9 @@ public interface DBLockProvider extends Provider {
enum Namespace {
DATABASE(1),
KEYCLOAK_BOOT(1000),
OFFLINE_SESSIONS(1001);
KEYCLOAK_BOOT(1000)
// OFFLINE_SESSIONS(1001) -- Not used anymore. Keeping to avoid reusing the number.
;
private final int id;
@ -55,7 +56,7 @@ public interface DBLockProvider extends Provider {
public int getId() {
return id;
}
};
}
/**
* Try to retrieve DB lock or wait if retrieve was unsuccessful.

View file

@ -175,16 +175,6 @@ public interface UserSessionProvider extends Provider {
*/
Stream<UserSessionModel> getOfflineUserSessionsStream(RealmModel realm, UserModel user);
/**
* Search user sessions by the broker session ID.
* @deprecated
* Instead of this method, use {@link #getOfflineUserSessionByBrokerUserIdStream(RealmModel, String)} to first get
* the sessions of a user, and then filter by the broker session ID as needed.
*
*/
@Deprecated
UserSessionModel getOfflineUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId);
/**
* Obtains the offline user sessions associated with the user that matches the specified {@code brokerUserId}.
*

View file

@ -23,9 +23,7 @@ import static org.keycloak.services.resources.LoginActionsService.SESSION_CODE;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.NoCache;
import org.keycloak.common.Profile;
import org.keycloak.http.HttpRequest;
import org.keycloak.Config;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.TokenVerifier;
@ -116,9 +114,6 @@ public class LogoutEndpoint {
private final EventBuilder event;
private final OIDCProviderConfig providerConfig;
// When enabled we cannot search offline sessions by brokerSessionId. We need to search by federated userId and then filter by brokerSessionId.
private final boolean offlineSessionsLazyLoadingEnabled;
private Cors cors;
public LogoutEndpoint(KeycloakSession session, TokenManager tokenManager, EventBuilder event, OIDCProviderConfig providerConfig) {
@ -128,10 +123,6 @@ public class LogoutEndpoint {
this.realm = session.getContext().getRealm();
this.event = event;
this.providerConfig = providerConfig;
this.offlineSessionsLazyLoadingEnabled = !Config.scope("userSessions").scope("infinispan").getBoolean("preloadOfflineSessionsFromDatabase", false);
if (!this.offlineSessionsLazyLoadingEnabled && !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.");
}
this.request = session.getContext().getHttpRequest();
this.headers = session.getContext().getRequestHeaders();
}
@ -631,11 +622,7 @@ public class LogoutEndpoint {
UserSessionModel userSession = session.sessions().getUserSessionByBrokerSessionId(realm, identityProviderAlias + "." + sessionId);
if (logoutOfflineSessions) {
if (offlineSessionsLazyLoadingEnabled) {
logoutOfflineUserSessionByBrokerUserId(identityProviderAlias + "." + federatedUserId, identityProviderAlias + "." + sessionId);
} else {
logoutOfflineUserSession(identityProviderAlias + "." + sessionId);
}
}
if (userSession != null) {
@ -646,14 +633,6 @@ public class LogoutEndpoint {
return backchannelLogoutResponse.get();
}
private void logoutOfflineUserSession(String brokerSessionId) {
UserSessionModel offlineUserSession =
session.sessions().getOfflineUserSessionByBrokerSessionId(realm, brokerSessionId);
if (offlineUserSession != null) {
new UserSessionManager(session).revokeOfflineUserSession(offlineUserSession);
}
}
private BackchannelLogoutResponse backchannelLogoutFederatedUserId(String federatedUserId,
Stream<String> identityProviderAliases,
boolean logoutOfflineSessions) {

View file

@ -419,7 +419,6 @@
"keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}",
"keycloak.connectionsInfinispan.remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:true}",
"keycloak.connectionsInfinispan.hotrodProtocolVersion": "${keycloak.connectionsInfinispan.hotrodProtocolVersion}",
"keycloak.userSessions.infinispan.preloadOfflineSessionsFromDatabase": "${keycloak.userSessions.infinispan.preloadOfflineSessionsFromDatabase:false}",
"keycloak.connectionsJpa.url": "${keycloak.connectionsJpa.url.crossdc:jdbc:h2:mem:test-dc-shared}",
"keycloak.connectionsJpa.driver": "${keycloak.connectionsJpa.driver.crossdc:org.h2.Driver}",
"keycloak.connectionsJpa.driverDialect": "${keycloak.connectionsJpa.driverDialect.crossdc:}"
@ -446,7 +445,6 @@
"keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}",
"keycloak.connectionsInfinispan.remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:true}",
"keycloak.connectionsInfinispan.hotrodProtocolVersion": "${keycloak.connectionsInfinispan.hotrodProtocolVersion}",
"keycloak.userSessions.infinispan.preloadOfflineSessionsFromDatabase": "${keycloak.userSessions.infinispan.preloadOfflineSessionsFromDatabase:false}",
"keycloak.connectionsJpa.url": "${keycloak.connectionsJpa.url.crossdc:jdbc:h2:mem:test-dc-shared}",
"keycloak.connectionsJpa.driver": "${keycloak.connectionsJpa.driver.crossdc:org.h2.Driver}",
"keycloak.connectionsJpa.driverDialect": "${keycloak.connectionsJpa.driverDialect.crossdc:}"
@ -474,7 +472,6 @@
"keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort.2:11222}",
"keycloak.connectionsInfinispan.remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:true}",
"keycloak.connectionsInfinispan.hotrodProtocolVersion": "${keycloak.connectionsInfinispan.hotrodProtocolVersion}",
"keycloak.userSessions.infinispan.preloadOfflineSessionsFromDatabase": "${keycloak.userSessions.infinispan.preloadOfflineSessionsFromDatabase:false}",
"keycloak.connectionsJpa.url": "${keycloak.connectionsJpa.url.crossdc:jdbc:h2:mem:test-dc-shared}",
"keycloak.connectionsJpa.driver": "${keycloak.connectionsJpa.driver.crossdc:org.h2.Driver}",
"keycloak.connectionsJpa.driverDialect": "${keycloak.connectionsJpa.driverDialect.crossdc:}"
@ -501,7 +498,6 @@
"keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort.2:11222}",
"keycloak.connectionsInfinispan.remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:true}",
"keycloak.connectionsInfinispan.hotrodProtocolVersion": "${keycloak.connectionsInfinispan.hotrodProtocolVersion}",
"keycloak.userSessions.infinispan.preloadOfflineSessionsFromDatabase": "${keycloak.userSessions.infinispan.preloadOfflineSessionsFromDatabase:false}",
"keycloak.connectionsJpa.url": "${keycloak.connectionsJpa.url.crossdc:jdbc:h2:mem:test-dc-shared}",
"keycloak.connectionsJpa.driver": "${keycloak.connectionsJpa.driver.crossdc:org.h2.Driver}",
"keycloak.connectionsJpa.driverDialect": "${keycloak.connectionsJpa.driverDialect.crossdc:}"

View file

@ -29,8 +29,6 @@
<jdbc.mvn.version>${h2.version}</jdbc.mvn.version>
<log4j.configuration>file:${project.build.directory}/dependency/log4j.properties</log4j.configuration>
<jacoco.skip>true</jacoco.skip>
<keycloak.userSessions.infinispan.preloadOfflineSessionsFromDatabase>false</keycloak.userSessions.infinispan.preloadOfflineSessionsFromDatabase>
<keycloak.profile.feature.offline_session_preloading>disabled</keycloak.profile.feature.offline_session_preloading>
</properties>
<dependencies>
@ -160,8 +158,6 @@
<keycloak.connectionsJpa.default.password>${keycloak.connectionsJpa.password}</keycloak.connectionsJpa.default.password>
<keycloak.connectionsJpa.default.url>${keycloak.connectionsJpa.url}</keycloak.connectionsJpa.default.url>
<log4j.configuration>file:${project.build.directory}/test-classes/log4j.properties</log4j.configuration> <!-- for the logging to properly work with tests in the 'other' module -->
<keycloak.userSessions.infinispan.preloadOfflineSessionsFromDatabase>${keycloak.userSessions.infinispan.preloadOfflineSessionsFromDatabase}</keycloak.userSessions.infinispan.preloadOfflineSessionsFromDatabase>
<keycloak.profile.feature.offline_session_preloading>${keycloak.profile.feature.offline_session_preloading}</keycloak.profile.feature.offline_session_preloading>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<org.jboss.logging.provider>log4j</org.jboss.logging.provider>
<infinispan.version>${infinispan.version}</infinispan.version>
@ -224,24 +220,6 @@
</properties>
</profile>
<profile>
<id>jpa+cross-dc-infinispan-offline-sessions-preloading</id>
<properties>
<keycloak.model.parameters>CrossDCInfinispan,Jpa</keycloak.model.parameters>
<keycloak.profile.feature.offline_session_preloading>enabled</keycloak.profile.feature.offline_session_preloading>
<keycloak.userSessions.infinispan.preloadOfflineSessionsFromDatabase>true</keycloak.userSessions.infinispan.preloadOfflineSessionsFromDatabase>
</properties>
</profile>
<profile>
<id>jpa+infinispan-offline-sessions-preloading</id>
<properties>
<keycloak.model.parameters>Infinispan,Jpa</keycloak.model.parameters>
<keycloak.profile.feature.offline_session_preloading>enabled</keycloak.profile.feature.offline_session_preloading>
<keycloak.userSessions.infinispan.preloadOfflineSessionsFromDatabase>true</keycloak.userSessions.infinispan.preloadOfflineSessionsFromDatabase>
</properties>
</profile>
<profile>
<id>jpa-federation+infinispan</id>
<properties>

View file

@ -50,7 +50,7 @@ public class DBLockTest extends KeycloakModelTest {
private static final int LOCK_RECHECK_MILLIS = 10;
@Before
public void before() throws Exception {
public void before() {
inComittedTransaction(1, (session , i) -> {
// Set timeouts for testing
DBLockManager lockManager = new DBLockManager(session);
@ -64,7 +64,7 @@ public class DBLockTest extends KeycloakModelTest {
}
@Test
public void simpleLockTest() throws Exception {
public void simpleLockTest() {
inComittedTransaction(1, (session , i) -> {
DBLockProvider dbLock = new DBLockManager(session).getDBLock();
dbLock.waitForLock(DBLockProvider.Namespace.DATABASE);
@ -79,7 +79,7 @@ public class DBLockTest extends KeycloakModelTest {
}
@Test
public void simpleNestedLockTest() throws Exception {
public void simpleNestedLockTest() {
inComittedTransaction(1, (session , i) -> {
// first session lock DATABASE
DBLockProvider dbLock1 = new DBLockManager(session).getDBLock();
@ -89,10 +89,10 @@ public class DBLockTest extends KeycloakModelTest {
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionLC2) -> {
// a second session/dblock-provider can lock another namespace OFFLINE_SESSIONS
DBLockProvider dbLock2 = new DBLockManager(sessionLC2).getDBLock();
dbLock2.waitForLock(DBLockProvider.Namespace.OFFLINE_SESSIONS);
dbLock2.waitForLock(DBLockProvider.Namespace.KEYCLOAK_BOOT);
try {
// getCurrentLock is local, each provider instance has one
Assert.assertEquals(DBLockProvider.Namespace.OFFLINE_SESSIONS, dbLock2.getCurrentLock());
Assert.assertEquals(DBLockProvider.Namespace.KEYCLOAK_BOOT, dbLock2.getCurrentLock());
} finally {
dbLock2.releaseLock();
}
@ -107,7 +107,7 @@ public class DBLockTest extends KeycloakModelTest {
}
@Test
public void testLockConcurrentlyGeneral() throws Exception {
public void testLockConcurrentlyGeneral() {
inComittedTransaction(1, (session , i) -> {
testLockConcurrentlyInternal(session, DBLockProvider.Namespace.DATABASE);
return null;
@ -115,23 +115,23 @@ public class DBLockTest extends KeycloakModelTest {
}
@Test
public void testLockConcurrentlyOffline() throws Exception {
public void testLockConcurrentlyKeycloakBoot() {
inComittedTransaction(1, (session , i) -> {
testLockConcurrentlyInternal(session, DBLockProvider.Namespace.OFFLINE_SESSIONS);
testLockConcurrentlyInternal(session, DBLockProvider.Namespace.KEYCLOAK_BOOT);
return null;
});
}
@Test
public void testTwoLocksCurrently() throws Exception {
public void testTwoLocksCurrently() {
inComittedTransaction(1, (session , i) -> {
testTwoLocksCurrentlyInternal(session, DBLockProvider.Namespace.DATABASE, DBLockProvider.Namespace.OFFLINE_SESSIONS);
testTwoLocksCurrentlyInternal(session, DBLockProvider.Namespace.DATABASE, DBLockProvider.Namespace.KEYCLOAK_BOOT);
return null;
});
}
@Test
public void testTwoNestedLocksCurrently() throws Exception {
public void testTwoNestedLocksCurrently() {
inComittedTransaction(1, (session , i) -> {
testTwoNestedLocksCurrentlyInternal(session, DBLockProvider.Namespace.KEYCLOAK_BOOT, DBLockProvider.Namespace.DATABASE);
return null;

View file

@ -37,9 +37,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.UserSessionSpi;
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.utils.ResetTimeOffsetEvent;
import org.keycloak.services.managers.UserSessionManager;
@ -338,12 +336,6 @@ public class UserSessionProviderOfflineModelTest extends KeycloakModelTest {
@Test
public void testOfflineSessionLazyLoadingPropagationBetweenNodes() throws InterruptedException {
// This test is only unstable after setting "keycloak.userSessions.infinispan.preloadOfflineSessionsFromDatabase" to "true" and
// CrossDC is enabled.
// This is tracked in https://github.com/keycloak/keycloak/issues/14020 to be resolved.
Assume.assumeFalse(Objects.equals(CONFIG.scope("userSessions.infinispan").get("preloadOfflineSessionsFromDatabase"), "true") &&
Objects.equals(CONFIG.scope("connectionsInfinispan.default").get("remoteStoreEnabled"), "true"));
// as one thread fills this list and the others read it, ensure that it is synchronized to avoid side effects
List<UserSessionModel> offlineUserSessions = Collections.synchronizedList(new LinkedList<>());
List<AuthenticatedClientSessionModel> offlineClientSessions = Collections.synchronizedList(new LinkedList<>());
@ -476,9 +468,8 @@ 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"));
// skip the test for CrossDC
Assume.assumeFalse(Objects.equals(CONFIG.scope("connectionsInfinispan.default").get("remoteStoreEnabled"), "true"));
createOfflineSessions("user1", 2, new LinkedList<>(), new LinkedList<>());