Make sure group searches are cached and entries invalidate accordingly

Closes #26983

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2024-02-28 10:39:51 -03:00
parent 6c0f7444b5
commit 326d63ce74
4 changed files with 94 additions and 39 deletions

View file

@ -32,6 +32,7 @@ import org.keycloak.storage.StorageId;
import org.keycloak.storage.client.ClientStorageProviderModel;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -1016,40 +1017,63 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public Stream<GroupModel> getTopLevelGroupsStream(RealmModel realm, String search, Boolean exact, Integer first, Integer max) {
String cacheKey = getTopGroupsQueryCacheKey(realm.getId() + search + first + max);
boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(cacheKey)
|| listInvalidations.contains(realm.getId());
if (queryDB) {
String cacheKey = getTopGroupsQueryCacheKey(realm.getId());
if (hasInvalidation(realm, cacheKey)) {
return getGroupDelegate().getTopLevelGroupsStream(realm, search, exact, first, max);
}
GroupListQuery query = cache.get(cacheKey, GroupListQuery.class);
if (Objects.nonNull(query)) {
logger.tracev("getTopLevelGroups cache hit: {0}", realm.getName());
}
String searchKey = Optional.ofNullable(search).orElse("") + "." + Optional.ofNullable(first).orElse(-1) + "." + Optional.ofNullable(max).orElse(-1);
Set<String> cached;
if (Objects.isNull(query)) {
// not cached yet
Long loaded = cache.getCurrentRevision(cacheKey);
List<GroupModel> model = getGroupDelegate().getTopLevelGroupsStream(realm, search, exact, first, max).collect(Collectors.toList());
if (model.isEmpty()) return Stream.empty();
Set<String> ids = new HashSet<>();
for (GroupModel client : model) ids.add(client.getId());
query = new GroupListQuery(loaded, cacheKey, realm, ids);
cached = getGroupDelegate().getTopLevelGroupsStream(realm, search, exact, first, max).map(GroupModel::getId).collect(Collectors.toSet());
query = new GroupListQuery(loaded, cacheKey, realm, searchKey, cached);
logger.tracev("adding realm getTopLevelGroups cache miss: realm {0} key {1}", realm.getName(), cacheKey);
cache.addRevisioned(query, startupRevision);
return model.stream();
}
List<GroupModel> list = new LinkedList<>();
for (String id : query.getGroups()) {
GroupModel group = session.groups().getGroupById(realm, id);
if (Objects.isNull(group)) {
invalidations.add(cacheKey);
return getGroupDelegate().getTopLevelGroupsStream(realm);
} else {
logger.tracev("getTopLevelGroups cache hit: {0}", realm.getName());
cached = query.getGroups(searchKey);
if (hasInvalidation(realm, cacheKey) || cached == null) {
// there is a cache entry, but the current search is not yet cached
cache.invalidateObject(cacheKey);
Long loaded = cache.getCurrentRevision(cacheKey);
cached = getGroupDelegate().getTopLevelGroupsStream(realm, search, exact, first, max).map(GroupModel::getId).collect(Collectors.toSet());
query = new GroupListQuery(loaded, cacheKey, realm, searchKey, cached, query);
logger.tracev("adding realm getTopLevelGroups search cache miss: realm {0} key {1}", realm.getName(), searchKey);
cache.addRevisioned(query, cache.getCurrentCounter());
}
list.add(group);
}
return list.stream().sorted(GroupModel.COMPARE_BY_NAME);
AtomicBoolean invalidate = new AtomicBoolean(false);
Stream<GroupModel> groups = cached.stream()
.map((id) -> session.groups().getGroupById(realm, id))
.takeWhile(group -> {
if (Objects.isNull(group)) {
invalidate.set(true);
return false;
}
return true;
})
.sorted(GroupModel.COMPARE_BY_NAME);
if (!invalidate.get()) {
return groups;
}
invalidations.add(cacheKey);
return getGroupDelegate().getTopLevelGroupsStream(realm, search, exact, first, max);
}
private boolean hasInvalidation(RealmModel realm, String cacheKey) {
return invalidations.contains(cacheKey) || listInvalidations.contains(cacheKey)
|| listInvalidations.contains(realm.getId());
}
@Override

View file

@ -2,27 +2,59 @@ package org.keycloak.models.cache.infinispan.entities;
import org.keycloak.models.RealmModel;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class GroupListQuery extends AbstractRevisioned implements GroupQuery {
private final Set<String> groups;
private final String realm;
private final String realmName;
private Map<String, Set<String>> searchKeys;
public GroupListQuery(Long revisioned, String id, RealmModel realm, Set<String> groups) {
public GroupListQuery(Long revisioned, String id, RealmModel realm, String searchKey, Set<String> result) {
super(revisioned, id);
this.realm = realm.getId();
this.realmName = realm.getName();
this.groups = groups;
this.searchKeys = new HashMap<>();
this.searchKeys.put(searchKey, result);
}
public GroupListQuery(Long revisioned, String id, RealmModel realm, String searchKey, Set<String> result, GroupListQuery previous) {
super(revisioned, id);
this.realm = realm.getId();
this.realmName = realm.getName();
this.searchKeys = new HashMap<>();
this.searchKeys.putAll(previous.searchKeys);
this.searchKeys.put(searchKey, result);
}
public GroupListQuery(Long revisioned, String id, RealmModel realm, Set<String> ids) {
super(revisioned, id);
this.realm = realm.getId();
this.realmName = realm.getName();
this.searchKeys = new HashMap<>();
this.searchKeys.put(id, ids);
}
@Override
public Set<String> getGroups() {
return groups;
Collection<Set<String>> values = searchKeys.values();
if (values.isEmpty()) {
return Set.of();
}
return values.stream().flatMap(Set::stream).collect(Collectors.toSet());
}
public Set<String> getGroups(String searchKey) {
return searchKeys.get(searchKey);
}
@Override
@ -30,6 +62,13 @@ public class GroupListQuery extends AbstractRevisioned implements GroupQuery {
return realm;
}
public Map<String, Set<String>> getSearchKeys() {
if (searchKeys == null) {
searchKeys = new HashMap<>();
}
return searchKeys;
}
@Override
public String toString() {
return "GroupListQuery{" +

View file

@ -20,8 +20,6 @@ package org.keycloak.models;
import org.keycloak.provider.Provider;
import org.keycloak.storage.group.GroupLookupProvider;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**

View file

@ -235,17 +235,11 @@ public class ConcurrencyTest extends AbstractConcurrencyTest {
c = realm.groups().group(id).toRepresentation();
assertNotNull(c);
boolean retry = true;
int i = 0;
do {
List<String> groups = realm.groups().groups().stream()
.map(GroupRepresentation::getName)
.filter(Objects::nonNull)
.collect(Collectors.toList());
retry = !groups.contains(name);
i++;
} while(retry && i < 3);
assertFalse("Group " + name + " [" + id + "] " + " not found in group list", retry);
assertTrue("Group " + name + " [" + id + "] " + " not found in group list",
realm.groups().groups().stream()
.map(GroupRepresentation::getName)
.filter(Objects::nonNull)
.anyMatch(name::equals));
}
}