8000 KEYCLOAK-11019 Initial support for lazy offline user-session loading by martin-kanis · Pull Request #8033 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
Expand Down Expand Up @@ -60,9 +61,11 @@

import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
Expand Down Expand Up @@ -105,6 +108,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
protected final RemoteCacheInvoker remoteCacheInvoker;
protected final InfinispanKeyGenerator keyGenerator;

protected final boolean loadOfflineSessionsStatsFromDatabase;

public InfinispanUserSessionProvider(KeycloakSession session,
RemoteCacheInvoker remoteCacheInvoker,
CrossDCLastSessionRefreshStore lastSessionRefreshStore,
Expand All @@ -114,7 +119,8 @@ public InfinispanUserSessionProvider(KeycloakSession session,
Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionCache,
Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionCache,
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache,
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionCache) {
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionCache,
boolean loadOfflineSessionsStatsFromDatabase) {
this.session = session;

this.sessionCache = sessionCache;
Expand All @@ -134,6 +140,7 @@ public InfinispanUserSessionProvider(KeycloakSession session,
this.persisterLastSessionRefreshStore = persisterLastSessionRefreshStore;
this.remoteCacheInvoker = remoteCacheInvoker;
this.keyGenerator = keyGenerator;
this.loadOfflineSessionsStatsFromDatabase = loadOfflineSessionsStatsFromDatabase;

session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx);
session.getTransactionManager().enlistAfterCompletion(sessionTx);
Expand Down Expand Up @@ -239,19 +246,78 @@ void updateSessionEntity(UserSessionEntity entity, RealmModel realm, UserModel u

entity.setStarted(currentTime);
entity.setLastSessionRefresh(currentTime);


}


@Override
public UserSessionModel getUserSession(RealmModel realm, String id) {
return getUserSession(realm, id, false);
}

protected UserSessionAdapter getUserSession(RealmModel realm, String id, boolean offline) {
UserSessionEntity entity = getUserSessionEntity(realm, id, offline);
return wrap(realm, entity, offline);

UserSessionEntity userSessionEntityFromCache = getUserSessionEntity(realm, id, offline);
if (userSessionEntityFromCache != null) {
return wrap(realm, userSessionEntityFromCache, offline);
}

if (!offline) {
return null;
}

// Try to recover from potentially lost offline-sessions by attempting to fetch and re-import
// the offline session information from the PersistenceProvider.
UserSessionEntity userSessionEntityFromPersistenceProvider = getUserSessionEntityFromPersistenceProvider(realm, id, offline);
if (userSessionEntityFromPersistenceProvider != null) {
// we successfully recovered the offline session!
return wrap(realm, userSessionEntityFromPersistenceProvider, offline);
}

// no luck, the session is really not there anymore
return null;
}

private UserSessionEntity getUserSessionEntityFromPersistenceProvider(RealmModel realm, String sessionId, boolean offline) {

log.debugf("Offline user-session not found in infinispan, attempting UserSessionPersisterProvider lookup for sessionId=%s", sessionId);
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
UserSessionModel persistentUserSession = persister.loadUserSession(realm, sessionId, offline);

if (persistentUserSession == null) {
log.debugf("Offline user-session not found in UserSessionPersisterProvider for sessionId=%s", sessionId);
return null;
}

return importUserSession(realm, offline, persistentUserSession);
}

private UserSessionEntity getUserSessionEntityFromCacheOrImportIfNecessary(RealmModel realm, boolean offline, UserSessionModel persistentUserSession) {

UserSessionEntity userSessionEntity = getUserSessionEntity(realm, persistentUserSession.getId(), offline);
if (userSessionEntity != null) {
// user session present in cache, return existing session
return userSessionEntity;
}

return importUserSession(realm, offline, persistentUserSession);
}

private UserSessionEntity importUserSession(RealmModel realm, boolean offline, UserSessionModel persistentUserSession) {

String sessionId = persistentUserSession.getId();

log.debugf("Attempting to import user-session for sessionId=%s offline=%s", sessionId, offline);
session.sessions().importUserSessions(Collections.singleton(persistentUserSession), offline);
log.debugf("user-session imported, trying another lookup for sessionId=%s offline=%s", sessionId, offline);

UserSessionEntity ispnUserSessionEntity = getUserSessionEntity(realm, sessionId, offline);

if (ispnUserSessionEntity != null) {
log.debugf("user-session found after import for sessionId=%s offline=%s", sessionId, offline);
return ispnUserSessionEntity;
}

log.debugf("user-session could not be found after import for sessionId=%s offline=%s", sessionId, offline);
return null;
}

private UserSessionEntity getUserSessionEntity(RealmModel realm, String id, boolean offline) {
Expand All @@ -263,8 +329,41 @@ private UserSessionEntity getUserSessionEntity(RealmModel realm, String id, bool
return entity;
}

private Stream<UserSessionModel> getUserSessionsFromPersistenceProviderStream(RealmModel realm, UserModel user, boolean offline) {
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
return persister.loadUserSessionsStream(realm, user, offline, 0, null)
.map(persistentUserSession -> getUserSessionEntityFromCacheOrImportIfNecessary(realm, offline, persistentUserSession))
.filter(Objects::nonNull)
.map(userSessionEntity -> wrap(realm, userSessionEntity, offline));
}


protected Stream<UserSessionModel> getUserSessionsStream(RealmModel realm, UserSessionPredicate predicate, boolean offline) {

if (offline && loadOfflineSessionsStatsFromDatabase) {

// fetch the offline user-sessions from the persistence provider
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);

UserModel user = session.users().getUserById(realm, predicate.getUserId());
if (user != null) {
return persister.loadUserSessionsStream(realm, user, offline, 0, null);
}

if (predicate.getBrokerSessionId() != null) {
// 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 brokerSessionId is currently only supported for preloaded sessions.");
}

if (predicate.getBrokerUserId() != null) {
// TODO add support for offline user-session lookup by brokerUserId
// currently it is not possible to access the brokerUserId in offline user-session in a database agnostic way
throw new ModelException("Dynamic database lookup for offline user-sessions by brokerUserId is currently only supported for preloaded sessions.");
}

}

protected Stream<UserSessionModel> getUserSessionsStream(RealmModel realm, Predicate<Map.Entry<String, SessionEntityWrapper<UserSessionEntit 8000 y>>> predicate, boolean offline) {
Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
cache = CacheDecorators.skipCacheLoaders(cache);

Expand Down Expand Up @@ -321,6 +420,13 @@ public Stream<UserSessionModel> getUserSessionsStream(RealmModel realm, ClientMo
}

protected Stream<UserSessionModel> getUserSessionsStream(final RealmModel realm, ClientModel client, Integer firstResult, Integer maxResults, final boolean offline) {

if (offline && loadOfflineSessionsStatsFromDatabase) {
// fetch the actual offline user session count from the database
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
return persister.loadUserSessionsStream(realm, client, offline, firstResult, maxResults);
}

final String clientUuid = client.getId();
UserSessionPredicate predicate = UserSessionPredicate.create(realm.getId()).client(clientUuid);

Expand Down Expand Up @@ -410,6 +516,12 @@ public long getActiveUserSessions(RealmModel realm, ClientModel client) {

@Override
public Map<String, Long> getActiveClientSessionStats(RealmModel realm, boolean offline) {

if (offline && loadOfflineSessionsStatsFromDatabase) {
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
return persister.getUserSessionsCountsByClients(realm, offline);
}

Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
cache = CacheDecorators.skipCacheLoaders(cache);
return cache.entrySet().stream()
Expand All @@ -424,6 +536,13 @@ public Map<String, Long> getActiveClientSessionStats(RealmModel realm, boolean o
}

protected long getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) {

if (offline && loadOfflineSessionsStatsFromDatabase) {
// fetch the actual offline user session count from the database
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
8BD0 return persister.getUserSessionsCount(realm, client, offline);
}

Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
cache = CacheDecorators.skipCacheLoaders(cache);

Expand Down Expand Up @@ -662,7 +781,12 @@ public AuthenticatedClientSessionModel createOfflineClientSession(AuthenticatedC

@Override
public Stream<UserSessionModel> getOfflineUserSessionsStream(RealmModel realm, UserModel user) {
return this.getUserSessionsStream(realm, UserSessionPredicate.create(realm.getId()).user(user.getId()), true);

if (loadOfflineSessionsStatsFromDatabase) {
return getUserSessionsFromPersistenceProviderStream(realm, user, true);
}

return getUserSessionsStream(realm, UserSessionPredicate.create(realm.getId()).user(user.getId()), true);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,10 @@ public InfinispanUserSessionProvider create(KeycloakSession session) {
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache = connections.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME);
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME);

boolean loadOfflineSessionsStatsFromDatabase = !isPreloadingOfflineSessionsFromDatabaseEnabled();

return new InfinispanUserSessionProvider(session, remoteCacheInvoker, lastSessionRefreshStore, offlineLastSessionRefreshStore,
persisterLastSessionRefreshStore, keyGenerator, cache, offlineSessionsCache, clientSessionCache, offlineClientSessionsCache);
persisterLastSessionRefreshStore, keyGenerator, cache, offlineSessionsCache, clientSessionCache, offlineClientSessionsCache, loadOfflineSessionsStatsFromDatabase);
}

@Override
Expand Down Expand Up @@ -145,6 +147,10 @@ public void onEvent(ProviderEvent event) {
});
}

private boolean isPreloadingOfflineSessionsFromDatabaseEnabled() {
return config.getBoolean("preloadOfflineSessionsFromDatabase", true);
}

// Max count of worker errors. Initialization will end with exception when this number is reached
private int getMaxErrors() {
return config.getInt("maxErrors", 20);
Expand All @@ -163,31 +169,40 @@ private int getTimeoutForPreloadingSessionsSeconds() {

@Override
public void loadPersistentSessions(final KeycloakSessionFactory sessionFactory, final int maxErrors, final int sessionsPerSegment) {
log.debug("Start pre-loading userSessions from persistent storage");

KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {

@Override
public void run(KeycloakSession session) {
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
Cache<String, Serializable> workCache = connections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);

InfinispanCacheInitializer ispnInitializer = new InfinispanCacheInitializer(sessionFactory, workCache,
new OfflinePersistentUserSessionLoader(sessionsPerSegment), "offlineUserSessions", sessionsPerSegment, maxErrors);
if (isPreloadingOfflineSessionsFromDatabaseEnabled()) {
// only preload offline-sessions if necessary
log.debug("Start pre-loading userSessions from persistent storage");

// 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);
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
Cache<String, Serializable> workCache = connections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);

initializer.initCache();
initializer.loadSessions();
InfinispanCacheInitializer ispnInitializer = new InfinispanCacheInitializer(sessionFactory, workCache,
new OfflinePersistentUserSessionLoader(sessionsPerSegment), "offlineUserSessions", sessionsPerSegment, maxErrors);

// 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);
}

});

log.debug("Pre-loading userSessions from persistent storage finished");

}


Expand Down
898C
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,22 @@ public UserSessionPredicate brokerUserId(String id) {
return this;
}

/**
* Returns the user id.
* @return
*/
public String getUserId() {
return user;
}

public String getBrokerSessionId() {
return brokerSessionId;
}

public String getBrokerUserId() {
return brokerUserId;
}

@Override
public boolean test(Map.Entry<String, SessionEntityWrapper<UserSessionEntity>> entry) {
UserSessionEntity entity = entry.getValue().getEntity();
Expand Down
Loading
0