Skip to content

Commit 2f6414f

Browse files
committed
Add items to cache immediately after apply
This improves the SSA helper by avoding a second no-op patch just to add items to the cache. Instead we calculate the new request identifier and add to the cache directly. Signed-off-by: Lennart Jern <[email protected]>
1 parent 739ca11 commit 2f6414f

File tree

2 files changed

+101
-23
lines changed

2 files changed

+101
-23
lines changed

internal/util/ssa/patch.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ func Patch(ctx context.Context, c client.Client, fieldManager string, modified c
8484
if err != nil {
8585
return err
8686
}
87+
modifiedUnstructuredBeforeApply := modifiedUnstructured.DeepCopy()
8788

8889
gvk, err := apiutil.GVKForObject(modifiedUnstructured, c.Scheme())
8990
if err != nil {
@@ -132,10 +133,16 @@ func Patch(ctx context.Context, c client.Client, fieldManager string, modified c
132133

133134
// Add the request to the cache only if dry-run was not used.
134135
if options.WithCachingProxy && !options.WithDryRun {
135-
// If the SSA call did not update the object, add the request to the cache.
136-
if options.Original.GetResourceVersion() == modifiedUnstructured.GetResourceVersion() {
137-
options.Cache.Add(requestIdentifier)
136+
// If the object changed, we need to recompute the request identifier before caching.
137+
if options.Original.GetResourceVersion() != modifiedUnstructured.GetResourceVersion() {
138+
// NOTE: This takes the resourceVersion from modifiedUnstructured, and the hash from
139+
// modifiedUnstructuredBeforeApply, which is what we want.
140+
requestIdentifier, err = ComputeRequestIdentifier(c.Scheme(), modifiedUnstructured, modifiedUnstructuredBeforeApply)
141+
if err != nil {
142+
return errors.Wrapf(err, "failed to compute request identifier after apply")
143+
}
138144
}
145+
options.Cache.Add(requestIdentifier)
139146
}
140147

141148
return nil

internal/util/ssa/patch_test.go

Lines changed: 91 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,48 @@ limitations under the License.
1717
package ssa
1818

1919
import (
20+
"context"
21+
"sync"
2022
"testing"
2123
"time"
2224

2325
. "github.com/onsi/gomega"
2426
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2527
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
28+
"k8s.io/apimachinery/pkg/runtime"
2629
"k8s.io/utils/ptr"
2730
"sigs.k8s.io/controller-runtime/pkg/client"
2831

2932
clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
3033
"sigs.k8s.io/cluster-api/util/test/builder"
3134
)
3235

36+
// applyCountingClient wraps a client and counts Apply calls.
37+
type applyCountingClient struct {
38+
client.Client
39+
applyCount int
40+
mu sync.Mutex
41+
}
42+
43+
func (c *applyCountingClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...client.ApplyOption) error {
44+
c.mu.Lock()
45+
c.applyCount++
46+
c.mu.Unlock()
47+
return c.Client.Apply(ctx, obj, opts...)
48+
}
49+
50+
func (c *applyCountingClient) getCount() int {
51+
c.mu.Lock()
52+
defer c.mu.Unlock()
53+
return c.applyCount
54+
}
55+
56+
func (c *applyCountingClient) resetCount() {
57+
c.mu.Lock()
58+
c.applyCount = 0
59+
c.mu.Unlock()
60+
}
61+
3362
func TestPatch(t *testing.T) {
3463
g := NewWithT(t)
3564

@@ -48,26 +77,43 @@ func TestPatch(t *testing.T) {
4877
fieldManager := "test-manager"
4978
ssaCache := NewCache("test-controller")
5079

80+
// Wrap the client to count API calls
81+
countingClient := &applyCountingClient{Client: env.GetClient()}
82+
5183
// 1. Create the object
5284
createObject := initialObject.DeepCopy()
53-
g.Expect(Patch(ctx, env.GetClient(), fieldManager, createObject)).To(Succeed())
85+
g.Expect(Patch(ctx, countingClient, fieldManager, createObject)).To(Succeed())
86+
g.Expect(countingClient.getCount()).To(Equal(1), "Expected 1 API call for create")
5487

55-
// 2. Update the object and verify that the request was not cached as the object was changed.
88+
// 2. Update the object and verify that the request was not cached with the old identifier,
89+
// but is cached with a new identifier (after apply).
5690
// Get the original object.
5791
originalObject := initialObject.DeepCopy()
5892
g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(originalObject), originalObject)).To(Succeed())
5993
// Modify the object
6094
modifiedObject := initialObject.DeepCopy()
6195
g.Expect(unstructured.SetNestedField(modifiedObject.Object, "baz", "spec", "foo")).To(Succeed())
62-
// Compute request identifier, so we can later verify that the update call was not cached.
96+
// Compute request identifier before the update, so we can later verify that the update call was not cached with this identifier.
6397
modifiedUnstructured, err := prepareModified(env.Scheme(), modifiedObject)
6498
g.Expect(err).ToNot(HaveOccurred())
65-
requestIdentifier, err := ComputeRequestIdentifier(env.GetScheme(), originalObject, modifiedUnstructured)
99+
oldRequestIdentifier, err := ComputeRequestIdentifier(env.GetScheme(), originalObject, modifiedUnstructured)
66100
g.Expect(err).ToNot(HaveOccurred())
101+
// Save a copy of modifiedUnstructured before apply to compute the new identifier later
102+
modifiedUnstructuredBeforeApply := modifiedUnstructured.DeepCopy()
67103
// Update the object
68-
g.Expect(Patch(ctx, env.GetClient(), fieldManager, modifiedObject, WithCachingProxy{Cache: ssaCache, Original: originalObject})).To(Succeed())
69-
// Verify that request was not cached (as it changed the object)
70-
g.Expect(ssaCache.Has(requestIdentifier, initialObject.GetKind())).To(BeFalse())
104+
countingClient.resetCount()
105+
g.Expect(Patch(ctx, countingClient, fieldManager, modifiedObject, WithCachingProxy{Cache: ssaCache, Original: originalObject})).To(Succeed())
106+
g.Expect(countingClient.getCount()).To(Equal(1), "Expected 1 API call for first update (object changed)")
107+
// Verify that request was not cached with the old identifier (as it changed the object)
108+
g.Expect(ssaCache.Has(oldRequestIdentifier, initialObject.GetKind())).To(BeFalse())
109+
// Get the actual object from server after apply to compute the new request identifier
110+
objectAfterApply := initialObject.DeepCopy()
111+
g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(objectAfterApply), objectAfterApply)).To(Succeed())
112+
// Compute the new request identifier (after apply)
113+
newRequestIdentifier, err := ComputeRequestIdentifier(env.GetScheme(), objectAfterApply, modifiedUnstructuredBeforeApply)
114+
g.Expect(err).ToNot(HaveOccurred())
115+
// Verify that request was cached with the new identifier (after apply)
116+
g.Expect(ssaCache.Has(newRequestIdentifier, initialObject.GetKind())).To(BeTrue())
71117

72118
// 3. Repeat the same update and verify that the request was cached as the object was not changed.
73119
// Get the original object.
@@ -79,12 +125,14 @@ func TestPatch(t *testing.T) {
79125
// Compute request identifier, so we can later verify that the update call was cached.
80126
modifiedUnstructured, err = prepareModified(env.Scheme(), modifiedObject)
81127
g.Expect(err).ToNot(HaveOccurred())
82-
requestIdentifier, err = ComputeRequestIdentifier(env.GetScheme(), originalObject, modifiedUnstructured)
128+
requestIdentifierNoOp, err := ComputeRequestIdentifier(env.GetScheme(), originalObject, modifiedUnstructured)
83129
g.Expect(err).ToNot(HaveOccurred())
84130
// Update the object
85-
g.Expect(Patch(ctx, env.GetClient(), fieldManager, modifiedObject, WithCachingProxy{Cache: ssaCache, Original: originalObject})).To(Succeed())
131+
countingClient.resetCount()
132+
g.Expect(Patch(ctx, countingClient, fieldManager, modifiedObject, WithCachingProxy{Cache: ssaCache, Original: originalObject})).To(Succeed())
133+
g.Expect(countingClient.getCount()).To(Equal(0), "Expected 0 API calls for repeat update (should hit cache)")
86134
// Verify that request was cached (as it did not change the object)
87-
g.Expect(ssaCache.Has(requestIdentifier, initialObject.GetKind())).To(BeTrue())
135+
g.Expect(ssaCache.Has(requestIdentifierNoOp, initialObject.GetKind())).To(BeTrue())
88136
})
89137

90138
t.Run("Test patch with Machine", func(*testing.T) {
@@ -123,30 +171,51 @@ func TestPatch(t *testing.T) {
123171
fieldManager := "test-manager"
124172
ssaCache := NewCache("test-controller")
125173

174+
// Wrap the client to count API calls
175+
countingClient := &applyCountingClient{Client: env.GetClient()}
176+
126177
// 1. Create the object
127178
createObject := initialObject.DeepCopy()
128-
g.Expect(Patch(ctx, env.GetClient(), fieldManager, createObject)).To(Succeed())
179+
g.Expect(Patch(ctx, countingClient, fieldManager, createObject)).To(Succeed())
180+
g.Expect(countingClient.getCount()).To(Equal(1), "Expected 1 API call for create")
129181
// Verify that gvk is still set
130182
g.Expect(createObject.GroupVersionKind()).To(Equal(initialObject.GroupVersionKind()))
131183

132-
// 2. Update the object and verify that the request was not cached as the object was changed.
184+
// 2. Update the object and verify that the request was not cached with the old identifier,
185+
// but is cached with a new identifier (after apply).
133186
// Get the original object.
134187
originalObject := initialObject.DeepCopy()
135188
g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(originalObject), originalObject)).To(Succeed())
136189
// Modify the object
137190
modifiedObject := initialObject.DeepCopy()
138191
modifiedObject.Spec.Deletion.NodeDrainTimeoutSeconds = ptr.To(int32(5))
139-
// Compute request identifier, so we can later verify that the update call was not cached.
192+
// Compute request identifier before the update, so we can later verify that the update call was not cached with this identifier.
140193
modifiedUnstructured, err := prepareModified(env.Scheme(), modifiedObject)
141194
g.Expect(err).ToNot(HaveOccurred())
142-
requestIdentifier, err := ComputeRequestIdentifier(env.GetScheme(), originalObject, modifiedUnstructured)
195+
oldRequestIdentifier, err := ComputeRequestIdentifier(env.GetScheme(), originalObject, modifiedUnstructured)
143196
g.Expect(err).ToNot(HaveOccurred())
197+
// Save a copy of modifiedUnstructured before apply to compute the new identifier later
198+
modifiedUnstructuredBeforeApply := modifiedUnstructured.DeepCopy()
144199
// Update the object
145-
g.Expect(Patch(ctx, env.GetClient(), fieldManager, modifiedObject, WithCachingProxy{Cache: ssaCache, Original: originalObject})).To(Succeed())
200+
countingClient.resetCount()
201+
g.Expect(Patch(ctx, countingClient, fieldManager, modifiedObject, WithCachingProxy{Cache: ssaCache, Original: originalObject})).To(Succeed())
202+
g.Expect(countingClient.getCount()).To(Equal(1), "Expected 1 API call for first update (object changed)")
146203
// Verify that gvk is still set
147204
g.Expect(modifiedObject.GroupVersionKind()).To(Equal(initialObject.GroupVersionKind()))
148-
// Verify that request was not cached (as it changed the object)
149-
g.Expect(ssaCache.Has(requestIdentifier, initialObject.GetObjectKind().GroupVersionKind().Kind)).To(BeFalse())
205+
// Verify that request was not cached with the old identifier (as it changed the object)
206+
g.Expect(ssaCache.Has(oldRequestIdentifier, initialObject.GetObjectKind().GroupVersionKind().Kind)).To(BeFalse())
207+
// Get the actual object from server after apply to compute the new request identifier
208+
objectAfterApply := initialObject.DeepCopy()
209+
g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(objectAfterApply), objectAfterApply)).To(Succeed())
210+
// Convert to unstructured WITHOUT filtering to preserve server fields (like resourceVersion)
211+
objectAfterApplyUnstructured := &unstructured.Unstructured{}
212+
err = env.GetScheme().Convert(objectAfterApply, objectAfterApplyUnstructured, nil)
213+
g.Expect(err).ToNot(HaveOccurred())
214+
// Compute the new request identifier (after apply)
215+
newRequestIdentifier, err := ComputeRequestIdentifier(env.GetScheme(), objectAfterApplyUnstructured, modifiedUnstructuredBeforeApply)
216+
g.Expect(err).ToNot(HaveOccurred())
217+
// Verify that request was cached with the new identifier (after apply)
218+
g.Expect(ssaCache.Has(newRequestIdentifier, initialObject.GetObjectKind().GroupVersionKind().Kind)).To(BeTrue())
150219

151220
// Wait for 1 second. We are also trying to verify in this test that the resourceVersion of the Machine
152221
// is not increased. Under some circumstances this would only happen if the timestamp in managedFields would
@@ -166,12 +235,14 @@ func TestPatch(t *testing.T) {
166235
// Compute request identifier, so we can later verify that the update call was cached.
167236
modifiedUnstructured, err = prepareModified(env.Scheme(), modifiedObject)
168237
g.Expect(err).ToNot(HaveOccurred())
169-
requestIdentifier, err = ComputeRequestIdentifier(env.GetScheme(), originalObject, modifiedUnstructured)
238+
requestIdentifierNoOp, err := ComputeRequestIdentifier(env.GetScheme(), originalObject, modifiedUnstructured)
170239
g.Expect(err).ToNot(HaveOccurred())
171240
// Update the object
172-
g.Expect(Patch(ctx, env.GetClient(), fieldManager, modifiedObject, WithCachingProxy{Cache: ssaCache, Original: originalObject})).To(Succeed())
241+
countingClient.resetCount()
242+
g.Expect(Patch(ctx, countingClient, fieldManager, modifiedObject, WithCachingProxy{Cache: ssaCache, Original: originalObject})).To(Succeed())
243+
g.Expect(countingClient.getCount()).To(Equal(0), "Expected 0 API calls for repeat update (should hit cache)")
173244
// Verify that request was cached (as it did not change the object)
174-
g.Expect(ssaCache.Has(requestIdentifier, initialObject.GetObjectKind().GroupVersionKind().Kind)).To(BeTrue())
245+
g.Expect(ssaCache.Has(requestIdentifierNoOp, initialObject.GetObjectKind().GroupVersionKind().Kind)).To(BeTrue())
175246
// Verify that gvk is still set
176247
g.Expect(modifiedObject.GroupVersionKind()).To(Equal(initialObject.GroupVersionKind()))
177248
})

0 commit comments

Comments
 (0)