keycloak-aplcache
Changes
model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java 12(+6 -6)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedClient.java 41(+41 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedClientRole.java 32(+32 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedClientTemplate.java 34(+34 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedGroup.java 31(+31 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedRealm.java 59(+59 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedRealmRole.java 31(+31 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedUser.java 30(+30 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/Revisioned.java 10(+10 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/RevisionedCacheRealmProvider.java 398(+398 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/RevisionedCacheRealmProviderFactory.java 163(+163 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/RevisionedConnectionProviderFactory.java 47(+47 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/RevisionedRealmCache.java 278(+278 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/skewed/RepeatableReadWriteSkewCacheRealmProviderFactory.java 160(+160 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/skewed/RepeatableReadWriteSkewConnectionProviderFactory.java 164(+164 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/skewed/RepeatableReadWriteSkewRealmCache.java 269(+269 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/skewed/RepeatableReadWriteSkewRealmCacheProvider.java 472(+472 -0)
model/infinispan/src/main/resources/META-INF/services/org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory 3(+2 -1)
model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.CacheRealmProviderFactory 3(+2 -1)
Details
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 45ce0d2..b90ef33 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
@@ -37,11 +37,11 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
protected static final Logger logger = Logger.getLogger(DefaultInfinispanConnectionProviderFactory.class);
- private Config.Scope config;
+ protected Config.Scope config;
- private EmbeddedCacheManager cacheManager;
+ protected EmbeddedCacheManager cacheManager;
- private boolean containerManaged;
+ protected boolean containerManaged;
@Override
public InfinispanConnectionProvider create(KeycloakSession session) {
@@ -73,7 +73,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
}
- private void lazyInit() {
+ protected void lazyInit() {
if (cacheManager == null) {
synchronized (this) {
if (cacheManager == null) {
@@ -88,7 +88,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
}
}
- private void initContainerManaged(String cacheContainerLookup) {
+ protected void initContainerManaged(String cacheContainerLookup) {
try {
cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup);
containerManaged = true;
@@ -99,7 +99,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
}
}
- private void initEmbedded() {
+ protected void initEmbedded() {
GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
boolean clustered = config.getBoolean("clustered", false);
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedClient.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedClient.java
new file mode 100755
index 0000000..c07725e
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedClient.java
@@ -0,0 +1,41 @@
+package org.keycloak.models.cache.infinispan.counter.entities;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RealmProvider;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.cache.RealmCache;
+import org.keycloak.models.cache.entities.CachedClient;
+import org.keycloak.models.cache.entities.CachedClientRole;
+import org.keycloak.models.cache.infinispan.counter.Revisioned;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class RevisionedCachedClient extends CachedClient implements Revisioned {
+
+ public RevisionedCachedClient(Long revision, RealmCache cache, RealmProvider delegate, RealmModel realm, ClientModel model) {
+ super(cache, delegate, realm, model);
+ this.revision = revision;
+ }
+
+ private Long revision;
+
+ @Override
+ public Long getRevision() {
+ return revision;
+ }
+
+ @Override
+ public void setRevision(Long revision) {
+ this.revision = revision;
+ }
+
+ @Override
+ protected void cacheRoles(RealmCache cache, RealmModel realm, ClientModel model) {
+ for (RoleModel role : model.getRoles()) {
+ roles.put(role.getName(), role.getId());
+ }
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedClientRole.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedClientRole.java
new file mode 100755
index 0000000..2bdb176
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedClientRole.java
@@ -0,0 +1,32 @@
+package org.keycloak.models.cache.infinispan.counter.entities;
+
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.cache.entities.CachedClientRole;
+import org.keycloak.models.cache.infinispan.counter.Revisioned;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class RevisionedCachedClientRole extends CachedClientRole implements Revisioned {
+
+ public RevisionedCachedClientRole(Long revision, String idClient, RoleModel model, RealmModel realm) {
+ super(idClient, model, realm);
+ this.revision = revision;
+ }
+
+ private Long revision;
+
+ @Override
+ public Long getRevision() {
+ return revision;
+ }
+
+ @Override
+ public void setRevision(Long revision) {
+ this.revision = revision;
+ }
+
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedClientTemplate.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedClientTemplate.java
new file mode 100755
index 0000000..eeca1f6
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedClientTemplate.java
@@ -0,0 +1,34 @@
+package org.keycloak.models.cache.infinispan.counter.entities;
+
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RealmProvider;
+import org.keycloak.models.cache.RealmCache;
+import org.keycloak.models.cache.entities.CachedClientTemplate;
+import org.keycloak.models.cache.infinispan.counter.Revisioned;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class RevisionedCachedClientTemplate extends CachedClientTemplate implements Revisioned {
+
+ public RevisionedCachedClientTemplate(Long revision, RealmCache cache, RealmProvider delegate, RealmModel realm, ClientTemplateModel model) {
+ super(cache, delegate, realm, model);
+ this.revision = revision;
+ }
+
+ private Long revision;
+
+ @Override
+ public Long getRevision() {
+ return revision;
+ }
+
+ @Override
+ public void setRevision(Long revision) {
+ this.revision = revision;
+ }
+
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedGroup.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedGroup.java
new file mode 100755
index 0000000..32529ea
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedGroup.java
@@ -0,0 +1,31 @@
+package org.keycloak.models.cache.infinispan.counter.entities;
+
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.cache.entities.CachedGroup;
+import org.keycloak.models.cache.infinispan.counter.Revisioned;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class RevisionedCachedGroup extends CachedGroup implements Revisioned {
+ public RevisionedCachedGroup(Long revision, RealmModel realm, GroupModel group) {
+ super(realm, group);
+ this.revision = revision;
+ }
+
+ private Long revision;
+
+ @Override
+ public Long getRevision() {
+ return revision;
+ }
+
+ @Override
+ public void setRevision(Long revision) {
+ this.revision = revision;
+ }
+
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedRealm.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedRealm.java
new file mode 100755
index 0000000..c527ea5
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedRealm.java
@@ -0,0 +1,59 @@
+package org.keycloak.models.cache.infinispan.counter.entities;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RealmProvider;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.cache.RealmCache;
+import org.keycloak.models.cache.entities.CachedClient;
+import org.keycloak.models.cache.entities.CachedClientTemplate;
+import org.keycloak.models.cache.entities.CachedRealm;
+import org.keycloak.models.cache.entities.CachedRealmRole;
+import org.keycloak.models.cache.entities.CachedRole;
+import org.keycloak.models.cache.infinispan.counter.Revisioned;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class RevisionedCachedRealm extends CachedRealm implements Revisioned {
+
+ public RevisionedCachedRealm(Long revision, RealmCache cache, RealmProvider delegate, RealmModel model) {
+ super(cache, delegate, model);
+ this.revision = revision;
+ }
+
+ private Long revision;
+
+ @Override
+ public Long getRevision() {
+ return revision;
+ }
+
+ @Override
+ public void setRevision(Long revision) {
+ this.revision = revision;
+ }
+
+ @Override
+ protected void cacheClientTemplates(RealmCache cache, RealmProvider delegate, RealmModel model) {
+ for (ClientTemplateModel template : model.getClientTemplates()) {
+ clientTemplates.add(template.getId());
+ }
+ }
+
+ @Override
+ protected void cacheClients(RealmCache cache, RealmProvider delegate, RealmModel model) {
+ for (ClientModel client : model.getClients()) {
+ clients.put(client.getClientId(), client.getId());
+ }
+ }
+
+ @Override
+ protected void cacheRealmRoles(RealmCache cache, RealmModel model) {
+ for (RoleModel role : model.getRoles()) {
+ realmRoles.put(role.getName(), role.getId());
+ }
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedRealmRole.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedRealmRole.java
new file mode 100755
index 0000000..776b885
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedRealmRole.java
@@ -0,0 +1,31 @@
+package org.keycloak.models.cache.infinispan.counter.entities;
+
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.cache.entities.CachedRealmRole;
+import org.keycloak.models.cache.infinispan.counter.Revisioned;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class RevisionedCachedRealmRole extends CachedRealmRole implements Revisioned {
+
+ public RevisionedCachedRealmRole(Long revision, RoleModel model, RealmModel realm) {
+ super(model, realm);
+ this.revision = revision;
+ }
+
+ private Long revision;
+
+ @Override
+ public Long getRevision() {
+ return revision;
+ }
+
+ @Override
+ public void setRevision(Long revision) {
+ this.revision = revision;
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedUser.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedUser.java
new file mode 100755
index 0000000..f281bef
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/entities/RevisionedCachedUser.java
@@ -0,0 +1,30 @@
+package org.keycloak.models.cache.infinispan.counter.entities;
+
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.cache.entities.CachedUser;
+import org.keycloak.models.cache.infinispan.counter.Revisioned;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class RevisionedCachedUser extends CachedUser implements Revisioned {
+ public RevisionedCachedUser(Long revision, RealmModel realm, UserModel user) {
+ super(realm, user);
+ this.revision = revision;
+ }
+
+ private Long revision;
+
+ @Override
+ public Long getRevision() {
+ return revision;
+ }
+
+ @Override
+ public void setRevision(Long revision) {
+ this.revision = revision;
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/Revisioned.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/Revisioned.java
new file mode 100755
index 0000000..f4d4c7a
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/Revisioned.java
@@ -0,0 +1,10 @@
+package org.keycloak.models.cache.infinispan.counter;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface Revisioned {
+ Long getRevision();
+ void setRevision(Long revision);
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/RevisionedCacheRealmProvider.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/RevisionedCacheRealmProvider.java
new file mode 100755
index 0000000..e4b68f6
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/RevisionedCacheRealmProvider.java
@@ -0,0 +1,398 @@
+/*
+ * 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.counter;
+
+import org.jboss.logging.Logger;
+import org.keycloak.migration.MigrationModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakTransaction;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RealmProvider;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.cache.CacheRealmProvider;
+import org.keycloak.models.cache.RealmCache;
+import org.keycloak.models.cache.entities.CachedClient;
+import org.keycloak.models.cache.entities.CachedClientRole;
+import org.keycloak.models.cache.entities.CachedClientTemplate;
+import org.keycloak.models.cache.entities.CachedGroup;
+import org.keycloak.models.cache.entities.CachedRealm;
+import org.keycloak.models.cache.entities.CachedRealmRole;
+import org.keycloak.models.cache.entities.CachedRole;
+import org.keycloak.models.cache.infinispan.ClientAdapter;
+import org.keycloak.models.cache.infinispan.ClientTemplateAdapter;
+import org.keycloak.models.cache.infinispan.GroupAdapter;
+import org.keycloak.models.cache.infinispan.RealmAdapter;
+import org.keycloak.models.cache.infinispan.RoleAdapter;
+import org.keycloak.models.cache.infinispan.counter.entities.RevisionedCachedClient;
+import org.keycloak.models.cache.infinispan.counter.entities.RevisionedCachedClientRole;
+import org.keycloak.models.cache.infinispan.counter.entities.RevisionedCachedClientTemplate;
+import org.keycloak.models.cache.infinispan.counter.entities.RevisionedCachedGroup;
+import org.keycloak.models.cache.infinispan.counter.entities.RevisionedCachedRealm;
+import org.keycloak.models.cache.infinispan.counter.entities.RevisionedCachedRealmRole;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class RevisionedCacheRealmProvider implements CacheRealmProvider {
+ protected static final Logger logger = Logger.getLogger(RevisionedCacheRealmProvider.class);
+ protected RevisionedRealmCache cache;
+ protected KeycloakSession session;
+ protected RealmProvider delegate;
+ protected boolean transactionActive;
+ protected boolean setRollbackOnly;
+
+ protected Set<String> realmInvalidations = new HashSet<>();
+ protected Set<String> appInvalidations = new HashSet<>();
+ protected Set<String> clientTemplateInvalidations = new HashSet<>();
+ protected Set<String> roleInvalidations = new HashSet<>();
+ protected Set<String> groupInvalidations = new HashSet<>();
+ protected Map<String, RealmModel> managedRealms = new HashMap<>();
+ protected Map<String, ClientModel> managedApplications = new HashMap<>();
+ protected Map<String, ClientTemplateModel> managedClientTemplates = new HashMap<>();
+ protected Map<String, RoleModel> managedRoles = new HashMap<>();
+ protected Map<String, GroupModel> managedGroups = new HashMap<>();
+
+ protected boolean clearAll;
+
+ public RevisionedCacheRealmProvider(RevisionedRealmCache cache, KeycloakSession session) {
+ this.cache = cache;
+ this.session = session;
+
+ session.getTransaction().enlistAfterCompletion(getTransaction());
+ }
+
+ @Override
+ public void clear() {
+ cache.clear();
+ }
+
+ @Override
+ public MigrationModel getMigrationModel() {
+ return getDelegate().getMigrationModel();
+ }
+
+ @Override
+ public RealmProvider getDelegate() {
+ if (!transactionActive) throw new IllegalStateException("Cannot access delegate without a transaction");
+ if (delegate != null) return delegate;
+ delegate = session.getProvider(RealmProvider.class);
+ return delegate;
+ }
+
+ @Override
+ public void registerRealmInvalidation(String id) {
+ realmInvalidations.add(id);
+ }
+
+ @Override
+ public void registerApplicationInvalidation(String id) {
+ appInvalidations.add(id);
+ }
+ @Override
+ public void registerClientTemplateInvalidation(String id) {
+ clientTemplateInvalidations.add(id);
+ }
+
+ @Override
+ public void registerRoleInvalidation(String id) {
+ roleInvalidations.add(id);
+ }
+
+ @Override
+ public void registerGroupInvalidation(String id) {
+ groupInvalidations.add(id);
+
+ }
+
+ protected void runInvalidations() {
+ for (String id : realmInvalidations) {
+ cache.invalidateCachedRealmById(id);
+ }
+ for (String id : roleInvalidations) {
+ cache.invalidateRoleById(id);
+ }
+ for (String id : groupInvalidations) {
+ cache.invalidateGroupById(id);
+ }
+ for (String id : appInvalidations) {
+ cache.invalidateCachedApplicationById(id);
+ }
+ for (String id : clientTemplateInvalidations) {
+ cache.invalidateCachedClientTemplateById(id);
+ }
+ }
+
+ private KeycloakTransaction getTransaction() {
+ return new KeycloakTransaction() {
+ @Override
+ public void begin() {
+ transactionActive = true;
+ }
+
+ @Override
+ public void commit() {
+ if (delegate == null) return;
+ if (clearAll) {
+ cache.clear();
+ }
+ runInvalidations();
+ transactionActive = false;
+ }
+
+ @Override
+ public void rollback() {
+ setRollbackOnly = true;
+ runInvalidations();
+ transactionActive = false;
+ }
+
+ @Override
+ public void setRollbackOnly() {
+ setRollbackOnly = true;
+ }
+
+ @Override
+ public boolean getRollbackOnly() {
+ return setRollbackOnly;
+ }
+
+ @Override
+ public boolean isActive() {
+ return transactionActive;
+ }
+ };
+ }
+
+ @Override
+ public RealmModel createRealm(String name) {
+ RealmModel realm = getDelegate().createRealm(name);
+ registerRealmInvalidation(realm.getId());
+ return realm;
+ }
+
+ @Override
+ public RealmModel createRealm(String id, String name) {
+ RealmModel realm = getDelegate().createRealm(id, name);
+ registerRealmInvalidation(realm.getId());
+ return realm;
+ }
+
+ @Override
+ public RealmModel getRealm(String id) {
+ CachedRealm cached = cache.getCachedRealm(id);
+ if (cached != null) {
+ logger.tracev("by id cache hit: {0}", cached.getName());
+ }
+ if (cached == null) {
+ Long loaded = cache.getCurrentRevision(id);
+ RealmModel model = getDelegate().getRealm(id);
+ if (model == null) return null;
+ if (realmInvalidations.contains(id)) return model;
+ cached = new RevisionedCachedRealm(loaded, cache, this, model);
+ cache.addCachedRealm(cached);
+ } else if (realmInvalidations.contains(id)) {
+ return getDelegate().getRealm(id);
+ } else if (managedRealms.containsKey(id)) {
+ return managedRealms.get(id);
+ }
+ RealmAdapter adapter = new RealmAdapter(cached, this);
+ managedRealms.put(id, adapter);
+ return adapter;
+ }
+
+ @Override
+ public RealmModel getRealmByName(String name) {
+ CachedRealm cached = cache.getCachedRealmByName(name);
+ if (cached != null) {
+ logger.tracev("by name cache hit: {0}", cached.getName());
+ }
+ if (cached == null) {
+ RealmModel model = getDelegate().getRealmByName(name);
+ if (model == null) return null;
+ if (realmInvalidations.contains(model.getId())) return model;
+ cached = new RevisionedCachedRealm(null, cache, this, model);
+ cache.addCachedRealm(cached);
+ } else if (realmInvalidations.contains(cached.getId())) {
+ return getDelegate().getRealmByName(name);
+ } else if (managedRealms.containsKey(cached.getId())) {
+ return managedRealms.get(cached.getId());
+ }
+ RealmAdapter adapter = new RealmAdapter(cached, this);
+ managedRealms.put(cached.getId(), adapter);
+ return adapter;
+ }
+
+ @Override
+ public List<RealmModel> getRealms() {
+ // Retrieve realms from backend
+ List<RealmModel> backendRealms = getDelegate().getRealms();
+
+ // Return cache delegates to ensure cache invalidated during write operations
+ List<RealmModel> cachedRealms = new LinkedList<RealmModel>();
+ for (RealmModel realm : backendRealms) {
+ RealmModel cached = getRealm(realm.getId());
+ cachedRealms.add(cached);
+ }
+ return cachedRealms;
+ }
+
+ @Override
+ public boolean removeRealm(String id) {
+ cache.invalidateCachedRealmById(id);
+
+ RealmModel realm = getDelegate().getRealm(id);
+ Set<RoleModel> realmRoles = null;
+ if (realm != null) {
+ realmRoles = realm.getRoles();
+ }
+
+ boolean didIt = getDelegate().removeRealm(id);
+ realmInvalidations.add(id);
+
+ // TODO: Temporary workaround to invalidate cached realm roles
+ if (didIt && realmRoles != null) {
+ for (RoleModel role : realmRoles) {
+ roleInvalidations.add(role.getId());
+ }
+ }
+
+ return didIt;
+ }
+
+ @Override
+ public void close() {
+ if (delegate != null) delegate.close();
+ }
+
+ @Override
+ public RoleModel getRoleById(String id, RealmModel realm) {
+ CachedRole cached = cache.getRole(id);
+ if (cached != null && !cached.getRealm().equals(realm.getId())) {
+ cached = null;
+ }
+
+ if (cached == null) {
+ Long loaded = cache.getCurrentRevision(id);
+ RoleModel model = getDelegate().getRoleById(id, realm);
+ if (model == null) return null;
+ if (roleInvalidations.contains(id)) return model;
+ if (model.getContainer() instanceof ClientModel) {
+ cached = new RevisionedCachedClientRole(loaded, ((ClientModel) model.getContainer()).getId(), model, realm);
+ } else {
+ cached = new RevisionedCachedRealmRole(loaded, model, realm);
+ }
+ cache.addCachedRole(cached);
+
+ } else if (roleInvalidations.contains(id)) {
+ return getDelegate().getRoleById(id, realm);
+ } else if (managedRoles.containsKey(id)) {
+ return managedRoles.get(id);
+ }
+ RoleAdapter adapter = new RoleAdapter(cached, cache, this, realm);
+ managedRoles.put(id, adapter);
+ return adapter;
+ }
+
+ @Override
+ public GroupModel getGroupById(String id, RealmModel realm) {
+ CachedGroup cached = cache.getGroup(id);
+ if (cached != null && !cached.getRealm().equals(realm.getId())) {
+ cached = null;
+ }
+
+ if (cached == null) {
+ Long loaded = cache.getCurrentRevision(id);
+ GroupModel model = getDelegate().getGroupById(id, realm);
+ if (model == null) return null;
+ if (groupInvalidations.contains(id)) return model;
+ cached = new RevisionedCachedGroup(loaded, realm, model);
+ cache.addCachedGroup(cached);
+
+ } else if (groupInvalidations.contains(id)) {
+ return getDelegate().getGroupById(id, realm);
+ } else if (managedGroups.containsKey(id)) {
+ return managedGroups.get(id);
+ }
+ GroupAdapter adapter = new GroupAdapter(cached, this, session, realm);
+ managedGroups.put(id, adapter);
+ return adapter;
+ }
+
+ @Override
+ public ClientModel getClientById(String id, RealmModel realm) {
+ CachedClient cached = cache.getApplication(id);
+ if (cached != null && !cached.getRealm().equals(realm.getId())) {
+ cached = null;
+ }
+ if (cached != null && cached.getClientId().equals("client")) {
+ logger.infov("client by id cache hit: {0}", cached.getClientId());
+ }
+
+ if (cached == null) {
+ Long loaded = cache.getCurrentRevision(id);
+ ClientModel model = getDelegate().getClientById(id, realm);
+ if (model == null) return null;
+ if (appInvalidations.contains(id)) return model;
+ cached = new RevisionedCachedClient(loaded, cache, getDelegate(), realm, model);
+ cache.addCachedClient(cached);
+ } else if (appInvalidations.contains(id)) {
+ return getDelegate().getClientById(id, realm);
+ } else if (managedApplications.containsKey(id)) {
+ return managedApplications.get(id);
+ }
+ ClientAdapter adapter = new ClientAdapter(realm, cached, this, cache);
+ managedApplications.put(id, adapter);
+ return adapter;
+ }
+ @Override
+ public ClientTemplateModel getClientTemplateById(String id, RealmModel realm) {
+ CachedClientTemplate cached = cache.getClientTemplate(id);
+ if (cached != null && !cached.getRealm().equals(realm.getId())) {
+ cached = null;
+ }
+
+ if (cached == null) {
+ Long loaded = cache.getCurrentRevision(id);
+ ClientTemplateModel model = getDelegate().getClientTemplateById(id, realm);
+ if (model == null) return null;
+ if (clientTemplateInvalidations.contains(id)) return model;
+ cached = new RevisionedCachedClientTemplate(loaded, cache, getDelegate(), realm, model);
+ cache.addCachedClientTemplate(cached);
+ } else if (clientTemplateInvalidations.contains(id)) {
+ return getDelegate().getClientTemplateById(id, realm);
+ } else if (managedClientTemplates.containsKey(id)) {
+ return managedClientTemplates.get(id);
+ }
+ ClientTemplateModel adapter = new ClientTemplateAdapter(realm, cached, this, cache);
+ managedClientTemplates.put(id, adapter);
+ return adapter;
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/RevisionedCacheRealmProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/RevisionedCacheRealmProviderFactory.java
new file mode 100755
index 0000000..ae7ca69
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/RevisionedCacheRealmProviderFactory.java
@@ -0,0 +1,163 @@
+/*
+ * 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.counter;
+
+import org.infinispan.Cache;
+import org.infinispan.notifications.Listener;
+import org.infinispan.notifications.cachelistener.annotation.CacheEntriesEvicted;
+import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
+import org.infinispan.notifications.cachelistener.annotation.CacheEntryInvalidated;
+import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved;
+import org.infinispan.notifications.cachelistener.event.CacheEntriesEvictedEvent;
+import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent;
+import org.infinispan.notifications.cachelistener.event.CacheEntryInvalidatedEvent;
+import org.infinispan.notifications.cachelistener.event.CacheEntryRemovedEvent;
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.cache.CacheRealmProvider;
+import org.keycloak.models.cache.CacheRealmProviderFactory;
+import org.keycloak.models.cache.entities.CachedClient;
+import org.keycloak.models.cache.entities.CachedRealm;
+import org.keycloak.models.cache.infinispan.DefaultCacheRealmProvider;
+import org.keycloak.models.cache.infinispan.InfinispanRealmCache;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class RevisionedCacheRealmProviderFactory implements CacheRealmProviderFactory {
+
+ private static final Logger log = Logger.getLogger(RevisionedCacheRealmProviderFactory.class);
+
+ protected volatile RevisionedRealmCache realmCache;
+
+ protected final ConcurrentHashMap<String, String> realmLookup = new ConcurrentHashMap<>();
+
+ @Override
+ public CacheRealmProvider create(KeycloakSession session) {
+ lazyInit(session);
+ return new RevisionedCacheRealmProvider(realmCache, session);
+ }
+
+ private void lazyInit(KeycloakSession session) {
+ if (realmCache == null) {
+ synchronized (this) {
+ if (realmCache == null) {
+ Cache<String, Object> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
+ Cache<String, Long> counterCache = session.getProvider(InfinispanConnectionProvider.class).getCache(RevisionedConnectionProviderFactory.COUNTER_CACHE_NAME);
+ cache.addListener(new CacheListener());
+ realmCache = new RevisionedRealmCache(cache, counterCache, realmLookup);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public String getId() {
+ return "infinispan-revisioned";
+ }
+
+ @Listener
+ public class CacheListener {
+
+ @CacheEntryCreated
+ public void created(CacheEntryCreatedEvent<String, Object> event) {
+ if (!event.isPre()) {
+ Object object = event.getValue();
+ if (object != null) {
+ if (object instanceof CachedRealm) {
+ CachedRealm realm = (CachedRealm) object;
+ realmLookup.put(realm.getName(), realm.getId());
+ log.tracev("Realm added realm={0}", realm.getName());
+ }
+ }
+ }
+ }
+
+ @CacheEntryRemoved
+ public void removed(CacheEntryRemovedEvent<String, Object> event) {
+ if (event.isPre()) {
+ Object object = event.getValue();
+ if (object != null) {
+ remove(object);
+ }
+ }
+ }
+
+ @CacheEntryInvalidated
+ public void removed(CacheEntryInvalidatedEvent<String, Object> event) {
+ if (event.isPre()) {
+ Object object = event.getValue();
+ if (object != null) {
+ remove(object);
+ }
+ }
+ }
+
+ @CacheEntriesEvicted
+ public void userEvicted(CacheEntriesEvictedEvent<String, Object> event) {
+ for (Object object : event.getEntries().values()) {
+ remove(object);
+ }
+ }
+
+ private void remove(Object object) {
+ if (object instanceof CachedRealm) {
+ CachedRealm realm = (CachedRealm) object;
+
+ realmLookup.remove(realm.getName());
+
+ for (String r : realm.getRealmRoles().values()) {
+ realmCache.evictCachedRoleById(r);
+ }
+
+ for (String c : realm.getClients().values()) {
+ realmCache.evictCachedApplicationById(c);
+ }
+
+ log.tracev("Realm removed realm={0}", realm.getName());
+ } else if (object instanceof CachedClient) {
+ CachedClient client = (CachedClient) object;
+
+ for (String r : client.getRoles().values()) {
+ realmCache.evictCachedRoleById(r);
+ }
+
+ log.tracev("Client removed client={0}", client.getId());
+ }
+ }
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/RevisionedConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/RevisionedConnectionProviderFactory.java
new file mode 100755
index 0000000..4b8c3f6
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/RevisionedConnectionProviderFactory.java
@@ -0,0 +1,47 @@
+/*
+ * 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.counter;
+
+import org.infinispan.configuration.cache.Configuration;
+import org.infinispan.configuration.cache.ConfigurationBuilder;
+import org.jboss.logging.Logger;
+import org.keycloak.connections.infinispan.DefaultInfinispanConnectionProviderFactory;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class RevisionedConnectionProviderFactory extends DefaultInfinispanConnectionProviderFactory {
+ public static final String COUNTER_CACHE_NAME = "COUNTER_CACHE";
+
+ protected static final Logger logger = Logger.getLogger(RevisionedConnectionProviderFactory.class);
+
+ @Override
+ public String getId() {
+ return "revisioned";
+ }
+
+
+ protected void initEmbedded() {
+ super.initEmbedded();
+ ConfigurationBuilder counterConfigBuilder = new ConfigurationBuilder();
+ Configuration counterCacheConfiguration = counterConfigBuilder.build();
+
+ cacheManager.defineConfiguration(COUNTER_CACHE_NAME, counterCacheConfiguration);
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/RevisionedRealmCache.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/RevisionedRealmCache.java
new file mode 100755
index 0000000..c2b6d6f
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/RevisionedRealmCache.java
@@ -0,0 +1,278 @@
+/*
+ * 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.counter;
+
+import org.infinispan.Cache;
+import org.jboss.logging.Logger;
+import org.keycloak.models.cache.RealmCache;
+import org.keycloak.models.cache.entities.CachedClient;
+import org.keycloak.models.cache.entities.CachedClientTemplate;
+import org.keycloak.models.cache.entities.CachedGroup;
+import org.keycloak.models.cache.entities.CachedRealm;
+import org.keycloak.models.cache.entities.CachedRole;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class RevisionedRealmCache implements RealmCache {
+
+ protected static final Logger logger = Logger.getLogger(RevisionedRealmCache.class);
+
+ protected final Cache<String, Long> revisions;
+ protected final Cache<String, Object> cache;
+ final AtomicLong realmCounter = new AtomicLong();
+ final AtomicLong clientCounter = new AtomicLong();
+ final AtomicLong clientTemplateCounter = new AtomicLong();
+ final AtomicLong roleCounter = new AtomicLong();
+ final AtomicLong groupCounter = new AtomicLong();
+
+ protected final ConcurrentHashMap<String, String> realmLookup;
+
+ public RevisionedRealmCache(Cache<String, Object> cache, Cache<String, Long> revisions, ConcurrentHashMap<String, String> realmLookup) {
+ this.cache = cache;
+ this.realmLookup = realmLookup;
+ this.revisions = revisions;
+ }
+
+ public Cache<String, Object> getCache() {
+ return cache;
+ }
+
+ public Long getCurrentRevision(String id) {
+ return revisions.get(id);
+ }
+ @Override
+ public void clear() {
+ cache.clear();
+ }
+
+ public AtomicLong getRealmCounter() {
+ return realmCounter;
+ }
+
+ public AtomicLong getClientCounter() {
+ return clientCounter;
+ }
+
+ public AtomicLong getClientTemplateCounter() {
+ return clientTemplateCounter;
+ }
+
+ public AtomicLong getRoleCounter() {
+ return roleCounter;
+ }
+
+ public AtomicLong getGroupCounter() {
+ return groupCounter;
+ }
+
+ @Override
+ public CachedRealm getCachedRealm(String id) {
+ return get(id, CachedRealm.class);
+ }
+
+ @Override
+ public void invalidateCachedRealm(CachedRealm realm) {
+ logger.tracev("Invalidating realm {0}", realm.getId());
+ invalidateObject(realm.getId(), realmCounter);
+ realmLookup.remove(realm.getName());
+ }
+
+ @Override
+ public void invalidateCachedRealmById(String id) {
+ CachedRealm cached = (CachedRealm) invalidateObject(id, realmCounter);
+ if (cached != null) realmLookup.remove(cached.getName());
+ }
+
+ protected Object invalidateObject(String id, AtomicLong counter) {
+ revisions.put(id, counter.incrementAndGet());
+ Object removed = cache.remove(id);
+ revisions.put(id, counter.incrementAndGet());
+ return removed;
+ }
+
+ @Override
+ public void addCachedRealm(CachedRealm realm) {
+ logger.tracev("Adding realm {0}", realm.getId());
+ addRevisioned(realm.getId(), (Revisioned) realm, realmCounter);
+ realmLookup.put(realm.getName(), realm.getId());
+ }
+
+ protected void addRevisioned(String id, Revisioned object, AtomicLong counter) {
+ Long rev = revisions.get(id);
+ if (rev == null) {
+ rev = counter.incrementAndGet();
+ revisions.put(id, rev);
+ }
+ if (rev.equals(object.getRevision())) {
+ cache.putForExternalRead(id, object);
+ }
+ }
+
+ @Override
+ public CachedRealm getCachedRealmByName(String name) {
+ String id = realmLookup.get(name);
+ return id != null ? getCachedRealm(id) : null;
+ }
+
+ @Override
+ public CachedClient getApplication(String id) {
+ return get(id, CachedClient.class);
+ }
+
+ @Override
+ public void invalidateApplication(CachedClient app) {
+ logger.infov("Removing application {0}", app.getClientAuthenticatorType());
+ invalidateObject(app.getId(), clientCounter);
+ }
+
+ @Override
+ public void addCachedClient(CachedClient app) {
+ logger.tracev("Adding application {0}", app.getId());
+ addRevisioned(app.getId(), (Revisioned) app, clientCounter);
+ }
+
+ @Override
+ public void invalidateCachedApplicationById(String id) {
+ CachedClient client = (CachedClient)invalidateObject(id, clientCounter);
+ if (client != null) logger.infov("Removing application {0}", client.getClientId());
+ }
+
+ @Override
+ public void evictCachedApplicationById(String id) {
+ logger.tracev("Evicting application {0}", id);
+ cache.evict(id);
+ }
+
+ @Override
+ public CachedGroup getGroup(String id) {
+ return get(id, CachedGroup.class);
+ }
+
+ @Override
+ public void invalidateGroup(CachedGroup role) {
+ logger.tracev("Removing group {0}", role.getId());
+ invalidateObject(role.getId(), groupCounter);
+ }
+
+ @Override
+ public void addCachedGroup(CachedGroup role) {
+ logger.tracev("Adding group {0}", role.getId());
+ addRevisioned(role.getId(), (Revisioned) role, groupCounter);
+ }
+
+ @Override
+ public void invalidateCachedGroupById(String id) {
+ logger.tracev("Removing group {0}", id);
+ invalidateObject(id, groupCounter);
+
+ }
+
+ @Override
+ public void invalidateGroupById(String id) {
+ logger.tracev("Removing group {0}", id);
+ invalidateObject(id, groupCounter);
+ }
+
+ @Override
+ public CachedRole getRole(String id) {
+ return get(id, CachedRole.class);
+ }
+
+ @Override
+ public void invalidateRole(CachedRole role) {
+ logger.tracev("Removing role {0}", role.getId());
+ invalidateObject(role.getId(), roleCounter);
+ }
+
+ @Override
+ public void invalidateRoleById(String id) {
+ logger.tracev("Removing role {0}", id);
+ invalidateObject(id, roleCounter);
+ }
+
+ @Override
+ public void evictCachedRoleById(String id) {
+ logger.tracev("Evicting role {0}", id);
+ cache.evict(id);
+ }
+
+ @Override
+ public void addCachedRole(CachedRole role) {
+ logger.tracev("Adding role {0}", role.getId());
+ addRevisioned(role.getId(), (Revisioned) role, roleCounter);
+ }
+
+ @Override
+ public void invalidateCachedRoleById(String id) {
+ logger.tracev("Removing role {0}", id);
+ invalidateObject(id, roleCounter);
+ }
+
+ private <T> T get(String id, Class<T> type) {
+ Revisioned o = (Revisioned)cache.get(id);
+ if (o == null) {
+ return null;
+ }
+ Long rev = revisions.get(id);
+ if (rev == null) {
+ logger.tracev("get() missing rev");
+ return null;
+ }
+ long oRev = o.getRevision() == null ? -1L : o.getRevision().longValue();
+ if (rev > oRev) {
+ logger.tracev("get() rev: {0} o.rev: {1}", rev.longValue(), oRev);
+ return null;
+ }
+ return o != null && type.isInstance(o) ? type.cast(o) : null;
+ }
+
+ @Override
+ public CachedClientTemplate getClientTemplate(String id) {
+ return get(id, CachedClientTemplate.class);
+ }
+
+ @Override
+ public void invalidateClientTemplate(CachedClientTemplate app) {
+ logger.tracev("Removing client template {0}", app.getId());
+ invalidateObject(app.getId(), clientTemplateCounter);
+ }
+
+ @Override
+ public void addCachedClientTemplate(CachedClientTemplate app) {
+ logger.tracev("Adding client template {0}", app.getId());
+ addRevisioned(app.getId(), (Revisioned) app, clientTemplateCounter);
+ }
+
+ @Override
+ public void invalidateCachedClientTemplateById(String id) {
+ logger.tracev("Removing client template {0}", id);
+ invalidateObject(id, clientTemplateCounter);
+ }
+
+ @Override
+ public void evictCachedClientTemplateById(String id) {
+ logger.tracev("Evicting client template {0}", id);
+ cache.evict(id);
+ }
+
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/skewed/RepeatableReadWriteSkewCacheRealmProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/skewed/RepeatableReadWriteSkewCacheRealmProviderFactory.java
new file mode 100755
index 0000000..7dd49c5
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/skewed/RepeatableReadWriteSkewCacheRealmProviderFactory.java
@@ -0,0 +1,160 @@
+/*
+ * 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.skewed;
+
+import org.infinispan.Cache;
+import org.infinispan.notifications.Listener;
+import org.infinispan.notifications.cachelistener.annotation.CacheEntriesEvicted;
+import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
+import org.infinispan.notifications.cachelistener.annotation.CacheEntryInvalidated;
+import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved;
+import org.infinispan.notifications.cachelistener.event.CacheEntriesEvictedEvent;
+import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent;
+import org.infinispan.notifications.cachelistener.event.CacheEntryInvalidatedEvent;
+import org.infinispan.notifications.cachelistener.event.CacheEntryRemovedEvent;
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.cache.CacheRealmProvider;
+import org.keycloak.models.cache.CacheRealmProviderFactory;
+import org.keycloak.models.cache.entities.CachedClient;
+import org.keycloak.models.cache.entities.CachedRealm;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class RepeatableReadWriteSkewCacheRealmProviderFactory implements CacheRealmProviderFactory {
+
+ private static final Logger log = Logger.getLogger(RepeatableReadWriteSkewCacheRealmProviderFactory.class);
+
+ protected volatile RepeatableReadWriteSkewRealmCache realmCache;
+
+ protected final ConcurrentHashMap<String, String> realmLookup = new ConcurrentHashMap<>();
+
+ @Override
+ public CacheRealmProvider create(KeycloakSession session) {
+ lazyInit(session);
+ return new RepeatableReadWriteSkewRealmCacheProvider(realmCache, session);
+ }
+
+ private void lazyInit(KeycloakSession session) {
+ if (realmCache == null) {
+ synchronized (this) {
+ if (realmCache == null) {
+ Cache<String, Object> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
+ cache.addListener(new CacheListener());
+ realmCache = new RepeatableReadWriteSkewRealmCache(cache, realmLookup);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public String getId() {
+ return "infinispan-versioned";
+ }
+
+ @Listener
+ public class CacheListener {
+
+ @CacheEntryCreated
+ public void created(CacheEntryCreatedEvent<String, Object> event) {
+ if (!event.isPre()) {
+ Object object = event.getValue();
+ if (object != null) {
+ if (object instanceof CachedRealm) {
+ CachedRealm realm = (CachedRealm) object;
+ realmLookup.put(realm.getName(), realm.getId());
+ log.tracev("Realm added realm={0}", realm.getName());
+ }
+ }
+ }
+ }
+
+ @CacheEntryRemoved
+ public void removed(CacheEntryRemovedEvent<String, Object> event) {
+ if (event.isPre()) {
+ Object object = event.getValue();
+ if (object != null) {
+ remove(object);
+ }
+ }
+ }
+
+ @CacheEntryInvalidated
+ public void removed(CacheEntryInvalidatedEvent<String, Object> event) {
+ if (event.isPre()) {
+ Object object = event.getValue();
+ if (object != null) {
+ remove(object);
+ }
+ }
+ }
+
+ @CacheEntriesEvicted
+ public void userEvicted(CacheEntriesEvictedEvent<String, Object> event) {
+ for (Object object : event.getEntries().values()) {
+ remove(object);
+ }
+ }
+
+ private void remove(Object object) {
+ if (object instanceof CachedRealm) {
+ CachedRealm realm = (CachedRealm) object;
+
+ realmLookup.remove(realm.getName());
+
+ for (String r : realm.getRealmRoles().values()) {
+ realmCache.evictCachedRoleById(r);
+ }
+
+ for (String c : realm.getClients().values()) {
+ realmCache.evictCachedApplicationById(c);
+ }
+
+ log.tracev("Realm removed realm={0}", realm.getName());
+ } else if (object instanceof CachedClient) {
+ CachedClient client = (CachedClient) object;
+
+ for (String r : client.getRoles().values()) {
+ realmCache.evictCachedRoleById(r);
+ }
+
+ log.tracev("Client removed client={0}", client.getId());
+ }
+ }
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/skewed/RepeatableReadWriteSkewConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/skewed/RepeatableReadWriteSkewConnectionProviderFactory.java
new file mode 100755
index 0000000..b389347
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/skewed/RepeatableReadWriteSkewConnectionProviderFactory.java
@@ -0,0 +1,164 @@
+/*
+ * 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.skewed;
+
+import org.infinispan.configuration.cache.CacheMode;
+import org.infinispan.configuration.cache.Configuration;
+import org.infinispan.configuration.cache.ConfigurationBuilder;
+import org.infinispan.configuration.cache.VersioningScheme;
+import org.infinispan.configuration.global.GlobalConfigurationBuilder;
+import org.infinispan.manager.DefaultCacheManager;
+import org.infinispan.manager.EmbeddedCacheManager;
+import org.infinispan.transaction.lookup.DummyTransactionManagerLookup;
+import org.infinispan.util.concurrent.IsolationLevel;
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.connections.infinispan.DefaultInfinispanConnectionProvider;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+import javax.naming.InitialContext;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class RepeatableReadWriteSkewConnectionProviderFactory implements InfinispanConnectionProviderFactory {
+
+ protected static final Logger logger = Logger.getLogger(RepeatableReadWriteSkewConnectionProviderFactory.class);
+
+ private Config.Scope config;
+
+ private EmbeddedCacheManager cacheManager;
+
+ private boolean containerManaged;
+
+ @Override
+ public InfinispanConnectionProvider create(KeycloakSession session) {
+ lazyInit();
+
+ return new DefaultInfinispanConnectionProvider(cacheManager);
+ }
+
+ @Override
+ public void close() {
+ if (cacheManager != null && !containerManaged) {
+ cacheManager.stop();
+ }
+ cacheManager = null;
+ }
+
+ @Override
+ public String getId() {
+ return "versioned";
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ this.config = config;
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ private void lazyInit() {
+ if (cacheManager == null) {
+ synchronized (this) {
+ if (cacheManager == null) {
+ String cacheContainer = config.get("cacheContainer");
+ if (cacheContainer != null) {
+ initContainerManaged(cacheContainer);
+ } else {
+ initEmbedded();
+ }
+ }
+ }
+ }
+ }
+
+ private void initContainerManaged(String cacheContainerLookup) {
+ try {
+ cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup);
+ containerManaged = true;
+
+ logger.debugv("Using container managed Infinispan cache container, lookup={1}", cacheContainerLookup);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to retrieve cache container", e);
+ }
+ }
+
+ private void initEmbedded() {
+ GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
+
+ boolean clustered = config.getBoolean("clustered", false);
+ boolean async = config.getBoolean("async", true);
+ boolean allowDuplicateJMXDomains = config.getBoolean("allowDuplicateJMXDomains", true);
+
+ if (clustered) {
+ gcb.transport().defaultTransport();
+ }
+ gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
+
+ cacheManager = new DefaultCacheManager(gcb.build());
+ containerManaged = false;
+
+ logger.debug("Started embedded Infinispan cache container");
+
+ ConfigurationBuilder invalidationConfigBuilder = new ConfigurationBuilder();
+ if (clustered) {
+ invalidationConfigBuilder.clustering().cacheMode(async ? CacheMode.INVALIDATION_ASYNC : CacheMode.INVALIDATION_SYNC);
+ }
+
+ invalidationConfigBuilder.transaction().transactionManagerLookup(new DummyTransactionManagerLookup());
+ invalidationConfigBuilder.locking().isolationLevel(IsolationLevel.REPEATABLE_READ).writeSkewCheck(true).versioning().enable().scheme(VersioningScheme.SIMPLE);
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_CACHE_NAME, invalidationConfigBuilder.build());
+
+ ConfigurationBuilder userConfigBuilder = new ConfigurationBuilder();
+ if (clustered) {
+ userConfigBuilder.clustering().cacheMode(async ? CacheMode.INVALIDATION_ASYNC : CacheMode.INVALIDATION_SYNC);
+ }
+ Configuration userCacheConfiguration = userConfigBuilder.build();
+
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_CACHE_NAME, userCacheConfiguration);
+
+ ConfigurationBuilder sessionConfigBuilder = new ConfigurationBuilder();
+ if (clustered) {
+ String sessionsMode = config.get("sessionsMode", "distributed");
+ if (sessionsMode.equalsIgnoreCase("replicated")) {
+ sessionConfigBuilder.clustering().cacheMode(async ? CacheMode.REPL_ASYNC : CacheMode.REPL_SYNC);
+ } else if (sessionsMode.equalsIgnoreCase("distributed")) {
+ sessionConfigBuilder.clustering().cacheMode(async ? CacheMode.DIST_ASYNC : CacheMode.DIST_SYNC);
+ } else {
+ throw new RuntimeException("Invalid value for sessionsMode");
+ }
+
+ sessionConfigBuilder.clustering().hash()
+ .numOwners(config.getInt("sessionsOwners", 2))
+ .numSegments(config.getInt("sessionsSegments", 60)).build();
+ }
+
+ Configuration sessionCacheConfiguration = sessionConfigBuilder.build();
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, sessionCacheConfiguration);
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, sessionCacheConfiguration);
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, sessionCacheConfiguration);
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/skewed/RepeatableReadWriteSkewRealmCache.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/skewed/RepeatableReadWriteSkewRealmCache.java
new file mode 100755
index 0000000..bb0b807
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/skewed/RepeatableReadWriteSkewRealmCache.java
@@ -0,0 +1,269 @@
+/*
+ * 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.skewed;
+
+import org.infinispan.Cache;
+import org.jboss.logging.Logger;
+import org.keycloak.models.cache.RealmCache;
+import org.keycloak.models.cache.entities.CachedClient;
+import org.keycloak.models.cache.entities.CachedClientTemplate;
+import org.keycloak.models.cache.entities.CachedGroup;
+import org.keycloak.models.cache.entities.CachedRealm;
+import org.keycloak.models.cache.entities.CachedRole;
+
+import javax.transaction.NotSupportedException;
+import javax.transaction.Status;
+import javax.transaction.SystemException;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class RepeatableReadWriteSkewRealmCache implements RealmCache {
+
+ protected static final Logger logger = Logger.getLogger(RepeatableReadWriteSkewRealmCache.class);
+
+ protected final Cache<String, Object> cache;
+ protected final ConcurrentHashMap<String, String> realmLookup;
+
+ public RepeatableReadWriteSkewRealmCache(Cache<String, Object> cache, ConcurrentHashMap<String, String> realmLookup) {
+ this.cache = cache;
+ this.realmLookup = realmLookup;
+ }
+
+ public Cache<String, Object> getCache() {
+ return cache;
+ }
+
+ public void startBatch() {
+ logger.trace("*** START BATCH ***");
+ try {
+ if (cache.getAdvancedCache().getTransactionManager().getStatus() == Status.STATUS_NO_TRANSACTION) {
+ cache.getAdvancedCache().getTransactionManager().begin();
+ }
+ } catch (NotSupportedException e) {
+ throw new RuntimeException(e);
+ } catch (SystemException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ public void endBatch(boolean commit) {
+ logger.trace("*** END BATCH ***");
+ try {
+ if (cache.getAdvancedCache().getTransactionManager().getStatus() == Status.STATUS_ACTIVE) {
+ if (commit) {
+ cache.getAdvancedCache().getTransactionManager().commit();
+
+ } else {
+ cache.getAdvancedCache().getTransactionManager().rollback();
+
+ }
+ }
+ } catch (Exception e) {
+ //throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void clear() {
+ cache.clear();
+ }
+
+ @Override
+ public CachedRealm getCachedRealm(String id) {
+ return get(id, CachedRealm.class);
+ }
+
+ @Override
+ public void invalidateCachedRealm(CachedRealm realm) {
+ logger.tracev("Invalidating realm {0}", realm.getId());
+ invalidate(realm.getId());
+ realmLookup.remove(realm.getName());
+ }
+
+ protected Object invalidate(String id) {
+ startBatch();
+ Object rtn = cache.remove(id);
+ logger.trace("*** END BATCH ***");
+ try {
+ if (cache.getAdvancedCache().getTransactionManager().getStatus() == Status.STATUS_ACTIVE) {
+ if (true) {
+ cache.getAdvancedCache().getTransactionManager().commit();
+
+ } else {
+ cache.getAdvancedCache().getTransactionManager().rollback();
+
+ }
+ }
+ } catch (Exception e) {
+ logger.info("Failed to commit invalidate");
+ }
+ return rtn;
+ }
+
+ @Override
+ public void invalidateCachedRealmById(String id) {
+ CachedRealm cached = (CachedRealm) invalidate(id);
+ if (cached != null) realmLookup.remove(cached.getName());
+ }
+
+ @Override
+ public void addCachedRealm(CachedRealm realm) {
+ logger.tracev("Adding realm {0}", realm.getId());
+ cache.putForExternalRead(realm.getId(), realm);
+ realmLookup.put(realm.getName(), realm.getId());
+ }
+
+ @Override
+ public CachedRealm getCachedRealmByName(String name) {
+ String id = realmLookup.get(name);
+ return id != null ? getCachedRealm(id) : null;
+ }
+
+ @Override
+ public CachedClient getApplication(String id) {
+ return get(id, CachedClient.class);
+ }
+
+ @Override
+ public void invalidateApplication(CachedClient app) {
+ logger.tracev("Removing application {0}", app.getId());
+ invalidate(app.getId());
+ }
+
+ @Override
+ public void addCachedClient(CachedClient app) {
+ logger.tracev("Adding application {0}", app.getId());
+ cache.putForExternalRead(app.getId(), app);
+ }
+
+ @Override
+ public void invalidateCachedApplicationById(String id) {
+ logger.tracev("Removing application {0}", id);
+ invalidate(id);
+ }
+
+ @Override
+ public void evictCachedApplicationById(String id) {
+ logger.tracev("Evicting application {0}", id);
+ cache.evict(id);
+ }
+
+ @Override
+ public CachedGroup getGroup(String id) {
+ return get(id, CachedGroup.class);
+ }
+
+ @Override
+ public void invalidateGroup(CachedGroup role) {
+ logger.tracev("Removing group {0}", role.getId());
+ invalidate(role.getId());
+ }
+
+ @Override
+ public void addCachedGroup(CachedGroup role) {
+ logger.tracev("Adding group {0}", role.getId());
+ cache.putForExternalRead(role.getId(), role);
+ }
+
+ @Override
+ public void invalidateCachedGroupById(String id) {
+ logger.tracev("Removing group {0}", id);
+ invalidate(id);
+
+ }
+
+ @Override
+ public void invalidateGroupById(String id) {
+ logger.tracev("Removing group {0}", id);
+ invalidate(id);
+ }
+
+ @Override
+ public CachedRole getRole(String id) {
+ return get(id, CachedRole.class);
+ }
+
+ @Override
+ public void invalidateRole(CachedRole role) {
+ logger.tracev("Removing role {0}", role.getId());
+ invalidate(role.getId());
+ }
+
+ @Override
+ public void invalidateRoleById(String id) {
+ logger.tracev("Removing role {0}", id);
+ invalidate(id);
+ }
+
+ @Override
+ public void evictCachedRoleById(String id) {
+ logger.tracev("Evicting role {0}", id);
+ cache.evict(id);
+ }
+
+ @Override
+ public void addCachedRole(CachedRole role) {
+ logger.tracev("Adding role {0}", role.getId());
+ cache.putForExternalRead(role.getId(), role);
+ }
+
+ @Override
+ public void invalidateCachedRoleById(String id) {
+ logger.tracev("Removing role {0}", id);
+ invalidate(id);
+ }
+
+ private <T> T get(String id, Class<T> type) {
+ Object o = cache.get(id);
+ return o != null && type.isInstance(o) ? type.cast(o) : null;
+ }
+
+ @Override
+ public CachedClientTemplate getClientTemplate(String id) {
+ return get(id, CachedClientTemplate.class);
+ }
+
+ @Override
+ public void invalidateClientTemplate(CachedClientTemplate app) {
+ logger.tracev("Removing client template {0}", app.getId());
+ invalidate(app.getId());
+ }
+
+ @Override
+ public void addCachedClientTemplate(CachedClientTemplate app) {
+ logger.tracev("Adding client template {0}", app.getId());
+ cache.putForExternalRead(app.getId(), app);
+ }
+
+ @Override
+ public void invalidateCachedClientTemplateById(String id) {
+ logger.tracev("Removing client template {0}", id);
+ invalidate(id);
+ }
+
+ @Override
+ public void evictCachedClientTemplateById(String id) {
+ logger.tracev("Evicting client template {0}", id);
+ invalidate(id);
+ }
+
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/skewed/RepeatableReadWriteSkewRealmCacheProvider.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/skewed/RepeatableReadWriteSkewRealmCacheProvider.java
new file mode 100755
index 0000000..fe7d6c2
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/skewed/RepeatableReadWriteSkewRealmCacheProvider.java
@@ -0,0 +1,472 @@
+/*
+ * 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.skewed;
+
+import org.jboss.logging.Logger;
+import org.keycloak.migration.MigrationModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakTransaction;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RealmProvider;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.cache.CacheRealmProvider;
+import org.keycloak.models.cache.entities.CachedClient;
+import org.keycloak.models.cache.entities.CachedClientRole;
+import org.keycloak.models.cache.entities.CachedClientTemplate;
+import org.keycloak.models.cache.entities.CachedGroup;
+import org.keycloak.models.cache.entities.CachedRealm;
+import org.keycloak.models.cache.entities.CachedRealmRole;
+import org.keycloak.models.cache.entities.CachedRole;
+import org.keycloak.models.cache.infinispan.ClientAdapter;
+import org.keycloak.models.cache.infinispan.ClientTemplateAdapter;
+import org.keycloak.models.cache.infinispan.GroupAdapter;
+import org.keycloak.models.cache.infinispan.RealmAdapter;
+import org.keycloak.models.cache.infinispan.RoleAdapter;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+
+/**
+ * DO NOT USE THIS!!
+ *
+ * Tries unsuccessfully to use Infinispan with REPEATABLE_READ, write-skew-checking
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class RepeatableReadWriteSkewRealmCacheProvider implements CacheRealmProvider {
+ protected static final Logger logger = Logger.getLogger(RepeatableReadWriteSkewRealmCacheProvider.class);
+
+ protected RepeatableReadWriteSkewRealmCache cache;
+ protected KeycloakSession session;
+ protected RealmProvider delegate;
+ protected boolean transactionActive;
+ protected boolean setRollbackOnly;
+
+ protected Set<String> realmInvalidations = new HashSet<>();
+ protected Set<String> appInvalidations = new HashSet<>();
+ protected Set<String> clientTemplateInvalidations = new HashSet<>();
+ protected Set<String> roleInvalidations = new HashSet<>();
+ protected Set<String> groupInvalidations = new HashSet<>();
+ protected Map<String, RealmModel> managedRealms = new HashMap<>();
+ protected Map<String, ClientModel> managedApplications = new HashMap<>();
+ protected Map<String, ClientTemplateModel> managedClientTemplates = new HashMap<>();
+ protected Map<String, RoleModel> managedRoles = new HashMap<>();
+ protected Map<String, GroupModel> managedGroups = new HashMap<>();
+
+ protected boolean clearAll;
+
+ public RepeatableReadWriteSkewRealmCacheProvider(RepeatableReadWriteSkewRealmCache cache, KeycloakSession session) {
+ this.cache = cache;
+ this.session = session;
+
+ session.getTransaction().enlistAfterCompletion(getTransaction());
+ }
+
+ @Override
+ public void clear() {
+ cache.clear();
+ }
+
+ @Override
+ public MigrationModel getMigrationModel() {
+ return getDelegate().getMigrationModel();
+ }
+
+ @Override
+ public RealmProvider getDelegate() {
+ if (!transactionActive) throw new IllegalStateException("Cannot access delegate without a transaction");
+ if (delegate != null) return delegate;
+ delegate = session.getProvider(RealmProvider.class);
+ return delegate;
+ }
+
+ @Override
+ public void registerRealmInvalidation(String id) {
+ realmInvalidations.add(id);
+ }
+
+ @Override
+ public void registerApplicationInvalidation(String id) {
+ appInvalidations.add(id);
+ }
+ @Override
+ public void registerClientTemplateInvalidation(String id) {
+ clientTemplateInvalidations.add(id);
+ }
+
+ @Override
+ public void registerRoleInvalidation(String id) {
+ roleInvalidations.add(id);
+ }
+
+ @Override
+ public void registerGroupInvalidation(String id) {
+ groupInvalidations.add(id);
+
+ }
+
+ protected void runInvalidations() {
+ for (String id : realmInvalidations) {
+ cache.invalidateCachedRealmById(id);
+ }
+ for (String id : roleInvalidations) {
+ cache.invalidateRoleById(id);
+ }
+ for (String id : groupInvalidations) {
+ cache.invalidateGroupById(id);
+ }
+ for (String id : appInvalidations) {
+ cache.invalidateCachedApplicationById(id);
+ }
+ for (String id : clientTemplateInvalidations) {
+ cache.invalidateCachedClientTemplateById(id);
+ }
+ }
+
+ private KeycloakTransaction getTransaction() {
+ return new KeycloakTransaction() {
+ @Override
+ public void begin() {
+ transactionActive = true;
+ }
+
+ @Override
+ public void commit() {
+ if (delegate == null) return;
+ if (clearAll) {
+ cache.clear();
+ }
+ runInvalidations();
+ transactionActive = false;
+ }
+
+ @Override
+ public void rollback() {
+ setRollbackOnly = true;
+ runInvalidations();
+ transactionActive = false;
+ }
+
+ @Override
+ public void setRollbackOnly() {
+ setRollbackOnly = true;
+ }
+
+ @Override
+ public boolean getRollbackOnly() {
+ return setRollbackOnly;
+ }
+
+ @Override
+ public boolean isActive() {
+ return transactionActive;
+ }
+ };
+ }
+
+ @Override
+ public RealmModel createRealm(String name) {
+ RealmModel realm = getDelegate().createRealm(name);
+ registerRealmInvalidation(realm.getId());
+ return realm;
+ }
+
+ @Override
+ public RealmModel createRealm(String id, String name) {
+ RealmModel realm = getDelegate().createRealm(id, name);
+ registerRealmInvalidation(realm.getId());
+ return realm;
+ }
+
+ @Override
+ public RealmModel getRealm(String id) {
+ //cache.startBatch();
+ cache.startBatch();
+ boolean batchEnded = false;
+ try {
+ CachedRealm cached = cache.getCachedRealm(id);
+ boolean wasNull = cached == null;
+ if (cached == null) {
+ RealmModel model = getDelegate().getRealm(id);
+ if (model == null) return null;
+ if (realmInvalidations.contains(id)) return model;
+ cached = new CachedRealm(cache, this, model);
+ cache.addCachedRealm(cached);
+ try {
+ batchEnded = true;
+ cache.endBatch(true);
+ logger.trace("returning new cached realm");
+ } catch (Exception exception) {
+ logger.info("failed to add to cache", exception);
+ return model;
+ }
+ } else if (realmInvalidations.contains(id)) {
+ return getDelegate().getRealm(id);
+ } else if (managedRealms.containsKey(id)) {
+ return managedRealms.get(id);
+ }
+ if (!wasNull) logger.trace("returning cached realm: " + cached.getName());
+ RealmAdapter adapter = new RealmAdapter(cached, this);
+ managedRealms.put(id, adapter);
+ return adapter;
+ } finally {
+ if (!batchEnded) cache.endBatch(true);
+ }
+ }
+
+ @Override
+ public RealmModel getRealmByName(String name) {
+ cache.startBatch();
+ boolean batchEnded = false;
+ try {
+ CachedRealm cached = cache.getCachedRealmByName(name);
+ boolean wasNull = cached == null;
+ if (cached == null) {
+ RealmModel model = getDelegate().getRealmByName(name);
+ if (model == null) return null;
+ if (realmInvalidations.contains(model.getId())) return model;
+ cached = new CachedRealm(cache, this, model);
+ cache.addCachedRealm(cached);
+ try {
+ batchEnded = true;
+ cache.endBatch(true);
+ logger.trace("returning new cached realm: " + cached.getName());
+ } catch (Exception exception) {
+ logger.info("failed to add to cache", exception);
+ return model;
+ }
+ } else if (realmInvalidations.contains(cached.getId())) {
+ return getDelegate().getRealmByName(name);
+ } else if (managedRealms.containsKey(cached.getId())) {
+ return managedRealms.get(cached.getId());
+ }
+ if (!wasNull) logger.trace("returning cached realm: " + cached.getName());
+ RealmAdapter adapter = new RealmAdapter(cached, this);
+ managedRealms.put(cached.getId(), adapter);
+ return adapter;
+ } finally {
+ if (!batchEnded) cache.endBatch(true);
+
+ }
+ }
+
+ @Override
+ public List<RealmModel> getRealms() {
+ // Retrieve realms from backend
+ List<RealmModel> backendRealms = getDelegate().getRealms();
+
+ // Return cache delegates to ensure cache invalidated during write operations
+ List<RealmModel> cachedRealms = new LinkedList<RealmModel>();
+ for (RealmModel realm : backendRealms) {
+ RealmModel cached = getRealm(realm.getId());
+ cachedRealms.add(cached);
+ }
+ return cachedRealms;
+ }
+
+ @Override
+ public boolean removeRealm(String id) {
+ cache.invalidateCachedRealmById(id);
+
+ RealmModel realm = getDelegate().getRealm(id);
+ Set<RoleModel> realmRoles = null;
+ if (realm != null) {
+ realmRoles = realm.getRoles();
+ }
+
+ boolean didIt = getDelegate().removeRealm(id);
+ realmInvalidations.add(id);
+
+ // TODO: Temporary workaround to invalidate cached realm roles
+ if (didIt && realmRoles != null) {
+ for (RoleModel role : realmRoles) {
+ roleInvalidations.add(role.getId());
+ }
+ }
+
+ return didIt;
+ }
+
+ @Override
+ public void close() {
+ if (delegate != null) delegate.close();
+ }
+
+ @Override
+ public RoleModel getRoleById(String id, RealmModel realm) {
+ cache.startBatch();
+ boolean batchEnded = false;
+ try {
+ CachedRole cached = cache.getRole(id);
+ if (cached != null && !cached.getRealm().equals(realm.getId())) {
+ cached = null;
+ }
+
+ if (cached == null) {
+ RoleModel model = getDelegate().getRoleById(id, realm);
+ if (model == null) return null;
+ if (roleInvalidations.contains(id)) return model;
+ if (model.getContainer() instanceof ClientModel) {
+ cached = new CachedClientRole(((ClientModel) model.getContainer()).getId(), model, realm);
+ } else {
+ cached = new CachedRealmRole(model, realm);
+ }
+ cache.addCachedRole(cached);
+ try {
+ batchEnded = true;
+ cache.endBatch(true);
+ } catch (Exception exception) {
+ logger.info("failed to add to cache", exception);
+ return model;
+ }
+
+ } else if (roleInvalidations.contains(id)) {
+ return getDelegate().getRoleById(id, realm);
+ } else if (managedRoles.containsKey(id)) {
+ return managedRoles.get(id);
+ }
+ RoleAdapter adapter = new RoleAdapter(cached, cache, this, realm);
+ managedRoles.put(id, adapter);
+ return adapter;
+ } finally {
+ if (!batchEnded) cache.endBatch(true);
+
+ }
+ }
+
+ @Override
+ public GroupModel getGroupById(String id, RealmModel realm) {
+ cache.startBatch();
+ boolean batchEnded = false;
+ try {
+ CachedGroup cached = cache.getGroup(id);
+ if (cached != null && !cached.getRealm().equals(realm.getId())) {
+ cached = null;
+ }
+
+ if (cached == null) {
+ GroupModel model = getDelegate().getGroupById(id, realm);
+ if (model == null) return null;
+ if (groupInvalidations.contains(id)) return model;
+ cached = new CachedGroup(realm, model);
+ cache.addCachedGroup(cached);
+ try {
+ batchEnded = true;
+ cache.endBatch(true);
+ } catch (Exception exception) {
+ logger.info("failed to add to cache", exception);
+ return model;
+ }
+
+ } else if (groupInvalidations.contains(id)) {
+ return getDelegate().getGroupById(id, realm);
+ } else if (managedGroups.containsKey(id)) {
+ return managedGroups.get(id);
+ }
+ GroupAdapter adapter = new GroupAdapter(cached, this, session, realm);
+ managedGroups.put(id, adapter);
+ return adapter;
+ } finally {
+ if (!batchEnded) cache.endBatch(true);
+
+ }
+ }
+
+ @Override
+ public ClientModel getClientById(String id, RealmModel realm) {
+ cache.startBatch();
+ boolean batchEnded = false;
+ CachedClient cached = cache.getApplication(id);
+ try {
+ if (cached != null && !cached.getRealm().equals(realm.getId())) {
+ cached = null;
+ }
+
+ if (cached == null) {
+ ClientModel model = getDelegate().getClientById(id, realm);
+ if (model == null) return null;
+ if (appInvalidations.contains(id)) return model;
+ cached = new CachedClient(cache, getDelegate(), realm, model);
+ cache.addCachedClient(cached);
+ try {
+ batchEnded = true;
+ cache.endBatch(true);
+ } catch (Exception exception) {
+ logger.info("failed to add to cache", exception);
+ return model;
+ }
+ } else if (appInvalidations.contains(id)) {
+ return getDelegate().getClientById(id, realm);
+ } else if (managedApplications.containsKey(id)) {
+ return managedApplications.get(id);
+ }
+ ClientAdapter adapter = new ClientAdapter(realm, cached, this, cache);
+ managedApplications.put(id, adapter);
+ return adapter;
+ } finally {
+ if (!batchEnded) cache.endBatch(true);
+
+ }
+ }
+ @Override
+ public ClientTemplateModel getClientTemplateById(String id, RealmModel realm) {
+ cache.startBatch();
+ boolean batchEnded = false;
+ try {
+ CachedClientTemplate cached = cache.getClientTemplate(id);
+ if (cached != null && !cached.getRealm().equals(realm.getId())) {
+ cached = null;
+ }
+
+ if (cached == null) {
+ ClientTemplateModel model = getDelegate().getClientTemplateById(id, realm);
+ if (model == null) return null;
+ if (clientTemplateInvalidations.contains(id)) return model;
+ cached = new CachedClientTemplate(cache, getDelegate(), realm, model);
+ cache.addCachedClientTemplate(cached);
+ try {
+ batchEnded = true;
+ cache.endBatch(true);
+ } catch (Exception exception) {
+ logger.info("failed to add to cache", exception);
+ return model;
+ }
+ } else if (clientTemplateInvalidations.contains(id)) {
+ return getDelegate().getClientTemplateById(id, realm);
+ } else if (managedClientTemplates.containsKey(id)) {
+ return managedClientTemplates.get(id);
+ }
+ ClientTemplateModel adapter = new ClientTemplateAdapter(realm, cached, this, cache);
+ managedClientTemplates.put(id, adapter);
+ return adapter;
+ } finally {
+ if (!batchEnded) cache.endBatch(true);
+
+ }
+ }
+
+}
diff --git a/model/infinispan/src/main/resources/META-INF/services/org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory
old mode 100644
new mode 100755
index 58a6bda..cff9ef1
--- a/model/infinispan/src/main/resources/META-INF/services/org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory
+++ b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory
@@ -15,4 +15,5 @@
# limitations under the License.
#
-org.keycloak.connections.infinispan.DefaultInfinispanConnectionProviderFactory
\ No newline at end of file
+org.keycloak.connections.infinispan.DefaultInfinispanConnectionProviderFactory
+org.keycloak.models.cache.infinispan.counter.RevisionedConnectionProviderFactory
\ No newline at end of file
diff --git a/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.CacheRealmProviderFactory b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.CacheRealmProviderFactory
index 1b79a2f..2d264cf 100755
--- a/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.CacheRealmProviderFactory
+++ b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.CacheRealmProviderFactory
@@ -15,4 +15,5 @@
# limitations under the License.
#
-org.keycloak.models.cache.infinispan.InfinispanCacheRealmProviderFactory
\ No newline at end of file
+org.keycloak.models.cache.infinispan.InfinispanCacheRealmProviderFactory
+org.keycloak.models.cache.infinispan.counter.RevisionedCacheRealmProviderFactory
\ No newline at end of file
diff --git a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/ConcurrencyVersioningTest.java b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/ConcurrencyVersioningTest.java
new file mode 100755
index 0000000..b35ce24
--- /dev/null
+++ b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/ConcurrencyVersioningTest.java
@@ -0,0 +1,267 @@
+package org.keycloak.models.sessions.infinispan.initializer;
+
+import org.infinispan.Cache;
+import org.infinispan.commons.CacheException;
+import org.infinispan.configuration.cache.CacheMode;
+import org.infinispan.configuration.cache.Configuration;
+import org.infinispan.configuration.cache.ConfigurationBuilder;
+import org.infinispan.configuration.cache.VersioningScheme;
+import org.infinispan.configuration.global.GlobalConfigurationBuilder;
+import org.infinispan.manager.DefaultCacheManager;
+import org.infinispan.manager.EmbeddedCacheManager;
+import org.infinispan.transaction.TransactionMode;
+import org.infinispan.transaction.TransactionProtocol;
+import org.infinispan.transaction.lookup.DummyTransactionManagerLookup;
+import org.infinispan.util.concurrent.IsolationLevel;
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+
+import javax.transaction.NotSupportedException;
+import javax.transaction.Status;
+import javax.transaction.SystemException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Unit tests to make sure our model caching concurrency model will work.
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ConcurrencyVersioningTest {
+
+ public static abstract class AbstractThread implements Runnable {
+ EmbeddedCacheManager cacheManager;
+ boolean success;
+ CountDownLatch latch = new CountDownLatch(1);
+
+ public AbstractThread(EmbeddedCacheManager cacheManager) {
+ this.cacheManager = cacheManager;
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public CountDownLatch getLatch() {
+ return latch;
+ }
+ }
+
+ public static class RemoveThread extends AbstractThread {
+ public RemoveThread(EmbeddedCacheManager cacheManager) {
+ super(cacheManager);
+ }
+
+ public void run() {
+ Cache<String, String> cache = cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
+ try {
+ startBatch(cache);
+ cache.remove("key");
+ //cache.getAdvancedCache().getTransactionManager().commit();
+ endBatch(cache);
+ success = true;
+ } catch (Exception e) {
+ success = false;
+ }
+ latch.countDown();
+ }
+
+ }
+
+
+ public static class UpdateThread extends AbstractThread {
+ public UpdateThread(EmbeddedCacheManager cacheManager) {
+ super(cacheManager);
+ }
+
+ public void run() {
+ Cache<String, String> cache = cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
+ try {
+ startBatch(cache);
+ cache.putForExternalRead("key", "value2");
+ //cache.getAdvancedCache().getTransactionManager().commit();
+ endBatch(cache);
+ success = true;
+ } catch (Exception e) {
+ success = false;
+ }
+ latch.countDown();
+ }
+
+ }
+
+ /**
+ * Tests that if remove executes before put, then put still succeeds.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testGetRemovePutOnNonExisting() throws Exception {
+ final DefaultCacheManager cacheManager = getVersionedCacheManager();
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+
+ RemoveThread removeThread = new RemoveThread(cacheManager);
+
+ Cache<String, String> cache = cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
+ cache.remove("key");
+ startBatch(cache);
+ cache.get("key");
+ executor.execute(removeThread);
+ removeThread.getLatch().await();
+ cache.putForExternalRead("key", "value1");
+ endBatch(cache);
+ Assert.assertEquals(cache.get("key"), "value1");
+ Assert.assertTrue(removeThread.isSuccess());
+ }
+
+
+ /**
+ * Test that if a put of an existing key is removed after the put and before tx commit, it is evicted
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testGetRemovePutOnExisting() throws Exception {
+ final DefaultCacheManager cacheManager = getVersionedCacheManager();
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+
+ RemoveThread removeThread = new RemoveThread(cacheManager);
+
+ Cache<String, String> cache = cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
+ cache.put("key", "value0");
+ startBatch(cache);
+ cache.get("key");
+ executor.execute(removeThread);
+ removeThread.getLatch().await();
+ cache.put("key", "value1");
+ try {
+ endBatch(cache);
+ Assert.fail("Write skew should be detected");
+ } catch (Exception e) {
+
+
+ }
+ Assert.assertNull(cache.get("key"));
+ Assert.assertTrue(removeThread.isSuccess());
+ }
+
+ /**
+ * Test that if a put of an existing key is removed after the put and before tx commit, it is evicted
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testGetRemovePutEternalOnExisting() throws Exception {
+ final DefaultCacheManager cacheManager = getVersionedCacheManager();
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+
+ RemoveThread removeThread = new RemoveThread(cacheManager);
+
+ Cache<String, String> cache = cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
+ cache.put("key", "value0");
+ startBatch(cache);
+ cache.get("key");
+ executor.execute(removeThread);
+ cache.putForExternalRead("key", "value1");
+ removeThread.getLatch().await();
+ try {
+ endBatch(cache);
+// Assert.fail("Write skew should be detected");
+ } catch (Exception e) {
+
+ }
+ Assert.assertNull(cache.get("key"));
+ Assert.assertTrue(removeThread.isSuccess());
+ }
+
+ @Test
+ public void testPutExternalRemoveOnExisting() throws Exception {
+ final DefaultCacheManager cacheManager = getVersionedCacheManager();
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+
+ RemoveThread removeThread = new RemoveThread(cacheManager);
+
+ Cache<String, String> cache = cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
+ cache.put("key", "value0");
+ startBatch(cache);
+ cache.putForExternalRead("key", "value1");
+ executor.execute(removeThread);
+ removeThread.getLatch().await();
+ try {
+ endBatch(cache);
+// Assert.fail("Write skew should be detected");
+ } catch (Exception e) {
+
+ }
+ Assert.assertNull(cache.get("key"));
+ Assert.assertTrue(removeThread.isSuccess());
+ }
+
+
+ public static void startBatch(Cache<String, String> cache) {
+ try {
+ if (cache.getAdvancedCache().getTransactionManager().getStatus() == Status.STATUS_NO_TRANSACTION) {
+ System.out.println("begin");
+ cache.getAdvancedCache().getTransactionManager().begin();
+ }
+ } catch (NotSupportedException e) {
+ throw new RuntimeException(e);
+ } catch (SystemException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ public static void endBatch(Cache<String, String> cache) {
+ boolean commit = true;
+ try {
+ if (cache.getAdvancedCache().getTransactionManager().getStatus() == Status.STATUS_ACTIVE) {
+ if (commit) {
+ cache.getAdvancedCache().getTransactionManager().commit();
+
+ } else {
+ cache.getAdvancedCache().getTransactionManager().rollback();
+
+ }
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+
+ protected DefaultCacheManager getVersionedCacheManager() {
+ GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
+
+
+ boolean clustered = false;
+ boolean async = false;
+ boolean allowDuplicateJMXDomains = true;
+
+ if (clustered) {
+ gcb.transport().defaultTransport();
+ }
+ gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
+
+ final DefaultCacheManager cacheManager = new DefaultCacheManager(gcb.build());
+ ConfigurationBuilder invalidationConfigBuilder = new ConfigurationBuilder();
+ invalidationConfigBuilder
+ //.invocationBatching().enable()
+ .transaction().transactionMode(TransactionMode.TRANSACTIONAL)
+ .transaction().transactionManagerLookup(new DummyTransactionManagerLookup())
+ .locking().isolationLevel(IsolationLevel.REPEATABLE_READ).writeSkewCheck(true).versioning().enable().scheme(VersioningScheme.SIMPLE);
+
+
+ //invalidationConfigBuilder.locking().isolationLevel(IsolationLevel.REPEATABLE_READ).writeSkewCheck(true).versioning().enable().scheme(VersioningScheme.SIMPLE);
+
+ if (clustered) {
+ invalidationConfigBuilder.clustering().cacheMode(async ? CacheMode.INVALIDATION_ASYNC : CacheMode.INVALIDATION_SYNC);
+ }
+ Configuration invalidationCacheConfiguration = invalidationConfigBuilder.build();
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_CACHE_NAME, invalidationCacheConfiguration);
+ return cacheManager;
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
index da639d4..53b9f68 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
@@ -145,8 +145,9 @@ public class RealmEntity {
@OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm")
Collection<UserFederationMapperEntity> userFederationMappers = new ArrayList<UserFederationMapperEntity>();
- @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true)
- @JoinTable(name="REALM_CLIENT", joinColumns={ @JoinColumn(name="REALM_ID") }, inverseJoinColumns={ @JoinColumn(name="CLIENT_ID") })
+ @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy="realm")
+ //@OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true)
+ //@JoinTable(name="REALM_CLIENT", joinColumns={ @JoinColumn(name="REALM_ID") }, inverseJoinColumns={ @JoinColumn(name="CLIENT_ID") })
Collection<ClientEntity> clients = new ArrayList<>();
@OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true)
diff --git a/server-spi/src/main/java/org/keycloak/models/cache/entities/CachedClient.java b/server-spi/src/main/java/org/keycloak/models/cache/entities/CachedClient.java
index 3cd37af..ca3e35b 100755
--- a/server-spi/src/main/java/org/keycloak/models/cache/entities/CachedClient.java
+++ b/server-spi/src/main/java/org/keycloak/models/cache/entities/CachedClient.java
@@ -39,43 +39,43 @@ import java.util.TreeMap;
*/
public class CachedClient implements Serializable {
- private String id;
- private String clientId;
- private String name;
- private String description;
- private String realm;
- private Set<String> redirectUris = new HashSet<String>();
- private boolean enabled;
- private String clientAuthenticatorType;
- private String secret;
- private String registrationToken;
- private String protocol;
- private Map<String, String> attributes = new HashMap<String, String>();
- private boolean publicClient;
- private boolean fullScopeAllowed;
- private boolean frontchannelLogout;
- private int notBefore;
- private Set<String> scope = new HashSet<String>();
- private Set<String> webOrigins = new HashSet<String>();
- private Set<ProtocolMapperModel> protocolMappers = new HashSet<ProtocolMapperModel>();
- private boolean surrogateAuthRequired;
- private String managementUrl;
- private String rootUrl;
- private String baseUrl;
- private List<String> defaultRoles = new LinkedList<String>();
- private boolean bearerOnly;
- private boolean consentRequired;
- private boolean standardFlowEnabled;
- private boolean implicitFlowEnabled;
- private boolean directAccessGrantsEnabled;
- private boolean serviceAccountsEnabled;
- private Map<String, String> roles = new HashMap<String, String>();
- private int nodeReRegistrationTimeout;
- private Map<String, Integer> registeredNodes;
- private String clientTemplate;
- private boolean useTemplateScope;
- private boolean useTemplateConfig;
- private boolean useTemplateMappers;
+ protected String id;
+ protected String clientId;
+ protected String name;
+ protected String description;
+ protected String realm;
+ protected Set<String> redirectUris = new HashSet<String>();
+ protected boolean enabled;
+ protected String clientAuthenticatorType;
+ protected String secret;
+ protected String registrationToken;
+ protected String protocol;
+ protected Map<String, String> attributes = new HashMap<String, String>();
+ protected boolean publicClient;
+ protected boolean fullScopeAllowed;
+ protected boolean frontchannelLogout;
+ protected int notBefore;
+ protected Set<String> scope = new HashSet<String>();
+ protected Set<String> webOrigins = new HashSet<String>();
+ protected Set<ProtocolMapperModel> protocolMappers = new HashSet<ProtocolMapperModel>();
+ protected boolean surrogateAuthRequired;
+ protected String managementUrl;
+ protected String rootUrl;
+ protected String baseUrl;
+ protected List<String> defaultRoles = new LinkedList<String>();
+ protected boolean bearerOnly;
+ protected boolean consentRequired;
+ protected boolean standardFlowEnabled;
+ protected boolean implicitFlowEnabled;
+ protected boolean directAccessGrantsEnabled;
+ protected boolean serviceAccountsEnabled;
+ protected Map<String, String> roles = new HashMap<String, String>();
+ protected int nodeReRegistrationTimeout;
+ protected Map<String, Integer> registeredNodes;
+ protected String clientTemplate;
+ protected boolean useTemplateScope;
+ protected boolean useTemplateConfig;
+ protected boolean useTemplateMappers;
public CachedClient(RealmCache cache, RealmProvider delegate, RealmModel realm, ClientModel model) {
id = model.getId();
@@ -112,10 +112,7 @@ public class CachedClient implements Serializable {
implicitFlowEnabled = model.isImplicitFlowEnabled();
directAccessGrantsEnabled = model.isDirectAccessGrantsEnabled();
serviceAccountsEnabled = model.isServiceAccountsEnabled();
- for (RoleModel role : model.getRoles()) {
- roles.put(role.getName(), role.getId());
- cache.addCachedRole(new CachedClientRole(id, role, realm));
- }
+ cacheRoles(cache, realm, model);
nodeReRegistrationTimeout = model.getNodeReRegistrationTimeout();
registeredNodes = new TreeMap<String, Integer>(model.getRegisteredNodes());
@@ -126,6 +123,14 @@ public class CachedClient implements Serializable {
useTemplateMappers = model.useTemplateMappers();
useTemplateScope = model.useTemplateScope();
}
+
+ protected void cacheRoles(RealmCache cache, RealmModel realm, ClientModel model) {
+ for (RoleModel role : model.getRoles()) {
+ roles.put(role.getName(), role.getId());
+ cache.addCachedRole(new CachedClientRole(id, role, realm));
+ }
+ }
+
public String getId() {
return id;
}
diff --git a/server-spi/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java b/server-spi/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
index dd19d6e..42ed3bc 100755
--- a/server-spi/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
+++ b/server-spi/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
@@ -53,90 +53,90 @@ import java.util.Set;
*/
public class CachedRealm implements Serializable {
- private String id;
- private String name;
- private String displayName;
- private String displayNameHtml;
- private boolean enabled;
- private SslRequired sslRequired;
- private boolean registrationAllowed;
- private boolean registrationEmailAsUsername;
- private boolean rememberMe;
- private boolean verifyEmail;
- private boolean resetPasswordAllowed;
- private boolean identityFederationEnabled;
- private boolean editUsernameAllowed;
+ protected String id;
+ protected String name;
+ protected String displayName;
+ protected String displayNameHtml;
+ protected boolean enabled;
+ protected SslRequired sslRequired;
+ protected boolean registrationAllowed;
+ protected boolean registrationEmailAsUsername;
+ protected boolean rememberMe;
+ protected boolean verifyEmail;
+ protected boolean resetPasswordAllowed;
+ protected boolean identityFederationEnabled;
+ protected boolean editUsernameAllowed;
//--- brute force settings
- private boolean bruteForceProtected;
- private int maxFailureWaitSeconds;
- private int minimumQuickLoginWaitSeconds;
- private int waitIncrementSeconds;
- private long quickLoginCheckMilliSeconds;
- private int maxDeltaTimeSeconds;
- private int failureFactor;
+ protected boolean bruteForceProtected;
+ protected int maxFailureWaitSeconds;
+ protected int minimumQuickLoginWaitSeconds;
+ protected int waitIncrementSeconds;
+ protected long quickLoginCheckMilliSeconds;
+ protected int maxDeltaTimeSeconds;
+ protected int failureFactor;
//--- end brute force settings
- private boolean revokeRefreshToken;
- private int ssoSessionIdleTimeout;
- private int ssoSessionMaxLifespan;
- private int offlineSessionIdleTimeout;
- private int accessTokenLifespan;
- private int accessTokenLifespanForImplicitFlow;
- private int accessCodeLifespan;
- private int accessCodeLifespanUserAction;
- private int accessCodeLifespanLogin;
- private int notBefore;
- private PasswordPolicy passwordPolicy;
- private OTPPolicy otpPolicy;
-
- private String publicKeyPem;
- private String privateKeyPem;
- private String certificatePem;
- private String codeSecret;
-
- private String loginTheme;
- private String accountTheme;
- private String adminTheme;
- private String emailTheme;
- private String masterAdminClient;
-
- private List<RequiredCredentialModel> requiredCredentials = new ArrayList<RequiredCredentialModel>();
- private List<UserFederationProviderModel> userFederationProviders = new ArrayList<UserFederationProviderModel>();
- private MultivaluedHashMap<String, UserFederationMapperModel> userFederationMappers = new MultivaluedHashMap<String, UserFederationMapperModel>();
- private List<IdentityProviderModel> identityProviders = new ArrayList<IdentityProviderModel>();
-
- private Map<String, String> browserSecurityHeaders = new HashMap<String, String>();
- private Map<String, String> smtpConfig = new HashMap<String, String>();
- private Map<String, AuthenticationFlowModel> authenticationFlows = new HashMap<>();
- private Map<String, AuthenticatorConfigModel> authenticatorConfigs = new HashMap<>();
- private Map<String, RequiredActionProviderModel> requiredActionProviders = new HashMap<>();
- private Map<String, RequiredActionProviderModel> requiredActionProvidersByAlias = new HashMap<>();
- private MultivaluedHashMap<String, AuthenticationExecutionModel> authenticationExecutions = new MultivaluedHashMap<>();
- private Map<String, AuthenticationExecutionModel> executionsById = new HashMap<>();
-
- private AuthenticationFlowModel browserFlow;
- private AuthenticationFlowModel registrationFlow;
- private AuthenticationFlowModel directGrantFlow;
- private AuthenticationFlowModel resetCredentialsFlow;
- private AuthenticationFlowModel clientAuthenticationFlow;
-
- private boolean eventsEnabled;
- private long eventsExpiration;
- private Set<String> eventsListeners = new HashSet<String>();
- private Set<String> enabledEventTypes = new HashSet<String>();
+ protected boolean revokeRefreshToken;
+ protected int ssoSessionIdleTimeout;
+ protected int ssoSessionMaxLifespan;
+ protected int offlineSessionIdleTimeout;
+ protected int accessTokenLifespan;
+ protected int accessTokenLifespanForImplicitFlow;
+ protected int accessCodeLifespan;
+ protected int accessCodeLifespanUserAction;
+ protected int accessCodeLifespanLogin;
+ protected int notBefore;
+ protected PasswordPolicy passwordPolicy;
+ protected OTPPolicy otpPolicy;
+
+ protected String publicKeyPem;
+ protected String privateKeyPem;
+ protected String certificatePem;
+ protected String codeSecret;
+
+ protected String loginTheme;
+ protected String accountTheme;
+ protected String adminTheme;
+ protected String emailTheme;
+ protected String masterAdminClient;
+
+ protected List<RequiredCredentialModel> requiredCredentials = new ArrayList<RequiredCredentialModel>();
+ protected List<UserFederationProviderModel> userFederationProviders = new ArrayList<UserFederationProviderModel>();
+ protected MultivaluedHashMap<String, UserFederationMapperModel> userFederationMappers = new MultivaluedHashMap<String, UserFederationMapperModel>();
+ protected List<IdentityProviderModel> identityProviders = new ArrayList<IdentityProviderModel>();
+
+ protected Map<String, String> browserSecurityHeaders = new HashMap<String, String>();
+ protected Map<String, String> smtpConfig = new HashMap<String, String>();
+ protected Map<String, AuthenticationFlowModel> authenticationFlows = new HashMap<>();
+ protected Map<String, AuthenticatorConfigModel> authenticatorConfigs = new HashMap<>();
+ protected Map<String, RequiredActionProviderModel> requiredActionProviders = new HashMap<>();
+ protected Map<String, RequiredActionProviderModel> requiredActionProvidersByAlias = new HashMap<>();
+ protected MultivaluedHashMap<String, AuthenticationExecutionModel> authenticationExecutions = new MultivaluedHashMap<>();
+ protected Map<String, AuthenticationExecutionModel> executionsById = new HashMap<>();
+
+ protected AuthenticationFlowModel browserFlow;
+ protected AuthenticationFlowModel registrationFlow;
+ protected AuthenticationFlowModel directGrantFlow;
+ protected AuthenticationFlowModel resetCredentialsFlow;
+ protected AuthenticationFlowModel clientAuthenticationFlow;
+
+ protected boolean eventsEnabled;
+ protected long eventsExpiration;
+ protected Set<String> eventsListeners = new HashSet<String>();
+ protected Set<String> enabledEventTypes = new HashSet<String>();
protected boolean adminEventsEnabled;
protected Set<String> adminEnabledEventOperations = new HashSet<String>();
protected boolean adminEventsDetailsEnabled;
- private List<String> defaultRoles = new LinkedList<String>();
- private List<String> defaultGroups = new LinkedList<String>();
- private Set<String> groups = new HashSet<String>();
- private Map<String, String> realmRoles = new HashMap<String, String>();
- private Map<String, String> clients = new HashMap<String, String>();
- private List<String> clientTemplates= new LinkedList<>();
- private boolean internationalizationEnabled;
- private Set<String> supportedLocales = new HashSet<String>();
- private String defaultLocale;
- private MultivaluedHashMap<String, IdentityProviderMapperModel> identityProviderMappers = new MultivaluedHashMap<>();
+ protected List<String> defaultRoles = new LinkedList<String>();
+ protected List<String> defaultGroups = new LinkedList<String>();
+ protected Set<String> groups = new HashSet<String>();
+ protected Map<String, String> realmRoles = new HashMap<String, String>();
+ protected Map<String, String> clients = new HashMap<String, String>();
+ protected List<String> clientTemplates= new LinkedList<>();
+ protected boolean internationalizationEnabled;
+ protected Set<String> supportedLocales = new HashSet<String>();
+ protected String defaultLocale;
+ protected MultivaluedHashMap<String, IdentityProviderMapperModel> identityProviderMappers = new MultivaluedHashMap<>();
public CachedRealm() {
}
@@ -221,23 +221,11 @@ public class CachedRealm implements Serializable {
ClientModel masterAdminClient = model.getMasterAdminClient();
this.masterAdminClient = (masterAdminClient != null) ? masterAdminClient.getId() : null;
- for (RoleModel role : model.getRoles()) {
- realmRoles.put(role.getName(), role.getId());
- CachedRole cachedRole = new CachedRealmRole(role, model);
- cache.addCachedRole(cachedRole);
- }
+ cacheRealmRoles(cache, model);
- for (ClientModel client : model.getClients()) {
- clients.put(client.getClientId(), client.getId());
- CachedClient cachedClient = new CachedClient(cache, delegate, model, client);
- cache.addCachedClient(cachedClient);
- }
+ cacheClients(cache, delegate, model);
- for (ClientTemplateModel template : model.getClientTemplates()) {
- clientTemplates.add(template.getId());
- CachedClientTemplate cachedClient = new CachedClientTemplate(cache, delegate, model, template);
- cache.addCachedClientTemplate(cachedClient);
- }
+ cacheClientTemplates(cache, delegate, model);
internationalizationEnabled = model.isInternationalizationEnabled();
supportedLocales.addAll(model.getSupportedLocales());
@@ -273,6 +261,30 @@ public class CachedRealm implements Serializable {
}
+ protected void cacheClientTemplates(RealmCache cache, RealmProvider delegate, RealmModel model) {
+ for (ClientTemplateModel template : model.getClientTemplates()) {
+ clientTemplates.add(template.getId());
+ CachedClientTemplate cachedClient = new CachedClientTemplate(cache, delegate, model, template);
+ cache.addCachedClientTemplate(cachedClient);
+ }
+ }
+
+ protected void cacheClients(RealmCache cache, RealmProvider delegate, RealmModel model) {
+ for (ClientModel client : model.getClients()) {
+ clients.put(client.getClientId(), client.getId());
+ CachedClient cachedClient = new CachedClient(cache, delegate, model, client);
+ cache.addCachedClient(cachedClient);
+ }
+ }
+
+ protected void cacheRealmRoles(RealmCache cache, RealmModel model) {
+ for (RoleModel role : model.getRoles()) {
+ realmRoles.put(role.getName(), role.getId());
+ CachedRole cachedRole = new CachedRealmRole(role, model);
+ cache.addCachedRole(cachedRole);
+ }
+ }
+
public String getId() {
return id;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ConcurrencyTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ConcurrencyTest.java
index 2f52649..6584b40 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ConcurrencyTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ConcurrencyTest.java
@@ -44,8 +44,8 @@ public class ConcurrencyTest extends AbstractClientTest {
private static final Logger log = Logger.getLogger(ConcurrencyTest.class);
- private static final int DEFAULT_THREADS = 3;
- private static final int DEFAULT_ITERATIONS = 10;
+ private static final int DEFAULT_THREADS = 5;
+ private static final int DEFAULT_ITERATIONS = 30;
// If enabled only one request is allowed at the time. Useful for checking that test is working.
private static final boolean SYNCHRONIZED = false;
diff --git a/testsuite/integration/src/test/resources/log4j.properties b/testsuite/integration/src/test/resources/log4j.properties
index 68b1ba6..91c9646 100755
--- a/testsuite/integration/src/test/resources/log4j.properties
+++ b/testsuite/integration/src/test/resources/log4j.properties
@@ -30,7 +30,9 @@ log4j.logger.org.keycloak=info
# log4j.logger.org.keycloak.services.DefaultKeycloakSessionFactory=debug
# log4j.logger.org.keycloak.provider.ProviderManager=debug
# log4j.logger.org.keycloak.provider.FileSystemProviderLoaderFactory=debug
-
+#log4j.logger.org.infinispan.transaction.impl.TransactionCoordinator=OFF
+#log4j.logger.org.infinispan.transaction.tm.DummyTransaction=OFF
+#log4j.logger.org.infinispan.container.entries.RepeatableReadEntry=OFF
# Broker logging
keycloak.testsuite.logging.level=info
log4j.logger.org.keycloak.testsuite=${keycloak.testsuite.logging.level}