Skip to content

Commit 0b3fda4

Browse files
[release-1.8] 🌱 Cache DiscoveryVariables calls (#11600)
* Cache DiscoveryVariables calls * internal/util/cache cherry-pick --------- Co-authored-by: fabriziopandini <[email protected]>
1 parent c05c3e6 commit 0b3fda4

File tree

11 files changed

+453
-22
lines changed

11 files changed

+453
-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
"sigs.k8s.io/cluster-api/feature"
4747
tlog "sigs.k8s.io/cluster-api/internal/log"
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/annotations"
5153
"sigs.k8s.io/cluster-api/util/conditions"
5254
"sigs.k8s.io/cluster-api/util/conversion"
@@ -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 {
@@ -84,6 +90,8 @@ func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opt
8490
if err != nil {
8591
return errors.Wrap(err, "failed setting up with a controller manager")
8692
}
93+
94+
r.discoverVariablesCache = cache.New[runtimeclient.CallExtensionCacheEntry]()
8795
return nil
8896
}
8997

@@ -236,8 +244,13 @@ func (r *Reconciler) reconcileVariables(ctx context.Context, clusterClass *clust
236244
req := &runtimehooksv1.DiscoverVariablesRequest{}
237245
req.Settings = patch.External.Settings
238246

247+
// We temporarily cache the response of a DiscoveryVariables call to improve performance in case there are
248+
// many ClusterClasses using the same runtime extension/settings combination.
249+
// This also mitigates spikes when ClusterClass re-syncs happen or when changes to the ExtensionConfig are applied.
250+
// DiscoverVariables is expected to return a "static" response and usually there are few ExtensionConfigs in a mgmt cluster.
239251
resp := &runtimehooksv1.DiscoverVariablesResponse{}
240-
err := r.RuntimeClient.CallExtension(ctx, runtimehooksv1.DiscoverVariables, clusterClass, *patch.External.DiscoverVariablesExtension, req, resp)
252+
err := r.RuntimeClient.CallExtension(ctx, runtimehooksv1.DiscoverVariables, clusterClass, *patch.External.DiscoverVariablesExtension, req, resp,
253+
runtimeclient.WithCaching{Cache: r.discoverVariablesCache, CacheKeyFunc: cacheKeyFunc})
241254
if err != nil {
242255
errs = append(errs, errors.Wrapf(err, "failed to call DiscoverVariables for patch %s", patch.Name))
243256
continue
@@ -460,3 +473,12 @@ func matchNamespace(ctx context.Context, c client.Client, selector labels.Select
460473
}
461474
return selector.Matches(labels.Set(ns.GetLabels()))
462475
}
476+
477+
func cacheKeyFunc(registration *runtimeregistry.ExtensionRegistration, request runtimehooksv1.RequestObject) string {
478+
// Note: registration.Name is identical to the value of the patch.External.DiscoverVariablesExtension field in the ClusterClass.
479+
s := fmt.Sprintf("%s-%s", registration.Name, registration.ExtensionConfigResourceVersion)
480+
for k, v := range request.GetSettings() {
481+
s += fmt.Sprintf(",%s=%s", k, v)
482+
}
483+
return s
484+
}

internal/controllers/clusterclass/clusterclass_controller_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ import (
4141
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
4242
"sigs.k8s.io/cluster-api/feature"
4343
tlog "sigs.k8s.io/cluster-api/internal/log"
44+
runtimeclient "sigs.k8s.io/cluster-api/internal/runtime/client"
4445
fakeruntimeclient "sigs.k8s.io/cluster-api/internal/runtime/client/fake"
4546
"sigs.k8s.io/cluster-api/internal/test/builder"
47+
"sigs.k8s.io/cluster-api/internal/util/cache"
4648
)
4749

4850
func TestClusterClassReconciler_reconcile(t *testing.T) {
@@ -1193,7 +1195,8 @@ func TestReconciler_reconcileVariables(t *testing.T) {
11931195
Build()
11941196

11951197
r := &Reconciler{
1196-
RuntimeClient: fakeRuntimeClient,
1198+
RuntimeClient: fakeRuntimeClient,
1199+
discoverVariablesCache: cache.New[runtimeclient.CallExtensionCacheEntry](),
11971200
}
11981201

11991202
err := r.reconcileVariables(ctx, tt.clusterClass)

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)