keycloak-uncached
Changes
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml 3(+3 -0)
misc/CrossDataCenter.md 116(+116 -0)
model/infinispan/pom.xml 4(+4 -0)
model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java 93(+93 -0)
model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java 92(+22 -70)
model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java 105(+64 -41)
model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java 204(+204 -0)
model/infinispan/src/main/java/org/keycloak/cluster/infinispan/KeycloakHotRodMarshallerFactory.java 33(+33 -0)
model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java 54(+44 -10)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java 9(+1 -8)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/ClientTemplateQuery.java 11(+0 -11)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/GroupListQuery.java 4(+1 -3)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/RoleListQuery.java 3(+2 -1)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientAddedEvent.java 55(+55 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientRemovedEvent.java 74(+74 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientTemplateEvent.java 52(+52 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientUpdatedEvent.java 55(+55 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupAddedEvent.java 54(+54 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupMovedEvent.java 64(+64 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupRemovedEvent.java 59(+59 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupUpdatedEvent.java 52(+52 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/InvalidationEvent.java 43(+43 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmCacheInvalidationEvent.java 31(+31 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmRemovedEvent.java 53(+53 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmUpdatedEvent.java 53(+53 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleAddedEvent.java 53(+53 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleRemovedEvent.java 55(+55 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleUpdatedEvent.java 55(+55 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheInvalidationEvent.java 31(+31 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheRealmInvalidationEvent.java 51(+51 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserConsentsUpdatedEvent.java 51(+51 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkRemovedEvent.java 72(+72 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkUpdatedEvent.java 50(+50 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFullInvalidationEvent.java 78(+78 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserUpdatedEvent.java 57(+57 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java 19(+14 -5)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java 22(+17 -5)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java 137(+40 -97)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java 198(+127 -71)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/ClientQueryPredicate.java 48(+0 -48)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/ClientTemplateQueryPredicate.java 40(+0 -40)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/GroupQueryPredicate.java 40(+0 -40)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/RealmQueryPredicate.java 40(+0 -40)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/RoleQueryPredicate.java 40(+0 -40)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java 68(+61 -7)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java 115(+65 -50)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java 13(+10 -3)
model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java 231(+231 -0)
pom.xml 5(+5 -0)
testsuite/integration/pom.xml 4(+4 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java 2(+1 -1)
Details
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml
index a80a008..e7fdb8a 100755
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml
@@ -30,6 +30,9 @@
<module name="org.keycloak.keycloak-server-spi"/>
<module name="org.keycloak.keycloak-server-spi-private"/>
<module name="org.infinispan"/>
+ <module name="org.infinispan.commons"/>
+ <module name="org.infinispan.cachestore.remote"/>
+ <module name="org.infinispan.client.hotrod"/>
<module name="org.jboss.logging"/>
<module name="javax.api"/>
</dependencies>
diff --git a/distribution/server-overlay/src/main/cli/keycloak-install-ha.cli b/distribution/server-overlay/src/main/cli/keycloak-install-ha.cli
index 17fd5f0..a3b85f1 100644
--- a/distribution/server-overlay/src/main/cli/keycloak-install-ha.cli
+++ b/distribution/server-overlay/src/main/cli/keycloak-install-ha.cli
@@ -2,9 +2,9 @@ embed-server --server-config=standalone-ha.xml
/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",jta=false,driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true)
/subsystem=infinispan/cache-container=keycloak:add(jndi-name="infinispan/Keycloak")
/subsystem=infinispan/cache-container=keycloak/transport=TRANSPORT:add(lock-timeout=60000)
-/subsystem=infinispan/cache-container=keycloak/invalidation-cache=realms:add(mode="SYNC")
-/subsystem=infinispan/cache-container=keycloak/invalidation-cache=users:add(mode="SYNC")
-/subsystem=infinispan/cache-container=keycloak/invalidation-cache=users/eviction=EVICTION:add(max-entries=10000,strategy=LRU)
+/subsystem=infinispan/cache-container=keycloak/local-cache=realms:add()
+/subsystem=infinispan/cache-container=keycloak/local-cache=users:add()
+/subsystem=infinispan/cache-container=keycloak/local-cache=users/eviction=EVICTION:add(max-entries=10000,strategy=LRU)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:add(mode="SYNC",owners="1")
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:add(mode="SYNC",owners="1")
/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add(mode="SYNC",owners="1")
misc/CrossDataCenter.md 116(+116 -0)
diff --git a/misc/CrossDataCenter.md b/misc/CrossDataCenter.md
new file mode 100644
index 0000000..4146eaa
--- /dev/null
+++ b/misc/CrossDataCenter.md
@@ -0,0 +1,116 @@
+Test Cross-Data-Center scenario (test with external JDG server)
+===============================================================
+
+These are temporary notes. This docs should be removed once we have cross-DC support finished and properly documented.
+
+What is working right now is:
+- Propagating of invalidation messages for "realms" and "users" caches
+- All the other things provided by ClusterProvider, which is:
+-- ClusterStartupTime (used for offlineSessions and revokeRefreshToken) is shared for all clusters in all datacenters
+-- Periodic userStorage synchronization is always executed just on one node at a time. It won't be never executed concurrently on more nodes (Assuming "nodes" refer to all servers in all clusters in all datacenters)
+
+What doesn't work right now:
+- UserSessionProvider and offline sessions
+
+
+Basic setup
+===========
+
+This is setup with 2 keycloak nodes, which are NOT in cluster. They just share the same database and they will be configured with "work" infinispan cache with remoteStore, which will point
+to external JDG server.
+
+JDG Server setup
+----------------
+- Download JDG 7.0 server and unzip to some folder
+
+- Add this into JDG_HOME/standalone/configuration/standalone.xml under cache-container named "local" :
+
+```
+<local-cache name="work" start="EAGER" batching="false" />
+```
+
+- Start server:
+```
+cd JDG_HOME/bin
+./standalone.sh -Djboss.socket.binding.port-offset=100
+```
+
+Keycloak servers setup
+----------------------
+You need to setup 2 Keycloak nodes in this way.
+
+For now, it's recommended to test Keycloak overlay on EAP7 because of infinispan bug, which is fixed in EAP 7.0 (infinispan 8.1.2), but not
+yet on Wildfly 10 (infinispan 8.1.0). See below for details.
+
+1) Configure shared database in KEYCLOAK_HOME/standalone/configuration/standalone.xml . For example MySQL
+
+2) Add `module` attribute to the infinispan keycloak container:
+
+```
+<cache-container name="keycloak" jndi-name="infinispan/Keycloak" module="org.keycloak.keycloak-model-infinispan">
+```
+
+3) Configure `work` cache to use remoteStore. You should use this:
+
+```
+<local-cache name="work">
+ <remote-store passivation="false" fetch-state="false" purge="false" preload="false" shared="true" cache="work" remote-servers="remote-cache">
+ <property name="rawValues">true</property>
+ <property name="marshaller">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>
+ </remote-store>
+</local-cache>
+```
+
+4) Configure connection to the external JDG server. Because we used port offset 100 for JDG (see above), the HotRod endpoint is running on 11322 .
+So add the config like this to the bottom of standalone.xml under `socket-binding-group` element:
+
+```
+<outbound-socket-binding name="remote-cache">
+ <remote-destination host="localhost" port="11322"/>
+</outbound-socket-binding>
+```
+
+5) Optional: Configure logging in standalone.xml to see what invalidation events were send:
+````
+<logger category="org.keycloak.cluster.infinispan">
+ <level name="TRACE"/>
+</logger>
+<logger category="org.keycloak.models.cache.infinispan">
+ <level name="DEBUG"/>
+</logger>
+````
+
+6) Setup Keycloak node2 . Just copy Keycloak to another location on your laptop and repeat steps 1-5 above for second server too.
+
+7) Run server 1 with parameters like (assuming you have virtual hosts "node1" and "node2" defined in your `/etc/hosts` ):
+```
+./standalone.sh -Djboss.node.name=node1 -b node1 -bmanagement node1
+```
+
+and server2 with:
+```
+./standalone.sh -Djboss.node.name=node2 -b node2 -bmanagement node2
+```
+
+8) Note something like this in both `KEYCLOAK_HOME/standalone/log/server.log` on both nodes. Note that cluster Startup Time will be same time on both nodes:
+```
+2016-11-16 22:12:52,080 DEBUG [org.keycloak.cluster.infinispan.InfinispanClusterProviderFactory] (ServerService Thread Pool -- 62) My address: node1-1953169551
+2016-11-16 22:12:52,081 DEBUG [org.keycloak.cluster.infinispan.CrossDCAwareCacheFactory] (ServerService Thread Pool -- 62) RemoteStore is available. Cross-DC scenario will be used
+2016-11-16 22:12:52,119 DEBUG [org.keycloak.cluster.infinispan.InfinispanClusterProviderFactory] (ServerService Thread Pool -- 62) Loaded cluster startup time: Wed Nov 16 22:09:48 CET 2016
+2016-11-16 22:12:52,128 DEBUG [org.keycloak.cluster.infinispan.InfinispanNotificationsManager] (ServerService Thread Pool -- 62) Added listener for HotRod remoteStore cache: work
+```
+
+9) Login to node1. Then change any realm on node2. You will see in the node2 server.log that RealmUpdatedEvent was sent and on node1 that this event was received.
+
+This is done even if node1 and node2 are NOT in cluster as it's the external JDG used for communication between 2 keycloak servers and sending/receiving cache invalidation events. But note that userSession
+doesn't yet work (eg. if you login to node1, you won't see the userSession on node2).
+
+
+WARNING: Previous steps works on Keycloak server overlay deployed on EAP 7.0 . With deploy on Wildfly 10.0.0.Final, you will see exception
+at startup caused by the bug https://issues.jboss.org/browse/ISPN-6203 .
+
+There is a workaround to add this line into KEYCLOAK_HOME/modules/system/layers/base/org/wildfly/clustering/service/main/module.xml :
+
+```
+<module name="org.infinispan.client.hotrod"/>
+```
model/infinispan/pom.xml 4(+4 -0)
diff --git a/model/infinispan/pom.xml b/model/infinispan/pom.xml
index f10fa60..fba921c 100755
--- a/model/infinispan/pom.xml
+++ b/model/infinispan/pom.xml
@@ -49,6 +49,10 @@
<artifactId>infinispan-core</artifactId>
</dependency>
<dependency>
+ <groupId>org.infinispan</groupId>
+ <artifactId>infinispan-cachestore-remote</artifactId>
+ </dependency>
+ <dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java
new file mode 100644
index 0000000..17795ca
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java
@@ -0,0 +1,93 @@
+/*
+ * 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.cluster.infinispan;
+
+import java.io.Serializable;
+import java.util.Set;
+
+import org.infinispan.Cache;
+import org.infinispan.client.hotrod.Flag;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.commons.api.BasicCache;
+import org.infinispan.persistence.remote.RemoteStore;
+import org.jboss.logging.Logger;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+abstract class CrossDCAwareCacheFactory {
+
+ protected static final Logger logger = Logger.getLogger(CrossDCAwareCacheFactory.class);
+
+
+ abstract BasicCache<String, Serializable> getCache();
+
+
+ static CrossDCAwareCacheFactory getFactory(Cache<String, Serializable> workCache, Set<RemoteStore> remoteStores) {
+ if (remoteStores.isEmpty()) {
+ logger.debugf("No configured remoteStore available. Cross-DC scenario is not used");
+ return new InfinispanCacheWrapperFactory(workCache);
+ } else {
+ logger.debugf("RemoteStore is available. Cross-DC scenario will be used");
+
+ if (remoteStores.size() > 1) {
+ logger.warnf("More remoteStores configured for work cache. Will use just the first one");
+ }
+
+ // For cross-DC scenario, we need to return underlying remoteCache for atomic operations to work properly
+ RemoteStore remoteStore = remoteStores.iterator().next();
+ RemoteCache remoteCache = remoteStore.getRemoteCache();
+ return new RemoteCacheWrapperFactory(remoteCache);
+ }
+ }
+
+
+ // We don't have external JDG configured. No cross-DC.
+ private static class InfinispanCacheWrapperFactory extends CrossDCAwareCacheFactory {
+
+ private final Cache<String, Serializable> workCache;
+
+ InfinispanCacheWrapperFactory(Cache<String, Serializable> workCache) {
+ this.workCache = workCache;
+ }
+
+ @Override
+ BasicCache<String, Serializable> getCache() {
+ return workCache;
+ }
+
+ }
+
+
+ // We have external JDG configured. Cross-DC should be enabled
+ private static class RemoteCacheWrapperFactory extends CrossDCAwareCacheFactory {
+
+ private final RemoteCache<String, Serializable> remoteCache;
+
+ RemoteCacheWrapperFactory(RemoteCache<String, Serializable> remoteCache) {
+ this.remoteCache = remoteCache;
+ }
+
+ @Override
+ BasicCache<String, Serializable> getCache() {
+ // Flags are per-invocation!
+ return remoteCache.withFlags(Flag.FORCE_RETURN_VALUE);
+ }
+
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java
index 8b77c25..5a4bdb7 100644
--- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java
@@ -17,20 +17,15 @@
package org.keycloak.cluster.infinispan;
-import org.infinispan.Cache;
-import org.infinispan.context.Flag;
-import org.infinispan.lifecycle.ComponentStatus;
-import org.infinispan.remoting.transport.Transport;
import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterEvent;
import org.keycloak.cluster.ClusterListener;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.cluster.ExecutionResult;
import org.keycloak.common.util.Time;
-import org.keycloak.models.KeycloakSession;
-import java.io.Serializable;
import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
/**
*
@@ -43,34 +38,22 @@ public class InfinispanClusterProvider implements ClusterProvider {
public static final String CLUSTER_STARTUP_TIME_KEY = "cluster-start-time";
private static final String TASK_KEY_PREFIX = "task::";
- private final InfinispanClusterProviderFactory factory;
- private final KeycloakSession session;
- private final Cache<String, Serializable> cache;
+ private final int clusterStartupTime;
+ private final String myAddress;
+ private final CrossDCAwareCacheFactory crossDCAwareCacheFactory;
+ private final InfinispanNotificationsManager notificationsManager; // Just to extract notifications related stuff to separate class
- public InfinispanClusterProvider(InfinispanClusterProviderFactory factory, KeycloakSession session, Cache<String, Serializable> cache) {
- this.factory = factory;
- this.session = session;
- this.cache = cache;
+ public InfinispanClusterProvider(int clusterStartupTime, String myAddress, CrossDCAwareCacheFactory crossDCAwareCacheFactory, InfinispanNotificationsManager notificationsManager) {
+ this.myAddress = myAddress;
+ this.clusterStartupTime = clusterStartupTime;
+ this.crossDCAwareCacheFactory = crossDCAwareCacheFactory;
+ this.notificationsManager = notificationsManager;
}
@Override
public int getClusterStartupTime() {
- Integer existingClusterStartTime = (Integer) cache.get(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY);
- if (existingClusterStartTime != null) {
- return existingClusterStartTime;
- } else {
- // clusterStartTime not yet initialized. Let's try to put our startupTime
- int serverStartTime = (int) (session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
-
- existingClusterStartTime = (Integer) cache.putIfAbsent(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY, serverStartTime);
- if (existingClusterStartTime == null) {
- logger.debugf("Initialized cluster startup time to %s", Time.toDate(serverStartTime).toString());
- return serverStartTime;
- } else {
- return existingClusterStartTime;
- }
- }
+ return clusterStartupTime;
}
@@ -104,56 +87,33 @@ public class InfinispanClusterProvider implements ClusterProvider {
@Override
public void registerListener(String taskKey, ClusterListener task) {
- factory.registerListener(taskKey, task);
+ this.notificationsManager.registerListener(taskKey, task);
}
@Override
- public void notify(String taskKey, ClusterEvent event) {
- // Put the value to the cache to notify listeners on all the nodes
- cache.put(taskKey, event);
+ public void notify(String taskKey, ClusterEvent event, boolean ignoreSender) {
+ this.notificationsManager.notify(taskKey, event, ignoreSender);
}
- private String getCurrentNode(Cache<String, Serializable> cache) {
- Transport transport = cache.getCacheManager().getTransport();
- return transport==null ? "local" : transport.getAddress().toString();
- }
-
-
- private LockEntry createLockEntry(Cache<String, Serializable> cache) {
+ private LockEntry createLockEntry() {
LockEntry lock = new LockEntry();
- lock.setNode(getCurrentNode(cache));
+ lock.setNode(myAddress);
lock.setTimestamp(Time.currentTime());
return lock;
}
private boolean tryLock(String cacheKey, int taskTimeoutInSeconds) {
- LockEntry myLock = createLockEntry(cache);
+ LockEntry myLock = createLockEntry();
- LockEntry existingLock = (LockEntry) cache.putIfAbsent(cacheKey, myLock);
+ LockEntry existingLock = (LockEntry) crossDCAwareCacheFactory.getCache().putIfAbsent(cacheKey, myLock, taskTimeoutInSeconds, TimeUnit.SECONDS);
if (existingLock != null) {
- // Task likely already in progress. Check if timestamp is not outdated
- int thatTime = existingLock.getTimestamp();
- int currentTime = Time.currentTime();
- if (thatTime + taskTimeoutInSeconds < currentTime) {
- if (logger.isTraceEnabled()) {
- logger.tracef("Task %s outdated when in progress by node %s. Will try to replace task with our node %s", cacheKey, existingLock.getNode(), myLock.getNode());
- }
- boolean replaced = cache.replace(cacheKey, existingLock, myLock);
- if (!replaced) {
- if (logger.isTraceEnabled()) {
- logger.tracef("Failed to replace the task %s. Other thread replaced in the meantime. Ignoring task.", cacheKey);
- }
- }
- return replaced;
- } else {
- if (logger.isTraceEnabled()) {
- logger.tracef("Task %s in progress already by node %s. Ignoring task.", cacheKey, existingLock.getNode());
- }
- return false;
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Task %s in progress already by node %s. Ignoring task.", cacheKey, existingLock.getNode());
}
+ return false;
} else {
if (logger.isTraceEnabled()) {
logger.tracef("Successfully acquired lock for task %s. Our node is %s", cacheKey, myLock.getNode());
@@ -168,20 +128,12 @@ public class InfinispanClusterProvider implements ClusterProvider {
int retry = 3;
while (true) {
try {
- cache.getAdvancedCache()
- .withFlags(Flag.IGNORE_RETURN_VALUES, Flag.FORCE_SYNCHRONOUS)
- .remove(cacheKey);
+ crossDCAwareCacheFactory.getCache().remove(cacheKey);
if (logger.isTraceEnabled()) {
logger.tracef("Task %s removed from the cache", cacheKey);
}
return;
} catch (RuntimeException e) {
- ComponentStatus status = cache.getStatus();
- if (status.isStopping() || status.isTerminated()) {
- logger.warnf("Failed to remove task %s from the cache. Cache is already terminating", cacheKey);
- logger.debug(e.getMessage(), e);
- return;
- }
retry--;
if (retry == 0) {
throw e;
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java
index 75aef45..a96621d 100644
--- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java
@@ -20,27 +20,24 @@ package org.keycloak.cluster.infinispan;
import org.infinispan.Cache;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.notifications.Listener;
-import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
-import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified;
-import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent;
-import org.infinispan.notifications.cachelistener.event.CacheEntryModifiedEvent;
import org.infinispan.notifications.cachemanagerlistener.annotation.ViewChanged;
import org.infinispan.notifications.cachemanagerlistener.event.ViewChangedEvent;
+import org.infinispan.persistence.manager.PersistenceManager;
+import org.infinispan.persistence.remote.RemoteStore;
import org.infinispan.remoting.transport.Address;
import org.infinispan.remoting.transport.Transport;
import org.jboss.logging.Logger;
import org.keycloak.Config;
-import org.keycloak.cluster.ClusterEvent;
-import org.keycloak.cluster.ClusterListener;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.cluster.ClusterProviderFactory;
+import org.keycloak.common.util.HostUtils;
+import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import java.io.Serializable;
import java.util.Collection;
-import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
@@ -49,6 +46,8 @@ import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
+ * This impl is aware of Cross-Data-Center scenario too
+ *
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanClusterProviderFactory implements ClusterProviderFactory {
@@ -57,28 +56,82 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory
protected static final Logger logger = Logger.getLogger(InfinispanClusterProviderFactory.class);
+ // Infinispan cache
private volatile Cache<String, Serializable> workCache;
- private Map<String, ClusterListener> listeners = new HashMap<>();
+ // Ensure that atomic operations (like putIfAbsent) must work correctly in any of: non-clustered, clustered or cross-Data-Center (cross-DC) setups
+ private CrossDCAwareCacheFactory crossDCAwareCacheFactory;
+
+ private String myAddress;
+
+ private int clusterStartupTime;
+
+ // Just to extract notifications related stuff to separate class
+ private InfinispanNotificationsManager notificationsManager;
@Override
public ClusterProvider create(KeycloakSession session) {
lazyInit(session);
- return new InfinispanClusterProvider(this, session, workCache);
+ return new InfinispanClusterProvider(clusterStartupTime, myAddress, crossDCAwareCacheFactory, notificationsManager);
}
private void lazyInit(KeycloakSession session) {
if (workCache == null) {
synchronized (this) {
if (workCache == null) {
- workCache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
+ InfinispanConnectionProvider ispnConnections = session.getProvider(InfinispanConnectionProvider.class);
+ workCache = ispnConnections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
+
workCache.getCacheManager().addListener(new ViewChangeListener());
- workCache.addListener(new CacheEntryListener());
+ initMyAddress();
+
+ Set<RemoteStore> remoteStores = getRemoteStores();
+ crossDCAwareCacheFactory = CrossDCAwareCacheFactory.getFactory(workCache, remoteStores);
+
+ clusterStartupTime = initClusterStartupTime(session);
+
+ notificationsManager = InfinispanNotificationsManager.create(workCache, myAddress, remoteStores);
}
}
}
}
+
+ // See if we have RemoteStore (external JDG) configured for cross-Data-Center scenario
+ private Set<RemoteStore> getRemoteStores() {
+ return workCache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class);
+ }
+
+
+ protected void initMyAddress() {
+ Transport transport = workCache.getCacheManager().getTransport();
+ this.myAddress = transport == null ? HostUtils.getHostName() + "-" + workCache.hashCode() : transport.getAddress().toString();
+ logger.debugf("My address: %s", this.myAddress);
+ }
+
+
+ protected int initClusterStartupTime(KeycloakSession session) {
+ Integer existingClusterStartTime = (Integer) crossDCAwareCacheFactory.getCache().get(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY);
+ if (existingClusterStartTime != null) {
+ logger.debugf("Loaded cluster startup time: %s", Time.toDate(existingClusterStartTime).toString());
+ return existingClusterStartTime;
+ } else {
+ // clusterStartTime not yet initialized. Let's try to put our startupTime
+ int serverStartTime = (int) (session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
+
+ existingClusterStartTime = (Integer) crossDCAwareCacheFactory.getCache().putIfAbsent(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY, serverStartTime);
+ if (existingClusterStartTime == null) {
+ logger.debugf("Initialized cluster startup time to %s", Time.toDate(serverStartTime).toString());
+ return serverStartTime;
+ } else {
+ logger.debugf("Loaded cluster startup time: %s", Time.toDate(existingClusterStartTime).toString());
+ return existingClusterStartTime;
+ }
+ }
+ }
+
+
+
@Override
public void init(Config.Scope config) {
}
@@ -167,34 +220,4 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory
}
- <T> void registerListener(String taskKey, ClusterListener task) {
- listeners.put(taskKey, task);
- }
-
- @Listener
- public class CacheEntryListener {
-
- @CacheEntryCreated
- public void cacheEntryCreated(CacheEntryCreatedEvent<String, Object> event) {
- if (!event.isPre()) {
- trigger(event.getKey(), event.getValue());
- }
- }
-
- @CacheEntryModified
- public void cacheEntryModified(CacheEntryModifiedEvent<String, Object> event) {
- if (!event.isPre()) {
- trigger(event.getKey(), event.getValue());
- }
- }
-
- private void trigger(String key, Object value) {
- ClusterListener task = listeners.get(key);
- if (task != null) {
- ClusterEvent event = (ClusterEvent) value;
- task.run(event);
- }
- }
- }
-
}
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java
new file mode 100644
index 0000000..57cc003
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java
@@ -0,0 +1,204 @@
+/*
+ * 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.cluster.infinispan;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.infinispan.Cache;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified;
+import org.infinispan.client.hotrod.annotation.ClientListener;
+import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
+import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
+import org.infinispan.client.hotrod.event.ClientEvent;
+import org.infinispan.context.Flag;
+import org.infinispan.marshall.core.MarshalledEntry;
+import org.infinispan.notifications.Listener;
+import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
+import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified;
+import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent;
+import org.infinispan.notifications.cachelistener.event.CacheEntryModifiedEvent;
+import org.infinispan.persistence.manager.PersistenceManager;
+import org.infinispan.persistence.remote.RemoteStore;
+import org.infinispan.remoting.transport.Transport;
+import org.jboss.logging.Logger;
+import org.keycloak.cluster.ClusterEvent;
+import org.keycloak.cluster.ClusterListener;
+import org.keycloak.cluster.ClusterProvider;
+import org.keycloak.common.util.HostUtils;
+import org.keycloak.common.util.MultivaluedHashMap;
+
+/**
+ * Impl for sending infinispan messages across cluster and listening to them
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class InfinispanNotificationsManager {
+
+ protected static final Logger logger = Logger.getLogger(InfinispanNotificationsManager.class);
+
+ private final MultivaluedHashMap<String, ClusterListener> listeners = new MultivaluedHashMap<>();
+
+ private final Cache<String, Serializable> workCache;
+
+ private final String myAddress;
+
+
+ protected InfinispanNotificationsManager(Cache<String, Serializable> workCache, String myAddress) {
+ this.workCache = workCache;
+ this.myAddress = myAddress;
+ }
+
+
+ // Create and init manager including all listeners etc
+ public static InfinispanNotificationsManager create(Cache<String, Serializable> workCache, String myAddress, Set<RemoteStore> remoteStores) {
+ InfinispanNotificationsManager manager = new InfinispanNotificationsManager(workCache, myAddress);
+
+ // We need CacheEntryListener just if we don't have remoteStore. With remoteStore will be all cluster nodes notified anyway from HotRod listener
+ if (remoteStores.isEmpty()) {
+ workCache.addListener(manager.new CacheEntryListener());
+
+ logger.debugf("Added listener for infinispan cache: %s", workCache.getName());
+ } else {
+ for (RemoteStore remoteStore : remoteStores) {
+ RemoteCache<Object, Object> remoteCache = remoteStore.getRemoteCache();
+ remoteCache.addClientListener(manager.new HotRodListener(remoteCache));
+
+ logger.debugf("Added listener for HotRod remoteStore cache: %s", remoteCache.getName());
+ }
+ }
+
+ return manager;
+ }
+
+
+ void registerListener(String taskKey, ClusterListener task) {
+ listeners.add(taskKey, task);
+ }
+
+
+ void notify(String taskKey, ClusterEvent event, boolean ignoreSender) {
+ WrapperClusterEvent wrappedEvent = new WrapperClusterEvent();
+ wrappedEvent.setDelegateEvent(event);
+ wrappedEvent.setIgnoreSender(ignoreSender);
+ wrappedEvent.setSender(myAddress);
+
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Sending event %s: %s", taskKey, event);
+ }
+
+ // Put the value to the cache to notify listeners on all the nodes
+ workCache.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES)
+ .put(taskKey, wrappedEvent, 120, TimeUnit.SECONDS);
+ }
+
+
+ @Listener(observation = Listener.Observation.POST)
+ public class CacheEntryListener {
+
+ @CacheEntryCreated
+ public void cacheEntryCreated(CacheEntryCreatedEvent<String, Serializable> event) {
+ eventReceived(event.getKey(), event.getValue());
+ }
+
+ @CacheEntryModified
+ public void cacheEntryModified(CacheEntryModifiedEvent<String, Serializable> event) {
+ eventReceived(event.getKey(), event.getValue());
+ }
+ }
+
+
+ @ClientListener
+ public class HotRodListener {
+
+ private final RemoteCache<Object, Object> remoteCache;
+
+ public HotRodListener(RemoteCache<Object, Object> remoteCache) {
+ this.remoteCache = remoteCache;
+ }
+
+
+ @ClientCacheEntryCreated
+ public void created(ClientCacheEntryCreatedEvent event) {
+ String key = event.getKey().toString();
+ hotrodEventReceived(key);
+ }
+
+
+ @ClientCacheEntryModified
+ public void updated(ClientCacheEntryModifiedEvent event) {
+ String key = event.getKey().toString();
+ hotrodEventReceived(key);
+ }
+
+ private void hotrodEventReceived(String key) {
+ // TODO: Look at CacheEventConverter stuff to possibly include value in the event and avoid additional remoteCache request
+ Object value = remoteCache.get(key);
+
+ Serializable rawValue;
+ if (value instanceof MarshalledEntry) {
+ Object rw = ((MarshalledEntry)value).getValue();
+ rawValue = (Serializable) rw;
+ } else {
+ rawValue = (Serializable) value;
+ }
+
+
+ eventReceived(key, rawValue);
+ }
+
+ }
+
+ private void eventReceived(String key, Serializable obj) {
+ if (!(obj instanceof WrapperClusterEvent)) {
+ return;
+ }
+
+ WrapperClusterEvent event = (WrapperClusterEvent) obj;
+
+ if (event.isIgnoreSender()) {
+ if (this.myAddress.equals(event.getSender())) {
+ return;
+ }
+ }
+
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Received event %s: %s", key, event);
+ }
+
+ ClusterEvent wrappedEvent = event.getDelegateEvent();
+
+ List<ClusterListener> myListeners = listeners.get(key);
+ if (myListeners != null) {
+ for (ClusterListener listener : myListeners) {
+ listener.eventReceived(wrappedEvent);
+ }
+ }
+
+ myListeners = listeners.get(ClusterProvider.ALL);
+ if (myListeners != null) {
+ for (ClusterListener listener : myListeners) {
+ listener.eventReceived(wrappedEvent);
+ }
+ }
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/KeycloakHotRodMarshallerFactory.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/KeycloakHotRodMarshallerFactory.java
new file mode 100644
index 0000000..4a73bf3
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/KeycloakHotRodMarshallerFactory.java
@@ -0,0 +1,33 @@
+/*
+ * 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.cluster.infinispan;
+
+import org.infinispan.commons.marshall.jboss.GenericJBossMarshaller;
+
+/**
+ * Needed on Wildfly, so that remoteStore (hotRod client) can find our classes
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class KeycloakHotRodMarshallerFactory {
+
+ public static GenericJBossMarshaller getInstance() {
+ return new GenericJBossMarshaller(KeycloakHotRodMarshallerFactory.class.getClassLoader());
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java
new file mode 100644
index 0000000..b03dd70
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java
@@ -0,0 +1,59 @@
+/*
+ * 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.cluster.infinispan;
+
+import org.keycloak.cluster.ClusterEvent;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class WrapperClusterEvent implements ClusterEvent {
+
+ private String sender; // will be null in non-clustered environment
+ private boolean ignoreSender;
+ private ClusterEvent delegateEvent;
+
+ public String getSender() {
+ return sender;
+ }
+
+ public void setSender(String sender) {
+ this.sender = sender;
+ }
+
+ public boolean isIgnoreSender() {
+ return ignoreSender;
+ }
+
+ public void setIgnoreSender(boolean ignoreSender) {
+ this.ignoreSender = ignoreSender;
+ }
+
+ public ClusterEvent getDelegateEvent() {
+ return delegateEvent;
+ }
+
+ public void setDelegateEvent(ClusterEvent delegateEvent) {
+ this.delegateEvent = delegateEvent;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("WrapperClusterEvent [ sender=%s, delegateEvent=%s ]", sender, delegateEvent.toString());
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
index 8ad75fd..473aab9 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
@@ -27,11 +27,14 @@ import org.infinispan.eviction.EvictionStrategy;
import org.infinispan.eviction.EvictionType;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
+import org.infinispan.persistence.remote.configuration.ExhaustedAction;
+import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
import org.infinispan.transaction.LockingMode;
import org.infinispan.transaction.TransactionMode;
import org.infinispan.transaction.lookup.DummyTransactionManagerLookup;
import org.jboss.logging.Logger;
import org.keycloak.Config;
+import org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@@ -126,7 +129,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
boolean clustered = config.getBoolean("clustered", false);
- boolean async = config.getBoolean("async", true);
+ boolean async = config.getBoolean("async", false);
boolean allowDuplicateJMXDomains = config.getBoolean("allowDuplicateJMXDomains", true);
if (clustered) {
@@ -139,14 +142,11 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
logger.debug("Started embedded Infinispan cache container");
- ConfigurationBuilder invalidationConfigBuilder = new ConfigurationBuilder();
- if (clustered) {
- invalidationConfigBuilder.clustering().cacheMode(async ? CacheMode.INVALIDATION_ASYNC : CacheMode.INVALIDATION_SYNC);
- }
- Configuration invalidationCacheConfiguration = invalidationConfigBuilder.build();
+ ConfigurationBuilder modelCacheConfigBuilder = new ConfigurationBuilder();
+ Configuration modelCacheConfiguration = modelCacheConfigBuilder.build();
- cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_CACHE_NAME, invalidationCacheConfiguration);
- cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_CACHE_NAME, invalidationCacheConfiguration);
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_CACHE_NAME, modelCacheConfiguration);
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_CACHE_NAME, modelCacheConfiguration);
ConfigurationBuilder sessionConfigBuilder = new ConfigurationBuilder();
if (clustered) {
@@ -174,8 +174,14 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
if (clustered) {
replicationConfigBuilder.clustering().cacheMode(async ? CacheMode.REPL_ASYNC : CacheMode.REPL_SYNC);
}
- Configuration replicationCacheConfiguration = replicationConfigBuilder.build();
- cacheManager.defineConfiguration(InfinispanConnectionProvider.WORK_CACHE_NAME, replicationCacheConfiguration);
+
+ boolean jdgEnabled = config.getBoolean("remoteStoreEnabled");
+ if (jdgEnabled) {
+ configureRemoteCacheStore(replicationConfigBuilder, async);
+ }
+
+ Configuration replicationEvictionCacheConfiguration = replicationConfigBuilder.build();
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.WORK_CACHE_NAME, replicationEvictionCacheConfiguration);
ConfigurationBuilder counterConfigBuilder = new ConfigurationBuilder();
counterConfigBuilder.invocationBatching().enable()
@@ -211,6 +217,34 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
return cb.build();
}
+ // Used for cross-data centers scenario. Usually integration with external JDG server, which itself handles communication between DCs.
+ private void configureRemoteCacheStore(ConfigurationBuilder builder, boolean async) {
+ String jdgServer = config.get("remoteStoreServer", "localhost");
+ Integer jdgPort = config.getInt("remoteStorePort", 11222);
+
+ builder.persistence()
+ .passivation(false)
+ .addStore(RemoteStoreConfigurationBuilder.class)
+ .fetchPersistentState(false)
+ .ignoreModifications(false)
+ .purgeOnStartup(false)
+ .preload(false)
+ .shared(true)
+ .remoteCacheName(InfinispanConnectionProvider.WORK_CACHE_NAME)
+ .rawValues(true)
+ .forceReturnValues(false)
+ .marshaller(KeycloakHotRodMarshallerFactory.class.getName())
+ .addServer()
+ .host(jdgServer)
+ .port(jdgPort)
+// .connectionPool()
+// .maxActive(100)
+// .exhaustedAction(ExhaustedAction.CREATE_NEW)
+ .async()
+ .enabled(async);
+
+ }
+
protected Configuration getKeysCacheConfig() {
ConfigurationBuilder cb = new ConfigurationBuilder();
cb.eviction().strategy(EvictionStrategy.LRU).type(EvictionType.COUNT).size(InfinispanConnectionProvider.KEYS_CACHE_DEFAULT_MAX);
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
index ad1ba26..c254ea7 100755
--- 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
@@ -1,14 +1,13 @@
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.cluster.ClusterProvider;
+import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
+import org.keycloak.models.KeycloakSession;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
+import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
@@ -55,7 +54,7 @@ import java.util.function.Predicate;
* @version $Revision: 1 $
*/
public abstract class CacheManager {
- protected static final Logger logger = Logger.getLogger(CacheManager.class);
+
protected final Cache<String, Long> revisions;
protected final Cache<String, Revisioned> cache;
protected final UpdateCounter counter = new UpdateCounter();
@@ -63,9 +62,10 @@ public abstract class CacheManager {
public CacheManager(Cache<String, Revisioned> cache, Cache<String, Long> revisions) {
this.cache = cache;
this.revisions = revisions;
- this.cache.addListener(this);
}
+ protected abstract Logger getLogger();
+
public Cache<String, Revisioned> getCache() {
return cache;
}
@@ -79,10 +79,7 @@ public abstract class CacheManager {
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;
}
@@ -101,12 +98,16 @@ public abstract class CacheManager {
}
Long rev = revisions.get(id);
if (rev == null) {
- RealmCacheManager.logger.tracev("get() missing rev");
+ if (getLogger().isTraceEnabled()) {
+ getLogger().tracev("get() missing rev {0}", id);
+ }
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);
+ if (getLogger().isTraceEnabled()) {
+ getLogger().tracev("get() rev: {0} o.rev: {1}", rev.longValue(), oRev);
+ }
return null;
}
return o != null && type.isInstance(o) ? type.cast(o) : null;
@@ -114,9 +115,11 @@ public abstract class CacheManager {
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);
+
+ if (getLogger().isTraceEnabled()) {
+ getLogger().tracef("Removed key='%s', value='%s' from cache", id, removed);
+ }
+
bumpVersion(id);
return removed;
}
@@ -137,37 +140,35 @@ public abstract class CacheManager {
//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");
+ if (getLogger().isTraceEnabled()) {
+ getLogger().tracev("Could not obtain version lock: {0}", id);
+ }
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);
+ if (getLogger().isTraceEnabled()) {
+ getLogger().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");
+ if (getLogger().isTraceEnabled()) getLogger().tracev("Skipped cache. Object revision {0}, Cache revision {1}", object.getRevision(), rev);
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());
if (lifespan < 0) cache.putForExternalRead(id, object);
else cache.putForExternalRead(id, object, lifespan, TimeUnit.MILLISECONDS);
@@ -196,63 +197,36 @@ public abstract class CacheManager {
.filter(predicate).iterator();
}
- @CacheEntryInvalidated
- public void cacheInvalidated(CacheEntryInvalidatedEvent<String, Object> 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<Map.Entry<String, Revisioned>> predicate = getInvalidationPredicate(object);
- if (predicate != null) runEvictions(predicate);
- RealmCacheManager.logger.tracev("invalidating: {0}" + object.getClass().getName());
- }
+ public void sendInvalidationEvents(KeycloakSession session, Collection<InvalidationEvent> invalidationEvents) {
+ ClusterProvider clusterProvider = session.getProvider(ClusterProvider.class);
+
+ // Maybe add InvalidationEvent, which will be collection of all invalidationEvents? That will reduce cluster traffic even more.
+ for (InvalidationEvent event : invalidationEvents) {
+ clusterProvider.notify(generateEventId(event), event, true);
}
}
- @CacheEntriesEvicted
- public void cacheEvicted(CacheEntriesEvictedEvent<String, Object> event) {
- if (!event.isPre())
- for (Map.Entry<String, Object> entry : event.getEntries().entrySet()) {
- Object object = entry.getValue();
- bumpVersion(entry.getKey());
- if (object == null) continue;
- RealmCacheManager.logger.tracev("evicting: {0}" + object.getClass().getName());
- Predicate<Map.Entry<String, Revisioned>> predicate = getInvalidationPredicate(object);
- if (predicate != null) runEvictions(predicate);
- }
+ protected String generateEventId(InvalidationEvent event) {
+ return new StringBuilder(event.getId())
+ .append("_")
+ .append(event.hashCode())
+ .toString();
}
- public void runEvictions(Predicate<Map.Entry<String, Revisioned>> current) {
- Set<String> 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 void invalidationEventReceived(InvalidationEvent event) {
+ Set<String> invalidations = new HashSet<>();
+
+ addInvalidationsFromEvent(event, invalidations);
+
+ getLogger().debugf("Invalidating %d cache items after received event %s", invalidations.size(), event);
+
+ for (String invalidation : invalidations) {
+ invalidateObject(invalidation);
}
}
- protected abstract Predicate<Map.Entry<String, Revisioned>> getInvalidationPredicate(Object object);
+ protected abstract void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations);
+
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
index 980957a..11c1c62 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
@@ -52,7 +52,7 @@ public class ClientAdapter implements ClientModel {
private void getDelegateForUpdate() {
if (updated == null) {
- cacheSession.registerClientInvalidation(cached.getId());
+ cacheSession.registerClientInvalidation(cached.getId(), cached.getClientId(), cachedRealm.getId());
updated = cacheSession.getDelegate().getClientById(cached.getId(), cachedRealm);
if (updated == null) throw new IllegalStateException("Not found in database");
}
@@ -577,18 +577,12 @@ public class ClientAdapter implements ClientModel {
@Override
public RoleModel addRole(String name) {
- getDelegateForUpdate();
- RoleModel role = updated.addRole(name);
- cacheSession.registerRoleInvalidation(role.getId());
- return role;
+ return cacheSession.addClientRole(getRealm(), this, name);
}
@Override
public RoleModel addRole(String id, String name) {
- getDelegateForUpdate();
- RoleModel role = updated.addRole(id, name);
- cacheSession.registerRoleInvalidation(role.getId());
- return role;
+ return cacheSession.addClientRole(getRealm(), this, id, name);
}
@Override
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
index 4647f74..5dd4bac 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
@@ -135,7 +135,6 @@ public class CachedRealm extends AbstractExtendableRevisioned {
}
protected List<String> defaultGroups = new LinkedList<String>();
- protected Set<String> groups = new HashSet<String>();
protected List<String> clientTemplates= new LinkedList<>();
protected boolean internationalizationEnabled;
protected Set<String> supportedLocales;
@@ -237,9 +236,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
executionsById.put(execution.getId(), execution);
}
}
- for (GroupModel group : model.getGroups()) {
- groups.add(group.getId());
- }
+
for (AuthenticatorConfigModel authenticator : model.getAuthenticatorConfigs()) {
authenticatorConfigs.put(authenticator.getId(), authenticator);
}
@@ -541,10 +538,6 @@ public class CachedRealm extends AbstractExtendableRevisioned {
return clientAuthenticationFlow;
}
- public Set<String> getGroups() {
- return groups;
- }
-
public List<String> getDefaultGroups() {
return defaultGroups;
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/RoleListQuery.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/RoleListQuery.java
index 21a73ad..e924c05 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/RoleListQuery.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/RoleListQuery.java
@@ -59,7 +59,8 @@ public class RoleListQuery extends AbstractRevisioned implements RoleQuery, InCl
public String toString() {
return "RoleListQuery{" +
"id='" + getId() + "'" +
- "realmName='" + realmName + '\'' +
+ ", realmName='" + realmName + '\'' +
+ ", clientUuid='" + client + '\'' +
'}';
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientAddedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientAddedEvent.java
new file mode 100644
index 0000000..1b022ca
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientAddedEvent.java
@@ -0,0 +1,55 @@
+/*
+ * 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.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientAddedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String clientUuid;
+ private String clientId;
+ private String realmId;
+
+ public static ClientAddedEvent create(String clientUuid, String clientId, String realmId) {
+ ClientAddedEvent event = new ClientAddedEvent();
+ event.clientUuid = clientUuid;
+ event.clientId = clientId;
+ event.realmId = realmId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return clientUuid;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ClientAddedEvent [ realmId=%s, clientUuid=%s, clientId=%s ]", realmId, clientUuid, clientId);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.clientAdded(realmId, clientUuid, clientId, invalidations);
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientRemovedEvent.java
new file mode 100644
index 0000000..2e620db
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientRemovedEvent.java
@@ -0,0 +1,74 @@
+/*
+ * 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.events;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String clientUuid;
+ private String clientId;
+ private String realmId;
+ // roleId -> roleName
+ private Map<String, String> clientRoles;
+
+ public static ClientRemovedEvent create(ClientModel client) {
+ ClientRemovedEvent event = new ClientRemovedEvent();
+
+ event.realmId = client.getRealm().getId();
+ event.clientUuid = client.getId();
+ event.clientId = client.getClientId();
+ event.clientRoles = new HashMap<>();
+ for (RoleModel clientRole : client.getRoles()) {
+ event.clientRoles.put(clientRole.getId(), clientRole.getName());
+ }
+
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return clientUuid;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ClientRemovedEvent [ realmId=%s, clientUuid=%s, clientId=%s, clientRoleIds=%s ]", realmId, clientUuid, clientId, clientRoles);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.clientRemoval(realmId, clientUuid, clientId, invalidations);
+
+ // Separate iteration for all client roles to invalidate records dependent on them
+ for (Map.Entry<String, String> clientRole : clientRoles.entrySet()) {
+ String roleId = clientRole.getKey();
+ String roleName = clientRole.getValue();
+ realmCache.roleRemoval(roleId, roleName, clientUuid, invalidations);
+ }
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientTemplateEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientTemplateEvent.java
new file mode 100644
index 0000000..7bb13a9
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientTemplateEvent.java
@@ -0,0 +1,52 @@
+/*
+ * 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.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientTemplateEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String clientTemplateId;
+
+ public static ClientTemplateEvent create(String clientTemplateId) {
+ ClientTemplateEvent event = new ClientTemplateEvent();
+ event.clientTemplateId = clientTemplateId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return clientTemplateId;
+ }
+
+
+ @Override
+ public String toString() {
+ return "ClientTemplateEvent [ " + clientTemplateId + " ]";
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ // Nothing. ID was already invalidated
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientUpdatedEvent.java
new file mode 100644
index 0000000..cc6c263
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientUpdatedEvent.java
@@ -0,0 +1,55 @@
+/*
+ * 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.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientUpdatedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String clientUuid;
+ private String clientId;
+ private String realmId;
+
+ public static ClientUpdatedEvent create(String clientUuid, String clientId, String realmId) {
+ ClientUpdatedEvent event = new ClientUpdatedEvent();
+ event.clientUuid = clientUuid;
+ event.clientId = clientId;
+ event.realmId = realmId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return clientUuid;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ClientUpdatedEvent [ realmId=%s, clientUuid=%s, clientId=%s ]", realmId, clientUuid, clientId);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.clientUpdated(realmId, clientUuid, clientId, invalidations);
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupAddedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupAddedEvent.java
new file mode 100644
index 0000000..77dcf69
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupAddedEvent.java
@@ -0,0 +1,54 @@
+/*
+ * 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.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class GroupAddedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String groupId;
+ private String realmId;
+
+ public static GroupAddedEvent create(String groupId, String realmId) {
+ GroupAddedEvent event = new GroupAddedEvent();
+ event.realmId = realmId;
+ event.groupId = groupId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return groupId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("GroupAddedEvent [ realmId=%s, groupId=%s ]", realmId, groupId);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.groupQueriesInvalidations(realmId, invalidations);
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupMovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupMovedEvent.java
new file mode 100644
index 0000000..2f5566a
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupMovedEvent.java
@@ -0,0 +1,64 @@
+/*
+ * 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.events;
+
+import java.util.Set;
+
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class GroupMovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String groupId;
+ private String newParentId; // null if moving to top-level
+ private String oldParentId; // null if moving from top-level
+ private String realmId;
+
+ public static GroupMovedEvent create(GroupModel group, GroupModel toParent, String realmId) {
+ GroupMovedEvent event = new GroupMovedEvent();
+ event.realmId = realmId;
+ event.groupId = group.getId();
+ event.oldParentId = group.getParentId();
+ event.newParentId = toParent==null ? null : toParent.getId();
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return groupId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("GroupMovedEvent [ realmId=%s, groupId=%s, newParentId=%s, oldParentId=%s ]", realmId, groupId, newParentId, oldParentId);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.groupQueriesInvalidations(realmId, invalidations);
+ if (newParentId != null) {
+ invalidations.add(newParentId);
+ }
+ if (oldParentId != null) {
+ invalidations.add(oldParentId);
+ }
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupRemovedEvent.java
new file mode 100644
index 0000000..37689fa
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupRemovedEvent.java
@@ -0,0 +1,59 @@
+/*
+ * 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.events;
+
+import java.util.Set;
+
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class GroupRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String groupId;
+ private String parentId;
+ private String realmId;
+
+ public static GroupRemovedEvent create(GroupModel group, String realmId) {
+ GroupRemovedEvent event = new GroupRemovedEvent();
+ event.realmId = realmId;
+ event.groupId = group.getId();
+ event.parentId = group.getParentId();
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return groupId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("GroupRemovedEvent [ realmId=%s, groupId=%s, parentId=%s ]", realmId, groupId, parentId);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.groupQueriesInvalidations(realmId, invalidations);
+ if (parentId != null) {
+ invalidations.add(parentId);
+ }
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupUpdatedEvent.java
new file mode 100644
index 0000000..c59021b
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupUpdatedEvent.java
@@ -0,0 +1,52 @@
+/*
+ * 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.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class GroupUpdatedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String groupId;
+
+ public static GroupUpdatedEvent create(String groupId) {
+ GroupUpdatedEvent event = new GroupUpdatedEvent();
+ event.groupId = groupId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return groupId;
+ }
+
+
+ @Override
+ public String toString() {
+ return "GroupUpdatedEvent [ " + groupId + " ]";
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ // Nothing. ID already invalidated
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/InvalidationEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/InvalidationEvent.java
new file mode 100644
index 0000000..ea59ff5
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/InvalidationEvent.java
@@ -0,0 +1,43 @@
+/*
+ * 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.events;
+
+import org.keycloak.cluster.ClusterEvent;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class InvalidationEvent implements ClusterEvent {
+
+ public abstract String getId();
+
+ @Override
+ public int hashCode() {
+ return getClass().hashCode() * 13 + getId().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) return false;
+ if (!obj.getClass().equals(this.getClass())) return false;
+
+ InvalidationEvent that = (InvalidationEvent) obj;
+ if (!that.getId().equals(getId())) return false;
+ return true;
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmCacheInvalidationEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmCacheInvalidationEvent.java
new file mode 100644
index 0000000..2876e08
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmCacheInvalidationEvent.java
@@ -0,0 +1,31 @@
+/*
+ * 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.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface RealmCacheInvalidationEvent {
+
+ void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations);
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmRemovedEvent.java
new file mode 100644
index 0000000..3558757
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmRemovedEvent.java
@@ -0,0 +1,53 @@
+/*
+ * 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.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RealmRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String realmId;
+ private String realmName;
+
+ public static RealmRemovedEvent create(String realmId, String realmName) {
+ RealmRemovedEvent event = new RealmRemovedEvent();
+ event.realmId = realmId;
+ event.realmName = realmName;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return realmId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("RealmRemovedEvent [ realmId=%s, realmName=%s ]", realmId, realmName);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.realmRemoval(realmId, realmName, invalidations);
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmUpdatedEvent.java
new file mode 100644
index 0000000..624fc6d
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmUpdatedEvent.java
@@ -0,0 +1,53 @@
+/*
+ * 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.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RealmUpdatedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String realmId;
+ private String realmName;
+
+ public static RealmUpdatedEvent create(String realmId, String realmName) {
+ RealmUpdatedEvent event = new RealmUpdatedEvent();
+ event.realmId = realmId;
+ event.realmName = realmName;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return realmId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("RealmUpdatedEvent [ realmId=%s, realmName=%s ]", realmId, realmName);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.realmUpdated(realmId, realmName, invalidations);
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleAddedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleAddedEvent.java
new file mode 100644
index 0000000..cb393e5
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleAddedEvent.java
@@ -0,0 +1,53 @@
+/*
+ * 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.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RoleAddedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String roleId;
+ private String containerId;
+
+ public static RoleAddedEvent create(String roleId, String containerId) {
+ RoleAddedEvent event = new RoleAddedEvent();
+ event.roleId = roleId;
+ event.containerId = containerId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return roleId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("RoleAddedEvent [ roleId=%s, containerId=%s ]", roleId, containerId);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.roleAdded(containerId, invalidations);
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleRemovedEvent.java
new file mode 100644
index 0000000..6137b1b
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleRemovedEvent.java
@@ -0,0 +1,55 @@
+/*
+ * 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.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RoleRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String roleId;
+ private String roleName;
+ private String containerId;
+
+ public static RoleRemovedEvent create(String roleId, String roleName, String containerId) {
+ RoleRemovedEvent event = new RoleRemovedEvent();
+ event.roleId = roleId;
+ event.roleName = roleName;
+ event.containerId = containerId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return roleId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("RoleRemovedEvent [ roleId=%s, containerId=%s ]", roleId, containerId);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.roleRemoval(roleId, roleName, containerId, invalidations);
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleUpdatedEvent.java
new file mode 100644
index 0000000..4b2ae5b
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleUpdatedEvent.java
@@ -0,0 +1,55 @@
+/*
+ * 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.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RoleUpdatedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String roleId;
+ private String roleName;
+ private String containerId;
+
+ public static RoleUpdatedEvent create(String roleId, String roleName, String containerId) {
+ RoleUpdatedEvent event = new RoleUpdatedEvent();
+ event.roleId = roleId;
+ event.roleName = roleName;
+ event.containerId = containerId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return roleId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("RoleUpdatedEvent [ roleId=%s, roleName=%s, containerId=%s ]", roleId, roleName, containerId);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.roleUpdated(containerId, roleName, invalidations);
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheInvalidationEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheInvalidationEvent.java
new file mode 100644
index 0000000..964e97a
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheInvalidationEvent.java
@@ -0,0 +1,31 @@
+/*
+ * 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.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.UserCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface UserCacheInvalidationEvent {
+
+ void addInvalidations(UserCacheManager userCache, Set<String> invalidations);
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheRealmInvalidationEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheRealmInvalidationEvent.java
new file mode 100644
index 0000000..3996181
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheRealmInvalidationEvent.java
@@ -0,0 +1,51 @@
+/*
+ * 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.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.UserCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserCacheRealmInvalidationEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
+
+ private String realmId;
+
+ public static UserCacheRealmInvalidationEvent create(String realmId) {
+ UserCacheRealmInvalidationEvent event = new UserCacheRealmInvalidationEvent();
+ event.realmId = realmId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return realmId; // Just a placeholder
+ }
+
+ @Override
+ public String toString() {
+ return String.format("UserCacheRealmInvalidationEvent [ realmId=%s ]", realmId);
+ }
+
+ @Override
+ public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
+ userCache.invalidateRealmUsers(realmId, invalidations);
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserConsentsUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserConsentsUpdatedEvent.java
new file mode 100644
index 0000000..021e841
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserConsentsUpdatedEvent.java
@@ -0,0 +1,51 @@
+/*
+ * 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.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.UserCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserConsentsUpdatedEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
+
+ private String userId;
+
+ public static UserConsentsUpdatedEvent create(String userId) {
+ UserConsentsUpdatedEvent event = new UserConsentsUpdatedEvent();
+ event.userId = userId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return userId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("UserConsentsUpdatedEvent [ userId=%s ]", userId);
+ }
+
+ @Override
+ public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
+ userCache.consentInvalidation(userId, invalidations);
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkRemovedEvent.java
new file mode 100644
index 0000000..15704df
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkRemovedEvent.java
@@ -0,0 +1,72 @@
+/*
+ * 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.events;
+
+import java.util.Set;
+
+import org.keycloak.models.FederatedIdentityModel;
+import org.keycloak.models.cache.infinispan.UserCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserFederationLinkRemovedEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
+
+ private String userId;
+ private String realmId;
+ private String identityProviderId;
+ private String socialUserId;
+
+ public static UserFederationLinkRemovedEvent create(String userId, String realmId, FederatedIdentityModel socialLink) {
+ UserFederationLinkRemovedEvent event = new UserFederationLinkRemovedEvent();
+ event.userId = userId;
+ event.realmId = realmId;
+ if (socialLink != null) {
+ event.identityProviderId = socialLink.getIdentityProvider();
+ event.socialUserId = socialLink.getUserId();
+ }
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return userId;
+ }
+
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public String getIdentityProviderId() {
+ return identityProviderId;
+ }
+
+ public String getSocialUserId() {
+ return socialUserId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("UserFederationLinkRemovedEvent [ userId=%s, identityProviderId=%s, socialUserId=%s ]", userId, identityProviderId, socialUserId);
+ }
+
+ @Override
+ public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
+ userCache.federatedIdentityLinkRemovedInvalidation(userId, realmId, identityProviderId, socialUserId, invalidations);
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkUpdatedEvent.java
new file mode 100644
index 0000000..8bbfb41
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkUpdatedEvent.java
@@ -0,0 +1,50 @@
+/*
+ * 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.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.UserCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserFederationLinkUpdatedEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
+
+ private String userId;
+
+ public static UserFederationLinkUpdatedEvent create(String userId) {
+ UserFederationLinkUpdatedEvent event = new UserFederationLinkUpdatedEvent();
+ event.userId = userId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return userId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("UserFederationLinkUpdatedEvent [ userId=%s ]", userId);
+ }
+
+ @Override
+ public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
+ userCache.federatedIdentityLinkUpdatedInvalidation(userId, invalidations);
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFullInvalidationEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFullInvalidationEvent.java
new file mode 100644
index 0000000..d637ac2
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFullInvalidationEvent.java
@@ -0,0 +1,78 @@
+/*
+ * 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.events;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.keycloak.models.FederatedIdentityModel;
+import org.keycloak.models.cache.infinispan.UserCacheManager;
+
+/**
+ * Used when user added/removed
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserFullInvalidationEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
+
+ private String userId;
+ private String username;
+ private String email;
+ private String realmId;
+ private boolean identityFederationEnabled;
+ private Map<String, String> federatedIdentities;
+
+ public static UserFullInvalidationEvent create(String userId, String username, String email, String realmId, boolean identityFederationEnabled, Collection<FederatedIdentityModel> federatedIdentities) {
+ UserFullInvalidationEvent event = new UserFullInvalidationEvent();
+ event.userId = userId;
+ event.username = username;
+ event.email = email;
+ event.realmId = realmId;
+
+ event.identityFederationEnabled = identityFederationEnabled;
+ if (identityFederationEnabled) {
+ event.federatedIdentities = new HashMap<>();
+ for (FederatedIdentityModel socialLink : federatedIdentities) {
+ event.federatedIdentities.put(socialLink.getIdentityProvider(), socialLink.getUserId());
+ }
+ }
+
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return userId;
+ }
+
+ public Map<String, String> getFederatedIdentities() {
+ return federatedIdentities;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("UserFullInvalidationEvent [ userId=%s, username=%s, email=%s ]", userId, username, email);
+ }
+
+ @Override
+ public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
+ userCache.fullUserInvalidation(userId, username, email, realmId, identityFederationEnabled, federatedIdentities, invalidations);
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserUpdatedEvent.java
new file mode 100644
index 0000000..429b4af
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserUpdatedEvent.java
@@ -0,0 +1,57 @@
+/*
+ * 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.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.UserCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserUpdatedEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
+
+ private String userId;
+ private String username;
+ private String email;
+ private String realmId;
+
+ public static UserUpdatedEvent create(String userId, String username, String email, String realmId) {
+ UserUpdatedEvent event = new UserUpdatedEvent();
+ event.userId = userId;
+ event.username = username;
+ event.email = email;
+ event.realmId = realmId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return userId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("UserUpdatedEvent [ userId=%s, username=%s, email=%s ]", userId, username, email);
+ }
+
+ @Override
+ public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
+ userCache.userUpdatedInvalidations(userId, username, email, realmId, invalidations);
+ }
+}
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 4bbe4c7..c2ad8ce 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
@@ -21,7 +21,6 @@ import org.infinispan.Cache;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.cluster.ClusterEvent;
-import org.keycloak.cluster.ClusterListener;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
@@ -29,6 +28,7 @@ import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.CacheRealmProviderFactory;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
+import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -54,14 +54,23 @@ public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFa
Cache<String, Revisioned> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
Cache<String, Long> revisions = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME);
realmCache = new RealmCacheManager(cache, revisions);
+
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
- cluster.registerListener(REALM_CLEAR_CACHE_EVENTS, new ClusterListener() {
- @Override
- public void run(ClusterEvent event) {
- realmCache.clear();
+ cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> {
+
+ if (event instanceof InvalidationEvent) {
+ InvalidationEvent invalidationEvent = (InvalidationEvent) event;
+ realmCache.invalidationEventReceived(invalidationEvent);
}
});
+ cluster.registerListener(REALM_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> {
+
+ realmCache.clear();
+
+ });
+
+ log.debug("Registered cluster listeners");
}
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java
index 14d420b..e8c2ba1 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java
@@ -21,7 +21,6 @@ import org.infinispan.Cache;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.cluster.ClusterEvent;
-import org.keycloak.cluster.ClusterListener;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
@@ -29,6 +28,7 @@ import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.cache.UserCache;
import org.keycloak.models.cache.UserCacheProviderFactory;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
+import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -55,13 +55,25 @@ public class InfinispanUserCacheProviderFactory implements UserCacheProviderFact
Cache<String, Revisioned> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_CACHE_NAME);
Cache<String, Long> revisions = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME);
userCache = new UserCacheManager(cache, revisions);
+
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
- cluster.registerListener(USER_CLEAR_CACHE_EVENTS, new ClusterListener() {
- @Override
- public void run(ClusterEvent event) {
- userCache.clear();
+
+ cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> {
+
+ if (event instanceof InvalidationEvent) {
+ InvalidationEvent invalidationEvent = (InvalidationEvent) event;
+ userCache.invalidationEventReceived(invalidationEvent);
}
+
});
+
+ cluster.registerListener(USER_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> {
+
+ userCache.clear();
+
+ });
+
+ log.debug("Registered cluster listeners");
}
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index 069b34a..1748e3c 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -39,13 +39,8 @@ import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.cache.CachedRealmModel;
import org.keycloak.models.cache.infinispan.entities.CachedRealm;
-import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.storage.UserStorageProvider;
-import java.security.Key;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@@ -75,7 +70,7 @@ public class RealmAdapter implements CachedRealmModel {
@Override
public RealmModel getDelegateForUpdate() {
if (updated == null) {
- cacheSession.registerRealmInvalidation(cached.getId());
+ cacheSession.registerRealmInvalidation(cached.getId(), cached.getName());
updated = cacheSession.getDelegate().getRealm(cached.getId());
if (updated == null) throw new IllegalStateException("Not found in database");
}
@@ -732,13 +727,6 @@ public class RealmAdapter implements CachedRealmModel {
}
@Override
- public boolean removeRoleById(String id) {
- cacheSession.registerRoleInvalidation(id);
- getDelegateForUpdate();
- return updated.removeRoleById(id);
- }
-
- @Override
public boolean isEventsEnabled() {
if (isUpdated()) return updated.isEventsEnabled();
return cached.isEventsEnabled();
@@ -837,18 +825,12 @@ public class RealmAdapter implements CachedRealmModel {
@Override
public RoleModel addRole(String name) {
- getDelegateForUpdate();
- RoleModel role = updated.addRole(name);
- cacheSession.registerRoleInvalidation(role.getId());
- return role;
+ return cacheSession.addRealmRole(this, name);
}
@Override
public RoleModel addRole(String id, String name) {
- getDelegateForUpdate();
- RoleModel role = updated.addRole(id, name);
- cacheSession.registerRoleInvalidation(role.getId());
- return role;
+ return cacheSession.addRealmRole(this, id, name);
}
@Override
@@ -1258,12 +1240,6 @@ public class RealmAdapter implements CachedRealmModel {
}
@Override
- public void addTopLevelGroup(GroupModel subGroup) {
- cacheSession.addTopLevelGroup(this, subGroup);
-
- }
-
- @Override
public void moveGroup(GroupModel group, GroupModel toParent) {
cacheSession.moveGroup(this, group, toParent);
}
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
index 55e3b38..b01dbab 100755
--- 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
@@ -18,145 +18,88 @@
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.events.InvalidationEvent;
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.events.RealmCacheInvalidationEvent;
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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-@Listener
public class RealmCacheManager extends CacheManager {
- protected static final Logger logger = Logger.getLogger(RealmCacheManager.class);
-
- public RealmCacheManager(Cache<String, Revisioned> cache, Cache<String, Long> revisions) {
- super(cache, revisions);
- }
-
-
- public void realmInvalidation(String id, Set<String> invalidations) {
- Predicate<Map.Entry<String, Revisioned>> predicate = getRealmInvalidationPredicate(id);
- addInvalidations(predicate, invalidations);
- }
-
- public Predicate<Map.Entry<String, Revisioned>> getRealmInvalidationPredicate(String id) {
- return RealmQueryPredicate.create().realm(id);
- }
-
- public void clientInvalidation(String id, Set<String> invalidations) {
- addInvalidations(getClientInvalidationPredicate(id), invalidations);
- }
-
- public Predicate<Map.Entry<String, Revisioned>> getClientInvalidationPredicate(String id) {
- return ClientQueryPredicate.create().client(id);
- }
-
- public void roleInvalidation(String id, Set<String> invalidations) {
- addInvalidations(getRoleInvalidationPredicate(id), invalidations);
+ private static final Logger logger = Logger.getLogger(RealmCacheManager.class);
+ @Override
+ protected Logger getLogger() {
+ return logger;
}
- public Predicate<Map.Entry<String, Revisioned>> getRoleInvalidationPredicate(String id) {
- return HasRolePredicate.create().role(id);
+ public RealmCacheManager(Cache<String, Revisioned> cache, Cache<String, Long> revisions) {
+ super(cache, revisions);
}
- public void groupInvalidation(String id, Set<String> invalidations) {
- addInvalidations(getGroupInvalidationPredicate(id), invalidations);
+ public void realmUpdated(String id, String name, Set<String> invalidations) {
+ invalidations.add(id);
+ invalidations.add(RealmCacheSession.getRealmByNameCacheKey(name));
}
- public Predicate<Map.Entry<String, Revisioned>> getGroupInvalidationPredicate(String id) {
- return GroupQueryPredicate.create().group(id);
- }
-
- public void clientTemplateInvalidation(String id, Set<String> invalidations) {
- addInvalidations(getClientTemplateInvalidationPredicate(id), invalidations);
+ public void realmRemoval(String id, String name, Set<String> invalidations) {
+ realmUpdated(id, name, invalidations);
+ addInvalidations(InRealmPredicate.create().realm(id), invalidations);
}
- public Predicate<Map.Entry<String, Revisioned>> getClientTemplateInvalidationPredicate(String id) {
- return ClientTemplateQueryPredicate.create().template(id);
+ public void roleAdded(String roleContainerId, Set<String> invalidations) {
+ invalidations.add(RealmCacheSession.getRolesCacheKey(roleContainerId));
}
- public void realmRemoval(String id, Set<String> invalidations) {
- Predicate<Map.Entry<String, Revisioned>> predicate = getRealmRemovalPredicate(id);
- addInvalidations(predicate, invalidations);
+ public void roleUpdated(String roleContainerId, String roleName, Set<String> invalidations) {
+ invalidations.add(RealmCacheSession.getRoleByNameCacheKey(roleContainerId, roleName));
}
- public Predicate<Map.Entry<String, Revisioned>> getRealmRemovalPredicate(String id) {
- Predicate<Map.Entry<String, Revisioned>> predicate = null;
- predicate = RealmQueryPredicate.create().realm(id)
- .or(InRealmPredicate.create().realm(id));
- return predicate;
- }
+ public void roleRemoval(String id, String roleName, String roleContainerId, Set<String> invalidations) {
+ invalidations.add(RealmCacheSession.getRolesCacheKey(roleContainerId));
+ invalidations.add(RealmCacheSession.getRoleByNameCacheKey(roleContainerId, roleName));
- public void clientAdded(String realmId, String id, Set<String> invalidations) {
- addInvalidations(getClientAddedPredicate(realmId), invalidations);
+ addInvalidations(HasRolePredicate.create().role(id), invalidations);
}
- public Predicate<Map.Entry<String, Revisioned>> getClientAddedPredicate(String realmId) {
- return ClientQueryPredicate.create().inRealm(realmId);
+ public void groupQueriesInvalidations(String realmId, Set<String> invalidations) {
+ invalidations.add(RealmCacheSession.getGroupsQueryCacheKey(realmId));
+ invalidations.add(RealmCacheSession.getTopGroupsQueryCacheKey(realmId)); // Just easier to always invalidate top-level too. It's not big performance penalty
}
- public void clientRemoval(String realmId, String id, Set<String> invalidations) {
- Predicate<Map.Entry<String, Revisioned>> predicate = null;
- predicate = getClientRemovalPredicate(realmId, id);
- addInvalidations(predicate, invalidations);
+ public void clientAdded(String realmId, String clientUUID, String clientId, Set<String> invalidations) {
+ invalidations.add(RealmCacheSession.getRealmClientsQueryCacheKey(realmId));
}
- public Predicate<Map.Entry<String, Revisioned>> getClientRemovalPredicate(String realmId, String id) {
- Predicate<Map.Entry<String, Revisioned>> predicate;
- predicate = ClientQueryPredicate.create().inRealm(realmId)
- .or(ClientQueryPredicate.create().client(id))
- .or(InClientPredicate.create().client(id));
- return predicate;
+ public void clientUpdated(String realmId, String clientUuid, String clientId, Set<String> invalidations) {
+ invalidations.add(RealmCacheSession.getClientByClientIdCacheKey(clientId, realmId));
}
- public void roleRemoval(String id, Set<String> invalidations) {
- addInvalidations(getRoleRemovalPredicate(id), invalidations);
+ // Client roles invalidated separately
+ public void clientRemoval(String realmId, String clientUUID, String clientId, Set<String> invalidations) {
+ invalidations.add(RealmCacheSession.getRealmClientsQueryCacheKey(realmId));
+ invalidations.add(RealmCacheSession.getClientByClientIdCacheKey(clientId, realmId));
+ addInvalidations(InClientPredicate.create().client(clientUUID), invalidations);
}
- public Predicate<Map.Entry<String, Revisioned>> getRoleRemovalPredicate(String id) {
- return getRoleInvalidationPredicate(id);
- }
@Override
- protected Predicate<Map.Entry<String, Revisioned>> 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<Map.Entry<String, Revisioned>> 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());
+ protected void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations) {
+ if (event instanceof RealmCacheInvalidationEvent) {
+ invalidations.add(event.getId());
+
+ ((RealmCacheInvalidationEvent) event).addInvalidations(this, invalidations);
}
- return null;
}
+
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
index 9321f47..d61a611 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
@@ -19,6 +19,7 @@ package org.keycloak.models.cache.infinispan;
import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterProvider;
+import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
import org.keycloak.migration.MigrationModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientTemplateModel;
@@ -38,8 +39,22 @@ import org.keycloak.models.cache.infinispan.entities.CachedRealm;
import org.keycloak.models.cache.infinispan.entities.CachedRealmRole;
import org.keycloak.models.cache.infinispan.entities.CachedRole;
import org.keycloak.models.cache.infinispan.entities.ClientListQuery;
+import org.keycloak.models.cache.infinispan.entities.GroupListQuery;
import org.keycloak.models.cache.infinispan.entities.RealmListQuery;
import org.keycloak.models.cache.infinispan.entities.RoleListQuery;
+import org.keycloak.models.cache.infinispan.events.ClientAddedEvent;
+import org.keycloak.models.cache.infinispan.events.ClientRemovedEvent;
+import org.keycloak.models.cache.infinispan.events.ClientTemplateEvent;
+import org.keycloak.models.cache.infinispan.events.ClientUpdatedEvent;
+import org.keycloak.models.cache.infinispan.events.GroupAddedEvent;
+import org.keycloak.models.cache.infinispan.events.GroupMovedEvent;
+import org.keycloak.models.cache.infinispan.events.GroupRemovedEvent;
+import org.keycloak.models.cache.infinispan.events.GroupUpdatedEvent;
+import org.keycloak.models.cache.infinispan.events.RealmRemovedEvent;
+import org.keycloak.models.cache.infinispan.events.RealmUpdatedEvent;
+import org.keycloak.models.cache.infinispan.events.RoleAddedEvent;
+import org.keycloak.models.cache.infinispan.events.RoleRemovedEvent;
+import org.keycloak.models.cache.infinispan.events.RoleUpdatedEvent;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.util.HashMap;
@@ -126,6 +141,7 @@ public class RealmCacheSession implements CacheRealmProvider {
protected Map<String, GroupAdapter> managedGroups = new HashMap<>();
protected Set<String> listInvalidations = new HashSet<>();
protected Set<String> invalidations = new HashSet<>();
+ protected Set<InvalidationEvent> invalidationEvents = new HashSet<>(); // Events to be sent across cluster
protected boolean clearAll;
protected final long startupRevision;
@@ -150,7 +166,7 @@ public class RealmCacheSession implements CacheRealmProvider {
public void clear() {
cache.clear();
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
- cluster.notify(InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS, new ClearCacheEvent());
+ cluster.notify(InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true);
}
@Override
@@ -167,21 +183,19 @@ public class RealmCacheSession implements CacheRealmProvider {
}
@Override
- public void registerRealmInvalidation(String id) {
- invalidateRealm(id);
- cache.realmInvalidation(id, invalidations);
- }
-
- private void invalidateRealm(String id) {
- invalidations.add(id);
+ public void registerRealmInvalidation(String id, String name) {
+ cache.realmUpdated(id, name, invalidations);
RealmAdapter adapter = managedRealms.get(id);
if (adapter != null) adapter.invalidateFlag();
+
+ invalidationEvents.add(RealmUpdatedEvent.create(id, name));
}
@Override
- public void registerClientInvalidation(String id) {
+ public void registerClientInvalidation(String id, String clientId, String realmId) {
invalidateClient(id);
- cache.clientInvalidation(id, invalidations);
+ invalidationEvents.add(ClientUpdatedEvent.create(id, clientId, realmId));
+ cache.clientUpdated(realmId, id, clientId, invalidations);
}
private void invalidateClient(String id) {
@@ -193,7 +207,9 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public void registerClientTemplateInvalidation(String id) {
invalidateClientTemplate(id);
- cache.clientTemplateInvalidation(id, invalidations);
+ // Note: Adding/Removing client template is supposed to invalidate CachedRealm as well, so the list of clientTemplates is invalidated.
+ // But separate RealmUpdatedEvent will be sent for it. So ClientTemplateEvent don't need to take care of it.
+ invalidationEvents.add(ClientTemplateEvent.create(id));
}
private void invalidateClientTemplate(String id) {
@@ -203,14 +219,15 @@ public class RealmCacheSession implements CacheRealmProvider {
}
@Override
- public void registerRoleInvalidation(String id) {
+ public void registerRoleInvalidation(String id, String roleName, String roleContainerId) {
invalidateRole(id);
- roleInvalidations(id);
+ cache.roleUpdated(roleContainerId, roleName, invalidations);
+ invalidationEvents.add(RoleUpdatedEvent.create(id, roleName, roleContainerId));
}
- private void roleInvalidations(String roleId) {
+ private void roleRemovalInvalidations(String roleId, String roleName, String roleContainerId) {
Set<String> newInvalidations = new HashSet<>();
- cache.roleInvalidation(roleId, newInvalidations);
+ cache.roleRemoval(roleId, roleName, roleContainerId, newInvalidations);
invalidations.addAll(newInvalidations);
// need to make sure that scope and group mapping clients and groups are invalidated
for (String id : newInvalidations) {
@@ -229,6 +246,11 @@ public class RealmCacheSession implements CacheRealmProvider {
clientTemplate.invalidate();
continue;
}
+ RoleAdapter role = managedRoles.get(id);
+ if (role != null) {
+ role.invalidate();
+ continue;
+ }
}
@@ -243,10 +265,26 @@ public class RealmCacheSession implements CacheRealmProvider {
if (adapter != null) adapter.invalidate();
}
+ private void addedRole(String roleId, String roleContainerId) {
+ // this is needed so that a new role that hasn't been committed isn't cached in a query
+ listInvalidations.add(roleContainerId);
+
+ invalidateRole(roleId);
+ cache.roleAdded(roleContainerId, invalidations);
+ invalidationEvents.add(RoleAddedEvent.create(roleId, roleContainerId));
+ }
+
@Override
public void registerGroupInvalidation(String id) {
+ invalidateGroup(id, null, false);
+ addGroupEventIfAbsent(GroupUpdatedEvent.create(id));
+ }
+
+ private void invalidateGroup(String id, String realmId, boolean invalidateQueries) {
invalidateGroup(id);
- cache.groupInvalidation(id, invalidations);
+ if (invalidateQueries) {
+ cache.groupQueriesInvalidations(realmId, invalidations);
+ }
}
private void invalidateGroup(String id) {
@@ -259,6 +297,8 @@ public class RealmCacheSession implements CacheRealmProvider {
for (String id : invalidations) {
cache.invalidateObject(id);
}
+
+ cache.sendInvalidationEvents(session, invalidationEvents);
}
private KeycloakTransaction getPrepareTransaction() {
@@ -358,14 +398,14 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public RealmModel createRealm(String name) {
RealmModel realm = getDelegate().createRealm(name);
- registerRealmInvalidation(realm.getId());
+ registerRealmInvalidation(realm.getId(), realm.getName());
return realm;
}
@Override
public RealmModel createRealm(String id, String name) {
RealmModel realm = getDelegate().createRealm(id, name);
- registerRealmInvalidation(realm.getId());
+ registerRealmInvalidation(realm.getId(), realm.getName());
return realm;
}
@@ -434,7 +474,7 @@ public class RealmCacheSession implements CacheRealmProvider {
}
}
- public String getRealmByNameCacheKey(String name) {
+ static String getRealmByNameCacheKey(String name) {
return "realm.query.by.name." + name;
}
@@ -457,20 +497,12 @@ public class RealmCacheSession implements CacheRealmProvider {
RealmModel realm = getRealm(id);
if (realm == null) return false;
- invalidations.add(getRealmClientsQueryCacheKey(id));
- invalidations.add(getRealmByNameCacheKey(realm.getName()));
cache.invalidateObject(id);
- cache.realmRemoval(id, invalidations);
+ invalidationEvents.add(RealmRemovedEvent.create(id, realm.getName()));
+ cache.realmRemoval(id, realm.getName(), invalidations);
return getDelegate().removeRealm(id);
}
- protected void invalidateClient(RealmModel realm, ClientModel client) {
- invalidateClient(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) {
@@ -486,30 +518,32 @@ public class RealmCacheSession 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
- 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
+
+ invalidateClient(client.getId());
+ // this is needed so that a client that hasn't been committed isn't cached in a query
listInvalidations.add(realm.getId());
+
+ invalidationEvents.add(ClientAddedEvent.create(client.getId(), client.getClientId(), realm.getId()));
+ cache.clientAdded(realm.getId(), client.getId(), client.getClientId(), invalidations);
return client;
}
- private String getRealmClientsQueryCacheKey(String realm) {
+ static String getRealmClientsQueryCacheKey(String realm) {
return realm + REALM_CLIENTS_QUERY_SUFFIX;
}
- private String getGroupsQueryCacheKey(String realm) {
+ static String getGroupsQueryCacheKey(String realm) {
return realm + ".groups";
}
- private String getTopGroupsQueryCacheKey(String realm) {
+ static String getTopGroupsQueryCacheKey(String realm) {
return realm + ".top.groups";
}
- private String getRolesCacheKey(String container) {
+ static String getRolesCacheKey(String container) {
return container + ROLES_QUERY_SUFFIX;
}
- private String getRoleByNameCacheKey(String container, String name) {
+ static String getRoleByNameCacheKey(String container, String name) {
return container + "." + name + ROLES_QUERY_SUFFIX;
}
@@ -541,6 +575,7 @@ public class RealmCacheSession implements CacheRealmProvider {
for (String id : query.getClients()) {
ClientModel client = session.realms().getClientById(id, realm);
if (client == null) {
+ // TODO: Handle with cluster invalidations too
invalidations.add(cacheKey);
return getDelegate().getClients(realm);
}
@@ -554,12 +589,16 @@ public class RealmCacheSession implements CacheRealmProvider {
public boolean removeClient(String id, RealmModel realm) {
ClientModel client = getClientById(id, realm);
if (client == null) return false;
- // need to invalidate realm client query cache every time client list is changed
- invalidateClient(realm, client);
- cache.clientRemoval(realm.getId(), id, invalidations);
+
+ invalidateClient(client.getId());
+ // this is needed so that a client that hasn't been committed isn't cached in a query
+ listInvalidations.add(realm.getId());
+
+ invalidationEvents.add(ClientRemovedEvent.create(client));
+ cache.clientRemoval(realm.getId(), id, client.getClientId(), invalidations);
+
for (RoleModel role : client.getRoles()) {
- String roleId = role.getId();
- roleInvalidations(roleId);
+ roleRemovalInvalidations(role.getId(), role.getName(), client.getId());
}
return getDelegate().removeClient(id, realm);
}
@@ -577,11 +616,8 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public RoleModel addRealmRole(RealmModel realm, String id, String name) {
- invalidations.add(getRolesCacheKey(realm.getId()));
- // this is needed so that a new role that hasn't been committed isn't cached in a query
- listInvalidations.add(realm.getId());
RoleModel role = getDelegate().addRealmRole(realm, name);
- invalidations.add(role.getId());
+ addedRole(role.getId(), realm.getId());
return role;
}
@@ -664,11 +700,8 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public RoleModel addClientRole(RealmModel realm, ClientModel client, String id, String name) {
- invalidations.add(getRolesCacheKey(client.getId()));
- // this is needed so that a new role that hasn't been committed isn't cached in a query
- listInvalidations.add(client.getId());
RoleModel role = getDelegate().addClientRole(realm, client, id, name);
- invalidateRole(role.getId());
+ addedRole(role.getId(), client.getId());
return role;
}
@@ -734,10 +767,12 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public boolean removeRole(RealmModel realm, RoleModel role) {
- invalidations.add(getRolesCacheKey(role.getContainer().getId()));
- invalidations.add(getRoleByNameCacheKey(role.getContainer().getId(), role.getName()));
listInvalidations.add(role.getContainer().getId());
- registerRoleInvalidation(role.getId());
+
+ invalidateRole(role.getId());
+ invalidationEvents.add(RoleRemovedEvent.create(role.getId(), role.getName(), role.getContainer().getId()));
+ roleRemovalInvalidations(role.getId(), role.getName(), role.getContainer().getId());
+
return getDelegate().removeRole(realm, role);
}
@@ -797,8 +832,11 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public void moveGroup(RealmModel realm, GroupModel group, GroupModel toParent) {
- registerGroupInvalidation(group.getId());
- if (toParent != null) registerGroupInvalidation(toParent.getId());
+ invalidateGroup(group.getId(), realm.getId(), true);
+ if (toParent != null) invalidateGroup(group.getId(), realm.getId(), false); // Queries already invalidated
+ listInvalidations.add(realm.getId());
+
+ invalidationEvents.add(GroupMovedEvent.create(group, toParent, realm.getId()));
getDelegate().moveGroup(realm, group, toParent);
}
@@ -876,14 +914,15 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public boolean removeGroup(RealmModel realm, GroupModel group) {
- registerGroupInvalidation(group.getId());
+ invalidateGroup(group.getId(), realm.getId(), true);
listInvalidations.add(realm.getId());
- invalidations.add(getGroupsQueryCacheKey(realm.getId()));
- if (group.getParentId() == null) {
- invalidations.add(getTopGroupsQueryCacheKey(realm.getId()));
- } else {
- registerGroupInvalidation(group.getParentId());
+ cache.groupQueriesInvalidations(realm.getId(), invalidations);
+ if (group.getParentId() != null) {
+ invalidateGroup(group.getParentId(), realm.getId(), false); // Queries already invalidated
}
+
+ invalidationEvents.add(GroupRemovedEvent.create(group, realm.getId()));
+
return getDelegate().removeGroup(realm, group);
}
@@ -893,11 +932,11 @@ public class RealmCacheSession implements CacheRealmProvider {
return groupAdded(realm, group);
}
- public GroupModel groupAdded(RealmModel realm, GroupModel group) {
+ private GroupModel groupAdded(RealmModel realm, GroupModel group) {
listInvalidations.add(realm.getId());
- invalidations.add(getGroupsQueryCacheKey(realm.getId()));
- invalidations.add(getTopGroupsQueryCacheKey(realm.getId()));
+ cache.groupQueriesInvalidations(realm.getId(), invalidations);
invalidations.add(group.getId());
+ invalidationEvents.add(GroupAddedEvent.create(group.getId(), realm.getId()));
return group;
}
@@ -909,15 +948,32 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public void addTopLevelGroup(RealmModel realm, GroupModel subGroup) {
- invalidations.add(getTopGroupsQueryCacheKey(realm.getId()));
- invalidations.add(subGroup.getId());
+ invalidateGroup(subGroup.getId(), realm.getId(), true);
if (subGroup.getParentId() != null) {
- registerGroupInvalidation(subGroup.getParentId());
+ invalidateGroup(subGroup.getParentId(), realm.getId(), false); // Queries already invalidated
}
+
+ addGroupEventIfAbsent(GroupMovedEvent.create(subGroup, null, realm.getId()));
+
getDelegate().addTopLevelGroup(realm, subGroup);
}
+ private void addGroupEventIfAbsent(InvalidationEvent eventToAdd) {
+ String groupId = eventToAdd.getId();
+
+ // Check if we have existing event with bigger priority
+ boolean eventAlreadyExists = invalidationEvents.stream().filter((InvalidationEvent event) -> {
+
+ return (event.getId().equals(groupId)) && (event instanceof GroupAddedEvent || event instanceof GroupMovedEvent || event instanceof GroupRemovedEvent);
+
+ }).findFirst().isPresent();
+
+ if (!eventAlreadyExists) {
+ invalidationEvents.add(eventToAdd);
+ }
+ }
+
@Override
public ClientModel getClientById(String id, RealmModel realm) {
CachedClient cached = cache.get(id, CachedClient.class);
@@ -948,7 +1004,7 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public ClientModel getClientByClientId(String clientId, RealmModel realm) {
- String cacheKey = getClientByClientIdCacheKey(clientId, realm);
+ String cacheKey = getClientByClientIdCacheKey(clientId, realm.getId());
ClientListQuery query = cache.get(cacheKey, ClientListQuery.class);
String id = null;
@@ -976,8 +1032,8 @@ public class RealmCacheSession implements CacheRealmProvider {
return getClientById(id, realm);
}
- public String getClientByClientIdCacheKey(String clientId, RealmModel realm) {
- return realm.getId() + ".client.query.by.clientId." + clientId;
+ static String getClientByClientIdCacheKey(String clientId, String realmId) {
+ return realmId + ".client.query.by.clientId." + clientId;
}
@Override
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java
index a43aeb8..b6862f5 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java
@@ -47,7 +47,7 @@ public class RoleAdapter implements RoleModel {
protected void getDelegateForUpdate() {
if (updated == null) {
- cacheSession.registerRoleInvalidation(cached.getId());
+ cacheSession.registerRoleInvalidation(cached.getId(), cached.getName(), getContainerId());
updated = cacheSession.getDelegate().getRoleById(cached.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/UserCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java
index ee8dc8b..e949314 100755
--- 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
@@ -18,40 +18,94 @@
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.events.InvalidationEvent;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
+import org.keycloak.models.cache.infinispan.events.UserCacheInvalidationEvent;
import org.keycloak.models.cache.infinispan.stream.InRealmPredicate;
import java.util.Map;
import java.util.Set;
-import java.util.function.Predicate;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-@Listener
public class UserCacheManager extends CacheManager {
- protected static final Logger logger = Logger.getLogger(UserCacheManager.class);
+ private static final Logger logger = Logger.getLogger(UserCacheManager.class);
protected volatile boolean enabled = true;
+
public UserCacheManager(Cache<String, Revisioned> cache, Cache<String, Long> revisions) {
super(cache, revisions);
}
@Override
+ protected Logger getLogger() {
+ return logger;
+ }
+
+ @Override
public void clear() {
cache.clear();
revisions.clear();
}
+
+ public void userUpdatedInvalidations(String userId, String username, String email, String realmId, Set<String> invalidations) {
+ invalidations.add(userId);
+ if (email != null) invalidations.add(UserCacheSession.getUserByEmailCacheKey(realmId, email));
+ invalidations.add(UserCacheSession.getUserByUsernameCacheKey(realmId, username));
+ }
+
+ // Fully invalidate user including consents and federatedIdentity links.
+ public void fullUserInvalidation(String userId, String username, String email, String realmId, boolean identityFederationEnabled, Map<String, String> federatedIdentities, Set<String> invalidations) {
+ userUpdatedInvalidations(userId, username, email, realmId, invalidations);
+
+ if (identityFederationEnabled) {
+ // Invalidate all keys for lookup this user by any identityProvider link
+ for (Map.Entry<String, String> socialLink : federatedIdentities.entrySet()) {
+ String fedIdentityCacheKey = UserCacheSession.getUserByFederatedIdentityCacheKey(realmId, socialLink.getKey(), socialLink.getValue());
+ invalidations.add(fedIdentityCacheKey);
+ }
+
+ // Invalidate federationLinks of user
+ invalidations.add(UserCacheSession.getFederatedIdentityLinksCacheKey(userId));
+ }
+
+ // Consents
+ invalidations.add(UserCacheSession.getConsentCacheKey(userId));
+ }
+
+ public void federatedIdentityLinkUpdatedInvalidation(String userId, Set<String> invalidations) {
+ invalidations.add(UserCacheSession.getFederatedIdentityLinksCacheKey(userId));
+ }
+
+ public void federatedIdentityLinkRemovedInvalidation(String userId, String realmId, String identityProviderId, String socialUserId, Set<String> invalidations) {
+ invalidations.add(UserCacheSession.getFederatedIdentityLinksCacheKey(userId));
+ if (identityProviderId != null) {
+ invalidations.add(UserCacheSession.getUserByFederatedIdentityCacheKey(realmId, identityProviderId, socialUserId));
+ }
+ }
+
+ public void consentInvalidation(String userId, Set<String> invalidations) {
+ invalidations.add(UserCacheSession.getConsentCacheKey(userId));
+ }
+
+
@Override
- protected Predicate<Map.Entry<String, Revisioned>> getInvalidationPredicate(Object object) {
- return null;
+ protected void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations) {
+ if (event instanceof UserCacheInvalidationEvent) {
+ ((UserCacheInvalidationEvent) event).addInvalidations(this, invalidations);
+ }
}
public void invalidateRealmUsers(String realm, Set<String> invalidations) {
- addInvalidations(InRealmPredicate.create().realm(realm), invalidations);
+ InRealmPredicate inRealmPredicate = getInRealmPredicate(realm);
+ addInvalidations(inRealmPredicate, invalidations);
+ }
+
+ private InRealmPredicate getInRealmPredicate(String realmId) {
+ return InRealmPredicate.create().realm(realmId);
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
index 5531de1..92d56a8 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
@@ -19,6 +19,7 @@ package org.keycloak.models.cache.infinispan;
import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterProvider;
+import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel;
@@ -42,6 +43,12 @@ import org.keycloak.models.cache.infinispan.entities.CachedUser;
import org.keycloak.models.cache.infinispan.entities.CachedUserConsent;
import org.keycloak.models.cache.infinispan.entities.CachedUserConsents;
import org.keycloak.models.cache.infinispan.entities.UserListQuery;
+import org.keycloak.models.cache.infinispan.events.UserCacheRealmInvalidationEvent;
+import org.keycloak.models.cache.infinispan.events.UserConsentsUpdatedEvent;
+import org.keycloak.models.cache.infinispan.events.UserFederationLinkRemovedEvent;
+import org.keycloak.models.cache.infinispan.events.UserFederationLinkUpdatedEvent;
+import org.keycloak.models.cache.infinispan.events.UserFullInvalidationEvent;
+import org.keycloak.models.cache.infinispan.events.UserUpdatedEvent;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel;
@@ -72,6 +79,7 @@ public class UserCacheSession implements UserCache {
protected Set<String> invalidations = new HashSet<>();
protected Set<String> realmInvalidations = new HashSet<>();
+ protected Set<InvalidationEvent> invalidationEvents = new HashSet<>(); // Events to be sent across cluster
protected Map<String, UserModel> managedUsers = new HashMap<>();
public UserCacheSession(UserCacheManager cache, KeycloakSession session) {
@@ -85,7 +93,7 @@ public class UserCacheSession implements UserCache {
public void clear() {
cache.clear();
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
- cluster.notify(InfinispanUserCacheProviderFactory.USER_CLEAR_CACHE_EVENTS, new ClearCacheEvent());
+ cluster.notify(InfinispanUserCacheProviderFactory.USER_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true);
}
public UserProvider getDelegate() {
@@ -97,10 +105,8 @@ public class UserCacheSession implements UserCache {
}
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()));
- if (realm.isIdentityFederationEnabled()) invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
+ cache.userUpdatedInvalidations(user.getId(), user.getUsername(), user.getEmail(), user.getRealm(), invalidations);
+ invalidationEvents.add(UserUpdatedEvent.create(user.getId(), user.getUsername(), user.getEmail(), user.getRealm()));
}
@Override
@@ -108,10 +114,8 @@ public class UserCacheSession implements UserCache {
if (user instanceof CachedUserModel) {
((CachedUserModel)user).invalidate();
} else {
- invalidations.add(user.getId());
- if (user.getEmail() != null) invalidations.add(getUserByEmailCacheKey(realm.getId(), user.getEmail()));
- invalidations.add(getUserByUsernameCacheKey(realm.getId(), user.getUsername()));
- if (realm.isIdentityFederationEnabled()) invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
+ cache.userUpdatedInvalidations(user.getId(), user.getUsername(), user.getEmail(), realm.getId(), invalidations);
+ invalidationEvents.add(UserUpdatedEvent.create(user.getId(), user.getUsername(), user.getEmail(), realm.getId()));
}
}
@@ -127,6 +131,8 @@ public class UserCacheSession implements UserCache {
for (String invalidation : invalidations) {
cache.invalidateObject(invalidation);
}
+
+ cache.sendInvalidationEvents(session, invalidationEvents);
}
private KeycloakTransaction getTransaction() {
@@ -201,19 +207,23 @@ public class UserCacheSession implements UserCache {
return adapter;
}
- public String getUserByUsernameCacheKey(String realmId, String username) {
+ static String getUserByUsernameCacheKey(String realmId, String username) {
return realmId + ".username." + username;
}
- public String getUserByEmailCacheKey(String realmId, String email) {
+ static String getUserByEmailCacheKey(String realmId, String email) {
return realmId + ".email." + email;
}
- public String getUserByFederatedIdentityCacheKey(String realmId, FederatedIdentityModel socialLink) {
- return realmId + ".idp." + socialLink.getIdentityProvider() + "." + socialLink.getUserId();
+ private static String getUserByFederatedIdentityCacheKey(String realmId, FederatedIdentityModel socialLink) {
+ return getUserByFederatedIdentityCacheKey(realmId, socialLink.getIdentityProvider(), socialLink.getUserId());
+ }
+
+ static String getUserByFederatedIdentityCacheKey(String realmId, String identityProvider, String socialUserId) {
+ return realmId + ".idp." + identityProvider + "." + socialUserId;
}
- public String getFederatedIdentityLinksCacheKey(String userId) {
+ static String getFederatedIdentityLinksCacheKey(String userId) {
return userId + ".idplinks";
}
@@ -655,27 +665,32 @@ public class UserCacheSession implements UserCache {
@Override
public void updateConsent(RealmModel realm, String userId, UserConsentModel consent) {
- invalidations.add(getConsentCacheKey(userId));
+ invalidateConsent(userId);
getDelegate().updateConsent(realm, userId, consent);
}
@Override
public boolean revokeConsentForClient(RealmModel realm, String userId, String clientInternalId) {
- invalidations.add(getConsentCacheKey(userId));
+ invalidateConsent(userId);
return getDelegate().revokeConsentForClient(realm, userId, clientInternalId);
}
- public String getConsentCacheKey(String userId) {
+ static String getConsentCacheKey(String userId) {
return userId + ".consents";
}
@Override
public void addConsent(RealmModel realm, String userId, UserConsentModel consent) {
- invalidations.add(getConsentCacheKey(userId));
+ invalidateConsent(userId);
getDelegate().addConsent(realm, userId, consent);
}
+ private void invalidateConsent(String userId) {
+ cache.consentInvalidation(userId, invalidations);
+ invalidationEvents.add(UserConsentsUpdatedEvent.create(userId));
+ }
+
@Override
public UserConsentModel getConsentByClient(RealmModel realm, String userId, String clientId) {
logger.tracev("getConsentByClient: {0}", userId);
@@ -754,7 +769,7 @@ public class UserCacheSession implements UserCache {
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);
+ fullyInvalidateUser(realm, user);
managedUsers.put(user.getId(), user);
return user;
}
@@ -763,94 +778,89 @@ public class UserCacheSession implements UserCache {
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);
+ fullyInvalidateUser(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
-
- if (realm.isIdentityFederationEnabled()) {
- // Invalidate all keys for lookup this user by any identityProvider link
- Set<FederatedIdentityModel> federatedIdentities = getFederatedIdentities(user, realm);
- for (FederatedIdentityModel socialLink : federatedIdentities) {
- String fedIdentityCacheKey = getUserByFederatedIdentityCacheKey(realm.getId(), socialLink);
- invalidations.add(fedIdentityCacheKey);
- }
+ // just in case the transaction is rolled back you need to invalidate the user and all cache queries for that user
+ protected void fullyInvalidateUser(RealmModel realm, UserModel user) {
+ Set<FederatedIdentityModel> federatedIdentities = realm.isIdentityFederationEnabled() ? getFederatedIdentities(user, realm) : null;
- // Invalidate federationLinks of user
- invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
- }
+ UserFullInvalidationEvent event = UserFullInvalidationEvent.create(user.getId(), user.getUsername(), user.getEmail(), realm.getId(), realm.isIdentityFederationEnabled(), federatedIdentities);
- invalidations.add(user.getId());
- if (user.getEmail() != null) invalidations.add(getUserByEmailCacheKey(realm.getId(), user.getEmail()));
- invalidations.add(getUserByUsernameCacheKey(realm.getId(), user.getUsername()));
+ cache.fullUserInvalidation(user.getId(), user.getUsername(), user.getEmail(), realm.getId(), realm.isIdentityFederationEnabled(), event.getFederatedIdentities(), invalidations);
+ invalidationEvents.add(event);
}
@Override
public boolean removeUser(RealmModel realm, UserModel user) {
- invalidateUser(realm, user);
+ fullyInvalidateUser(realm, user);
return getDelegate().removeUser(realm, user);
}
@Override
public void addFederatedIdentity(RealmModel realm, UserModel user, FederatedIdentityModel socialLink) {
- invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
+ invalidateFederationLink(user.getId());
getDelegate().addFederatedIdentity(realm, user, socialLink);
}
@Override
public void updateFederatedIdentity(RealmModel realm, UserModel federatedUser, FederatedIdentityModel federatedIdentityModel) {
- invalidations.add(getFederatedIdentityLinksCacheKey(federatedUser.getId()));
+ invalidateFederationLink(federatedUser.getId());
getDelegate().updateFederatedIdentity(realm, federatedUser, federatedIdentityModel);
}
+ private void invalidateFederationLink(String userId) {
+ cache.federatedIdentityLinkUpdatedInvalidation(userId, invalidations);
+ invalidationEvents.add(UserFederationLinkUpdatedEvent.create(userId));
+ }
+
@Override
public boolean removeFederatedIdentity(RealmModel realm, UserModel user, String socialProvider) {
// Needs to invalidate both directions
FederatedIdentityModel socialLink = getFederatedIdentity(user, socialProvider, realm);
- invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
- if (socialLink != null) {
- invalidations.add(getUserByFederatedIdentityCacheKey(realm.getId(), socialLink));
- }
+
+ UserFederationLinkRemovedEvent event = UserFederationLinkRemovedEvent.create(user.getId(), realm.getId(), socialLink);
+ cache.federatedIdentityLinkRemovedInvalidation(user.getId(), realm.getId(), event.getIdentityProviderId(), event.getSocialUserId(), invalidations);
+ invalidationEvents.add(event);
return getDelegate().removeFederatedIdentity(realm, user, socialProvider);
}
@Override
public void grantToAllUsers(RealmModel realm, RoleModel role) {
- realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
+ addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().grantToAllUsers(realm, role);
}
@Override
public void preRemove(RealmModel realm) {
- realmInvalidations.add(realm.getId());
+ addRealmInvalidation(realm.getId());
getDelegate().preRemove(realm);
}
@Override
public void preRemove(RealmModel realm, RoleModel role) {
- realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
+ addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, role);
}
@Override
public void preRemove(RealmModel realm, GroupModel group) {
- realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
+ addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, group);
}
@Override
public void preRemove(RealmModel realm, UserFederationProviderModel link) {
- realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
+ addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, link);
}
@Override
public void preRemove(RealmModel realm, ClientModel client) {
- realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
+ addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, client);
}
@@ -862,9 +872,14 @@ public class UserCacheSession implements UserCache {
@Override
public void preRemove(RealmModel realm, ComponentModel component) {
if (!component.getProviderType().equals(UserStorageProvider.class.getName())) return;
- realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
+ addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, component);
}
+ private void addRealmInvalidation(String realmId) {
+ realmInvalidations.add(realmId);
+ invalidationEvents.add(UserCacheRealmInvalidationEvent.create(realmId));
+ }
+
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java
index 1485da8..c332eea 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java
@@ -92,13 +92,13 @@ public class InfinispanUserSessionInitializer {
private boolean isFinished() {
- InitializerState state = (InitializerState) workCache.get(stateKey);
+ InitializerState state = getStateFromCache();
return state != null && state.isFinished();
}
private InitializerState getOrCreateInitializerState() {
- InitializerState state = (InitializerState) workCache.get(stateKey);
+ InitializerState state = getStateFromCache();
if (state == null) {
final int[] count = new int[1];
@@ -128,6 +128,12 @@ public class InfinispanUserSessionInitializer {
}
+ private InitializerState getStateFromCache() {
+ // TODO: We ignore cacheStore for now, so that in Cross-DC scenario (with RemoteStore enabled) is the remoteStore ignored. This means that every DC needs to load offline sessions separately.
+ return (InitializerState) workCache.getAdvancedCache()
+ .withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD)
+ .get(stateKey);
+ }
private void saveStateToCache(final InitializerState state) {
@@ -138,8 +144,9 @@ public class InfinispanUserSessionInitializer {
public void run() {
// Save this synchronously to ensure all nodes read correct state
+ // TODO: We ignore cacheStore for now, so that in Cross-DC scenario (with RemoteStore enabled) is the remoteStore ignored. This means that every DC needs to load offline sessions separately.
InfinispanUserSessionInitializer.this.workCache.getAdvancedCache().
- withFlags(Flag.IGNORE_RETURN_VALUES, Flag.FORCE_SYNCHRONOUS)
+ withFlags(Flag.IGNORE_RETURN_VALUES, Flag.FORCE_SYNCHRONOUS, Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD)
.put(stateKey, state);
}
diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java
new file mode 100644
index 0000000..e7c1337
--- /dev/null
+++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java
@@ -0,0 +1,231 @@
+/*
+ * 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.cluster.infinispan;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.infinispan.Cache;
+import org.infinispan.client.hotrod.Flag;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified;
+import org.infinispan.client.hotrod.annotation.ClientListener;
+import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
+import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
+import org.infinispan.configuration.cache.Configuration;
+import org.infinispan.configuration.cache.ConfigurationBuilder;
+import org.infinispan.configuration.global.GlobalConfigurationBuilder;
+import org.infinispan.manager.DefaultCacheManager;
+import org.infinispan.manager.EmbeddedCacheManager;
+import org.infinispan.persistence.manager.PersistenceManager;
+import org.infinispan.persistence.remote.RemoteStore;
+import org.infinispan.persistence.remote.configuration.ExhaustedAction;
+import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
+import org.junit.Ignore;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+
+/**
+ * Test concurrency for remoteStore (backed by HotRod RemoteCaches) against external JDG
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@Ignore
+public class ConcurrencyJDGRemoteCacheTest {
+
+ private static Map<String, EntryInfo> state = new HashMap<>();
+
+ public static void main(String[] args) throws Exception {
+ // Init map somehow
+ for (int i=0 ; i<100 ; i++) {
+ String key = "key-" + i;
+ state.put(key, new EntryInfo());
+ }
+
+ // Create caches, listeners and finally worker threads
+ Worker worker1 = createWorker(1);
+ Worker worker2 = createWorker(2);
+
+ // Start and join workers
+ worker1.start();
+ worker2.start();
+
+ worker1.join();
+ worker2.join();
+
+ // Output
+ for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
+ System.out.println(entry.getKey() + ":::" + entry.getValue());
+ worker1.cache.remove(entry.getKey());
+ }
+
+ // Finish JVM
+ worker1.cache.getCacheManager().stop();
+ worker2.cache.getCacheManager().stop();
+ }
+
+ private static Worker createWorker(int threadId) {
+ EmbeddedCacheManager manager = createManager(threadId);
+ Cache<String, Integer> cache = manager.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
+
+ System.out.println("Retrieved cache: " + threadId);
+
+ RemoteStore remoteStore = cache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class).iterator().next();
+ HotRodListener listener = new HotRodListener();
+ remoteStore.getRemoteCache().addClientListener(listener);
+
+ return new Worker(cache, threadId);
+ }
+
+ private static EmbeddedCacheManager createManager(int threadId) {
+ System.setProperty("java.net.preferIPv4Stack", "true");
+ System.setProperty("jgroups.tcp.port", "53715");
+ GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
+
+ boolean clustered = false;
+ boolean async = false;
+ boolean allowDuplicateJMXDomains = true;
+
+ if (clustered) {
+ gcb = gcb.clusteredDefault();
+ gcb.transport().clusterName("test-clustering");
+ }
+
+ gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
+
+ EmbeddedCacheManager cacheManager = new DefaultCacheManager(gcb.build());
+
+ Configuration invalidationCacheConfiguration = getCacheBackedByRemoteStore(threadId);
+
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.WORK_CACHE_NAME, invalidationCacheConfiguration);
+ return cacheManager;
+
+ }
+
+ private static Configuration getCacheBackedByRemoteStore(int threadId) {
+ ConfigurationBuilder cacheConfigBuilder = new ConfigurationBuilder();
+
+ // int port = threadId==1 ? 11222 : 11322;
+ int port = 11222;
+
+ return cacheConfigBuilder.persistence().addStore(RemoteStoreConfigurationBuilder.class)
+ .fetchPersistentState(false)
+ .ignoreModifications(false)
+ .purgeOnStartup(false)
+ .preload(false)
+ .shared(true)
+ .remoteCacheName(InfinispanConnectionProvider.WORK_CACHE_NAME)
+ .rawValues(true)
+ .forceReturnValues(false)
+ .addServer()
+ .host("localhost")
+ .port(port)
+ .connectionPool()
+ .maxActive(20)
+ .exhaustedAction(ExhaustedAction.CREATE_NEW)
+ .async()
+ . enabled(false).build();
+ }
+
+
+ @ClientListener
+ public static class HotRodListener {
+
+ //private AtomicInteger listenerCount = new AtomicInteger(0);
+
+ @ClientCacheEntryCreated
+ public void created(ClientCacheEntryCreatedEvent event) {
+ String cacheKey = (String) event.getKey();
+ state.get(cacheKey).successfulListenerWrites.incrementAndGet();
+ }
+
+ @ClientCacheEntryModified
+ public void updated(ClientCacheEntryModifiedEvent event) {
+ String cacheKey = (String) event.getKey();
+ state.get(cacheKey).successfulListenerWrites.incrementAndGet();
+ }
+
+ }
+
+
+ private static class Worker extends Thread {
+
+ private final Cache<String, Integer> cache;
+
+ private final int myThreadId;
+
+ private Worker(Cache<String, Integer> cache, int myThreadId) {
+ this.cache = cache;
+ this.myThreadId = myThreadId;
+ }
+
+ @Override
+ public void run() {
+ for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
+ String cacheKey = entry.getKey();
+ EntryInfo wrapper = state.get(cacheKey);
+
+ int val = getClusterStartupTime(this.cache, cacheKey, wrapper);
+ if (myThreadId == 1) {
+ wrapper.th1.set(val);
+ } else {
+ wrapper.th2.set(val);
+ }
+
+ }
+
+ System.out.println("Worker finished: " + myThreadId);
+ }
+
+ }
+
+ public static int getClusterStartupTime(Cache<String, Integer> cache, String cacheKey, EntryInfo wrapper) {
+ int startupTime = new Random().nextInt(1024);
+
+ // Concurrency doesn't work correctly with this
+ //Integer existingClusterStartTime = (Integer) cache.putIfAbsent(cacheKey, startupTime);
+
+ // Concurrency works fine with this
+ RemoteCache remoteCache = cache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class).iterator().next().getRemoteCache();
+ Integer existingClusterStartTime = (Integer) remoteCache.withFlags(Flag.FORCE_RETURN_VALUE).putIfAbsent(cacheKey, startupTime);
+
+ if (existingClusterStartTime == null) {
+ wrapper.successfulInitializations.incrementAndGet();
+ return startupTime;
+ } else {
+ return existingClusterStartTime;
+ }
+ }
+
+ private static class EntryInfo {
+ AtomicInteger successfulInitializations = new AtomicInteger(0);
+ AtomicInteger successfulListenerWrites = new AtomicInteger(0);
+ AtomicInteger th1 = new AtomicInteger();
+ AtomicInteger th2 = new AtomicInteger();
+
+ @Override
+ public String toString() {
+ return String.format("Inits: %d, listeners: %d, th1: %d, th2: %d", successfulInitializations.get(), successfulListenerWrites.get(), th1.get(), th2.get());
+ }
+ }
+
+
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
index f5d2666..55e4108 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
@@ -146,7 +146,8 @@ public class JpaRealmProvider implements RealmProvider {
query.setParameter("realm", realm.getId());
List<String> clients = query.getResultList();
for (String client : clients) {
- session.realms().removeClient(client, adapter);
+ // No need to go through cache. Clients were already invalidated
+ removeClient(client, adapter);
}
for (ClientTemplateEntity a : new LinkedList<>(realm.getClientTemplates())) {
@@ -154,7 +155,8 @@ public class JpaRealmProvider implements RealmProvider {
}
for (RoleModel role : adapter.getRoles()) {
- session.realms().removeRole(adapter, role);
+ // No need to go through cache. Roles were already invalidated
+ removeRole(adapter, role);
}
@@ -486,7 +488,8 @@ public class JpaRealmProvider implements RealmProvider {
session.users().preRemove(realm, client);
for (RoleModel role : client.getRoles()) {
- client.removeRole(role);
+ // No need to go through cache. Roles were already invalidated
+ removeRole(realm, role);
}
ClientEntity clientEntity = ((ClientAdapter)client).getEntity();
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 97aa4bd..f5b9d7d 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -953,13 +953,6 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
}
@Override
- public boolean removeRoleById(String id) {
- RoleModel role = getRoleById(id);
- if (role == null) return false;
- return role.getContainer().removeRole(role);
- }
-
- @Override
public PasswordPolicy getPasswordPolicy() {
if (passwordPolicy == null) {
passwordPolicy = PasswordPolicy.parse(session, realm.getPasswordPolicy());
@@ -1933,12 +1926,6 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
}
@Override
- public void addTopLevelGroup(GroupModel subGroup) {
- session.realms().addTopLevelGroup(this, subGroup);
-
- }
-
- @Override
public void moveGroup(GroupModel group, GroupModel toParent) {
session.realms().moveGroup(this, group, toParent);
}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index a79c478..bd00f9d 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -23,7 +23,6 @@ import org.keycloak.common.enums.SslRequired;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
-import org.keycloak.jose.jwk.JWKBuilder;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatorConfigModel;
@@ -62,10 +61,6 @@ import org.keycloak.models.mongo.keycloak.entities.UserFederationProviderEntity;
import org.keycloak.models.utils.ComponentUtil;
import org.keycloak.models.utils.KeycloakModelUtils;
-import java.security.Key;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -516,13 +511,6 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
}
@Override
- public boolean removeRoleById(String id) {
- RoleModel role = getRoleById(id);
- if (role == null) return false;
- return removeRole(role);
- }
-
- @Override
public Set<RoleModel> getRoles() {
DBObject query = new QueryBuilder()
.and("realmId").is(getId())
@@ -555,12 +543,6 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
}
@Override
- public void addTopLevelGroup(GroupModel subGroup) {
- session.realms().addTopLevelGroup(this, subGroup);
-
- }
-
- @Override
public void moveGroup(GroupModel group, GroupModel toParent) {
session.realms().moveGroup(this, group, toParent);
}
pom.xml 5(+5 -0)
diff --git a/pom.xml b/pom.xml
index bfa291a..afd4212 100755
--- a/pom.xml
+++ b/pom.xml
@@ -634,6 +634,11 @@
<version>${infinispan.version}</version>
</dependency>
<dependency>
+ <groupId>org.infinispan</groupId>
+ <artifactId>infinispan-cachestore-remote</artifactId>
+ <version>${infinispan.version}</version>
+ </dependency>
+ <dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>${liquibase.version}</version>
diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
index e9df047..09720a7 100755
--- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
@@ -351,8 +351,6 @@ public interface RealmModel extends RoleContainerModel {
void setNotBefore(int notBefore);
- boolean removeRoleById(String id);
-
boolean isEventsEnabled();
void setEventsEnabled(boolean enabled);
@@ -397,13 +395,6 @@ public interface RealmModel extends RoleContainerModel {
GroupModel createGroup(String name);
GroupModel createGroup(String id, String name);
- /**
- * Move Group to top realm level. Basically just sets group parent to null. You need to call this though
- * to make sure caches are set properly
- *
- * @param subGroup
- */
- void addTopLevelGroup(GroupModel subGroup);
GroupModel getGroupById(String id);
List<GroupModel> getGroups();
List<GroupModel> getTopLevelGroups();
diff --git a/server-spi-private/src/main/java/org/keycloak/cluster/ClusterListener.java b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterListener.java
index 41c65c0..2c07377 100644
--- a/server-spi-private/src/main/java/org/keycloak/cluster/ClusterListener.java
+++ b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterListener.java
@@ -29,6 +29,6 @@ public interface ClusterListener {
*
* @param event value of notification (Object added into the cache)
*/
- void run(ClusterEvent event);
+ void eventReceived(ClusterEvent event);
}
diff --git a/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java
index 6c22056..abed174 100644
--- a/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java
@@ -48,7 +48,8 @@ public interface ClusterProvider extends Provider {
/**
- * Register task (listener) under given key. When this key will be put to the cache on any cluster node, the task will be executed
+ * Register task (listener) under given key. When this key will be put to the cache on any cluster node, the task will be executed.
+ * When using {@link #ALL} as the taskKey, then listener will be always triggered for any value put into the cache.
*
* @param taskKey
* @param task
@@ -57,10 +58,18 @@ public interface ClusterProvider extends Provider {
/**
- * Notify registered listeners on all cluster nodes
+ * Notify registered listeners on all cluster nodes. It will notify listeners registered under given taskKey AND also listeners registered with {@link #ALL} key (those are always executed)
*
* @param taskKey
* @param event
+ * @param ignoreSender if true, then sender node itself won't receive the notification
*/
- void notify(String taskKey, ClusterEvent event);
+ void notify(String taskKey, ClusterEvent event, boolean ignoreSender);
+
+
+ /**
+ * Special value to be used with {@link #registerListener} to specify that particular listener will be always triggered for all notifications
+ * with any key.
+ */
+ String ALL = "ALL";
}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java b/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java
index 7bc1299..61ae1be 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java
@@ -27,12 +27,12 @@ public interface CacheRealmProvider extends RealmProvider {
void clear();
RealmProvider getDelegate();
- void registerRealmInvalidation(String id);
+ void registerRealmInvalidation(String id, String name);
- void registerClientInvalidation(String id);
+ void registerClientInvalidation(String id, String clientId, String realmId);
void registerClientTemplateInvalidation(String id);
- void registerRoleInvalidation(String id);
+ void registerRoleInvalidation(String id, String roleName, String roleContainerId);
void registerGroupInvalidation(String id);
}
diff --git a/services/src/main/java/org/keycloak/services/managers/UsersSyncManager.java b/services/src/main/java/org/keycloak/services/managers/UsersSyncManager.java
index d8c5dee..21b0cad 100755
--- a/services/src/main/java/org/keycloak/services/managers/UsersSyncManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/UsersSyncManager.java
@@ -155,7 +155,7 @@ public class UsersSyncManager {
// Ensure all cluster nodes are notified
public void notifyToRefreshPeriodicSync(KeycloakSession session, RealmModel realm, UserFederationProviderModel federationProvider, boolean removed) {
FederationProviderClusterEvent event = FederationProviderClusterEvent.createEvent(removed, realm.getId(), federationProvider);
- session.getProvider(ClusterProvider.class).notify(FEDERATION_TASK_KEY, event);
+ session.getProvider(ClusterProvider.class).notify(FEDERATION_TASK_KEY, event, false);
}
@@ -265,7 +265,7 @@ public class UsersSyncManager {
}
@Override
- public void run(ClusterEvent event) {
+ public void eventReceived(ClusterEvent event) {
final FederationProviderClusterEvent fedEvent = (FederationProviderClusterEvent) event;
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
diff --git a/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java b/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java
index 05b07f2..b1114fa 100755
--- a/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java
@@ -172,7 +172,7 @@ public class UserStorageSyncManager {
}
UserStorageProviderClusterEvent event = UserStorageProviderClusterEvent.createEvent(removed, realm.getId(), provider);
- session.getProvider(ClusterProvider.class).notify(USER_STORAGE_TASK_KEY, event);
+ session.getProvider(ClusterProvider.class).notify(USER_STORAGE_TASK_KEY, event, false);
}
@@ -282,7 +282,7 @@ public class UserStorageSyncManager {
}
@Override
- public void run(ClusterEvent event) {
+ public void eventReceived(ClusterEvent event) {
final UserStorageProviderClusterEvent fedEvent = (UserStorageProviderClusterEvent) event;
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index 363d6f4..2ea9992 100644
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -51,6 +51,7 @@ import org.keycloak.services.resources.admin.AdminRoot;
import org.keycloak.services.scheduled.ClearExpiredEvents;
import org.keycloak.services.scheduled.ClearExpiredUserSessions;
import org.keycloak.services.scheduled.ClusterAwareScheduledTaskRunner;
+import org.keycloak.services.scheduled.ScheduledTaskRunner;
import org.keycloak.services.util.JsonConfigProvider;
import org.keycloak.services.util.ObjectMapperResolver;
import org.keycloak.timer.TimerProvider;
@@ -321,7 +322,7 @@ public class KeycloakApplication extends Application {
try {
TimerProvider timer = session.getProvider(TimerProvider.class);
timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredEvents(), interval), interval, "ClearExpiredEvents");
- timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions(), interval), interval, "ClearExpiredUserSessions");
+ timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions()), interval, "ClearExpiredUserSessions");
new UsersSyncManager().bootstrapPeriodic(sessionFactory, timer);
new UserStorageSyncManager().bootstrapPeriodic(sessionFactory, timer);
} finally {
testsuite/integration/pom.xml 4(+4 -0)
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
index 873d99d..8a2dc4b 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -233,6 +233,10 @@
<artifactId>infinispan-core</artifactId>
</dependency>
<dependency>
+ <groupId>org.infinispan</groupId>
+ <artifactId>infinispan-cachestore-remote</artifactId>
+ </dependency>
+ <dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
</dependency>
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java
index 6632124..1fec6d9 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java
@@ -238,7 +238,7 @@ public class LDAPGroupMapperSyncTest {
GroupModel model1 = realm.createGroup("model1");
realm.moveGroup(model1, null);
GroupModel model2 = realm.createGroup("model2");
- kcGroup1.addChild(model2);
+ realm.moveGroup(model2, kcGroup1);
// Sync groups again from LDAP. Nothing deleted
syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java
new file mode 100644
index 0000000..f71d0da
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java
@@ -0,0 +1,420 @@
+/*
+ * 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.testsuite.model;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.infinispan.Cache;
+import org.infinispan.notifications.Listener;
+import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved;
+import org.infinispan.notifications.cachelistener.event.CacheEntryRemovedEvent;
+import org.jboss.logging.Logger;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserConsentModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.KeycloakServer;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.util.cli.TestCacheUtils;
+
+/**
+ * Requires execution with cluster (or external JDG) enabled and real database, which will be shared for both cluster nodes. Everything set by system properties:
+ *
+ * 1) Use those system properties to run against shared MySQL:
+ *
+ * -Dkeycloak.connectionsJpa.url=jdbc:mysql://localhost/keycloak -Dkeycloak.connectionsJpa.driver=com.mysql.jdbc.Driver -Dkeycloak.connectionsJpa.user=keycloak
+ * -Dkeycloak.connectionsJpa.password=keycloak
+ *
+ *
+ * 2) Then either choose from:
+ *
+ * 2.a) Run test with 2 keycloak nodes in cluster. Add this system property for that: -Dkeycloak.connectionsInfinispan.clustered=true
+ *
+ * 2.b) Run test with 2 keycloak nodes without cluster, but instead with external JDG. Both keycloak servers will send invalidation events to the JDG server and receive the events from this JDG server.
+ * They don't communicate with each other. So JDG is man-in-the-middle.
+ *
+ * This assumes that you have JDG 7.0 server running on localhost with HotRod endpoint on port 11222 (which is default port anyway).
+ *
+ * You also need to have this cache configured in JDG_HOME/standalone/configuration/standalone.xml to infinispan subsystem :
+ *
+ * <local-cache name="work" start="EAGER" batching="false" />
+ *
+ * Finally, add this system property when running the test: -Dkeycloak.connectionsInfinispan.remoteStoreEnabled=true
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@Ignore
+public class ClusterInvalidationTest {
+
+ protected static final Logger logger = Logger.getLogger(ClusterInvalidationTest.class);
+
+ private static final String REALM_NAME = "test";
+
+ private static final int SLEEP_TIME_MS = Integer.parseInt(System.getProperty("sleep.time", "500"));
+
+ private static TestListener listener1realms;
+ private static TestListener listener1users;
+ private static TestListener listener2realms;
+ private static TestListener listener2users;
+
+ @ClassRule
+ public static KeycloakRule server1 = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ InfinispanConnectionProvider infinispan = manager.getSession().getProvider(InfinispanConnectionProvider.class);
+
+ Cache cache = infinispan.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
+ listener1realms = new TestListener("server1 - realms", cache);
+ cache.addListener(listener1realms);
+
+ cache = infinispan.getCache(InfinispanConnectionProvider.USER_CACHE_NAME);
+ listener1users = new TestListener("server1 - users", cache);
+ cache.addListener(listener1users);
+ }
+
+ });
+
+ @ClassRule
+ public static KeycloakRule server2 = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ InfinispanConnectionProvider infinispan = manager.getSession().getProvider(InfinispanConnectionProvider.class);
+
+ Cache cache = infinispan.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
+ listener2realms = new TestListener("server2 - realms", cache);
+ cache.addListener(listener2realms);
+
+ cache = infinispan.getCache(InfinispanConnectionProvider.USER_CACHE_NAME);
+ listener2users = new TestListener("server2 - users", cache);
+ cache.addListener(listener2users);
+ }
+
+ }) {
+
+ @Override
+ protected void configureServer(KeycloakServer server) {
+ server.getConfig().setPort(8082);
+ }
+
+ @Override
+ protected void importRealm() {
+ }
+
+ @Override
+ protected void removeTestRealms() {
+ }
+
+ };
+
+ private static void clearListeners() {
+ listener1realms.getInvalidationsAndClear();
+ listener1users.getInvalidationsAndClear();
+ listener2realms.getInvalidationsAndClear();
+ listener2users.getInvalidationsAndClear();
+ }
+
+
+ @Test
+ public void testClusterInvalidation() throws Exception {
+ cacheEverything();
+
+ clearListeners();
+
+ KeycloakSession session1 = server1.startSession();
+
+
+ logger.info("UPDATE REALM");
+
+ RealmModel realm = session1.realms().getRealmByName(REALM_NAME);
+ realm.setDisplayName("foo");
+ session1 = commit(server1, session1, true);
+
+ assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 3, realm.getId());
+ assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 3, realm.getId());
+
+
+ // CREATES
+
+ logger.info("CREATE ROLE");
+ realm = session1.realms().getRealmByName(REALM_NAME);
+ realm.addRole("foo-role");
+ session1 = commit(server1, session1, true);
+
+ assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 1, "test.roles");
+ assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 1, "test.roles");
+
+
+ logger.info("CREATE CLIENT");
+ realm = session1.realms().getRealmByName(REALM_NAME);
+ realm.addClient("foo-client");
+ session1 = commit(server1, session1, true);
+
+ assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 1, "test.realm.clients");
+ assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 1, "test.realm.clients");
+
+ logger.info("CREATE GROUP");
+ realm = session1.realms().getRealmByName(REALM_NAME);
+ GroupModel group = realm.createGroup("foo-group");
+ session1 = commit(server1, session1, true);
+
+ assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 1, "test.top.groups");
+ assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 1, "test.top.groups");
+
+ logger.info("CREATE CLIENT TEMPLATE");
+ realm = session1.realms().getRealmByName(REALM_NAME);
+ realm.addClientTemplate("foo-template");
+ session1 = commit(server1, session1, true);
+
+ assertInvalidations(listener1realms.getInvalidationsAndClear(), 2, 3, realm.getId());
+ assertInvalidations(listener2realms.getInvalidationsAndClear(), 0, 2); // realm not cached on server2 due to previous invalidation
+
+
+ // UPDATES
+
+ logger.info("UPDATE ROLE");
+ realm = session1.realms().getRealmByName(REALM_NAME);
+ ClientModel testApp = realm.getClientByClientId("test-app");
+ RoleModel role = session1.realms().getClientRole(realm, testApp, "customer-user");
+ role.setDescription("Foo");
+ session1 = commit(server1, session1, true);
+
+ assertInvalidations(listener1realms.getInvalidationsAndClear(), 2, 3, role.getId());
+ assertInvalidations(listener2realms.getInvalidationsAndClear(), 2, 3, role.getId());
+
+ logger.info("UPDATE GROUP");
+ realm = session1.realms().getRealmByName(REALM_NAME);
+ group = KeycloakModelUtils.findGroupByPath(realm, "/topGroup");
+ group.grantRole(role);
+ session1 = commit(server1, session1, true);
+
+ assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 1, group.getId());
+ assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 1, group.getId());
+
+ logger.info("UPDATE CLIENT");
+ realm = session1.realms().getRealmByName(REALM_NAME);
+ testApp = realm.getClientByClientId("test-app");
+ testApp.setDescription("foo");;
+ session1 = commit(server1, session1, true);
+
+ assertInvalidations(listener1realms.getInvalidationsAndClear(), 2, 3, testApp.getId());
+ assertInvalidations(listener2realms.getInvalidationsAndClear(), 2, 3, testApp.getId());
+
+ // Cache client template on server2
+ KeycloakSession session2 = server2.startSession();
+ realm = session2.realms().getRealmByName(REALM_NAME);
+ realm.getClientTemplates().get(0);
+
+
+ logger.info("UPDATE CLIENT TEMPLATE");
+ realm = session1.realms().getRealmByName(REALM_NAME);
+ ClientTemplateModel clientTemplate = realm.getClientTemplates().get(0);
+ clientTemplate.setDescription("bar");
+
+ session1 = commit(server1, session1, true);
+
+ assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 1, clientTemplate.getId());
+ assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 1, clientTemplate.getId());
+
+ // Nothing yet invalidated in user cache
+ assertInvalidations(listener1users.getInvalidationsAndClear(), 0, 0);
+ assertInvalidations(listener2users.getInvalidationsAndClear(), 0, 0);
+
+ logger.info("UPDATE USER");
+ realm = session1.realms().getRealmByName(REALM_NAME);
+ UserModel user = session1.users().getUserByEmail("keycloak-user@localhost", realm);
+ user.setSingleAttribute("foo", "Bar");
+ session1 = commit(server1, session1, true);
+
+ assertInvalidations(listener1users.getInvalidationsAndClear(), 1, 5, user.getId(), "test.email.keycloak-user@localhost");
+ assertInvalidations(listener2users.getInvalidationsAndClear(), 1, 5, user.getId());
+
+ logger.info("UPDATE USER CONSENTS");
+ realm = session1.realms().getRealmByName(REALM_NAME);
+ testApp = realm.getClientByClientId("test-app");
+ user = session1.users().getUserByEmail("keycloak-user@localhost", realm);
+ session1.users().addConsent(realm, user.getId(), new UserConsentModel(testApp));
+ session1 = commit(server1, session1, true);
+
+ assertInvalidations(listener1users.getInvalidationsAndClear(), 1, 1, user.getId() + ".consents");
+ assertInvalidations(listener2users.getInvalidationsAndClear(), 1, 1, user.getId() + ".consents");
+
+
+ // REMOVALS
+
+ logger.info("REMOVE USER");
+ realm = session1.realms().getRealmByName(REALM_NAME);
+ user = session1.users().getUserByUsername("john-doh@localhost", realm);
+ session1.users().removeUser(realm, user);
+
+ session1 = commit(server1, session1, true);
+
+ assertInvalidations(listener1users.getInvalidationsAndClear(), 3, 5, user.getId(), user.getId() + ".consents", "test.username.john-doh@localhost");
+ assertInvalidations(listener2users.getInvalidationsAndClear(), 2, 5, user.getId(), user.getId() + ".consents");
+
+ cacheEverything();
+
+ logger.info("REMOVE CLIENT TEMPLATE");
+ realm = session1.realms().getRealmByName(REALM_NAME);
+ realm.removeClientTemplate(clientTemplate.getId());
+ session1 = commit(server1, session1, true);
+
+ assertInvalidations(listener1realms.getInvalidationsAndClear(), 2, 5, realm.getId(), clientTemplate.getId());
+ assertInvalidations(listener2realms.getInvalidationsAndClear(), 2, 5, realm.getId(), clientTemplate.getId());
+
+ cacheEverything();
+
+ logger.info("REMOVE ROLE");
+ realm = session1.realms().getRealmByName(REALM_NAME);
+ role = realm.getRole("user");
+ realm.removeRole(role);
+ ClientModel thirdparty = session1.realms().getClientByClientId("third-party", realm);
+ session1 = commit(server1, session1, true);
+
+ assertInvalidations(listener1realms.getInvalidationsAndClear(), 7, 10, role.getId(), realm.getId(), "test.roles", "test.user.roles", testApp.getId(), thirdparty.getId(), group.getId());
+ assertInvalidations(listener2realms.getInvalidationsAndClear(), 7, 10, role.getId(), realm.getId(), "test.roles", "test.user.roles", testApp.getId(), thirdparty.getId(), group.getId());
+
+ // all users invalidated
+ assertInvalidations(listener1users.getInvalidationsAndClear(), 10, 100);
+ assertInvalidations(listener2users.getInvalidationsAndClear(), 10, 100);
+
+ cacheEverything();
+
+ logger.info("REMOVE GROUP");
+ realm = session1.realms().getRealmByName(REALM_NAME);
+ group = realm.getGroupById(group.getId());
+ String subgroupId = group.getSubGroups().iterator().next().getId();
+ realm.removeGroup(group);
+ session1 = commit(server1, session1, true);
+
+ assertInvalidations(listener1realms.getInvalidationsAndClear(), 3, 5, group.getId(), subgroupId, "test.top.groups");
+ assertInvalidations(listener2realms.getInvalidationsAndClear(), 3, 5, group.getId(), subgroupId, "test.top.groups");
+
+ // all users invalidated
+ assertInvalidations(listener1users.getInvalidationsAndClear(), 10, 100);
+ assertInvalidations(listener2users.getInvalidationsAndClear(), 10, 100);
+
+ cacheEverything();
+
+ logger.info("REMOVE CLIENT");
+ realm = session1.realms().getRealmByName(REALM_NAME);
+ testApp = realm.getClientByClientId("test-app");
+ role = testApp.getRole("customer-user");
+ realm.removeClient(testApp.getId());
+ session1 = commit(server1, session1, true);
+
+ assertInvalidations(listener1realms.getInvalidationsAndClear(), 8, 12, testApp.getId(), testApp.getId() + ".roles", role.getId(), testApp.getId() + ".customer-user.roles", "test.realm.clients", thirdparty.getId());
+ assertInvalidations(listener2realms.getInvalidationsAndClear(), 8, 12, testApp.getId(), testApp.getId() + ".roles", role.getId(), testApp.getId() + ".customer-user.roles", "test.realm.clients", thirdparty.getId());
+
+ // all users invalidated
+ assertInvalidations(listener1users.getInvalidationsAndClear(), 10, 100);
+ assertInvalidations(listener2users.getInvalidationsAndClear(), 10, 100);
+
+ cacheEverything();
+
+ logger.info("REMOVE REALM");
+ realm = session1.realms().getRealmByName(REALM_NAME);
+ session1.realms().removeRealm(realm.getId());
+ session1 = commit(server1, session1, true);
+
+ assertInvalidations(listener1realms.getInvalidationsAndClear(), 50, 200, realm.getId(), thirdparty.getId());
+ assertInvalidations(listener2realms.getInvalidationsAndClear(), 50, 200, realm.getId(), thirdparty.getId());
+
+ // all users invalidated
+ assertInvalidations(listener1users.getInvalidationsAndClear(), 10, 100);
+ assertInvalidations(listener2users.getInvalidationsAndClear(), 10, 100);
+
+
+ //Thread.sleep(10000000);
+ }
+
+ private void assertInvalidations(Map<String, Object> invalidations, int low, int high, String... expectedNames) {
+ int size = invalidations.size();
+ Assert.assertTrue("Size was " + size + ". Entries were: " + invalidations.keySet(), size >= low);
+ Assert.assertTrue("Size was " + size + ". Entries were: " + invalidations.keySet(), size <= high);
+
+ for (String expected : expectedNames) {
+ Assert.assertTrue("Can't find " + expected + ". Entries were: " + invalidations.keySet(), invalidations.keySet().contains(expected));
+ }
+ }
+
+ private KeycloakSession commit(KeycloakRule rule, KeycloakSession session, boolean sleepAfterCommit) throws Exception {
+ session.getTransactionManager().commit();
+ session.close();
+
+ if (sleepAfterCommit) {
+ Thread.sleep(SLEEP_TIME_MS);
+ }
+
+ return rule.startSession();
+ }
+
+ private void cacheEverything() throws Exception {
+ KeycloakSession session1 = server1.startSession();
+ TestCacheUtils.cacheRealmWithEverything(session1, REALM_NAME);
+ session1 = commit(server1, session1, false);
+
+ KeycloakSession session2 = server2.startSession();
+ TestCacheUtils.cacheRealmWithEverything(session2, REALM_NAME);
+ session2 = commit(server1, session2, false);
+ }
+
+
+ @Listener(observation = Listener.Observation.PRE)
+ public static class TestListener {
+
+ private final String name;
+ private final Cache cache; // Just for debugging
+
+ private Map<String, Object> invalidations = new ConcurrentHashMap<>();
+
+ public TestListener(String name, Cache cache) {
+ this.name = name;
+ this.cache = cache;
+ }
+
+ @CacheEntryRemoved
+ public void cacheEntryRemoved(CacheEntryRemovedEvent event) {
+ logger.infof("%s: Invalidated %s: %s", name, event.getKey(), event.getValue());
+ invalidations.put(event.getKey().toString(), event.getValue());
+ }
+
+ Map<String, Object> getInvalidationsAndClear() {
+ Map<String, Object> newMap = new HashMap<>(invalidations);
+ invalidations.clear();
+ return newMap;
+ }
+
+ }
+
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java
new file mode 100644
index 0000000..0c7eff0
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java
@@ -0,0 +1,115 @@
+/*
+ * 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.testsuite.util.cli;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.infinispan.Cache;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class CacheCommands {
+
+ public static class ListCachesCommand extends AbstractCommand {
+
+ @Override
+ public String getName() {
+ return "listCaches";
+ }
+
+ @Override
+ protected void doRunCommand(KeycloakSession session) {
+ InfinispanConnectionProvider ispnProvider = session.getProvider(InfinispanConnectionProvider.class);
+ Set<String> cacheNames = ispnProvider.getCache("realms").getCacheManager().getCacheNames();
+ log.infof("Available caches: %s", cacheNames);
+ }
+
+ }
+
+
+ public static class GetCacheCommand extends AbstractCommand {
+
+ @Override
+ public String getName() {
+ return "getCache";
+ }
+
+ @Override
+ protected void doRunCommand(KeycloakSession session) {
+ String cacheName = getArg(0);
+ InfinispanConnectionProvider ispnProvider = session.getProvider(InfinispanConnectionProvider.class);
+ Cache<Object, Object> cache = ispnProvider.getCache(cacheName);
+ if (cache == null) {
+ log.errorf("Cache '%s' doesn't exist", cacheName);
+ throw new HandledException();
+ }
+
+ printCache(cache);
+ }
+
+ private void printCache(Cache<Object, Object> cache) {
+ int size = cache.size();
+ log.infof("Cache %s, size: %d", cache.getName(), size);
+
+ if (size > 50) {
+ log.info("Skip printing cache recors due to big size");
+ } else {
+ for (Map.Entry<Object, Object> entry : cache.entrySet()) {
+ log.infof("%s=%s", entry.getKey(), entry.getValue());
+ }
+ }
+ }
+
+ @Override
+ public String printUsage() {
+ return super.printUsage() + " <cache-name> . cache-name is name of the infinispan cache provided by InfinispanConnectionProvider";
+ }
+
+ }
+
+
+ public static class CacheRealmObjectsCommand extends AbstractCommand {
+
+ @Override
+ public String getName() {
+ return "cacheRealmObjects";
+ }
+
+ @Override
+ protected void doRunCommand(KeycloakSession session) {
+ String realmName = getArg(0);
+ RealmModel realm = session.realms().getRealmByName(realmName);
+ if (realm == null) {
+ log.errorf("Realm not found: %s", realmName);
+ throw new HandledException();
+ }
+
+ TestCacheUtils.cacheRealmWithEverything(session, realmName);
+ }
+
+ @Override
+ public String printUsage() {
+ return super.printUsage() + " <realm-name>";
+ }
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java
new file mode 100644
index 0000000..2c71c72
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java
@@ -0,0 +1,127 @@
+/*
+ * 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.testsuite.util.cli;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionTask;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleContainerModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RoleCommands {
+
+ public static class CreateRoles extends AbstractCommand {
+
+ private String rolePrefix;
+ private String roleContainer;
+
+ @Override
+ public String getName() {
+ return "createRoles";
+ }
+
+ private class StateHolder {
+ int firstInThisBatch;
+ int countInThisBatch;
+ int remaining;
+ };
+
+ @Override
+ protected void doRunCommand(KeycloakSession session) {
+ rolePrefix = getArg(0);
+ roleContainer = getArg(1);
+ int first = getIntArg(2);
+ int count = getIntArg(3);
+ int batchCount = getIntArg(4);
+
+ final StateHolder state = new StateHolder();
+ state.firstInThisBatch = first;
+ state.remaining = count;
+ state.countInThisBatch = Math.min(batchCount, state.remaining);
+ while (state.remaining > 0) {
+ KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), new KeycloakSessionTask() {
+
+ @Override
+ public void run(KeycloakSession session) {
+ createRolesInBatch(session, roleContainer, rolePrefix, state.firstInThisBatch, state.countInThisBatch);
+ }
+ });
+
+ // update state
+ state.firstInThisBatch = state.firstInThisBatch + state.countInThisBatch;
+ state.remaining = state.remaining - state.countInThisBatch;
+ state.countInThisBatch = Math.min(batchCount, state.remaining);
+ }
+
+ log.infof("Command finished. All roles from %s to %s created", rolePrefix + first, rolePrefix + (first + count - 1));
+ }
+
+ private void createRolesInBatch(KeycloakSession session, String roleContainer, String rolePrefix, int first, int count) {
+ RoleContainerModel container = getRoleContainer(session, roleContainer);
+
+ int last = first + count;
+ for (int counter = first; counter < last; counter++) {
+ String roleName = rolePrefix + counter;
+ RoleModel role = container.addRole(roleName);
+ }
+ log.infof("Roles from %s to %s created", rolePrefix + first, rolePrefix + (last - 1));
+ }
+
+ private RoleContainerModel getRoleContainer(KeycloakSession session, String roleContainer) {
+ String[] parts = roleContainer.split("/");
+ String realmName = parts[0];
+
+ RealmModel realm = session.realms().getRealmByName(realmName);
+ if (realm == null) {
+ log.errorf("Unknown realm: %s", realmName);
+ throw new HandledException();
+ }
+
+ if (parts.length == 1) {
+ return realm;
+ } else {
+ String clientId = parts[1];
+ ClientModel client = session.realms().getClientByClientId(clientId, realm);
+ if (client == null) {
+ log.errorf("Unknown client: %s", clientId);
+ throw new HandledException();
+ }
+
+ return client;
+ }
+ }
+
+ @Override
+ public String printUsage() {
+ return super.printUsage() + " <role-prefix> <role-container> <starting-role-offset> <total-count> <batch-size> . " +
+ "\n'total-count' refers to total count of newly created roles. 'batch-size' refers to number of created roles in each transaction. 'starting-role-offset' refers to starting role offset." +
+ "\nFor example if 'starting-role-offset' is 15 and total-count is 10 and role-prefix is 'test', it will create roles test15, test16, test17, ... , test24" +
+ "\n'role-container' is either realm (then use just realmName like 'demo' or client (then use realm/clientId like 'demo/my-client' .\n" +
+ "Example usage: " + super.printUsage() + " test demo 0 500 100";
+ }
+
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestCacheUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestCacheUtils.java
new file mode 100644
index 0000000..9792f9d
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestCacheUtils.java
@@ -0,0 +1,88 @@
+/*
+ * 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.testsuite.util.cli;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.FederatedIdentityModel;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleContainerModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class TestCacheUtils {
+
+ public static void cacheRealmWithEverything(KeycloakSession session, String realmName) {
+ RealmModel realm = session.realms().getRealmByName(realmName);
+
+ for (ClientModel client : realm.getClients()) {
+ realm.getClientById(client.getId());
+ realm.getClientByClientId(client.getClientId());
+
+ cacheRoles(session, realm, client);
+ }
+
+ cacheRoles(session, realm, realm);
+
+ for (GroupModel group : realm.getTopLevelGroups()) {
+ cacheGroupRecursive(realm, group);
+ }
+
+ for (ClientTemplateModel clientTemplate : realm.getClientTemplates()) {
+ realm.getClientTemplateById(clientTemplate.getId());
+ }
+
+ for (UserModel user : session.users().getUsers(realm)) {
+ session.users().getUserById(user.getId(), realm);
+ if (user.getEmail() != null) {
+ session.users().getUserByEmail(user.getEmail(), realm);
+ }
+ session.users().getUserByUsername(user.getUsername(), realm);
+
+ session.users().getConsents(realm, user.getId());
+
+ for (FederatedIdentityModel fedIdentity : session.users().getFederatedIdentities(user, realm)) {
+ session.users().getUserByFederatedIdentity(fedIdentity, realm);
+ }
+ }
+ }
+
+ private static void cacheRoles(KeycloakSession session, RealmModel realm, RoleContainerModel roleContainer) {
+ for (RoleModel role : roleContainer.getRoles()) {
+ realm.getRoleById(role.getId());
+ roleContainer.getRole(role.getName());
+ if (roleContainer instanceof RealmModel) {
+ session.realms().getRealmRole(realm, role.getName());
+ } else {
+ session.realms().getClientRole(realm, (ClientModel) roleContainer, role.getName());
+ }
+ }
+ }
+
+ private static void cacheGroupRecursive(RealmModel realm, GroupModel group) {
+ realm.getGroupById(group.getId());
+ for (GroupModel sub : group.getSubGroups()) {
+ cacheGroupRecursive(realm, sub);
+ }
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java
index 8e9582b..9b2c17a 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java
@@ -57,7 +57,11 @@ public class TestsuiteCLI {
UserCommands.Remove.class,
UserCommands.Count.class,
UserCommands.GetUser.class,
- SyncDummyFederationProviderCommand.class
+ SyncDummyFederationProviderCommand.class,
+ RoleCommands.CreateRoles.class,
+ CacheCommands.ListCachesCommand.class,
+ CacheCommands.GetCacheCommand.class,
+ CacheCommands.CacheRealmObjectsCommand.class
};
private final KeycloakSessionFactory sessionFactory;
diff --git a/testsuite/integration/src/test/resources/log4j.properties b/testsuite/integration/src/test/resources/log4j.properties
index f0ff6ac..2fa1d70 100755
--- a/testsuite/integration/src/test/resources/log4j.properties
+++ b/testsuite/integration/src/test/resources/log4j.properties
@@ -46,7 +46,8 @@ log4j.logger.org.keycloak.connections.jpa.updater.liquibase=${keycloak.liquibase
# log4j.logger.org.keycloak.models.sessions.infinispan.initializer=trace
# Enable to view cache activity
-# log4j.logger.org.keycloak.models.cache=trace
+#log4j.logger.org.keycloak.cluster.infinispan=trace
+#log4j.logger.org.keycloak.models.cache.infinispan=debug
# Enable to view database updates
# log4j.logger.org.keycloak.connections.mongo.updater.DefaultMongoUpdaterProvider=debug
diff --git a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
index 06b4e52..3f4ddd1 100755
--- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
@@ -97,7 +97,10 @@
"default": {
"clustered": "${keycloak.connectionsInfinispan.clustered:false}",
"async": "${keycloak.connectionsInfinispan.async:false}",
- "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}"
+ "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}",
+ "remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:false}",
+ "remoteStoreHost": "${keycloak.connectionsInfinispan.remoteStoreHost:localhost}",
+ "remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}"
}
},
diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
index 840fa2c..3c8f429 100755
--- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
+++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
@@ -92,10 +92,10 @@
<replacement placeholder="CACHE-CONTAINERS">
<cache-container name="keycloak" jndi-name="infinispan/Keycloak">
<transport lock-timeout="60000"/>
- <invalidation-cache name="realms" mode="SYNC"/>
- <invalidation-cache name="users" mode="SYNC">
+ <local-cache name="realms"/>
+ <local-cache name="users">
<eviction max-entries="10000" strategy="LRU"/>
- </invalidation-cache>
+ </local-cache>
<distributed-cache name="sessions" mode="SYNC" owners="1"/>
<distributed-cache name="offlineSessions" mode="SYNC" owners="1"/>
<distributed-cache name="loginFailures" mode="SYNC" owners="1"/>