Calling getTopLevelGroups is slow inside GroupLDAPStorageMapper#getLDAPGroupMappingsConverted (#8430)

Closes #14820 
---------
Co-authored-by: Michal Hajas <mhajas@redhat.com>
This commit is contained in:
Bernd Bohmann
2023-09-20 17:20:43 +02:00
committed by GitHub
parent f8a9e0134a
commit bb2f59df87
20 changed files with 471 additions and 42 deletions

View File

@@ -26,6 +26,7 @@ import org.keycloak.models.cache.infinispan.events.RealmCacheInvalidationEvent;
import org.keycloak.models.cache.infinispan.stream.GroupListPredicate;
import org.keycloak.models.cache.infinispan.stream.HasRolePredicate;
import org.keycloak.models.cache.infinispan.stream.InClientPredicate;
import org.keycloak.models.cache.infinispan.stream.InGroupPredicate;
import org.keycloak.models.cache.infinispan.stream.InRealmPredicate;
import java.util.Set;
@@ -96,6 +97,10 @@ public class RealmCacheManager extends CacheManager {
addInvalidations(GroupListPredicate.create().realm(realmId), invalidations);
}
public void groupNameInvalidations(String groupId, Set<String> invalidations) {
addInvalidations(InGroupPredicate.create().group(groupId), invalidations);
}
public void clientAdded(String realmId, String clientUUID, String clientId, Set<String> invalidations) {
invalidations.add(RealmCacheSession.getRealmClientsQueryCacheKey(realmId));
}

View File

@@ -277,6 +277,7 @@ public class RealmCacheSession implements CacheRealmProvider {
private void invalidateGroup(String id, String realmId, boolean invalidateQueries) {
invalidateGroup(id);
cache.groupNameInvalidations(id, invalidations);
if (invalidateQueries) {
cache.groupQueriesInvalidations(realmId, invalidations);
}
@@ -564,6 +565,14 @@ public class RealmCacheSession implements CacheRealmProvider {
return realm + ".top.groups";
}
static String getGroupByNameCacheKey(String realm, String parentId, String name) {
if (parentId != null) {
return realm + ".group." + parentId + "." + name;
} else {
return realm + ".group.top." + name;
}
}
static String getRolesCacheKey(String container) {
return container + ROLES_QUERY_SUFFIX;
}
@@ -886,6 +895,33 @@ public class RealmCacheSession implements CacheRealmProvider {
return adapter;
}
@Override
public GroupModel getGroupByName(RealmModel realm, GroupModel parent, String name) {
String cacheKey = getGroupByNameCacheKey(realm.getId(), parent != null? parent.getId(): null, name);
GroupNameQuery query = cache.get(cacheKey, GroupNameQuery.class);
if (query != null) {
logger.tracev("Group by name cache hit: {0}", name);
}
if (query == null) {
Long loaded = cache.getCurrentRevision(cacheKey);
GroupModel model = getGroupDelegate().getGroupByName(realm, parent, name);
if (model == null) return null;
if (invalidations.contains(model.getId())) return model;
query = new GroupNameQuery(loaded, cacheKey, model.getId(), realm);
cache.addRevisioned(query, startupRevision);
return model;
} else if (invalidations.contains(cacheKey)) {
return getGroupDelegate().getGroupByName(realm, parent, name);
} else {
String groupId = query.getGroupId();
if (invalidations.contains(groupId)) {
return getGroupDelegate().getGroupByName(realm, parent, name);
}
return getGroupById(realm, groupId);
}
}
@Override
public void moveGroup(RealmModel realm, GroupModel group, GroupModel toParent) {
invalidateGroup(group.getId(), realm.getId(), true);

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2022 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.entities;
import org.keycloak.models.RealmModel;
public class GroupNameQuery extends AbstractRevisioned implements InRealm {
private final String realm;
private final String realmName;
private final String groupId;
public GroupNameQuery(Long revisioned, String id, String groupId, RealmModel realm) {
super(revisioned, id);
this.realm = realm.getId();
this.realmName = realm.getName();
this.groupId = groupId;
}
public String getGroupId() {
return groupId;
}
public String getRealm() {
return realm;
}
@Override
public String toString() {
return "GroupNameQuery{" +
"id='" + getId() + "'" +
"realmName='" + realmName + '\'' +
'}';
}
}

View File

@@ -62,6 +62,7 @@ public class GroupMovedEvent extends InvalidationEvent implements RealmCacheInva
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.groupQueriesInvalidations(realmId, invalidations);
realmCache.groupNameInvalidations(groupId, invalidations);
if (newParentId != null) {
invalidations.add(newParentId);
}

View File

@@ -60,6 +60,7 @@ public class GroupRemovedEvent extends InvalidationEvent implements RealmCacheIn
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.groupQueriesInvalidations(realmId, invalidations);
realmCache.groupNameInvalidations(groupId, invalidations);
if (parentId != null) {
invalidations.add(parentId);
}

View File

@@ -54,7 +54,7 @@ public class GroupUpdatedEvent extends InvalidationEvent implements RealmCacheIn
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
// Nothing. ID already invalidated
realmCache.groupNameInvalidations(groupId, invalidations);
}
public static class ExternalizerImpl implements Externalizer<GroupUpdatedEvent> {

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2022 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.stream;
import org.keycloak.models.cache.infinispan.entities.GroupNameQuery;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Serializable;
import java.util.Map;
import java.util.function.Predicate;
import org.infinispan.commons.marshall.Externalizer;
import org.infinispan.commons.marshall.MarshallUtil;
import org.infinispan.commons.marshall.SerializeWith;
@SerializeWith(InGroupPredicate.ExternalizerImpl.class)
public class InGroupPredicate implements Predicate<Map.Entry<String, Revisioned>>, Serializable {
private String group;
public static InGroupPredicate create() {
return new InGroupPredicate();
}
public InGroupPredicate group(String id) {
group = id;
return this;
}
@Override
public boolean test(Map.Entry<String, Revisioned> entry) {
Object value = entry.getValue();
if (value == null) return false;
if (!(value instanceof GroupNameQuery)) return false;
return group.equals(((GroupNameQuery)value).getGroupId());
}
public static class ExternalizerImpl implements Externalizer<InGroupPredicate> {
private static final int VERSION_1 = 1;
@Override
public void writeObject(ObjectOutput output, InGroupPredicate obj) throws IOException {
output.writeByte(VERSION_1);
MarshallUtil.marshallString(obj.group, output);
}
@Override
public InGroupPredicate readObject(ObjectInput input) throws IOException, ClassNotFoundException {
switch (input.readByte()) {
case VERSION_1:
return readObjectVersion1(input);
default:
throw new IOException("Unknown version");
}
}
public InGroupPredicate readObjectVersion1(ObjectInput input) throws IOException, ClassNotFoundException {
InGroupPredicate res = new InGroupPredicate();
res.group = MarshallUtil.unmarshallString(input);
return res;
}
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2022 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.sessions.infinispan.entities.wildfly;
import org.keycloak.models.cache.infinispan.stream.InGroupPredicate;
public class InGroupPredicateWFExternalizer extends InfinispanExternalizerAdapter<InGroupPredicate> {
public InGroupPredicateWFExternalizer() {
super(InGroupPredicate.class, new InGroupPredicate.ExternalizerImpl());
}
}

View File

@@ -55,6 +55,7 @@ org.keycloak.models.sessions.infinispan.entities.wildfly.LockEntryWFExternalizer
org.keycloak.models.sessions.infinispan.entities.wildfly.ActionTokenValueEntityWFExternalizer
org.keycloak.models.sessions.infinispan.entities.wildfly.RoleAddedEventWFExternalizer
org.keycloak.models.sessions.infinispan.entities.wildfly.InClientPredicateWFExternalizer
org.keycloak.models.sessions.infinispan.entities.wildfly.InGroupPredicateWFExternalizer
org.keycloak.models.sessions.infinispan.entities.wildfly.UserFullInvalidationEventWFExternalizer
org.keycloak.models.sessions.infinispan.entities.wildfly.ClientRemovedSessionEventWFExternalizer
org.keycloak.models.sessions.infinispan.entities.wildfly.SessionPredicateWFExternalizer

View File

@@ -435,6 +435,20 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
return adapter;
}
@Override
public GroupModel getGroupByName(RealmModel realm, GroupModel parent, String name) {
TypedQuery<String> query = em.createNamedQuery("getGroupIdByNameAndParent", String.class);
query.setParameter("name", name);
query.setParameter("realm", realm.getId());
query.setParameter("parent", parent != null ? parent.getId() : GroupEntity.TOP_PARENT_ID);
List<String> entities = query.getResultList();
if (entities.isEmpty()) return null;
if (entities.size() > 1) throw new IllegalStateException("Should not be more than one Group with same name");
String id = query.getResultList().get(0);
return session.groups().getGroupById(realm, id);
}
@Override
public void moveGroup(RealmModel realm, GroupModel group, GroupModel toParent) {
if (toParent != null && group.getId().equals(toParent.getId())) {

View File

@@ -37,7 +37,8 @@ import java.util.LinkedList;
@NamedQuery(name="getGroupCountByNameContainingFromIdList", query="select count(u) from GroupEntity u where u.realm = :realm and lower(u.name) like lower(concat('%',:search,'%')) and u.id in :ids"),
@NamedQuery(name="getTopLevelGroupIds", query="select u.id from GroupEntity u where u.parentId = :parent and u.realm = :realm order by u.name ASC"),
@NamedQuery(name="getGroupCount", query="select count(u) from GroupEntity u where u.realm = :realm"),
@NamedQuery(name="getTopLevelGroupCount", query="select count(u) from GroupEntity u where u.realm = :realm and u.parentId = :parent")
@NamedQuery(name="getTopLevelGroupCount", query="select count(u) from GroupEntity u where u.realm = :realm and u.parentId = :parent"),
@NamedQuery(name="getGroupIdByNameAndParent", query="select u.id from GroupEntity u where u.realm = :realm and u.parentId = :parent and u.name = :name")
})
@Entity
@Table(name="KEYCLOAK_GROUP",

View File

@@ -55,6 +55,11 @@ public class GroupStorageManager extends AbstractStorageManager<GroupStorageProv
return provider.getGroupById(realm, id);
}
@Override
public GroupModel getGroupByName(RealmModel realm, GroupModel parent, String name) {
return localStorage().getGroupByName(realm, parent, name);
}
@Override
public Stream<GroupModel> searchGroupsByAttributes(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults) {
Stream<GroupModel> local = localStorage().searchGroupsByAttributes(realm, attributes, firstResult, maxResults);

View File

@@ -93,6 +93,28 @@ public class MapGroupProvider implements GroupProvider {
: entityToAdapterFunc(realm).apply(entity);
}
@Override
public GroupModel getGroupByName(RealmModel realm, GroupModel parent, String name) {
if (name == null) {
return null;
}
LOG.tracef("getGroupByName(%s, %s)%s", realm, name, getShortStackTrace());
DefaultModelCriteria<GroupModel> mcb = criteria();
mcb = mcb.compare(SearchableFields.NAME, Operator.EQ, name)
.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId());
if (parent != null) {
mcb = mcb.compare(SearchableFields.PARENT_ID, Operator.EQ, parent.getId());
} else {
mcb = mcb.compare(SearchableFields.PARENT_ID, Operator.NOT_EXISTS);
}
QueryParameters<GroupModel> queryParameters = withCriteria(mcb);
String groupId = storeWithRealm(realm).read(queryParameters).findFirst().map(MapGroupEntity::getId)
.orElse(null);
return groupId == null ? null : session.groups().getGroupById(realm, groupId);
}
@Override
public Stream<GroupModel> getGroupsStream(RealmModel realm) {
return getGroupsStreamInternal(realm, null, null);