diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java index e5a3472d9b..a9145b2bd1 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java @@ -546,7 +546,9 @@ public class ClientAdapter implements ClientModel { public RoleModel getRole(String name) { if (updated != null) return updated.getRole(name); String id = cached.getRoles().get(name); - if (id == null) return null; + if (id == null) { + return null; + } return cacheSession.getRoleById(id, cachedRealm); } 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 index e4b68f6083..7294db9d45 100755 --- 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 @@ -351,8 +351,8 @@ public class RevisionedCacheRealmProvider implements CacheRealmProvider { 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) { + logger.tracev("client by id cache hit: {0}", cached.getClientId()); } if (cached == null) { 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 index c2b6d6f040..b6fb89c340 100755 --- 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 @@ -38,11 +38,6 @@ public class RevisionedRealmCache implements RealmCache { protected final Cache revisions; protected final Cache 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 realmLookup; @@ -57,33 +52,56 @@ public class RevisionedRealmCache implements RealmCache { } public Long getCurrentRevision(String id) { - return revisions.get(id); + //return revisions.get(id); + return UpdateCounter.current(); } + + private T get(String id, Class type) { + Revisioned o = (Revisioned)cache.get(id); + if (o == null) { + return null; + } + Long rev = revisions.get(id); + if (rev == null) { + logger.tracev("get() missing rev"); + return null; + } + long oRev = o.getRevision() == null ? -1L : o.getRevision().longValue(); + if (rev > oRev) { + logger.tracev("stale rev: {0} o.rev: {1}", rev.longValue(), oRev); + return null; + } + return o != null && type.isInstance(o) ? type.cast(o) : null; + } + + protected Object invalidateObject(String id) { + Object removed = cache.remove(id); + revisions.put(id, UpdateCounter.next()); + return removed; + } + + protected void addRevisioned(String id, Revisioned object) { + Long rev = revisions.get(id); + if (rev == null) { + rev = UpdateCounter.next(); + revisions.put(id, rev); + } + if (rev.equals(object.getRevision())) { + cache.putForExternalRead(id, object); + } + } + + + + + + + @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); @@ -92,41 +110,23 @@ public class RevisionedRealmCache implements RealmCache { @Override public void invalidateCachedRealm(CachedRealm realm) { logger.tracev("Invalidating realm {0}", realm.getId()); - invalidateObject(realm.getId(), realmCounter); + invalidateObject(realm.getId()); realmLookup.remove(realm.getName()); } @Override public void invalidateCachedRealmById(String id) { - CachedRealm cached = (CachedRealm) invalidateObject(id, realmCounter); + CachedRealm cached = (CachedRealm) invalidateObject(id); 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); + addRevisioned(realm.getId(), (Revisioned) realm); 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); @@ -140,20 +140,20 @@ public class RevisionedRealmCache implements RealmCache { @Override public void invalidateApplication(CachedClient app) { - logger.infov("Removing application {0}", app.getClientAuthenticatorType()); - invalidateObject(app.getId(), clientCounter); + logger.tracev("Removing application {0}", app.getId()); + invalidateObject(app.getId()); } @Override public void addCachedClient(CachedClient app) { logger.tracev("Adding application {0}", app.getId()); - addRevisioned(app.getId(), (Revisioned) app, clientCounter); + addRevisioned(app.getId(), (Revisioned) app); } @Override public void invalidateCachedApplicationById(String id) { - CachedClient client = (CachedClient)invalidateObject(id, clientCounter); - if (client != null) logger.infov("Removing application {0}", client.getClientId()); + CachedClient client = (CachedClient)invalidateObject(id); + if (client != null) logger.tracev("Removing application {0}", client.getClientId()); } @Override @@ -170,26 +170,26 @@ public class RevisionedRealmCache implements RealmCache { @Override public void invalidateGroup(CachedGroup role) { logger.tracev("Removing group {0}", role.getId()); - invalidateObject(role.getId(), groupCounter); + invalidateObject(role.getId()); } @Override public void addCachedGroup(CachedGroup role) { logger.tracev("Adding group {0}", role.getId()); - addRevisioned(role.getId(), (Revisioned) role, groupCounter); + addRevisioned(role.getId(), (Revisioned) role); } @Override public void invalidateCachedGroupById(String id) { logger.tracev("Removing group {0}", id); - invalidateObject(id, groupCounter); + invalidateObject(id); } @Override public void invalidateGroupById(String id) { logger.tracev("Removing group {0}", id); - invalidateObject(id, groupCounter); + invalidateObject(id); } @Override @@ -200,13 +200,13 @@ public class RevisionedRealmCache implements RealmCache { @Override public void invalidateRole(CachedRole role) { logger.tracev("Removing role {0}", role.getId()); - invalidateObject(role.getId(), roleCounter); + invalidateObject(role.getId()); } @Override public void invalidateRoleById(String id) { logger.tracev("Removing role {0}", id); - invalidateObject(id, roleCounter); + invalidateObject(id); } @Override @@ -218,31 +218,13 @@ public class RevisionedRealmCache implements RealmCache { @Override public void addCachedRole(CachedRole role) { logger.tracev("Adding role {0}", role.getId()); - addRevisioned(role.getId(), (Revisioned) role, roleCounter); + addRevisioned(role.getId(), (Revisioned) role); } @Override public void invalidateCachedRoleById(String id) { logger.tracev("Removing role {0}", id); - invalidateObject(id, roleCounter); - } - - private T get(String id, Class type) { - Revisioned o = (Revisioned)cache.get(id); - if (o == null) { - return null; - } - Long rev = revisions.get(id); - if (rev == null) { - logger.tracev("get() missing rev"); - return null; - } - long oRev = o.getRevision() == null ? -1L : o.getRevision().longValue(); - if (rev > oRev) { - logger.tracev("get() rev: {0} o.rev: {1}", rev.longValue(), oRev); - return null; - } - return o != null && type.isInstance(o) ? type.cast(o) : null; + invalidateObject(id); } @Override @@ -253,19 +235,19 @@ public class RevisionedRealmCache implements RealmCache { @Override public void invalidateClientTemplate(CachedClientTemplate app) { logger.tracev("Removing client template {0}", app.getId()); - invalidateObject(app.getId(), clientTemplateCounter); + invalidateObject(app.getId()); } @Override public void addCachedClientTemplate(CachedClientTemplate app) { logger.tracev("Adding client template {0}", app.getId()); - addRevisioned(app.getId(), (Revisioned) app, clientTemplateCounter); + addRevisioned(app.getId(), (Revisioned) app); } @Override public void invalidateCachedClientTemplateById(String id) { logger.tracev("Removing client template {0}", id); - invalidateObject(id, clientTemplateCounter); + invalidateObject(id); } @Override diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/UpdateCounter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/UpdateCounter.java new file mode 100755 index 0000000000..88d598b3d8 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/counter/UpdateCounter.java @@ -0,0 +1,20 @@ +package org.keycloak.models.cache.infinispan.counter; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * @author Stian Thorgersen + */ +public class UpdateCounter { + + private static final AtomicLong counter = new AtomicLong(); + + public static long current() { + return counter.get(); + } + + public static long next() { + return counter.incrementAndGet(); + } + +} \ No newline at end of file diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/locking/LockingCacheRealmProvider.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/locking/LockingCacheRealmProvider.java new file mode 100755 index 0000000000..ea451b0876 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/locking/LockingCacheRealmProvider.java @@ -0,0 +1,463 @@ +/* + * 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.locking; + +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.CachedClientTemplate; +import org.keycloak.models.cache.entities.CachedGroup; +import org.keycloak.models.cache.entities.CachedRealm; +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.Collections; +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 Bill Burke + * @version $Revision: 1 $ + */ +public class LockingCacheRealmProvider implements CacheRealmProvider { + protected static final Logger logger = Logger.getLogger(LockingCacheRealmProvider.class); + protected LockingRealmCache cache; + protected KeycloakSession session; + protected RealmProvider delegate; + protected boolean transactionActive; + protected boolean setRollbackOnly; + + protected Set realmInvalidations = new HashSet<>(); + protected Set appInvalidations = new HashSet<>(); + protected Set clientTemplateInvalidations = new HashSet<>(); + protected Set roleInvalidations = new HashSet<>(); + protected Set groupInvalidations = new HashSet<>(); + protected Map managedRealms = new HashMap<>(); + protected Map managedApplications = new HashMap<>(); + protected Map managedClientTemplates = new HashMap<>(); + protected Map managedRoles = new HashMap<>(); + protected Map managedGroups = new HashMap<>(); + + protected boolean clearAll; + + public LockingCacheRealmProvider(LockingRealmCache cache, KeycloakSession session) { + this.cache = cache; + this.session = session; + + session.getTransaction().enlistPrepare(getPrepareTransaction()); + session.getTransaction().enlistAfterCompletion(getAfterTransaction()); + } + + @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 getPrepareTransaction() { + return new KeycloakTransaction() { + @Override + public void begin() { + transactionActive = true; + } + + @Override + public void commit() { + if (delegate == null) return; + List invalidates = new LinkedList<>(); + for (String id : realmInvalidations) { + invalidates.add(id); + } + for (String id : roleInvalidations) { + invalidates.add(id); + } + for (String id : groupInvalidations) { + invalidates.add(id); + } + for (String id : appInvalidations) { + invalidates.add(id); + } + for (String id : clientTemplateInvalidations) { + invalidates.add(id); + } + + Collections.sort(invalidates); // lock ordering + cache.getRevisions().startBatch(); + for (String id : invalidates) { + cache.getRevisions().getAdvancedCache().lock(id); + } + + } + + @Override + public void rollback() { + setRollbackOnly = true; + transactionActive = false; + } + + @Override + public void setRollbackOnly() { + setRollbackOnly = true; + } + + @Override + public boolean getRollbackOnly() { + return setRollbackOnly; + } + + @Override + public boolean isActive() { + return transactionActive; + } + }; + } + + private KeycloakTransaction getAfterTransaction() { + return new KeycloakTransaction() { + @Override + public void begin() { + transactionActive = true; + } + + @Override + public void commit() { + try { + if (delegate == null) return; + if (clearAll) { + cache.clear(); + } + runInvalidations(); + transactionActive = false; + } finally { + cache.endRevisionBatch(); + } + } + + @Override + public void rollback() { + try { + setRollbackOnly = true; + runInvalidations(); + transactionActive = false; + } finally { + cache.endRevisionBatch(); + } + } + + @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 getRealms() { + // Retrieve realms from backend + List backendRealms = getDelegate().getRealms(); + + // Return cache delegates to ensure cache invalidated during write operations + List cachedRealms = new LinkedList(); + 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 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.tracev("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/locking/LockingCacheRealmProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/locking/LockingCacheRealmProviderFactory.java new file mode 100755 index 0000000000..2009b670a1 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/locking/LockingCacheRealmProviderFactory.java @@ -0,0 +1,161 @@ +/* + * 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.locking; + +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 Bill Burke + * @author Stian Thorgersen + */ +public class LockingCacheRealmProviderFactory implements CacheRealmProviderFactory { + + private static final Logger log = Logger.getLogger(LockingCacheRealmProviderFactory.class); + + protected volatile LockingRealmCache realmCache; + + protected final ConcurrentHashMap realmLookup = new ConcurrentHashMap<>(); + + @Override + public CacheRealmProvider create(KeycloakSession session) { + lazyInit(session); + return new LockingCacheRealmProvider(realmCache, session); + } + + private void lazyInit(KeycloakSession session) { + if (realmCache == null) { + synchronized (this) { + if (realmCache == null) { + Cache cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.REALM_CACHE_NAME); + Cache counterCache = session.getProvider(InfinispanConnectionProvider.class).getCache(LockingConnectionProviderFactory.COUNTER_CACHE_NAME); + cache.addListener(new CacheListener()); + realmCache = new LockingRealmCache(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-locking"; + } + + @Listener + public class CacheListener { + + @CacheEntryCreated + public void created(CacheEntryCreatedEvent 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 event) { + if (event.isPre()) { + Object object = event.getValue(); + if (object != null) { + remove(object); + } + } + } + + @CacheEntryInvalidated + public void removed(CacheEntryInvalidatedEvent event) { + if (event.isPre()) { + Object object = event.getValue(); + if (object != null) { + remove(object); + } + } + } + + @CacheEntriesEvicted + public void userEvicted(CacheEntriesEvictedEvent 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/locking/LockingConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/locking/LockingConnectionProviderFactory.java new file mode 100755 index 0000000000..f3e5f294a2 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/locking/LockingConnectionProviderFactory.java @@ -0,0 +1,52 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.cache.infinispan.locking; + +import org.infinispan.configuration.cache.Configuration; +import org.infinispan.configuration.cache.ConfigurationBuilder; +import org.infinispan.transaction.LockingMode; +import org.infinispan.transaction.lookup.DummyTransactionManagerLookup; +import org.jboss.logging.Logger; +import org.keycloak.connections.infinispan.DefaultInfinispanConnectionProviderFactory; + +/** + * @author Stian Thorgersen + */ +public class LockingConnectionProviderFactory extends DefaultInfinispanConnectionProviderFactory { + public static final String COUNTER_CACHE_NAME = "COUNTER_CACHE"; + + protected static final Logger logger = Logger.getLogger(LockingConnectionProviderFactory.class); + + @Override + public String getId() { + return "locking"; + } + + + protected void initEmbedded() { + super.initEmbedded(); + ConfigurationBuilder counterConfigBuilder = new ConfigurationBuilder(); + counterConfigBuilder.invocationBatching().enable(); + counterConfigBuilder.transaction().transactionManagerLookup(new DummyTransactionManagerLookup()); + counterConfigBuilder.transaction().lockingMode(LockingMode.PESSIMISTIC); + Configuration counterCacheConfiguration = counterConfigBuilder.build(); + + cacheManager.defineConfiguration(COUNTER_CACHE_NAME, counterCacheConfiguration); + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/locking/LockingRealmCache.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/locking/LockingRealmCache.java new file mode 100755 index 0000000000..9c8e784aff --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/locking/LockingRealmCache.java @@ -0,0 +1,294 @@ +/* + * 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.locking; + +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 org.keycloak.models.cache.infinispan.counter.Revisioned; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +/** + * @author Stian Thorgersen + */ +public class LockingRealmCache implements RealmCache { + + protected static final Logger logger = Logger.getLogger(LockingRealmCache.class); + + protected final Cache revisions; + protected final Cache 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 realmLookup; + + public LockingRealmCache(Cache cache, Cache revisions, ConcurrentHashMap realmLookup) { + this.cache = cache; + this.realmLookup = realmLookup; + this.revisions = revisions; + } + + public Cache getCache() { + return cache; + } + + public Cache getRevisions() { + return revisions; + } + + public void startRevisionBatch() { + revisions.startBatch(); + } + + public void endRevisionBatch() { + try { + revisions.endBatch(true); + } catch (Exception e) { + } + + } + + private T get(String id, Class type) { + Revisioned o = (Revisioned)cache.get(id); + if (o == null) { + return null; + } + Long rev = revisions.get(id); + if (rev == null) { + logger.tracev("get() missing rev"); + return null; + } + long oRev = o.getRevision() == null ? -1L : o.getRevision().longValue(); + if (rev > oRev) { + logger.tracev("get() rev: {0} o.rev: {1}", rev.longValue(), oRev); + return null; + } + return o != null && type.isInstance(o) ? type.cast(o) : null; + } + + protected Object invalidateObject(String id, AtomicLong counter) { + Object removed = cache.remove(id); + revisions.put(id, counter.incrementAndGet()); + return removed; + } + + protected void addRevisioned(String id, Revisioned object, AtomicLong counter) { + //startRevisionBatch(); + try { + //revisions.getAdvancedCache().lock(id); + Long rev = revisions.get(id); + if (rev == null) { + rev = counter.incrementAndGet(); + revisions.put(id, rev); + return; + } + revisions.startBatch(); + revisions.getAdvancedCache().lock(id); + rev = revisions.get(id); + if (rev == null) { + rev = counter.incrementAndGet(); + revisions.put(id, rev); + return; + } + if (rev.equals(object.getRevision())) { + cache.putForExternalRead(id, object); + } + } finally { + endRevisionBatch(); + } + + } + + + + + public Long getCurrentRevision(String id) { + return revisions.get(id); + } + @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()); + 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()); + } + + @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()); + } + + + @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()); + 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.tracev("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); + } + + @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/resources/META-INF/services/org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory index cff9ef182b..ef880ece25 100755 --- 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 @@ -16,4 +16,5 @@ # org.keycloak.connections.infinispan.DefaultInfinispanConnectionProviderFactory -org.keycloak.models.cache.infinispan.counter.RevisionedConnectionProviderFactory \ No newline at end of file +org.keycloak.models.cache.infinispan.counter.RevisionedConnectionProviderFactory +org.keycloak.models.cache.infinispan.locking.LockingConnectionProviderFactory \ 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 2d264cfbec..655476558d 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 @@ -16,4 +16,5 @@ # org.keycloak.models.cache.infinispan.InfinispanCacheRealmProviderFactory -org.keycloak.models.cache.infinispan.counter.RevisionedCacheRealmProviderFactory \ No newline at end of file +org.keycloak.models.cache.infinispan.counter.RevisionedCacheRealmProviderFactory +org.keycloak.models.cache.infinispan.locking.LockingCacheRealmProviderFactory \ No newline at end of file diff --git a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/ConcurrencyLockingTest.java b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/ConcurrencyLockingTest.java new file mode 100755 index 0000000000..aa26b5ffa8 --- /dev/null +++ b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/ConcurrencyLockingTest.java @@ -0,0 +1,88 @@ +package org.keycloak.models.sessions.infinispan.initializer; + +import org.infinispan.Cache; +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.LockingMode; +import org.infinispan.transaction.TransactionMode; +import org.infinispan.transaction.lookup.DummyTransactionManagerLookup; +import org.infinispan.util.concurrent.IsolationLevel; +import org.junit.Ignore; +import org.junit.Test; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Ignore +public class ConcurrencyLockingTest { + + @Test + public void testLocking() throws Exception { + final DefaultCacheManager cacheManager = getVersionedCacheManager(); + Cache cache = cacheManager.getCache("COUNTER_CACHE"); + cache.put("key", "init"); + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.execute(new Runnable() { + @Override + public void run() { + Cache cache = cacheManager.getCache("COUNTER_CACHE"); + cache.startBatch(); + System.out.println("thread lock"); + cache.getAdvancedCache().lock("key"); + try { + Thread.sleep(100000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + cache.endBatch(true); + + } + }); + Thread.sleep(10); + cache.startBatch(); + cache.getAdvancedCache().lock("key"); + cache.put("key", "1234"); + System.out.println("after put"); + cache.endBatch(true); + + Thread.sleep(1000000); + + + + } + + protected DefaultCacheManager getVersionedCacheManager() { + GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder(); + + + boolean allowDuplicateJMXDomains = true; + + gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains); + + final DefaultCacheManager cacheManager = new DefaultCacheManager(gcb.build()); + ConfigurationBuilder invalidationConfigBuilder = new ConfigurationBuilder(); + Configuration invalidationCacheConfiguration = invalidationConfigBuilder.build(); + cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_CACHE_NAME, invalidationCacheConfiguration); + + ConfigurationBuilder counterConfigBuilder = new ConfigurationBuilder(); + counterConfigBuilder.invocationBatching().enable(); + counterConfigBuilder.transaction().transactionManagerLookup(new DummyTransactionManagerLookup()); + counterConfigBuilder.transaction().lockingMode(LockingMode.PESSIMISTIC); + Configuration counterCacheConfiguration = counterConfigBuilder.build(); + + cacheManager.defineConfiguration("COUNTER_CACHE", counterCacheConfiguration); + return cacheManager; + } + +} 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 index b35ce2453c..48a0e36143 100755 --- 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 @@ -14,6 +14,7 @@ import org.infinispan.transaction.TransactionProtocol; import org.infinispan.transaction.lookup.DummyTransactionManagerLookup; import org.infinispan.util.concurrent.IsolationLevel; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; @@ -30,6 +31,7 @@ import java.util.concurrent.Executors; * @author Bill Burke * @version $Revision: 1 $ */ +@Ignore public class ConcurrencyVersioningTest { public static abstract class AbstractThread implements Runnable { diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java index 5453164942..d4207d130b 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java @@ -633,8 +633,8 @@ public class ClientAdapter implements ClientModel { roleEntity.setClient(entity); roleEntity.setClientRole(true); roleEntity.setRealmId(realm.getId()); + //entity.getRoles().add(roleEntity); em.persist(roleEntity); - entity.getRoles().add(roleEntity); em.flush(); return new RoleAdapter(realm, em, roleEntity); } @@ -650,7 +650,7 @@ public class ClientAdapter implements ClientModel { RoleEntity role = RoleAdapter.toRoleEntity(roleModel, em); if (!role.isClientRole()) return false; - entity.getRoles().remove(role); + //entity.getRoles().remove(role); entity.getDefaultRoles().remove(role); String compositeRoleTable = JpaUtils.getTableNameForNativeQuery("COMPOSITE_ROLE", em); em.createNativeQuery("delete from " + compositeRoleTable + " where CHILD_ROLE = :role").setParameter("role", role).executeUpdate(); @@ -667,12 +667,22 @@ public class ClientAdapter implements ClientModel { @Override public Set getRoles() { Set list = new HashSet(); + /* Collection roles = entity.getRoles(); if (roles == null) return list; for (RoleEntity entity : roles) { list.add(new RoleAdapter(realm, em, entity)); } return list; + */ + TypedQuery query = em.createNamedQuery("getClientRoles", RoleEntity.class); + query.setParameter("client", entity); + List roles = query.getResultList(); + for (RoleEntity roleEntity : roles) { + list.add(new RoleAdapter(realm, em, roleEntity)); + } + return list; + } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java index be5cd9d905..86bc5a224f 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java @@ -124,9 +124,18 @@ public class JpaRealmProvider implements RealmProvider { .setParameter("realm", realm).executeUpdate(); num = em.createNamedQuery("deleteGroupsByRealm") .setParameter("realm", realm).executeUpdate(); + + TypedQuery query = em.createNamedQuery("getClientsByRealm", ClientEntity.class); + query.setParameter("realm", realm); + List clients = query.getResultList(); + for (ClientEntity a : clients) { + adapter.removeClient(a.getId()); + } + /* for (ClientEntity a : new LinkedList<>(realm.getClients())) { adapter.removeClient(a.getId()); } + */ for (ClientTemplateEntity a : new LinkedList<>(realm.getClientTemplates())) { adapter.removeClientTemplate(a.getId()); } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index d93c44ccb8..09e574f591 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -729,12 +729,14 @@ public class RealmAdapter implements RealmModel { @Override public List getClients() { - List list = new ArrayList(); - if (realm.getClients() == null) return list; - for (ClientEntity entity : realm.getClients()) { + List list = new LinkedList<>(); + TypedQuery query = em.createNamedQuery("getClientsByRealm", ClientEntity.class); + query.setParameter("realm", realm); + List clients = query.getResultList(); + for (ClientEntity entity : clients) { list.add(new ClientAdapter(this, em, session, entity)); } - return list; + return list; } @Override @@ -753,7 +755,7 @@ public class RealmAdapter implements RealmModel { entity.setEnabled(true); entity.setStandardFlowEnabled(true); entity.setRealm(realm); - realm.getClients().add(entity); + //realm.getClients().add(entity); em.persist(entity); em.flush(); final ClientModel resource = new ClientAdapter(this, em, session, entity); @@ -779,6 +781,7 @@ public class RealmAdapter implements RealmModel { client.removeRole(role); } + /* ClientEntity clientEntity = null; Iterator it = realm.getClients().iterator(); while (it.hasNext()) { @@ -794,11 +797,11 @@ public class RealmAdapter implements RealmModel { clientEntity = a; } } - if (client == null) { - return false; - } - em.remove(clientEntity); + */ + ClientEntity clientEntity = em.find(ClientEntity.class, id); + if (clientEntity == null) return false; em.createNamedQuery("deleteScopeMappingByClient").setParameter("client", clientEntity).executeUpdate(); + em.remove(clientEntity); em.flush(); return true; diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java index 848401a688..cc9b36aba0 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java @@ -28,6 +28,8 @@ import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToOne; import javax.persistence.MapKeyColumn; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.UniqueConstraint; @@ -44,6 +46,9 @@ import java.util.Set; */ @Entity @Table(name="CLIENT", uniqueConstraints = {@UniqueConstraint(columnNames = {"REALM_ID", "CLIENT_ID"})}) +@NamedQueries({ + @NamedQuery(name="getClientsByRealm", query="select client from ClientEntity client where client.realm = :realm"), +}) public class ClientEntity { @Id @@ -146,8 +151,8 @@ public class ClientEntity { @Column(name="NODE_REREG_TIMEOUT") private int nodeReRegistrationTimeout; - @OneToMany(fetch = FetchType.EAGER, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "client") - Collection roles = new ArrayList(); + //@OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, mappedBy = "client") + //Collection roles = new ArrayList(); @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true) @JoinTable(name="CLIENT_DEFAULT_ROLES", joinColumns = { @JoinColumn(name="CLIENT_ID")}, inverseJoinColumns = { @JoinColumn(name="ROLE_ID")}) @@ -343,6 +348,7 @@ public class ClientEntity { this.managementUrl = managementUrl; } + /* public Collection getRoles() { return roles; } @@ -350,6 +356,7 @@ public class ClientEntity { public void setRoles(Collection roles) { this.roles = roles; } + */ public Collection getDefaultRoles() { return defaultRoles; 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 53b9f68a51..87cc7e5592 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,16 +145,16 @@ public class RealmEntity { @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm") Collection userFederationMappers = new ArrayList(); - @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy="realm") + //@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 clients = new ArrayList<>(); + //Collection clients = new ArrayList<>(); @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true) @JoinTable(name="REALM_CLIENT_TEMPLATE", joinColumns={ @JoinColumn(name="REALM_ID") }, inverseJoinColumns={ @JoinColumn(name="CLIENT_TEMPLATE_ID") }) Collection clientTemplates = new ArrayList<>(); - @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm") + @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, mappedBy = "realm") Collection roles = new ArrayList(); @ElementCollection @@ -422,7 +422,7 @@ public class RealmEntity { public void setRequiredCredentials(Collection requiredCredentials) { this.requiredCredentials = requiredCredentials; } - + /* public Collection getClients() { return clients; } @@ -430,6 +430,7 @@ public class RealmEntity { public void setClients(Collection clients) { this.clients = clients; } + */ public Collection getRoles() { return roles; diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java index 1b96caef93..6a6ca5f621 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java @@ -41,6 +41,7 @@ import java.util.Collection; @UniqueConstraint(columnNames = { "NAME", "CLIENT_REALM_CONSTRAINT" }) }) @NamedQueries({ + @NamedQuery(name="getClientRoles", query="select role from RoleEntity role where role.client = :client"), @NamedQuery(name="getClientRoleByName", query="select role from RoleEntity role where role.name = :name and role.client = :client"), @NamedQuery(name="getRealmRoleByName", query="select role from RoleEntity role where role.clientRole = false and role.name = :name and role.realm = :realm") }) diff --git a/server-spi/src/main/java/org/keycloak/models/KeycloakTransactionManager.java b/server-spi/src/main/java/org/keycloak/models/KeycloakTransactionManager.java index 2b2a78346c..0e2dcbea97 100755 --- a/server-spi/src/main/java/org/keycloak/models/KeycloakTransactionManager.java +++ b/server-spi/src/main/java/org/keycloak/models/KeycloakTransactionManager.java @@ -26,4 +26,5 @@ public interface KeycloakTransactionManager extends KeycloakTransaction { void enlist(KeycloakTransaction transaction); void enlistAfterCompletion(KeycloakTransaction transaction); + void enlistPrepare(KeycloakTransaction transaction); } diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java index 23ae6fa2f8..fca6a9ed9d 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java @@ -30,6 +30,7 @@ public class DefaultKeycloakTransactionManager implements KeycloakTransactionMan public static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER; + private List prepare = new LinkedList(); private List transactions = new LinkedList(); private List afterCompletion = new LinkedList(); private boolean active; @@ -53,6 +54,15 @@ public class DefaultKeycloakTransactionManager implements KeycloakTransactionMan afterCompletion.add(transaction); } + @Override + public void enlistPrepare(KeycloakTransaction transaction) { + if (active && !transaction.isActive()) { + transaction.begin(); + } + + prepare.add(transaction); + } + @Override public void begin() { if (active) { @@ -69,6 +79,17 @@ public class DefaultKeycloakTransactionManager implements KeycloakTransactionMan @Override public void commit() { RuntimeException exception = null; + for (KeycloakTransaction tx : prepare) { + try { + tx.commit(); + } catch (RuntimeException e) { + exception = exception == null ? e : exception; + } + } + if (exception != null) { + rollback(exception); + return; + } for (KeycloakTransaction tx : transactions) { try { tx.commit(); @@ -105,6 +126,10 @@ public class DefaultKeycloakTransactionManager implements KeycloakTransactionMan @Override public void rollback() { RuntimeException exception = null; + rollback(exception); + } + + protected void rollback(RuntimeException exception) { for (KeycloakTransaction tx : transactions) { try { tx.rollback(); 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 6584b400f7..f3bacbaf9f 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 @@ -18,6 +18,7 @@ package org.keycloak.testsuite.admin; import org.jboss.logging.Logger; +import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; import org.keycloak.admin.client.Keycloak; @@ -30,6 +31,8 @@ import javax.ws.rs.core.Response; import java.util.LinkedList; import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -44,14 +47,51 @@ public class ConcurrencyTest extends AbstractClientTest { private static final Logger log = Logger.getLogger(ConcurrencyTest.class); - private static final int DEFAULT_THREADS = 5; - private static final int DEFAULT_ITERATIONS = 30; + private static final int DEFAULT_THREADS = 10; + private static final int DEFAULT_ITERATIONS = 100; // If enabled only one request is allowed at the time. Useful for checking that test is working. private static final boolean SYNCHRONIZED = false; + boolean passedCreateClient = false; + boolean passedCreateRole = false; + + //@Test + public void testAllConcurrently() throws Throwable { + Thread client = new Thread(new Runnable() { + @Override + public void run() { + try { + createClient(); + passedCreateClient = true; + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + } + }); + Thread role = new Thread(new Runnable() { + @Override + public void run() { + try { + createRole(); + passedCreateRole = true; + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + } + }); + + client.start(); + role.start(); + client.join(); + role.join(); + Assert.assertTrue(passedCreateClient); + Assert.assertTrue(passedCreateRole); + } + @Test public void createClient() throws Throwable { + long start = System.currentTimeMillis(); run(new KeycloakRunnable() { @Override public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) { @@ -75,10 +115,14 @@ public class ConcurrencyTest extends AbstractClientTest { } } }); + long end = System.currentTimeMillis() - start; + System.out.println("createClient took " + end); + } @Test public void createRole() throws Throwable { + long start = System.currentTimeMillis(); run(new KeycloakRunnable() { @Override public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) { @@ -88,10 +132,14 @@ public class ConcurrencyTest extends AbstractClientTest { assertNotNull(realm.roles().get(name).toRepresentation()); } }); + long end = System.currentTimeMillis() - start; + System.out.println("createRole took " + end); + } @Test public void createClientRole() throws Throwable { + long start = System.currentTimeMillis(); ClientRepresentation c = new ClientRepresentation(); c.setClientId("client"); Response response = realm.clients().create(c); @@ -110,6 +158,9 @@ public class ConcurrencyTest extends AbstractClientTest { assertNotNull(client.roles().get(name).toRepresentation()); } }); + long end = System.currentTimeMillis() - start; + System.out.println("createClientRole took " + end); + } private void run(final KeycloakRunnable runnable) throws Throwable { diff --git a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json index 401fc6be0b..155c8d0e65 100755 --- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json @@ -33,7 +33,8 @@ }, "realmCache": { - "infinispan" : { + "provider": "infinispan-revisioned", + "infinispan-locking" : { "enabled": true } }, @@ -85,7 +86,8 @@ }, "connectionsInfinispan": { - "default": { + "provider": "revisioned", + "locking": { "clustered": "${keycloak.connectionsInfinispan.clustered:false}", "async": "${keycloak.connectionsInfinispan.async:true}", "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}"