Skip to content

Commit 6c909a0

Browse files
authored
Merge pull request #11599 from k8s-infra-cherrypick-robot/cherry-pick-11592-to-release-1.9
[release-1.9] 🌱 Cache DiscoveryVariables calls
2 parents b04c79c + 284c3a5 commit 6c909a0

File tree

8 files changed

+187
-22
lines changed

8 files changed

+187
-22
lines changed

internal/controllers/clusterclass/clusterclass_controller.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ import (
4646
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
4747
"sigs.k8s.io/cluster-api/feature"
4848
runtimeclient "sigs.k8s.io/cluster-api/internal/runtime/client"
49+
runtimeregistry "sigs.k8s.io/cluster-api/internal/runtime/registry"
4950
"sigs.k8s.io/cluster-api/internal/topology/variables"
51+
"sigs.k8s.io/cluster-api/internal/util/cache"
5052
"sigs.k8s.io/cluster-api/util"
5153
"sigs.k8s.io/cluster-api/util/conversion"
5254
"sigs.k8s.io/cluster-api/util/patch"
@@ -67,6 +69,10 @@ type Reconciler struct {
6769

6870
// RuntimeClient is a client for calling runtime extensions.
6971
RuntimeClient runtimeclient.Client
72+
73+
// discoverVariablesCache is used to temporarily store the response of a DiscoveryVariables call for
74+
// a specific runtime extension/settings combination.
75+
discoverVariablesCache cache.Cache[runtimeclient.CallExtensionCacheEntry]
7076
}
7177

7278
func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
@@ -91,6 +97,8 @@ func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opt
9197
if err != nil {
9298
return errors.Wrap(err, "failed setting up with a controller manager")
9399
}
100+
101+
r.discoverVariablesCache = cache.New[runtimeclient.CallExtensionCacheEntry]()
94102
return nil
95103
}
96104

@@ -302,8 +310,13 @@ func (r *Reconciler) reconcileVariables(ctx context.Context, s *scope) (ctrl.Res
302310
req := &runtimehooksv1.DiscoverVariablesRequest{}
303311
req.Settings = patch.External.Settings
304312

313+
// We temporarily cache the response of a DiscoveryVariables call to improve performance in case there are
314+
// many ClusterClasses using the same runtime extension/settings combination.
315+
// This also mitigates spikes when ClusterClass re-syncs happen or when changes to the ExtensionConfig are applied.
316+
// DiscoverVariables is expected to return a "static" response and usually there are few ExtensionConfigs in a mgmt cluster.
305317
resp := &runtimehooksv1.DiscoverVariablesResponse{}
306-
err := r.RuntimeClient.CallExtension(ctx, runtimehooksv1.DiscoverVariables, clusterClass, *patch.External.DiscoverVariablesExtension, req, resp)
318+
err := r.RuntimeClient.CallExtension(ctx, runtimehooksv1.DiscoverVariables, clusterClass, *patch.External.DiscoverVariablesExtension, req, resp,
319+
runtimeclient.WithCaching{Cache: r.discoverVariablesCache, CacheKeyFunc: cacheKeyFunc})
307320
if err != nil {
308321
errs = append(errs, errors.Wrapf(err, "failed to call DiscoverVariables for patch %s", patch.Name))
309322
continue
@@ -492,3 +505,12 @@ func matchNamespace(ctx context.Context, c client.Client, selector labels.Select
492505
}
493506
return selector.Matches(labels.Set(ns.GetLabels()))
494507
}
508+
509+
func cacheKeyFunc(registration *runtimeregistry.ExtensionRegistration, request runtimehooksv1.RequestObject) string {
510+
// Note: registration.Name is identical to the value of the patch.External.DiscoverVariablesExtension field in the ClusterClass.
511+
s := fmt.Sprintf("%s-%s", registration.Name, registration.ExtensionConfigResourceVersion)
512+
for k, v := range request.GetSettings() {
513+
s += fmt.Sprintf(",%s=%s", k, v)
514+
}
515+
return s
516+
}

internal/controllers/clusterclass/clusterclass_controller_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ import (
4343
runtimecatalog "sigs.k8s.io/cluster-api/exp/runtime/catalog"
4444
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
4545
"sigs.k8s.io/cluster-api/feature"
46+
runtimeclient "sigs.k8s.io/cluster-api/internal/runtime/client"
4647
fakeruntimeclient "sigs.k8s.io/cluster-api/internal/runtime/client/fake"
48+
"sigs.k8s.io/cluster-api/internal/util/cache"
4749
"sigs.k8s.io/cluster-api/util/test/builder"
4850
)
4951

@@ -1154,7 +1156,8 @@ func TestReconciler_reconcileVariables(t *testing.T) {
11541156
Build()
11551157

11561158
r := &Reconciler{
1157-
RuntimeClient: fakeRuntimeClient,
1159+
RuntimeClient: fakeRuntimeClient,
1160+
discoverVariablesCache: cache.New[runtimeclient.CallExtensionCacheEntry](),
11581161
}
11591162

11601163
// Pin the compatibility version used in variable CEL validation to 1.29, so we don't have to continuously refactor

internal/controllers/topology/cluster/patches/external/external_patch_generator_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ func (f *fakeRuntimeClient) CallAllExtensions(_ context.Context, _ runtimecatalo
129129
panic("implement me")
130130
}
131131

132-
func (f *fakeRuntimeClient) CallExtension(_ context.Context, _ runtimecatalog.Hook, _ metav1.Object, _ string, request runtimehooksv1.RequestObject, _ runtimehooksv1.ResponseObject) error {
132+
func (f *fakeRuntimeClient) CallExtension(_ context.Context, _ runtimecatalog.Hook, _ metav1.Object, _ string, request runtimehooksv1.RequestObject, _ runtimehooksv1.ResponseObject, _ ...runtimeclient.CallExtensionOption) error {
133133
// Keep a copy of the request object.
134134
// We keep a copy because the request is modified after the call is made. So we keep a copy to perform assertions.
135135
f.callExtensionRequest = request.DeepCopyObject().(runtimehooksv1.RequestObject)

internal/runtime/client/client.go

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"net/http"
2828
"net/url"
2929
"path"
30+
"reflect"
3031
"strconv"
3132
"strings"
3233
"time"
@@ -49,6 +50,7 @@ import (
4950
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
5051
runtimemetrics "sigs.k8s.io/cluster-api/internal/runtime/metrics"
5152
runtimeregistry "sigs.k8s.io/cluster-api/internal/runtime/registry"
53+
"sigs.k8s.io/cluster-api/internal/util/cache"
5254
"sigs.k8s.io/cluster-api/util"
5355
)
5456

@@ -96,7 +98,7 @@ type Client interface {
9698
CallAllExtensions(ctx context.Context, hook runtimecatalog.Hook, forObject metav1.Object, request runtimehooksv1.RequestObject, response runtimehooksv1.ResponseObject) error
9799

98100
// CallExtension calls the ExtensionHandler with the given name.
99-
CallExtension(ctx context.Context, hook runtimecatalog.Hook, forObject metav1.Object, name string, request runtimehooksv1.RequestObject, response runtimehooksv1.ResponseObject) error
101+
CallExtension(ctx context.Context, hook runtimecatalog.Hook, forObject metav1.Object, name string, request runtimehooksv1.RequestObject, response runtimehooksv1.ResponseObject, opts ...CallExtensionOption) error
100102
}
101103

102104
var _ Client = &client{}
@@ -276,6 +278,44 @@ func aggregateSuccessfulResponses(aggregatedResponse runtimehooksv1.ResponseObje
276278
aggregatedResponse.SetMessage(strings.Join(messages, ", "))
277279
}
278280

281+
// CallExtensionOption is the interface for configuration that modifies CallExtensionOptions for a CallExtension call.
282+
type CallExtensionOption interface {
283+
// ApplyToOptions applies this configuration to the given CallExtensionOptions.
284+
ApplyToOptions(*CallExtensionOptions)
285+
}
286+
287+
// CallExtensionCacheEntry is a cache entry for the cache that can be used with the CallExtension call via
288+
// the WithCaching option.
289+
type CallExtensionCacheEntry struct {
290+
CacheKey string
291+
Response runtimehooksv1.ResponseObject
292+
}
293+
294+
// Key returns the cache key of a CallExtensionCacheEntry.
295+
func (c CallExtensionCacheEntry) Key() string {
296+
return c.CacheKey
297+
}
298+
299+
// WithCaching enables caching for the CallExtension call.
300+
type WithCaching struct {
301+
Cache cache.Cache[CallExtensionCacheEntry]
302+
CacheKeyFunc func(*runtimeregistry.ExtensionRegistration, runtimehooksv1.RequestObject) string
303+
}
304+
305+
// ApplyToOptions applies WithCaching to the given CallExtensionOptions.
306+
func (w WithCaching) ApplyToOptions(in *CallExtensionOptions) {
307+
in.WithCaching = true
308+
in.Cache = w.Cache
309+
in.CacheKeyFunc = w.CacheKeyFunc
310+
}
311+
312+
// CallExtensionOptions contains the options for the CallExtension call.
313+
type CallExtensionOptions struct {
314+
WithCaching bool
315+
Cache cache.Cache[CallExtensionCacheEntry]
316+
CacheKeyFunc func(*runtimeregistry.ExtensionRegistration, runtimehooksv1.RequestObject) string
317+
}
318+
279319
// CallExtension makes the call to the extension with the given name.
280320
// The response object passed will be updated with the response of the call.
281321
// An error is returned if the extension is not compatible with the hook.
@@ -288,7 +328,13 @@ func aggregateSuccessfulResponses(aggregatedResponse runtimehooksv1.ResponseObje
288328
// Nb. FailurePolicy does not affect the following kinds of errors:
289329
// - Internal errors. Examples: hooks is incompatible with ExtensionHandler, ExtensionHandler information is missing.
290330
// - Error when ExtensionHandler returns a response with `Status` set to `Failure`.
291-
func (c *client) CallExtension(ctx context.Context, hook runtimecatalog.Hook, forObject metav1.Object, name string, request runtimehooksv1.RequestObject, response runtimehooksv1.ResponseObject) error {
331+
func (c *client) CallExtension(ctx context.Context, hook runtimecatalog.Hook, forObject metav1.Object, name string, request runtimehooksv1.RequestObject, response runtimehooksv1.ResponseObject, opts ...CallExtensionOption) error {
332+
// Calculate the options.
333+
options := &CallExtensionOptions{}
334+
for _, opt := range opts {
335+
opt.ApplyToOptions(options)
336+
}
337+
292338
log := ctrl.LoggerFrom(ctx).WithValues("extensionHandler", name, "hook", runtimecatalog.HookName(hook))
293339
ctx = ctrl.LoggerInto(ctx, log)
294340
hookGVH, err := c.catalog.GroupVersionHook(hook)
@@ -331,15 +377,31 @@ func (c *client) CallExtension(ctx context.Context, hook runtimecatalog.Hook, fo
331377
// Prepare the request by merging the settings in the registration with the settings in the request.
332378
request = cloneAndAddSettings(request, registration.Settings)
333379

334-
opts := &httpCallOptions{
380+
var cacheKey string
381+
if options.WithCaching {
382+
// Return a cached response if response is cached.
383+
cacheKey = options.CacheKeyFunc(registration, request)
384+
if cacheEntry, ok := options.Cache.Has(cacheKey); ok {
385+
// Set response to cacheEntry.Response.
386+
outVal := reflect.ValueOf(response)
387+
cacheVal := reflect.ValueOf(cacheEntry.Response)
388+
if !cacheVal.Type().AssignableTo(outVal.Type()) {
389+
return fmt.Errorf("failed to call extension handler %q: cached response of type %s instead of type %s", name, cacheVal.Type(), outVal.Type())
390+
}
391+
reflect.Indirect(outVal).Set(reflect.Indirect(cacheVal))
392+
return nil
393+
}
394+
}
395+
396+
httpOpts := &httpCallOptions{
335397
catalog: c.catalog,
336398
config: registration.ClientConfig,
337399
registrationGVH: registration.GroupVersionHook,
338400
hookGVH: hookGVH,
339401
name: strings.TrimSuffix(registration.Name, "."+registration.ExtensionConfigName),
340402
timeout: timeoutDuration,
341403
}
342-
err = httpCall(ctx, request, response, opts)
404+
err = httpCall(ctx, request, response, httpOpts)
343405
if err != nil {
344406
// If the error is errCallingExtensionHandler then apply failure policy to calculate
345407
// the effective result of the operation.
@@ -368,6 +430,14 @@ func (c *client) CallExtension(ctx context.Context, hook runtimecatalog.Hook, fo
368430
log.V(4).Info("Extension handler returned success response")
369431
}
370432

433+
if options.WithCaching {
434+
// Add response to the cache.
435+
options.Cache.Add(CallExtensionCacheEntry{
436+
CacheKey: cacheKey,
437+
Response: response,
438+
})
439+
}
440+
371441
// Received a successful response from the extension handler. The `response` object
372442
// has been populated with the result. Return no error.
373443
return nil

0 commit comments

Comments
 (0)