Merge pull request #2327 from patriot1burke/master

cleanup cache
This commit is contained in:
Bill Burke 2016-03-04 11:08:50 -05:00
commit b709ca96b1
6 changed files with 38 additions and 105 deletions

View file

@ -66,8 +66,6 @@ import java.util.Set;
* it is added to the cache. So, we keep the version number around for this.
* - In a transaction, objects are registered to be invalidated. If an object is marked for invalidation within a transaction
* a cached object should never be returned. An DB adapter should always be returned.
* - At prepare phase of the transaction, a local lock on the revision cache will be obtained for each object marked for invalidation
* we sort the list of these keys to order local acquisition and avoid deadlocks.
* - After DB commits, the objects marked for invalidation are invalidated, or rather removed from the cache. At this time
* the revision cache entry for this object has its version number bumped.
* - Whenever an object is marked for invalidation, the cache is also searched for any objects that are related to this object
@ -88,15 +86,20 @@ import java.util.Set;
* - There is a Infinispan @Listener registered. If an invalidation event happens, this is treated like
* the object was removed from the database and will perform evictions based on that assumption.
* - Eviction events will also cascade other evictions, but not assume this is a db removal.
* - With an invalidation cache, if you remove an entry on node 1 and this entry does not exist on node 2, node 2 will not receive a @Listener invalidation event.
* so, hat we have to put a marker entry in the invalidation cache before we read from the DB, so if the DB changes in between reading and adding a cache entry, the cache will be notified and bump
* the version information.
*
* DBs with Repeatable Read:
* - DBs like MySQL are Repeatable Read by default. So, if you query a Client for instance, it will always return the same result in the same transaction even if the DB
* was updated in between these queries. This makes it possible to store stale cache entries. To avoid this problem, this class stores the current local version counter
* at the beginningof the transaction. Whenever an entry is added to the cache, the current coutner is compared against the counter at the beginning of the tx. If the current
* is greater, then don't cache.
*
* Groups and Roles:
* - roles are tricky because of composites. Composite lists are cached too. So, when a role is removed
* we also iterate and invalidate any role or group that contains that role being removed.
*
* - Clustering gotchyas. With an invalidation cache, if you remove an entry on node 1 and this entry does not exist on node 2, node 2 will not receive a @Listener invalidation event.
* so, hat we have to put a marker entry in the invalidation cache before we read from the DB, so if the DB changes in between reading and adding a cache entry, the cache will be notified and bump
* the version information.
*
* - any relationship should be resolved from session.realms(). For example if JPA.getClientByClientId() is invoked,
* JPA should find the id of the client and then call session.realms().getClientById(). THis is to ensure that the cached
* object is invoked and all proper invalidation are being invoked.
@ -124,14 +127,20 @@ public class StreamCacheRealmProvider implements CacheRealmProvider {
protected Set<String> invalidations = new HashSet<>();
protected boolean clearAll;
protected final long startupRevision;
public StreamCacheRealmProvider(StreamRealmCache cache, KeycloakSession session) {
this.cache = cache;
this.session = session;
this.startupRevision = UpdateCounter.current();
session.getTransaction().enlistPrepare(getPrepareTransaction());
session.getTransaction().enlistAfterCompletion(getAfterTransaction());
}
public long getStartupRevision() {
return startupRevision;
}
@Override
public void clear() {
cache.clear();
@ -305,7 +314,7 @@ public class StreamCacheRealmProvider implements CacheRealmProvider {
if (model == null) return null;
if (invalidations.contains(id)) return model;
cached = new CachedRealm(loaded, model);
cache.addRevisioned(cached, session);
cache.addRevisioned(cached, startupRevision);
} else if (invalidations.contains(id)) {
return getDelegate().getRealm(id);
} else if (managedRealms.containsKey(id)) {
@ -329,7 +338,7 @@ public class StreamCacheRealmProvider implements CacheRealmProvider {
if (model == null) return null;
if (invalidations.contains(model.getId())) return model;
query = new RealmListQuery(loaded, cacheKey, model.getId());
cache.addRevisioned(query, session);
cache.addRevisioned(query, startupRevision);
return model;
} else if (invalidations.contains(cacheKey)) {
return getDelegate().getRealmByName(name);
@ -435,7 +444,7 @@ public class StreamCacheRealmProvider implements CacheRealmProvider {
for (ClientModel client : model) ids.add(client.getId());
query = new ClientListQuery(loaded, cacheKey, realm, ids);
logger.tracev("adding realm clients cache miss: realm {0} key {1}", realm.getName(), cacheKey);
cache.addRevisioned(query, session);
cache.addRevisioned(query, startupRevision);
return model;
}
List<ClientModel> list = new LinkedList<>();
@ -508,7 +517,7 @@ public class StreamCacheRealmProvider implements CacheRealmProvider {
for (RoleModel role : model) ids.add(role.getId());
query = new RoleListQuery(loaded, cacheKey, realm, ids);
logger.tracev("adding realm roles cache miss: realm {0} key {1}", realm.getName(), cacheKey);
cache.addRevisioned(query, session);
cache.addRevisioned(query, startupRevision);
return model;
}
Set<RoleModel> list = new HashSet<>();
@ -544,7 +553,7 @@ public class StreamCacheRealmProvider implements CacheRealmProvider {
for (RoleModel role : model) ids.add(role.getId());
query = new RoleListQuery(loaded, cacheKey, realm, ids, client.getClientId());
logger.tracev("adding client roles cache miss: client {0} key {1}", client.getClientId(), cacheKey);
cache.addRevisioned(query, session);
cache.addRevisioned(query, startupRevision);
return model;
}
Set<RoleModel> list = new HashSet<>();
@ -593,7 +602,7 @@ public class StreamCacheRealmProvider implements CacheRealmProvider {
if (model == null) return null;
query = new RoleListQuery(loaded, cacheKey, realm, model.getId());
logger.tracev("adding realm role cache miss: client {0} key {1}", realm.getName(), cacheKey);
cache.addRevisioned(query, session);
cache.addRevisioned(query, startupRevision);
return model;
}
RoleModel role = getRoleById(query.getRoles().iterator().next(), realm);
@ -623,7 +632,7 @@ public class StreamCacheRealmProvider implements CacheRealmProvider {
if (model == null) return null;
query = new RoleListQuery(loaded, cacheKey, realm, model.getId(), client.getClientId());
logger.tracev("adding client role cache miss: client {0} key {1}", client.getClientId(), cacheKey);
cache.addRevisioned(query, session);
cache.addRevisioned(query, startupRevision);
return model;
}
RoleModel role = getRoleById(query.getRoles().iterator().next(), realm);
@ -660,7 +669,7 @@ public class StreamCacheRealmProvider implements CacheRealmProvider {
} else {
cached = new CachedRealmRole(loaded, model, realm);
}
cache.addRevisioned(cached, session);
cache.addRevisioned(cached, startupRevision);
} else if (invalidations.contains(id)) {
return getDelegate().getRoleById(id, realm);
@ -685,7 +694,7 @@ public class StreamCacheRealmProvider implements CacheRealmProvider {
if (model == null) return null;
if (invalidations.contains(id)) return model;
cached = new CachedGroup(loaded, realm, model);
cache.addRevisioned(cached, session);
cache.addRevisioned(cached, startupRevision);
} else if (invalidations.contains(id)) {
return getDelegate().getGroupById(id, realm);
@ -725,7 +734,7 @@ public class StreamCacheRealmProvider implements CacheRealmProvider {
for (GroupModel client : model) ids.add(client.getId());
query = new GroupListQuery(loaded, cacheKey, realm, ids);
logger.tracev("adding realm getGroups cache miss: realm {0} key {1}", realm.getName(), cacheKey);
cache.addRevisioned(query, session);
cache.addRevisioned(query, startupRevision);
return model;
}
List<GroupModel> list = new LinkedList<>();
@ -761,7 +770,7 @@ public class StreamCacheRealmProvider implements CacheRealmProvider {
for (GroupModel client : model) ids.add(client.getId());
query = new GroupListQuery(loaded, cacheKey, realm, ids);
logger.tracev("adding realm getTopLevelGroups cache miss: realm {0} key {1}", realm.getName(), cacheKey);
cache.addRevisioned(query, session);
cache.addRevisioned(query, startupRevision);
return model;
}
List<GroupModel> list = new LinkedList<>();
@ -837,7 +846,7 @@ public class StreamCacheRealmProvider implements CacheRealmProvider {
if (invalidations.contains(id)) return model;
cached = new CachedClient(loaded, realm, model);
logger.tracev("adding client by id cache miss: {0}", cached.getClientId());
cache.addRevisioned(cached, session);
cache.addRevisioned(cached, startupRevision);
} else if (invalidations.contains(id)) {
return getDelegate().getClientById(id, realm);
} else if (managedApplications.containsKey(id)) {
@ -866,7 +875,7 @@ public class StreamCacheRealmProvider implements CacheRealmProvider {
id = model.getId();
query = new ClientListQuery(loaded, cacheKey, realm, id);
logger.tracev("adding client by name cache miss: {0}", clientId);
cache.addRevisioned(query, session);
cache.addRevisioned(query, startupRevision);
} else if (invalidations.contains(cacheKey)) {
return getDelegate().getClientByClientId(clientId, realm);
} else {
@ -895,7 +904,7 @@ public class StreamCacheRealmProvider implements CacheRealmProvider {
if (model == null) return null;
if (invalidations.contains(id)) return model;
cached = new CachedClientTemplate(loaded, realm, model);
cache.addRevisioned(cached, session);
cache.addRevisioned(cached, startupRevision);
} else if (invalidations.contains(id)) {
return getDelegate().getClientTemplateById(id, realm);
} else if (managedClientTemplates.containsKey(id)) {

View file

@ -39,7 +39,6 @@ import org.keycloak.models.cache.infinispan.stream.HasRolePredicate;
import org.keycloak.models.cache.infinispan.stream.InClientPredicate;
import org.keycloak.models.cache.infinispan.stream.InRealmPredicate;
import org.keycloak.models.cache.infinispan.stream.RealmQueryPredicate;
import org.keycloak.models.utils.UpdateCounter;
import java.util.HashSet;
import java.util.Iterator;
@ -124,7 +123,7 @@ public class StreamRealmCache {
Object rev = revisions.put(id, next);
}
public void addRevisioned(Revisioned object, KeycloakSession session) {
public void addRevisioned(Revisioned object, long startupRevision) {
//startRevisionBatch();
String id = object.getId();
try {
@ -145,9 +144,9 @@ public class StreamRealmCache {
if (id.endsWith("realm.clients")) logger.trace("addRevisioned rev2 == null realm.clients");
return;
}
if (rev > session.getTransaction().getStartupRevision()) { // revision is ahead transaction start. Other transaction updated in the meantime. Don't cache
if (rev > startupRevision) { // revision is ahead transaction start. Other transaction updated in the meantime. Don't cache
if (logger.isTraceEnabled()) {
logger.tracev("Skipped cache. Current revision {0}, Transaction start revision {1}", object.getRevision(), session.getTransaction().getStartupRevision());
logger.tracev("Skipped cache. Current revision {0}, Transaction start revision {1}", object.getRevision(), startupRevision);
}
return;
}

View file

@ -1,4 +1,4 @@
package org.keycloak.models.utils;
package org.keycloak.models.cache.infinispan;
import java.util.concurrent.atomic.AtomicLong;

View file

@ -55,15 +55,6 @@ public class JpaRealmProvider implements RealmProvider {
private final KeycloakSession session;
protected EntityManager em;
// we have a local map of adapter classes for two reasons
// 1. So we don't have to allocate one again
// 2. So that we can do em.refresh() on the entity to make sure the state
// is up to date. If em.find is called a second time without a session, the same
// entity instance is returned. With the caching layer above it, it will cache stale
// entries.
protected Map<String, Object> adapters = new HashMap<>();
public JpaRealmProvider(KeycloakSession session, EntityManager em) {
this.session = session;
this.em = em;
@ -93,20 +84,14 @@ public class JpaRealmProvider implements RealmProvider {
return adapter;
}
});
adapters.put(id, adapter);
return adapter;
}
@Override
public RealmModel getRealm(String id) {
RealmAdapter adapter = (RealmAdapter)findJpaModel(id);
if (adapter != null) {
return adapter;
}
RealmEntity realm = em.find(RealmEntity.class, id);
if (realm == null) return null;
adapter = new RealmAdapter(session, em, realm);
adapters.put(id, adapter);
RealmAdapter adapter = new RealmAdapter(session, em, realm);
return adapter;
}
@ -144,7 +129,6 @@ public class JpaRealmProvider implements RealmProvider {
em.refresh(realm);
RealmAdapter adapter = new RealmAdapter(session, em, realm);
session.users().preRemove(adapter);
adapters.remove(id);
int num = em.createNamedQuery("deleteGroupRoleMappingsByRealm")
.setParameter("realm", realm).executeUpdate();
num = em.createNamedQuery("deleteGroupAttributesByRealm")
@ -194,7 +178,6 @@ public class JpaRealmProvider implements RealmProvider {
em.persist(entity);
em.flush();
RoleAdapter adapter = new RoleAdapter(session, realm, em, entity);
adapters.put(id, adapter);
return adapter;
}
@ -225,7 +208,6 @@ public class JpaRealmProvider implements RealmProvider {
em.persist(roleEntity);
em.flush();
RoleAdapter adapter = new RoleAdapter(session, realm, em, roleEntity);
adapters.put(id, adapter);
return adapter;
}
@ -274,7 +256,6 @@ public class JpaRealmProvider implements RealmProvider {
if (container.getDefaultRoles().contains(role.getName())) {
container.removeDefaultRoles(role.getName());
}
adapters.remove(role.getId());
RoleEntity roleEntity = em.getReference(RoleEntity.class, role.getId());
String compositeRoleTable = JpaUtils.getTableNameForNativeQuery("COMPOSITE_ROLE", em);
em.createNativeQuery("delete from " + compositeRoleTable + " where CHILD_ROLE = :role").setParameter("role", roleEntity).executeUpdate();
@ -290,28 +271,19 @@ public class JpaRealmProvider implements RealmProvider {
@Override
public RoleModel getRoleById(String id, RealmModel realm) {
RoleAdapter adapter = (RoleAdapter)findJpaModel(id);
if (adapter != null) {
if (!realm.getId().equals(adapter.getEntity().getRealmId())) return null;
return adapter;
}
RoleEntity entity = em.find(RoleEntity.class, id);
if (entity == null) return null;
if (!realm.getId().equals(entity.getRealmId())) return null;
adapter = new RoleAdapter(session, realm, em, entity);
adapters.put(id, adapter);
RoleAdapter adapter = new RoleAdapter(session, realm, em, entity);
return adapter;
}
@Override
public GroupModel getGroupById(String id, RealmModel realm) {
GroupAdapter adapter = (GroupAdapter)findJpaModel(id);
if (adapter != null) return adapter;
GroupEntity groupEntity = em.find(GroupEntity.class, id);
if (groupEntity == null) return null;
if (!groupEntity.getRealm().getId().equals(realm.getId())) return null;
adapter = new GroupAdapter(realm, em, groupEntity);
adapters.put(id, adapter);
GroupAdapter adapter = new GroupAdapter(realm, em, groupEntity);
return adapter;
}
@ -360,7 +332,6 @@ public class JpaRealmProvider implements RealmProvider {
}
session.users().preRemove(realm, group);
adapters.remove(group.getId());
realm.removeDefaultGroup(group);
for (GroupModel subGroup : group.getSubGroups()) {
@ -397,7 +368,6 @@ public class JpaRealmProvider implements RealmProvider {
em.persist(groupEntity);
GroupAdapter adapter = new GroupAdapter(realm, em, groupEntity);
adapters.put(id, adapter);
return adapter;
}
@ -428,7 +398,6 @@ public class JpaRealmProvider implements RealmProvider {
em.persist(entity);
em.flush();
final ClientModel resource = new ClientAdapter(realm, em, session, entity);
adapters.put(id, resource);
em.flush();
session.getKeycloakSessionFactory().publish(new RealmModel.ClientCreationEvent() {
@ -455,37 +424,12 @@ public class JpaRealmProvider implements RealmProvider {
}
protected JpaModel findJpaModel(String id) {
// we have a local map of adapter classes for two reasons
// 1. So we don't have to allocate one again
// 2. So that we can do em.refresh() on the entity to make sure the state
// is up to date. If em.find is called a second time without a session, the same
// entity instance is returned as its already in the first level cache. With the caching layer above it, it will cache stale
// entries.
JpaModel client = (JpaModel)adapters.get(id);
if (client != null) {
if (em.contains(client.getEntity())) {
em.flush(); // have to flush as refresh blows away updates
em.refresh(client.getEntity());
return client;
}
}
return null;
}
@Override
public ClientModel getClientById(String id, RealmModel realm) {
ClientAdapter client = (ClientAdapter)findJpaModel(id);
if (client != null) {
if (!realm.getId().equals(client.getRealm().getId())) return null;
return client;
}
ClientEntity app = em.find(ClientEntity.class, id);
// Check if application belongs to this realm
if (app == null || !realm.getId().equals(app.getRealm().getId())) return null;
client = new ClientAdapter(realm, em, session, app);
adapters.put(id, client);
ClientAdapter client = new ClientAdapter(realm, em, session, app);
return client;
}
@ -523,23 +467,16 @@ public class JpaRealmProvider implements RealmProvider {
logger.errorv("Unable to delete client entity: {0} from realm {1}", client.getClientId(), realm.getName());
throw e;
}
adapters.remove(id);
return true;
}
@Override
public ClientTemplateModel getClientTemplateById(String id, RealmModel realm) {
ClientTemplateAdapter adapter = (ClientTemplateAdapter)findJpaModel(id);
if (adapter != null) {
if (!realm.getId().equals(adapter.getRealm().getId())) return null;
return adapter;
}
ClientTemplateEntity app = em.find(ClientTemplateEntity.class, id);
// Check if application belongs to this realm
if (app == null || !realm.getId().equals(app.getRealm().getId())) return null;
adapter = new ClientTemplateAdapter(realm, em, session, app);
adapters.put(id, adapter);
ClientTemplateAdapter adapter = new ClientTemplateAdapter(realm, em, session, app);
return adapter;
}
}

View file

@ -23,8 +23,6 @@ package org.keycloak.models;
*/
public interface KeycloakTransactionManager extends KeycloakTransaction {
long getStartupRevision();
void enlist(KeycloakTransaction transaction);
void enlistAfterCompletion(KeycloakTransaction transaction);

View file

@ -18,8 +18,6 @@ package org.keycloak.services;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.KeycloakTransactionManager;
import org.keycloak.models.utils.UpdateCounter;
import org.keycloak.services.ServicesLogger;
import java.util.LinkedList;
import java.util.List;
@ -36,12 +34,6 @@ public class DefaultKeycloakTransactionManager implements KeycloakTransactionMan
private List<KeycloakTransaction> afterCompletion = new LinkedList<KeycloakTransaction>();
private boolean active;
private boolean rollback;
private long startupRevision;
@Override
public long getStartupRevision() {
return startupRevision;
}
@Override
public void enlist(KeycloakTransaction transaction) {
@ -76,8 +68,6 @@ public class DefaultKeycloakTransactionManager implements KeycloakTransactionMan
throw new IllegalStateException("Transaction already active");
}
startupRevision = UpdateCounter.current();
for (KeycloakTransaction tx : transactions) {
tx.begin();
}