Skip to content

Commit 6b2984e

Browse files
feat: More optimal IterateHierarchyV2 and iterateChildrenV2 [#600] (#601)
* chore: More optimal IterateHierarchyV2 and iterateChildrenV2 [#600] Closes #600 The existing (effectively v1) implementations are suboptimal since they don't construct a graph before the iteration. They search for children by looking at all namespace resources and checking `isParentOf`, which can give `O(tree_size * namespace_resources_count)` time complexity. The v2 algorithms construct the graph and have `O(namespace_resources_count)` time complexity. See more details in the linked issues. Signed-off-by: Andrii Korotkov <andrii.korotkov@verkada.com> * improvements to graph building Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * use old name Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * chore: More optimal IterateHierarchyV2 and iterateChildrenV2 [#600] Closes #600 The existing (effectively v1) implementations are suboptimal since they don't construct a graph before the iteration. They search for children by looking at all namespace resources and checking `isParentOf`, which can give `O(tree_size * namespace_resources_count)` time complexity. The v2 algorithms construct the graph and have `O(namespace_resources_count)` time complexity. See more details in the linked issues. Signed-off-by: Andrii Korotkov <andrii.korotkov@verkada.com> * finish merge Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * chore: More optimal IterateHierarchyV2 and iterateChildrenV2 [#600] Closes #600 The existing (effectively v1) implementations are suboptimal since they don't construct a graph before the iteration. They search for children by looking at all namespace resources and checking `isParentOf`, which can give `O(tree_size * namespace_resources_count)` time complexity. The v2 algorithms construct the graph and have `O(namespace_resources_count)` time complexity. See more details in the linked issues. Signed-off-by: Andrii Korotkov <andrii.korotkov@verkada.com> * discard unneeded copies of child resources as we go Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * remove unnecessary comment Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * make childrenByUID sparse Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * eliminate duplicate map Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * fix comment Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * add useful comment back Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * use nsNodes instead of dupe map Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * remove unused struct Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * skip invalid APIVersion Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> --------- Signed-off-by: Andrii Korotkov <andrii.korotkov@verkada.com> Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
1 parent 7d150d0 commit 6b2984e

File tree

5 files changed

+405
-34
lines changed

5 files changed

+405
-34
lines changed

pkg/cache/cluster.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ type ClusterCache interface {
122122
// IterateHierarchy iterates resource tree starting from the specified top level resource and executes callback for each resource in the tree.
123123
// The action callback returns true if iteration should continue and false otherwise.
124124
IterateHierarchy(key kube.ResourceKey, action func(resource *Resource, namespaceResources map[kube.ResourceKey]*Resource) bool)
125+
// IterateHierarchyV2 iterates resource tree starting from the specified top level resources and executes callback for each resource in the tree.
126+
// The action callback returns true if iteration should continue and false otherwise.
127+
IterateHierarchyV2(keys []kube.ResourceKey, action func(resource *Resource, namespaceResources map[kube.ResourceKey]*Resource) bool)
125128
// IsNamespaced answers if specified group/kind is a namespaced resource API or not
126129
IsNamespaced(gk schema.GroupKind) (bool, error)
127130
// GetManagedLiveObjs helps finding matching live K8S resources for a given resources list.
@@ -1023,6 +1026,107 @@ func (c *clusterCache) IterateHierarchy(key kube.ResourceKey, action func(resour
10231026
}
10241027
}
10251028

1029+
// IterateHierarchy iterates resource tree starting from the specified top level resources and executes callback for each resource in the tree
1030+
func (c *clusterCache) IterateHierarchyV2(keys []kube.ResourceKey, action func(resource *Resource, namespaceResources map[kube.ResourceKey]*Resource) bool) {
1031+
c.lock.RLock()
1032+
defer c.lock.RUnlock()
1033+
keysPerNamespace := make(map[string][]kube.ResourceKey)
1034+
for _, key := range keys {
1035+
_, ok := c.resources[key]
1036+
if !ok {
1037+
continue
1038+
}
1039+
keysPerNamespace[key.Namespace] = append(keysPerNamespace[key.Namespace], key)
1040+
}
1041+
for namespace, namespaceKeys := range keysPerNamespace {
1042+
nsNodes := c.nsIndex[namespace]
1043+
graph := buildGraph(nsNodes)
1044+
visited := make(map[kube.ResourceKey]int)
1045+
for _, key := range namespaceKeys {
1046+
visited[key] = 0
1047+
}
1048+
for _, key := range namespaceKeys {
1049+
// The check for existence of key is done above.
1050+
res := c.resources[key]
1051+
if visited[key] == 2 || !action(res, nsNodes) {
1052+
continue
1053+
}
1054+
visited[key] = 1
1055+
if _, ok := graph[key]; ok {
1056+
for _, child := range graph[key] {
1057+
if visited[child.ResourceKey()] == 0 && action(child, nsNodes) {
1058+
child.iterateChildrenV2(graph, nsNodes, visited, func(err error, child *Resource, namespaceResources map[kube.ResourceKey]*Resource) bool {
1059+
if err != nil {
1060+
c.log.V(2).Info(err.Error())
1061+
return false
1062+
}
1063+
return action(child, namespaceResources)
1064+
})
1065+
}
1066+
}
1067+
}
1068+
visited[key] = 2
1069+
}
1070+
}
1071+
}
1072+
1073+
func buildGraph(nsNodes map[kube.ResourceKey]*Resource) map[kube.ResourceKey]map[types.UID]*Resource {
1074+
// Prepare to construct a graph
1075+
nodesByUID := make(map[types.UID][]*Resource, len(nsNodes))
1076+
for _, node := range nsNodes {
1077+
nodesByUID[node.Ref.UID] = append(nodesByUID[node.Ref.UID], node)
1078+
}
1079+
1080+
// In graph, they key is the parent and the value is a list of children.
1081+
graph := make(map[kube.ResourceKey]map[types.UID]*Resource)
1082+
1083+
// Loop through all nodes, calling each one "childNode," because we're only bothering with it if it has a parent.
1084+
for _, childNode := range nsNodes {
1085+
for i, ownerRef := range childNode.OwnerRefs {
1086+
// First, backfill UID of inferred owner child references.
1087+
if ownerRef.UID == "" {
1088+
group, err := schema.ParseGroupVersion(ownerRef.APIVersion)
1089+
if err != nil {
1090+
// APIVersion is invalid, so we couldn't find the parent.
1091+
continue
1092+
}
1093+
graphKeyNode, ok := nsNodes[kube.ResourceKey{Group: group.Group, Kind: ownerRef.Kind, Namespace: childNode.Ref.Namespace, Name: ownerRef.Name}]
1094+
if ok {
1095+
ownerRef.UID = graphKeyNode.Ref.UID
1096+
childNode.OwnerRefs[i] = ownerRef
1097+
} else {
1098+
// No resource found with the given graph key, so move on.
1099+
continue
1100+
}
1101+
}
1102+
1103+
// Now that we have the UID of the parent, update the graph.
1104+
uidNodes, ok := nodesByUID[ownerRef.UID]
1105+
if ok {
1106+
for _, uidNode := range uidNodes {
1107+
// Update the graph for this owner to include the child.
1108+
if _, ok := graph[uidNode.ResourceKey()]; !ok {
1109+
graph[uidNode.ResourceKey()] = make(map[types.UID]*Resource)
1110+
}
1111+
r, ok := graph[uidNode.ResourceKey()][childNode.Ref.UID]
1112+
if !ok {
1113+
graph[uidNode.ResourceKey()][childNode.Ref.UID] = childNode
1114+
} else if r != nil {
1115+
// The object might have multiple children with the same UID (e.g. replicaset from apps and extensions group).
1116+
// It is ok to pick any object, but we need to make sure we pick the same child after every refresh.
1117+
key1 := r.ResourceKey()
1118+
key2 := childNode.ResourceKey()
1119+
if strings.Compare(key1.String(), key2.String()) > 0 {
1120+
graph[uidNode.ResourceKey()][childNode.Ref.UID] = childNode
1121+
}
1122+
}
1123+
}
1124+
}
1125+
}
1126+
}
1127+
return graph
1128+
}
1129+
10261130
// IsNamespaced answers if specified group/kind is a namespaced resource API or not
10271131
func (c *clusterCache) IsNamespaced(gk schema.GroupKind) (bool, error) {
10281132
if isNamespaced, ok := c.namespacedResources[gk]; ok {

0 commit comments

Comments
 (0)