diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/StreamCacheRealmProvider.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/StreamCacheRealmProvider.java index d21599c196..0a50575c94 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/StreamCacheRealmProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/StreamCacheRealmProvider.java @@ -49,6 +49,50 @@ import java.util.Set; /** + * - the high level architecture of this cache is an invalidation cache. + * - the cache is manual/custom versioned. When a model is updated, we remove it from the cache + * which causes an invalidation message to be sent across the cluster. + * - We had to do it this way because Infinispan REPEATABLE_READ + * wouldn't cut it in invalidation mode. Also, REPEATABLE_READ doesn't work very well on relationships and items that are + * not in the cache. + * - There are two Infinispan caches. One clustered that holds actual objects and a another local one that holds revision + * numbers of cached objects. Whenever a cached object is removed (invalidated), the local revision + * cache number or that key is bumped higher based on a local version counter. Whenever a cache entry is fetched, this + * revision number is also fetched and compared against the revision number in the cache entry to see if the cache entry + * is stale. Whenever a cache entry is added, this revision number is also checked against the revision cache. + * - Revision entries are actually never removed (although they could be evicted by cache eviction policies). The reason for this + * is that it is possible for a stale object to be inserted if one thread loads and the data is updated in the database before + * 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 + * and need to also be evicted/removed. We use the Infinispan Stream SPI for this. + * + * ClientList caches: + * - lists of clients are cached in a specific cache entry i.e. realm clients, find client by clientId + * - realm client lists need to be invalidated and evited whenever a client is added or removed from a realm. RealmProvider + * now has addClient/removeClient at its top level. All adapaters should use these methods so that the appropriate invalidations + * can be registered. + * - whenever a client is added/removed the realm of the client is added to a clientListInvalidations set + * this set must be checked before sending back or caching a cached query. This check is required to + * avoid caching an uncommitted removal/add in a query cache. + * - when a client is removed, any queries that contain that client must also be removed. + * - a client removal will also cause anything that is contained and cached within that client to be removed + * + * Clustered caches: + * - 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. + * + * 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. + * + * * * - 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