Introduce cache for lookups of desired resource state, to reduce the amount of allocations in the controller (#4568)

This commit is contained in:
Nikola Jokic
2026-09-07 13:49:28 +02:00
committed by GitHub
parent 87592be6d3
commit 54147cfa5e
13 changed files with 860 additions and 26 deletions
@@ -0,0 +1,306 @@
package actionsgithubcom
import (
"reflect"
"slices"
"strings"
"sync"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/actions/actions-runner-controller/hash"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const (
resourceCacheInitialEntries = 4096
resourceCacheInitialMainUIDEntries = 4096
resourceCacheInitialOwnerEntries = 8
resourceCacheMaxDependencyRefs = 4
)
type ResourceCacheObjectRef struct {
ObjectType schema.GroupVersionKind
Namespace string
Name string
UID types.UID
ResourceVersion string
}
type ResourceCacheKey struct {
MainUID types.UID
Namespace string
Name string
}
type ResourceCacheValue[T client.Object] struct {
MainObject ResourceCacheObjectRef
ResourceVersion string
dependencyKey resourceCacheDependencyKey
Object T
}
type resourceCacheDependencyKey struct {
count int
refs [resourceCacheMaxDependencyRefs]ResourceCacheObjectRef
}
type ResourceCache struct {
autoscalingListener *resourceCacheState[*v1alpha1.AutoscalingListener]
ephemeralRunnerSet *resourceCacheState[*v1alpha1.EphemeralRunnerSet]
listenerPod *resourceCacheState[*corev1.Pod]
listenerServiceAccount *resourceCacheState[*corev1.ServiceAccount]
listenerRole *resourceCacheState[*rbacv1.Role]
listenerRoleBinding *resourceCacheState[*rbacv1.RoleBinding]
}
func NewResourceCache() ResourceCache {
return ResourceCache{
autoscalingListener: newResourceCacheState[*v1alpha1.AutoscalingListener](),
ephemeralRunnerSet: newResourceCacheState[*v1alpha1.EphemeralRunnerSet](),
listenerPod: newResourceCacheState[*corev1.Pod](),
listenerServiceAccount: newResourceCacheState[*corev1.ServiceAccount](),
listenerRole: newResourceCacheState[*rbacv1.Role](),
listenerRoleBinding: newResourceCacheState[*rbacv1.RoleBinding](),
}
}
type resourceCacheState[T client.Object] struct {
mu sync.RWMutex
entries map[ResourceCacheKey]ResourceCacheValue[T]
entriesByMainUID map[types.UID]map[ResourceCacheKey]struct{}
}
func newResourceCacheState[T client.Object]() *resourceCacheState[T] {
return &resourceCacheState[T]{
entries: make(map[ResourceCacheKey]ResourceCacheValue[T], resourceCacheInitialEntries),
entriesByMainUID: make(map[types.UID]map[ResourceCacheKey]struct{}, resourceCacheInitialMainUIDEntries),
}
}
func (s *resourceCacheState[T]) Get(
mainObject client.Object,
desiredObject T,
dependencies ...client.Object,
) (T, bool) {
var zero T
if s == nil || isNilResourceCacheObject(mainObject) || isNilResourceCacheObject(desiredObject) {
return zero, false
}
dependencyKey, ok := newResourceCacheDependencyKey(dependencies...)
if !ok {
return zero, false
}
if mainObject.GetUID() == "" {
return zero, false
}
key := newResourceCacheKey(mainObject, desiredObject)
mainObjectRef := newResourceCacheObjectRef(mainObject)
s.mu.RLock()
value, ok := s.entries[key]
if ok && value.MainObject == mainObjectRef && value.dependencyKey.Equal(dependencyKey) {
s.mu.RUnlock()
return value.Object, true
}
s.mu.RUnlock()
return zero, false
}
func (s *resourceCacheState[T]) Upsert(
mainObject client.Object,
desiredObject T,
dependencies ...client.Object,
) (ResourceCacheValue[T], bool) {
var zero ResourceCacheValue[T]
if s == nil || isNilResourceCacheObject(mainObject) || isNilResourceCacheObject(desiredObject) {
return zero, false
}
dependencyKey, ok := newResourceCacheDependencyKey(dependencies...)
if !ok {
return zero, false
}
if mainObject.GetUID() == "" {
return zero, false
}
key := newResourceCacheKey(mainObject, desiredObject)
mainObjectRef := newResourceCacheObjectRef(mainObject)
resourceVersion := desiredObject.GetResourceVersion()
s.mu.RLock()
previous, ok := s.entries[key]
if ok && previous.MainObject == mainObjectRef && previous.ResourceVersion == resourceVersion && previous.dependencyKey.Equal(dependencyKey) {
s.mu.RUnlock()
return previous, false
}
s.mu.RUnlock()
s.mu.Lock()
defer s.mu.Unlock()
previous, ok = s.entries[key]
if ok && previous.MainObject == mainObjectRef && previous.ResourceVersion == resourceVersion && previous.dependencyKey.Equal(dependencyKey) {
return previous, false
}
value := ResourceCacheValue[T]{
MainObject: mainObjectRef,
ResourceVersion: resourceVersion,
dependencyKey: dependencyKey,
Object: desiredObject,
}
s.entries[key] = value
s.indexKeyLocked(key)
return value, true
}
func (c *ResourceCache) Delete(mainObject client.Object) {
if mainObject == nil {
return
}
c.autoscalingListener.Delete(mainObject)
c.ephemeralRunnerSet.Delete(mainObject)
c.listenerPod.Delete(mainObject)
c.listenerServiceAccount.Delete(mainObject)
c.listenerRole.Delete(mainObject)
c.listenerRoleBinding.Delete(mainObject)
}
func (s *resourceCacheState[T]) Delete(mainObject client.Object) {
if s == nil || mainObject == nil {
return
}
uid := mainObject.GetUID()
if uid == "" {
return
}
s.mu.Lock()
defer s.mu.Unlock()
for key := range s.entriesByMainUID[uid] {
delete(s.entries, key)
}
delete(s.entriesByMainUID, uid)
}
func (s *resourceCacheState[T]) indexKeyLocked(key ResourceCacheKey) {
keys, ok := s.entriesByMainUID[key.MainUID]
if !ok {
keys = make(map[ResourceCacheKey]struct{}, resourceCacheInitialOwnerEntries)
s.entriesByMainUID[key.MainUID] = keys
}
keys[key] = struct{}{}
}
func newResourceCacheKey(mainObject client.Object, desiredObject client.Object) ResourceCacheKey {
return ResourceCacheKey{
MainUID: mainObject.GetUID(),
Namespace: desiredObject.GetNamespace(),
Name: resourceCacheObjectName(desiredObject),
}
}
func newResourceCacheDependencyKey(objects ...client.Object) (resourceCacheDependencyKey, bool) {
if len(objects) > resourceCacheMaxDependencyRefs {
return resourceCacheDependencyKey{}, false
}
key := resourceCacheDependencyKey{count: len(objects)}
for i, object := range objects {
if isNilResourceCacheObject(object) {
return resourceCacheDependencyKey{}, false
}
key.refs[i] = newResourceCacheObjectRef(object)
}
slices.SortFunc(key.refs[:key.count], func(a, b ResourceCacheObjectRef) int {
return compareResourceCacheObjectRefs(a, b)
})
return key, true
}
func (k resourceCacheDependencyKey) Equal(other resourceCacheDependencyKey) bool {
if k.count != other.count {
return false
}
if k.count > len(k.refs) || other.count > len(other.refs) {
return false
}
for i := range k.count {
if k.refs[i] != other.refs[i] {
return false
}
}
return true
}
func newResourceCacheObjectRef(object client.Object) ResourceCacheObjectRef {
resourceVersion := object.GetResourceVersion()
if resourceVersion == "" {
resourceVersion = object.GetAnnotations()[annotationKeyIntegrityHash]
}
if resourceVersion == "" {
resourceVersion = hash.ComputeTemplateHash(object)
}
return ResourceCacheObjectRef{
ObjectType: object.GetObjectKind().GroupVersionKind(),
Namespace: object.GetNamespace(),
Name: resourceCacheObjectName(object),
UID: object.GetUID(),
ResourceVersion: resourceVersion,
}
}
func compareResourceCacheObjectRefs(a, b ResourceCacheObjectRef) int {
if c := compareGroupVersionKinds(a.ObjectType, b.ObjectType); c != 0 {
return c
}
if c := strings.Compare(a.Namespace, b.Namespace); c != 0 {
return c
}
if c := strings.Compare(a.Name, b.Name); c != 0 {
return c
}
if c := strings.Compare(string(a.UID), string(b.UID)); c != 0 {
return c
}
return strings.Compare(a.ResourceVersion, b.ResourceVersion)
}
func compareGroupVersionKinds(a, b schema.GroupVersionKind) int {
if c := strings.Compare(a.Group, b.Group); c != 0 {
return c
}
if c := strings.Compare(a.Version, b.Version); c != 0 {
return c
}
return strings.Compare(a.Kind, b.Kind)
}
func resourceCacheObjectName(object client.Object) string {
if object.GetName() != "" {
return object.GetName()
}
return object.GetGenerateName()
}
func isNilResourceCacheObject[T client.Object](object T) bool {
var clientObject client.Object = object
if clientObject == nil {
return true
}
value := reflect.ValueOf(clientObject)
return value.Kind() == reflect.Pointer && value.IsNil()
}