diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java new file mode 100755 index 0000000000..e41913d4f6 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java @@ -0,0 +1,217 @@ +package org.keycloak.models.cache.infinispan; + +import org.infinispan.Cache; +import org.infinispan.notifications.cachelistener.annotation.CacheEntriesEvicted; +import org.infinispan.notifications.cachelistener.annotation.CacheEntryInvalidated; +import org.infinispan.notifications.cachelistener.event.CacheEntriesEvictedEvent; +import org.infinispan.notifications.cachelistener.event.CacheEntryInvalidatedEvent; +import org.jboss.logging.Logger; +import org.keycloak.models.cache.infinispan.entities.AbstractRevisioned; +import org.keycloak.models.cache.infinispan.entities.Revisioned; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public abstract class CacheManager { + protected static final Logger logger = Logger.getLogger(CacheManager.class); + protected final Cache revisions; + protected final Cache cache; + protected final UpdateCounter counter = new UpdateCounter(); + + public CacheManager(Cache cache, Cache revisions) { + this.cache = cache; + this.revisions = revisions; + this.cache.addListener(this); + } + + public Cache getCache() { + return cache; + } + + public long getCurrentCounter() { + return counter.current(); + } + + public Long getCurrentRevision(String id) { + Long revision = revisions.get(id); + if (revision == null) { + revision = counter.current(); + } + // if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event + // so, we do this to force this. + String invalidationKey = "invalidation.key" + id; + cache.putForExternalRead(invalidationKey, new AbstractRevisioned(-1L, invalidationKey)); + return revision; + } + + public void endRevisionBatch() { + try { + revisions.endBatch(true); + } catch (Exception e) { + } + + } + + public T get(String id, Class type) { + Revisioned o = (Revisioned)cache.get(id); + if (o == null) { + return null; + } + Long rev = revisions.get(id); + if (rev == null) { + RealmCacheManager.logger.tracev("get() missing rev"); + return null; + } + long oRev = o.getRevision() == null ? -1L : o.getRevision().longValue(); + if (rev > oRev) { + RealmCacheManager.logger.tracev("get() rev: {0} o.rev: {1}", rev.longValue(), oRev); + return null; + } + return o != null && type.isInstance(o) ? type.cast(o) : null; + } + + public Object invalidateObject(String id) { + Revisioned removed = (Revisioned)cache.remove(id); + // if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event + // so, we do this to force the event. + cache.remove("invalidation.key" + id); + bumpVersion(id); + return removed; + } + + protected void bumpVersion(String id) { + long next = counter.next(); + Object rev = revisions.put(id, next); + } + + public void addRevisioned(Revisioned object, long startupRevision) { + //startRevisionBatch(); + String id = object.getId(); + try { + //revisions.getAdvancedCache().lock(id); + Long rev = revisions.get(id); + if (rev == null) { + if (id.endsWith("realm.clients")) RealmCacheManager.logger.trace("addRevisioned rev == null realm.clients"); + rev = counter.current(); + revisions.put(id, rev); + } + revisions.startBatch(); + if (!revisions.getAdvancedCache().lock(id)) { + RealmCacheManager.logger.trace("Could not obtain version lock"); + return; + } + rev = revisions.get(id); + if (rev == null) { + if (id.endsWith("realm.clients")) RealmCacheManager.logger.trace("addRevisioned rev2 == null realm.clients"); + return; + } + if (rev > startupRevision) { // revision is ahead transaction start. Other transaction updated in the meantime. Don't cache + if (RealmCacheManager.logger.isTraceEnabled()) { + RealmCacheManager.logger.tracev("Skipped cache. Current revision {0}, Transaction start revision {1}", object.getRevision(), startupRevision); + } + return; + } + if (rev.equals(object.getRevision())) { + if (id.endsWith("realm.clients")) RealmCacheManager.logger.tracev("adding Object.revision {0} rev {1}", object.getRevision(), rev); + cache.putForExternalRead(id, object); + return; + } + if (rev > object.getRevision()) { // revision is ahead, don't cache + if (id.endsWith("realm.clients")) RealmCacheManager.logger.trace("addRevisioned revision is ahead realm.clients"); + return; + } + // revisions cache has a lower value than the object.revision, so update revision and add it to cache + if (id.endsWith("realm.clients")) RealmCacheManager.logger.tracev("adding Object.revision {0} rev {1}", object.getRevision(), rev); + revisions.put(id, object.getRevision()); + cache.putForExternalRead(id, object); + } finally { + endRevisionBatch(); + } + + } + + public void clear() { + cache.clear(); + } + + public void addInvalidations(Predicate> predicate, Set invalidations) { + Iterator> it = getEntryIterator(predicate); + while (it.hasNext()) { + invalidations.add(it.next().getKey()); + } + } + + private Iterator> getEntryIterator(Predicate> predicate) { + return cache + .entrySet() + .stream() + .filter(predicate).iterator(); + } + + @CacheEntryInvalidated + public void cacheInvalidated(CacheEntryInvalidatedEvent event) { + if (event.isPre()) { + String key = event.getKey(); + if (key.startsWith("invalidation.key")) { + // if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event + // so, we do this to force this. + String bump = key.substring("invalidation.key".length()); + RealmCacheManager.logger.tracev("bumping invalidation key {0}", bump); + bumpVersion(bump); + return; + } + + } else { + //if (!event.isPre()) { + String key = event.getKey(); + if (key.startsWith("invalidation.key")) { + // if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event + // so, we do this to force this. + String bump = key.substring("invalidation.key".length()); + bumpVersion(bump); + RealmCacheManager.logger.tracev("bumping invalidation key {0}", bump); + return; + } + bumpVersion(key); + Object object = event.getValue(); + if (object != null) { + bumpVersion(key); + Predicate> predicate = getInvalidationPredicate(object); + if (predicate != null) runEvictions(predicate); + RealmCacheManager.logger.tracev("invalidating: {0}" + object.getClass().getName()); + } + } + } + + @CacheEntriesEvicted + public void cacheEvicted(CacheEntriesEvictedEvent event) { + if (!event.isPre()) + for (Map.Entry entry : event.getEntries().entrySet()) { + Object object = entry.getValue(); + bumpVersion(entry.getKey()); + if (object == null) continue; + RealmCacheManager.logger.tracev("evicting: {0}" + object.getClass().getName()); + Predicate> predicate = getInvalidationPredicate(object); + if (predicate != null) runEvictions(predicate); + } + } + + public void runEvictions(Predicate> current) { + Set evictions = new HashSet<>(); + addInvalidations(current, evictions); + RealmCacheManager.logger.tracev("running evictions size: {0}", evictions.size()); + for (String key : evictions) { + cache.evict(key); + bumpVersion(key); + } + } + + protected abstract Predicate> getInvalidationPredicate(Object object); +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java index fb9946b28e..f32ac69369 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java @@ -35,12 +35,12 @@ public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFa private static final Logger log = Logger.getLogger(InfinispanCacheRealmProviderFactory.class); - protected volatile StreamRealmCache realmCache; + protected volatile RealmCacheManager realmCache; @Override public CacheRealmProvider create(KeycloakSession session) { lazyInit(session); - return new StreamCacheRealmProvider(realmCache, session); + return new RealmCacheSession(realmCache, session); } private void lazyInit(KeycloakSession session) { @@ -49,7 +49,7 @@ public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFa if (realmCache == null) { Cache cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.REALM_CACHE_NAME); Cache revisions = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.VERSION_CACHE_NAME); - realmCache = new StreamRealmCache(cache, revisions); + realmCache = new RealmCacheManager(cache, revisions); } } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java index e8657ff579..9e93d55141 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java @@ -29,6 +29,7 @@ import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.cache.CacheUserProvider; import org.keycloak.models.cache.CacheUserProviderFactory; import org.keycloak.models.cache.infinispan.entities.CachedUser; +import org.keycloak.models.cache.infinispan.entities.Revisioned; import java.util.concurrent.ConcurrentHashMap; @@ -39,25 +40,23 @@ public class InfinispanCacheUserProviderFactory implements CacheUserProviderFact private static final Logger log = Logger.getLogger(InfinispanCacheUserProviderFactory.class); - protected volatile InfinispanUserCache userCache; + protected volatile UserCacheManager userCache; - protected final RealmLookup usernameLookup = new RealmLookup(); - protected final RealmLookup emailLookup = new RealmLookup(); @Override public CacheUserProvider create(KeycloakSession session) { lazyInit(session); - return new DefaultCacheUserProvider(userCache, session); + return new UserCacheSession(userCache, session); } private void lazyInit(KeycloakSession session) { if (userCache == null) { synchronized (this) { if (userCache == null) { - Cache cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_CACHE_NAME); - cache.addListener(new CacheListener()); - userCache = new InfinispanUserCache(cache, usernameLookup, emailLookup); + Cache cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_CACHE_NAME); + Cache revisions = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.VERSION_CACHE_NAME); + userCache = new UserCacheManager(cache, revisions); } } } @@ -81,100 +80,5 @@ public class InfinispanCacheUserProviderFactory implements CacheUserProviderFact return "default"; } - @Listener - public class CacheListener { - - @CacheEntryCreated - public void userCreated(CacheEntryCreatedEvent event) { - if (!event.isPre()) { - CachedUser user = event.getValue(); - if (user != null) { - String realm = user.getRealm(); - - usernameLookup.put(realm, user.getUsername(), user.getId()); - if (user.getEmail() != null) { - emailLookup.put(realm, user.getEmail(), user.getId()); - } - - log.tracev("User added realm={0}, id={1}, username={2}", realm, user.getId(), user.getUsername()); - } - } - } - - @CacheEntryRemoved - public void userRemoved(CacheEntryRemovedEvent event) { - if (event.isPre()) { - CachedUser user = event.getValue(); - if (user != null) { - removeUser(user); - - log.tracev("User invalidated realm={0}, id={1}, username={2}", user.getRealm(), user.getId(), user.getUsername()); - } - } - } - - @CacheEntryInvalidated - public void userInvalidated(CacheEntryInvalidatedEvent event) { - if (event.isPre()) { - CachedUser user = event.getValue(); - if (user != null) { - removeUser(user); - - log.tracev("User invalidated realm={0}, id={1}, username={2}", user.getRealm(), user.getId(), user.getUsername()); - } - } - } - - @CacheEntriesEvicted - public void userEvicted(CacheEntriesEvictedEvent event) { - for (CachedUser user : event.getEntries().values()) { - removeUser(user); - - log.tracev("User evicted realm={0}, id={1}, username={2}", user.getRealm(), user.getId(), user.getUsername()); - } - } - - private void removeUser(CachedUser cachedUser) { - String realm = cachedUser.getRealm(); - usernameLookup.remove(realm, cachedUser.getUsername()); - if (cachedUser.getEmail() != null) { - emailLookup.remove(realm, cachedUser.getEmail()); - } - } - - } - - static class RealmLookup { - - protected final ConcurrentHashMap> lookup = new ConcurrentHashMap<>(); - - public void put(String realm, String key, String value) { - ConcurrentHashMap map = lookup.get(realm); - if(map == null) { - map = new ConcurrentHashMap<>(); - ConcurrentHashMap p = lookup.putIfAbsent(realm, map); - if (p != null) { - map = p; - } - } - map.put(key, value); - } - - public String get(String realm, String key) { - ConcurrentHashMap map = lookup.get(realm); - return map != null ? map.get(key) : null; - } - - public void remove(String realm, String key) { - ConcurrentHashMap map = lookup.get(realm); - if (map != null) { - map.remove(key); - if (map.isEmpty()) { - lookup.remove(realm); - } - } - } - - } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCache.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCache.java deleted file mode 100755 index bb7196e779..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCache.java +++ /dev/null @@ -1,88 +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.cache.infinispan; - -import org.infinispan.Cache; -import org.jboss.logging.Logger; -import org.keycloak.models.cache.infinispan.entities.CachedUser; - -/** - * @author Stian Thorgersen - */ -public class InfinispanUserCache implements UserCache { - - protected static final Logger logger = Logger.getLogger(InfinispanUserCache.class); - - protected volatile boolean enabled = true; - - protected final Cache cache; - - protected final InfinispanCacheUserProviderFactory.RealmLookup usernameLookup; - - protected final InfinispanCacheUserProviderFactory.RealmLookup emailLookup; - - public InfinispanUserCache(Cache cache, InfinispanCacheUserProviderFactory.RealmLookup usernameLookup, InfinispanCacheUserProviderFactory.RealmLookup emailLookup) { - this.cache = cache; - this.usernameLookup = usernameLookup; - this.emailLookup = emailLookup; - } - - @Override - public CachedUser getCachedUser(String realmId, String id) { - if (realmId == null || id == null) return null; - CachedUser user = cache.get(id); - return user != null && realmId.equals(user.getRealm()) ? user : null; - } - - @Override - public void invalidateCachedUserById(String realmId, String id) { - logger.tracev("Invalidating user {0}", id); - cache.remove(id); - } - - @Override - public void addCachedUser(String realmId, CachedUser user) { - logger.tracev("Adding user {0}", user.getId()); - cache.putForExternalRead(user.getId(), user); - } - - @Override - public CachedUser getCachedUserByUsername(String realmId, String name) { - String id = usernameLookup.get(realmId, name); - return id != null ? getCachedUser(realmId, id) : null; - } - - @Override - public CachedUser getCachedUserByEmail(String realmId, String email) { - String id = emailLookup.get(realmId, email); - return id != null ? getCachedUser(realmId, id) : null; - } - - @Override - public void invalidateRealmUsers(String realmId) { - logger.tracev("Invalidating users for realm {0}", realmId); - - cache.clear(); - } - - @Override - public void clear() { - cache.clear(); - } - -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java new file mode 100755 index 0000000000..55e3b38777 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java @@ -0,0 +1,162 @@ +/* + * 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.cache.infinispan; + +import org.infinispan.Cache; +import org.infinispan.notifications.Listener; +import org.jboss.logging.Logger; +import org.keycloak.models.cache.infinispan.entities.CachedClient; +import org.keycloak.models.cache.infinispan.entities.CachedClientTemplate; +import org.keycloak.models.cache.infinispan.entities.CachedGroup; +import org.keycloak.models.cache.infinispan.entities.CachedRealm; +import org.keycloak.models.cache.infinispan.entities.CachedRole; +import org.keycloak.models.cache.infinispan.entities.Revisioned; +import org.keycloak.models.cache.infinispan.stream.ClientQueryPredicate; +import org.keycloak.models.cache.infinispan.stream.ClientTemplateQueryPredicate; +import org.keycloak.models.cache.infinispan.stream.GroupQueryPredicate; +import org.keycloak.models.cache.infinispan.stream.HasRolePredicate; +import org.keycloak.models.cache.infinispan.stream.InClientPredicate; +import org.keycloak.models.cache.infinispan.stream.InRealmPredicate; +import org.keycloak.models.cache.infinispan.stream.RealmQueryPredicate; + +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +/** + * @author Stian Thorgersen + */ +@Listener +public class RealmCacheManager extends CacheManager { + + protected static final Logger logger = Logger.getLogger(RealmCacheManager.class); + + public RealmCacheManager(Cache cache, Cache revisions) { + super(cache, revisions); + } + + + public void realmInvalidation(String id, Set invalidations) { + Predicate> predicate = getRealmInvalidationPredicate(id); + addInvalidations(predicate, invalidations); + } + + public Predicate> getRealmInvalidationPredicate(String id) { + return RealmQueryPredicate.create().realm(id); + } + + public void clientInvalidation(String id, Set invalidations) { + addInvalidations(getClientInvalidationPredicate(id), invalidations); + } + + public Predicate> getClientInvalidationPredicate(String id) { + return ClientQueryPredicate.create().client(id); + } + + public void roleInvalidation(String id, Set invalidations) { + addInvalidations(getRoleInvalidationPredicate(id), invalidations); + + } + + public Predicate> getRoleInvalidationPredicate(String id) { + return HasRolePredicate.create().role(id); + } + + public void groupInvalidation(String id, Set invalidations) { + addInvalidations(getGroupInvalidationPredicate(id), invalidations); + + } + + public Predicate> getGroupInvalidationPredicate(String id) { + return GroupQueryPredicate.create().group(id); + } + + public void clientTemplateInvalidation(String id, Set invalidations) { + addInvalidations(getClientTemplateInvalidationPredicate(id), invalidations); + + } + + public Predicate> getClientTemplateInvalidationPredicate(String id) { + return ClientTemplateQueryPredicate.create().template(id); + } + + public void realmRemoval(String id, Set invalidations) { + Predicate> predicate = getRealmRemovalPredicate(id); + addInvalidations(predicate, invalidations); + } + + public Predicate> getRealmRemovalPredicate(String id) { + Predicate> predicate = null; + predicate = RealmQueryPredicate.create().realm(id) + .or(InRealmPredicate.create().realm(id)); + return predicate; + } + + public void clientAdded(String realmId, String id, Set invalidations) { + addInvalidations(getClientAddedPredicate(realmId), invalidations); + } + + public Predicate> getClientAddedPredicate(String realmId) { + return ClientQueryPredicate.create().inRealm(realmId); + } + + public void clientRemoval(String realmId, String id, Set invalidations) { + Predicate> predicate = null; + predicate = getClientRemovalPredicate(realmId, id); + addInvalidations(predicate, invalidations); + } + + public Predicate> getClientRemovalPredicate(String realmId, String id) { + Predicate> predicate; + predicate = ClientQueryPredicate.create().inRealm(realmId) + .or(ClientQueryPredicate.create().client(id)) + .or(InClientPredicate.create().client(id)); + return predicate; + } + + public void roleRemoval(String id, Set invalidations) { + addInvalidations(getRoleRemovalPredicate(id), invalidations); + + } + + public Predicate> getRoleRemovalPredicate(String id) { + return getRoleInvalidationPredicate(id); + } + + @Override + protected Predicate> getInvalidationPredicate(Object object) { + if (object instanceof CachedRealm) { + CachedRealm cached = (CachedRealm)object; + return getRealmRemovalPredicate(cached.getId()); + } else if (object instanceof CachedClient) { + CachedClient cached = (CachedClient)object; + Predicate> predicate = getClientRemovalPredicate(cached.getRealm(), cached.getId()); + return predicate; + } else if (object instanceof CachedRole) { + CachedRole cached = (CachedRole)object; + return getRoleRemovalPredicate(cached.getId()); + } else if (object instanceof CachedGroup) { + CachedGroup cached = (CachedGroup)object; + return getGroupInvalidationPredicate(cached.getId()); + } else if (object instanceof CachedClientTemplate) { + CachedClientTemplate cached = (CachedClientTemplate)object; + return getClientTemplateInvalidationPredicate(cached.getId()); + } + return null; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/StreamCacheRealmProvider.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java similarity index 98% rename from model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/StreamCacheRealmProvider.java rename to model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java index b33a0e81eb..4a149ba65e 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/StreamCacheRealmProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java @@ -40,7 +40,6 @@ import org.keycloak.models.cache.infinispan.entities.RealmListQuery; import org.keycloak.models.cache.infinispan.entities.RoleListQuery; import org.keycloak.models.utils.KeycloakModelUtils; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; @@ -107,12 +106,12 @@ import java.util.Set; * @author Bill Burke * @version $Revision: 1 $ */ -public class StreamCacheRealmProvider implements CacheRealmProvider { - protected static final Logger logger = Logger.getLogger(StreamCacheRealmProvider.class); +public class RealmCacheSession implements CacheRealmProvider { + protected static final Logger logger = Logger.getLogger(RealmCacheSession.class); public static final String REALM_CLIENTS_QUERY_SUFFIX = ".realm.clients"; public static final String ROLES_QUERY_SUFFIX = ".roles"; public static final String ROLE_BY_NAME_QUERY_SUFFIX = ".role.by-name"; - protected StreamRealmCache cache; + protected RealmCacheManager cache; protected KeycloakSession session; protected RealmProvider delegate; protected boolean transactionActive; @@ -129,10 +128,10 @@ public class StreamCacheRealmProvider implements CacheRealmProvider { protected boolean clearAll; protected final long startupRevision; - public StreamCacheRealmProvider(StreamRealmCache cache, KeycloakSession session) { + public RealmCacheSession(RealmCacheManager cache, KeycloakSession session) { this.cache = cache; this.session = session; - this.startupRevision = UpdateCounter.current(); + this.startupRevision = cache.getCurrentCounter(); session.getTransaction().enlistPrepare(getPrepareTransaction()); session.getTransaction().enlistAfterCompletion(getAfterTransaction()); } @@ -381,6 +380,14 @@ public class StreamCacheRealmProvider implements CacheRealmProvider { return getDelegate().removeRealm(id); } + protected void invalidateClient(RealmModel realm, ClientModel client) { + invalidations.add(client.getId()); + invalidations.add(getRealmClientsQueryCacheKey(realm.getId())); + invalidations.add(getClientByClientIdCacheKey(client.getClientId(), realm)); + listInvalidations.add(realm.getId()); + } + + @Override public ClientModel addClient(RealmModel realm, String clientId) { ClientModel client = getDelegate().addClient(realm, clientId); @@ -396,8 +403,7 @@ public class StreamCacheRealmProvider implements CacheRealmProvider { private ClientModel addedClient(RealmModel realm, ClientModel client) { logger.trace("added Client....."); // need to invalidate realm client query cache every time as it may not be loaded on this node, but loaded on another - invalidations.add(getRealmClientsQueryCacheKey(realm.getId())); - invalidations.add(client.getId()); + invalidateClient(realm, client); cache.clientAdded(realm.getId(), client.getId(), invalidations); // this is needed so that a new client that hasn't been committed isn't cached in a query listInvalidations.add(realm.getId()); @@ -465,10 +471,7 @@ public class StreamCacheRealmProvider implements CacheRealmProvider { ClientModel client = getClientById(id, realm); if (client == null) return false; // need to invalidate realm client query cache every time client list is changed - invalidations.add(getRealmClientsQueryCacheKey(realm.getId())); - invalidations.add(getClientByClientIdCacheKey(client.getClientId(), realm)); - listInvalidations.add(realm.getId()); - registerClientInvalidation(id); + invalidateClient(realm, client); cache.clientRemoval(realm.getId(), id, invalidations); for (RoleModel role : client.getRoles()) { cache.roleInvalidation(role.getId(), invalidations); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/StreamRealmCache.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/StreamRealmCache.java deleted file mode 100755 index 119db6fc32..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/StreamRealmCache.java +++ /dev/null @@ -1,357 +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.cache.infinispan; - -import org.infinispan.Cache; -import org.infinispan.notifications.Listener; -import org.infinispan.notifications.cachelistener.annotation.CacheEntriesEvicted; -import org.infinispan.notifications.cachelistener.annotation.CacheEntryInvalidated; -import org.infinispan.notifications.cachelistener.event.CacheEntriesEvictedEvent; -import org.infinispan.notifications.cachelistener.event.CacheEntryInvalidatedEvent; -import org.jboss.logging.Logger; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.cache.infinispan.entities.AbstractRevisioned; -import org.keycloak.models.cache.infinispan.entities.CachedClient; -import org.keycloak.models.cache.infinispan.entities.CachedClientTemplate; -import org.keycloak.models.cache.infinispan.entities.CachedGroup; -import org.keycloak.models.cache.infinispan.entities.CachedRealm; -import org.keycloak.models.cache.infinispan.entities.CachedRole; -import org.keycloak.models.cache.infinispan.entities.Revisioned; -import org.keycloak.models.cache.infinispan.stream.ClientQueryPredicate; -import org.keycloak.models.cache.infinispan.stream.ClientTemplateQueryPredicate; -import org.keycloak.models.cache.infinispan.stream.GroupQueryPredicate; -import org.keycloak.models.cache.infinispan.stream.HasRolePredicate; -import org.keycloak.models.cache.infinispan.stream.InClientPredicate; -import org.keycloak.models.cache.infinispan.stream.InRealmPredicate; -import org.keycloak.models.cache.infinispan.stream.RealmQueryPredicate; - -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; -import java.util.function.Predicate; - -/** - * @author Stian Thorgersen - */ -@Listener -public class StreamRealmCache { - - protected static final Logger logger = Logger.getLogger(StreamRealmCache.class); - - protected final Cache revisions; - protected final Cache cache; - - public StreamRealmCache(Cache cache, Cache revisions) { - this.cache = cache; - this.cache.addListener(this); - this.revisions = revisions; - } - - public Cache getCache() { - return cache; - } - - public Cache getRevisions() { - return revisions; - } - - public Long getCurrentRevision(String id) { - Long revision = revisions.get(id); - if (revision == null) { - revision = UpdateCounter.current(); - } - // if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event - // so, we do this to force this. - String invalidationKey = "invalidation.key" + id; - cache.putForExternalRead(invalidationKey, new AbstractRevisioned(-1L, invalidationKey)); - return revision; - } - - public void endRevisionBatch() { - try { - revisions.endBatch(true); - } catch (Exception e) { - } - - } - - public T get(String id, Class type) { - Revisioned o = (Revisioned)cache.get(id); - if (o == null) { - return null; - } - Long rev = revisions.get(id); - if (rev == null) { - logger.tracev("get() missing rev"); - return null; - } - long oRev = o.getRevision() == null ? -1L : o.getRevision().longValue(); - if (rev > oRev) { - logger.tracev("get() rev: {0} o.rev: {1}", rev.longValue(), oRev); - return null; - } - return o != null && type.isInstance(o) ? type.cast(o) : null; - } - - public Object invalidateObject(String id) { - Revisioned removed = (Revisioned)cache.remove(id); - // if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event - // so, we do this to force the event. - cache.remove("invalidation.key" + id); - bumpVersion(id); - return removed; - } - - protected void bumpVersion(String id) { - long next = UpdateCounter.next(); - Object rev = revisions.put(id, next); - } - - public void addRevisioned(Revisioned object, long startupRevision) { - //startRevisionBatch(); - String id = object.getId(); - try { - //revisions.getAdvancedCache().lock(id); - Long rev = revisions.get(id); - if (rev == null) { - if (id.endsWith("realm.clients")) logger.trace("addRevisioned rev == null realm.clients"); - rev = UpdateCounter.current(); - revisions.put(id, rev); - } - revisions.startBatch(); - if (!revisions.getAdvancedCache().lock(id)) { - logger.trace("Could not obtain version lock"); - return; - } - rev = revisions.get(id); - if (rev == null) { - if (id.endsWith("realm.clients")) logger.trace("addRevisioned rev2 == null realm.clients"); - return; - } - if (rev > startupRevision) { // revision is ahead transaction start. Other transaction updated in the meantime. Don't cache - if (logger.isTraceEnabled()) { - logger.tracev("Skipped cache. Current revision {0}, Transaction start revision {1}", object.getRevision(), startupRevision); - } - return; - } - if (rev.equals(object.getRevision())) { - if (id.endsWith("realm.clients")) logger.tracev("adding Object.revision {0} rev {1}", object.getRevision(), rev); - cache.putForExternalRead(id, object); - return; - } - if (rev > object.getRevision()) { // revision is ahead, don't cache - if (id.endsWith("realm.clients")) logger.trace("addRevisioned revision is ahead realm.clients"); - return; - } - // revisions cache has a lower value than the object.revision, so update revision and add it to cache - if (id.endsWith("realm.clients")) logger.tracev("adding Object.revision {0} rev {1}", object.getRevision(), rev); - revisions.put(id, object.getRevision()); - cache.putForExternalRead(id, object); - } finally { - endRevisionBatch(); - } - - } - - - - public void clear() { - cache.clear(); - } - - public void realmInvalidation(String id, Set invalidations) { - Predicate> predicate = getRealmInvalidationPredicate(id); - addInvalidations(predicate, invalidations); - } - - public Predicate> getRealmInvalidationPredicate(String id) { - return RealmQueryPredicate.create().realm(id); - } - - public void clientInvalidation(String id, Set invalidations) { - addInvalidations(getClientInvalidationPredicate(id), invalidations); - } - - public Predicate> getClientInvalidationPredicate(String id) { - return ClientQueryPredicate.create().client(id); - } - - public void roleInvalidation(String id, Set invalidations) { - addInvalidations(getRoleInvalidationPredicate(id), invalidations); - - } - - public Predicate> getRoleInvalidationPredicate(String id) { - return HasRolePredicate.create().role(id); - } - - public void groupInvalidation(String id, Set invalidations) { - addInvalidations(getGroupInvalidationPredicate(id), invalidations); - - } - - public Predicate> getGroupInvalidationPredicate(String id) { - return GroupQueryPredicate.create().group(id); - } - - public void clientTemplateInvalidation(String id, Set invalidations) { - addInvalidations(getClientTemplateInvalidationPredicate(id), invalidations); - - } - - public Predicate> getClientTemplateInvalidationPredicate(String id) { - return ClientTemplateQueryPredicate.create().template(id); - } - - public void realmRemoval(String id, Set invalidations) { - Predicate> predicate = getRealmRemovalPredicate(id); - addInvalidations(predicate, invalidations); - } - - public Predicate> getRealmRemovalPredicate(String id) { - Predicate> predicate = null; - predicate = RealmQueryPredicate.create().realm(id) - .or(InRealmPredicate.create().realm(id)); - return predicate; - } - - public void clientAdded(String realmId, String id, Set invalidations) { - addInvalidations(getClientAddedPredicate(realmId), invalidations); - } - - public Predicate> getClientAddedPredicate(String realmId) { - return ClientQueryPredicate.create().inRealm(realmId); - } - - public void clientRemoval(String realmId, String id, Set invalidations) { - Predicate> predicate = null; - predicate = getClientRemovalPredicate(realmId, id); - addInvalidations(predicate, invalidations); - } - - public Predicate> getClientRemovalPredicate(String realmId, String id) { - Predicate> predicate; - predicate = ClientQueryPredicate.create().inRealm(realmId) - .or(ClientQueryPredicate.create().client(id)) - .or(InClientPredicate.create().client(id)); - return predicate; - } - - public void roleRemoval(String id, Set invalidations) { - addInvalidations(getRoleRemovalPredicate(id), invalidations); - - } - - public Predicate> getRoleRemovalPredicate(String id) { - return getRoleInvalidationPredicate(id); - } - - public void addInvalidations(Predicate> predicate, Set invalidations) { - Iterator> it = getEntryIterator(predicate); - while (it.hasNext()) { - invalidations.add(it.next().getKey()); - } - } - - private Iterator> getEntryIterator(Predicate> predicate) { - return cache - .entrySet() - .stream() - .filter(predicate).iterator(); - } - - @CacheEntryInvalidated - public void cacheInvalidated(CacheEntryInvalidatedEvent event) { - if (event.isPre()) { - String key = event.getKey(); - if (key.startsWith("invalidation.key")) { - // if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event - // so, we do this to force this. - String bump = key.substring("invalidation.key".length()); - logger.tracev("bumping invalidation key {0}", bump); - bumpVersion(bump); - return; - } - - } else { - //if (!event.isPre()) { - String key = event.getKey(); - if (key.startsWith("invalidation.key")) { - // if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event - // so, we do this to force this. - String bump = key.substring("invalidation.key".length()); - bumpVersion(bump); - logger.tracev("bumping invalidation key {0}", bump); - return; - } - bumpVersion(key); - Object object = event.getValue(); - if (object != null) { - bumpVersion(key); - Predicate> predicate = getInvalidationPredicate(object); - if (predicate != null) runEvictions(predicate); - logger.tracev("invalidating: {0}" + object.getClass().getName()); - } - } - } - - @CacheEntriesEvicted - public void cacheEvicted(CacheEntriesEvictedEvent event) { - if (!event.isPre()) - for (Map.Entry entry : event.getEntries().entrySet()) { - Object object = entry.getValue(); - bumpVersion(entry.getKey()); - if (object == null) continue; - logger.tracev("evicting: {0}" + object.getClass().getName()); - Predicate> predicate = getInvalidationPredicate(object); - if (predicate != null) runEvictions(predicate); - } - } - - public void runEvictions(Predicate> current) { - Set evictions = new HashSet<>(); - addInvalidations(current, evictions); - logger.tracev("running evictions size: {0}", evictions.size()); - for (String key : evictions) { - cache.evict(key); - bumpVersion(key); - } - } - - protected Predicate> getInvalidationPredicate(Object object) { - if (object instanceof CachedRealm) { - CachedRealm cached = (CachedRealm)object; - return getRealmRemovalPredicate(cached.getId()); - } else if (object instanceof CachedClient) { - CachedClient cached = (CachedClient)object; - Predicate> predicate = getClientRemovalPredicate(cached.getRealm(), cached.getId()); - return predicate; - } else if (object instanceof CachedRole) { - CachedRole cached = (CachedRole)object; - return getRoleRemovalPredicate(cached.getId()); - } else if (object instanceof CachedGroup) { - CachedGroup cached = (CachedGroup)object; - return getGroupInvalidationPredicate(cached.getId()); - } else if (object instanceof CachedClientTemplate) { - CachedClientTemplate cached = (CachedClientTemplate)object; - return getClientTemplateInvalidationPredicate(cached.getId()); - } - return null; - } -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UpdateCounter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UpdateCounter.java index 8097074b5a..966b1f4430 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UpdateCounter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UpdateCounter.java @@ -9,13 +9,13 @@ import java.util.concurrent.atomic.AtomicLong; */ public class UpdateCounter { - private static final AtomicLong counter = new AtomicLong(); + private final AtomicLong counter = new AtomicLong(); - public static long current() { + public long current() { return counter.get(); } - public static long next() { + public long next() { return counter.incrementAndGet(); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java index 6421ca2fb5..6e53626592 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java @@ -31,11 +31,11 @@ import java.util.*; public class UserAdapter implements UserModel { protected UserModel updated; protected CachedUser cached; - protected CacheUserProvider userProviderCache; + protected UserCacheSession userProviderCache; protected KeycloakSession keycloakSession; protected RealmModel realm; - public UserAdapter(CachedUser cached, CacheUserProvider userProvider, KeycloakSession keycloakSession, RealmModel realm) { + public UserAdapter(CachedUser cached, UserCacheSession userProvider, KeycloakSession keycloakSession, RealmModel realm) { this.cached = cached; this.userProviderCache = userProvider; this.keycloakSession = keycloakSession; @@ -44,7 +44,7 @@ public class UserAdapter implements UserModel { protected void getDelegateForUpdate() { if (updated == null) { - userProviderCache.registerUserInvalidation(realm, getId()); + userProviderCache.registerUserInvalidation(realm, cached); updated = userProviderCache.getDelegate().getUserById(getId(), realm); if (updated == null) throw new IllegalStateException("Not found in database"); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCache.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCache.java deleted file mode 100755 index 2df2da63e5..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCache.java +++ /dev/null @@ -1,42 +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.cache.infinispan; - -import org.keycloak.models.cache.infinispan.entities.CachedUser; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public interface UserCache { - - void clear(); - - CachedUser getCachedUser(String realmId, String id); - - void addCachedUser(String realmId, CachedUser user); - - CachedUser getCachedUserByUsername(String realmId, String name); - - CachedUser getCachedUserByEmail(String realmId, String name); - - void invalidateCachedUserById(String realmId, String id); - - void invalidateRealmUsers(String realmId); - -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java new file mode 100755 index 0000000000..e1fb79cbdc --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java @@ -0,0 +1,58 @@ +/* + * 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.cache.infinispan; + +import org.infinispan.Cache; +import org.infinispan.notifications.Listener; +import org.jboss.logging.Logger; +import org.keycloak.models.RealmModel; +import org.keycloak.models.cache.infinispan.entities.CachedUser; +import org.keycloak.models.cache.infinispan.entities.Revisioned; +import org.keycloak.models.cache.infinispan.stream.InRealmPredicate; + +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +/** + * @author Stian Thorgersen + */ +@Listener +public class UserCacheManager extends CacheManager { + + protected static final Logger logger = Logger.getLogger(UserCacheManager.class); + + protected volatile boolean enabled = true; + public UserCacheManager(Cache cache, Cache revisions) { + super(cache, revisions); + } + + @Override + public void clear() { + cache.clear(); + } + + @Override + protected Predicate> getInvalidationPredicate(Object object) { + return null; + } + + public void invalidateRealmUsers(String realm, Set invalidations) { + addInvalidations(InRealmPredicate.create().realm(realm), invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java similarity index 62% rename from model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java rename to model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java index e8f74bc9b8..c611bd37e4 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java @@ -17,9 +17,11 @@ package org.keycloak.models.cache.infinispan; +import org.jboss.logging.Logger; import org.keycloak.models.*; import org.keycloak.models.cache.CacheUserProvider; import org.keycloak.models.cache.infinispan.entities.CachedUser; +import org.keycloak.models.cache.infinispan.entities.UserListQuery; import java.util.*; @@ -27,21 +29,24 @@ import java.util.*; * @author Bill Burke * @version $Revision: 1 $ */ -public class DefaultCacheUserProvider implements CacheUserProvider { - protected UserCache cache; +public class UserCacheSession implements CacheUserProvider { + protected static final Logger logger = Logger.getLogger(UserCacheSession.class); + protected UserCacheManager cache; protected KeycloakSession session; protected UserProvider delegate; protected boolean transactionActive; protected boolean setRollbackOnly; + protected final long startupRevision; - protected Map userInvalidations = new HashMap<>(); + + protected Set invalidations = new HashSet<>(); protected Set realmInvalidations = new HashSet<>(); protected Map managedUsers = new HashMap<>(); - public DefaultCacheUserProvider(UserCache cache, KeycloakSession session) { + public UserCacheSession(UserCacheManager cache, KeycloakSession session) { this.cache = cache; this.session = session; - + this.startupRevision = cache.getCurrentCounter(); session.getTransaction().enlistAfterCompletion(getTransaction()); } @@ -55,20 +60,22 @@ public class DefaultCacheUserProvider implements CacheUserProvider { if (!transactionActive) throw new IllegalStateException("Cannot access delegate without a transaction"); if (delegate != null) return delegate; delegate = session.getProvider(UserProvider.class); + return delegate; } - @Override - public void registerUserInvalidation(RealmModel realm, String id) { - userInvalidations.put(id, realm.getId()); + public void registerUserInvalidation(RealmModel realm,CachedUser user) { + invalidations.add(user.getId()); + if (user.getEmail() != null) invalidations.add(getUserByEmailCacheKey(realm.getId(), user.getEmail())); + invalidations.add(getUserByUsernameCacheKey(realm.getId(), user.getUsername())); } protected void runInvalidations() { - for (Map.Entry invalidation : userInvalidations.entrySet()) { - cache.invalidateCachedUserById(invalidation.getValue(), invalidation.getKey()); - } for (String realmId : realmInvalidations) { - cache.invalidateRealmUsers(realmId); + cache.invalidateRealmUsers(realmId, invalidations); + } + for (String invalidation : invalidations) { + cache.invalidateObject(invalidation); } } @@ -111,81 +118,145 @@ public class DefaultCacheUserProvider implements CacheUserProvider { } private boolean isRegisteredForInvalidation(RealmModel realm, String userId) { - return realmInvalidations.contains(realm.getId()) || userInvalidations.containsKey(userId); + return realmInvalidations.contains(realm.getId()) || invalidations.contains(userId); } @Override public UserModel getUserById(String id, RealmModel realm) { + logger.tracev("getuserById {0}", id); if (isRegisteredForInvalidation(realm, id)) { + logger.trace("registered for invalidation return delegate"); return getDelegate().getUserById(id, realm); } - CachedUser cached = cache.getCachedUser(realm.getId(), id); + CachedUser cached = cache.get(id, CachedUser.class); if (cached == null) { + logger.trace("not cached"); + Long loaded = cache.getCurrentRevision(id); UserModel model = getDelegate().getUserById(id, realm); - if (model == null) return null; - if (managedUsers.containsKey(id)) return managedUsers.get(id); - if (userInvalidations.containsKey(id)) return model; - cached = new CachedUser(realm, model); - cache.addCachedUser(realm.getId(), cached); + if (model == null) { + logger.trace("delegate returning null"); + return null; + } + if (managedUsers.containsKey(id)) { + logger.trace("return managedusers"); + return managedUsers.get(id); + } + if (invalidations.contains(id)) return model; + cached = new CachedUser(loaded, realm, model); + cache.addRevisioned(cached, startupRevision); } else if (managedUsers.containsKey(id)) { + logger.trace("return managedusers"); return managedUsers.get(id); } + logger.trace("returning new cache adapter"); UserAdapter adapter = new UserAdapter(cached, this, session, realm); managedUsers.put(id, adapter); return adapter; } + public String getUserByUsernameCacheKey(String realmId, String username) { + return realmId + ".username." + username; + } + + public String getUserByEmailCacheKey(String realmId, String email) { + return realmId + ".email." + email; + } + @Override public UserModel getUserByUsername(String username, RealmModel realm) { - + logger.tracev("getUserByUsername: {0}", username); username = username.toLowerCase(); - if (realmInvalidations.contains(realm.getId())) { + logger.tracev("realmInvalidations"); return getDelegate().getUserByUsername(username, realm); } - CachedUser cached = cache.getCachedUserByUsername(realm.getId(), username); - if (cached == null) { - UserModel model = getDelegate().getUserByUsername(username, realm); - if (model == null) return null; - if (managedUsers.containsKey(model.getId())) return managedUsers.get(model.getId()); - if (userInvalidations.containsKey(model.getId())) return model; - cached = new CachedUser(realm, model); - cache.addCachedUser(realm.getId(), cached); - } else if (userInvalidations.containsKey(cached.getId())) { - return getDelegate().getUserById(cached.getId(), realm); - } else if (managedUsers.containsKey(cached.getId())) { - return managedUsers.get(cached.getId()); + String cacheKey = getUserByUsernameCacheKey(realm.getId(), username); + if (invalidations.contains(cacheKey)) { + logger.tracev("invalidations"); + return getDelegate().getUserByUsername(username, realm); + } + UserListQuery query = cache.get(cacheKey, UserListQuery.class); + + String userId = null; + if (query == null) { + logger.tracev("query null"); + Long loaded = cache.getCurrentRevision(cacheKey); + UserModel model = getDelegate().getUserByUsername(username, realm); + if (model == null) { + logger.tracev("model from delegate null"); + return null; + } + userId = model.getId(); + query = new UserListQuery(loaded, cacheKey, realm, model.getId()); + cache.addRevisioned(query, startupRevision); + if (invalidations.contains(userId)) return model; + if (managedUsers.containsKey(userId)) { + logger.tracev("return managed user"); + return managedUsers.get(userId); + } + + CachedUser cached = cache.get(userId, CachedUser.class); + if (cached == null) { + cached = new CachedUser(loaded, realm, model); + cache.addRevisioned(cached, startupRevision); + } + logger.trace("return new cache adapter"); + UserAdapter adapter = new UserAdapter(cached, this, session, realm); + managedUsers.put(userId, adapter); + return adapter; + } else { + userId = query.getUsers().iterator().next(); + if (invalidations.contains(userId)) { + logger.tracev("invalidated cache return delegate"); + return getDelegate().getUserByUsername(username, realm); + + } + logger.trace("return getUserById"); + return getUserById(userId, realm); } - UserAdapter adapter = new UserAdapter(cached, this, session, realm); - managedUsers.put(cached.getId(), adapter); - return adapter; } @Override public UserModel getUserByEmail(String email, RealmModel realm) { if (email == null) return null; - email = email.toLowerCase(); - if (realmInvalidations.contains(realm.getId())) { return getDelegate().getUserByEmail(email, realm); } - CachedUser cached = cache.getCachedUserByEmail(realm.getId(), email); - if (cached == null) { + String cacheKey = getUserByEmailCacheKey(realm.getId(), email); + if (invalidations.contains(cacheKey)) { + return getDelegate().getUserByEmail(email, realm); + } + UserListQuery query = cache.get(cacheKey, UserListQuery.class); + + String userId = null; + if (query == null) { + Long loaded = cache.getCurrentRevision(cacheKey); UserModel model = getDelegate().getUserByEmail(email, realm); if (model == null) return null; - if (userInvalidations.containsKey(model.getId())) return model; - cached = new CachedUser(realm, model); - cache.addCachedUser(realm.getId(), cached); - } else if (userInvalidations.containsKey(cached.getId())) { - return getDelegate().getUserByEmail(email, realm); - } else if (managedUsers.containsKey(cached.getId())) { - return managedUsers.get(cached.getId()); + userId = model.getId(); + query = new UserListQuery(loaded, cacheKey, realm, model.getId()); + cache.addRevisioned(query, startupRevision); + if (invalidations.contains(userId)) return model; + if (managedUsers.containsKey(userId)) return managedUsers.get(userId); + + CachedUser cached = cache.get(userId, CachedUser.class); + if (cached == null) { + cached = new CachedUser(loaded, realm, model); + cache.addRevisioned(cached, startupRevision); + } + UserAdapter adapter = new UserAdapter(cached, this, session, realm); + managedUsers.put(userId, adapter); + return adapter; + } else { + userId = query.getUsers().iterator().next(); + if (invalidations.contains(userId)) { + return getDelegate().getUserByEmail(email, realm); + + } + return getUserById(userId, realm); } - UserAdapter adapter = new UserAdapter(cached, this, session, realm); - managedUsers.put(cached.getId(), adapter); - return adapter; } @Override @@ -266,6 +337,8 @@ public class DefaultCacheUserProvider implements CacheUserProvider { @Override public UserModel addUser(RealmModel realm, String id, String username, boolean addDefaultRoles, boolean addDefaultRequiredActions) { UserModel user = getDelegate().addUser(realm, id, username, addDefaultRoles, addDefaultRoles); + // just in case the transaction is rolled back you need to invalidate the user and all cache queries for that user + invalidateUser(realm, user); managedUsers.put(user.getId(), user); return user; } @@ -273,13 +346,23 @@ public class DefaultCacheUserProvider implements CacheUserProvider { @Override public UserModel addUser(RealmModel realm, String username) { UserModel user = getDelegate().addUser(realm, username); + // just in case the transaction is rolled back you need to invalidate the user and all cache queries for that user + invalidateUser(realm, user); managedUsers.put(user.getId(), user); return user; } + protected void invalidateUser(RealmModel realm, UserModel user) { + // just in case the transaction is rolled back you need to invalidate the user and all cache queries for that user + invalidations.add(user.getId()); + if (user.getEmail() != null) invalidations.add(getUserByEmailCacheKey(realm.getId(), user.getEmail())); + invalidations.add(getUserByUsernameCacheKey(realm.getId(), user.getUsername())); + + } + @Override public boolean removeUser(RealmModel realm, UserModel user) { - registerUserInvalidation(realm, user.getId()); + invalidateUser(realm, user); return getDelegate().removeUser(realm, user); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java index c1bf4cd56a..af300001a6 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java @@ -34,8 +34,7 @@ import java.util.Set; * @author Bill Burke * @version $Revision: 1 $ */ -public class CachedUser implements Serializable { - private String id; +public class CachedUser extends AbstractRevisioned implements InRealm { private String realm; private String username; private Long createdTimestamp; @@ -53,8 +52,10 @@ public class CachedUser implements Serializable { private Set roleMappings = new HashSet<>(); private Set groups = new HashSet<>(); - public CachedUser(RealmModel realm, UserModel user) { - this.id = user.getId(); + + + public CachedUser(Long revision, RealmModel realm, UserModel user) { + super(revision, user.getId()); this.realm = realm.getId(); this.username = user.getUsername(); this.createdTimestamp = user.getCreatedTimestamp(); @@ -80,10 +81,6 @@ public class CachedUser implements Serializable { } } - public String getId() { - return id; - } - public String getRealm() { return realm; } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/UserListQuery.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/UserListQuery.java new file mode 100755 index 0000000000..c19e7aada1 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/UserListQuery.java @@ -0,0 +1,49 @@ +package org.keycloak.models.cache.infinispan.entities; + +import org.keycloak.models.RealmModel; + +import java.util.HashSet; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class UserListQuery extends AbstractRevisioned implements UserQuery { + private final Set users; + private final String realm; + private final String realmName; + + public UserListQuery(Long revisioned, String id, RealmModel realm, Set users) { + super(revisioned, id); + this.realm = realm.getId(); + this.realmName = realm.getName(); + this.users = users; + } + + public UserListQuery(Long revisioned, String id, RealmModel realm, String user) { + super(revisioned, id); + this.realm = realm.getId(); + this.realmName = realm.getName(); + this.users = new HashSet<>(); + this.users.add(user); + } + + @Override + public Set getUsers() { + return users; + } + + @Override + public String getRealm() { + return realm; + } + + @Override + public String toString() { + return "UserListQuery{" + + "id='" + getId() + "'" + + "realmName='" + realmName + '\'' + + '}'; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/UserQuery.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/UserQuery.java new file mode 100755 index 0000000000..5f890a2d3d --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/UserQuery.java @@ -0,0 +1,11 @@ +package org.keycloak.models.cache.infinispan.entities; + +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface UserQuery extends InRealm { + Set getUsers(); +} diff --git a/server-spi/src/main/java/org/keycloak/models/cache/CacheUserProvider.java b/server-spi/src/main/java/org/keycloak/models/cache/CacheUserProvider.java index d9c9e566e1..63c97061d6 100755 --- a/server-spi/src/main/java/org/keycloak/models/cache/CacheUserProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/cache/CacheUserProvider.java @@ -27,5 +27,4 @@ import org.keycloak.models.UserProvider; public interface CacheUserProvider extends UserProvider { void clear(); UserProvider getDelegate(); - void registerUserInvalidation(RealmModel realm, String id); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java index e53ebfbe70..7ccd8d70f0 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java @@ -93,7 +93,7 @@ public class OAuthClient { public AuthorizationCodeResponse doLogin(String username, String password) { openLoginForm(); - + String src = driver.getPageSource(); driver.findElement(By.id("username")).sendKeys(username); driver.findElement(By.id("password")).sendKeys(password); driver.findElement(By.name("login")).click(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperTest.java old mode 100644 new mode 100755 index 45afc47d3d..43a9634527 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperTest.java @@ -190,6 +190,7 @@ public class LDAPGroupMapperTest { public void test02_readOnlyGroupMappings() { KeycloakSession session = keycloakRule.startSession(); try { + System.out.println("starting test02_readOnlyGroupMappings"); RealmModel appRealm = session.realms().getRealmByName("test"); UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper"); @@ -221,6 +222,7 @@ public class LDAPGroupMapperTest { Assert.assertTrue(maryGroups.contains(group12)); // Assert that access through DB will have just DB mapped groups + System.out.println("******"); UserModel maryDB = session.userStorage().getUserByUsername("marykeycloak", appRealm); Set maryDBGroups = maryDB.getGroups(); Assert.assertFalse(maryDBGroups.contains(group1));