Skip to content

Commit d2145c0

Browse files
alvaroalemank8s-infra-cherrypick-robot
authored and
k8s-infra-cherrypick-robot
committed
bug: Fakeclient: Fix dataraces when writing to the scheme
We have a scheme write lock but plenty of other codeptaths that read from the scheme and that don't do looking, resulting in dataraces if the two happen in parallel. This change introduces a simple RW lock and makes the fakeclient acquire read locking for all its operations except when needing the write lock. This isn't particularly smart, but given that we only have one codepath that writes to the scheme, it seems good enough.
1 parent 13450ba commit d2145c0

File tree

2 files changed

+108
-4
lines changed

2 files changed

+108
-4
lines changed

Diff for: pkg/client/fake/client.go

+20-4
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ type fakeClient struct {
7474
trackerWriteLock sync.Mutex
7575
tracker versionedTracker
7676

77-
schemeWriteLock sync.Mutex
78-
scheme *runtime.Scheme
77+
schemeLock sync.RWMutex
78+
scheme *runtime.Scheme
7979

8080
restMapper meta.RESTMapper
8181
withStatusSubresource sets.Set[schema.GroupVersionKind]
@@ -509,6 +509,8 @@ func (t versionedTracker) updateObject(gvr schema.GroupVersionResource, obj runt
509509
}
510510

511511
func (c *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
512+
c.schemeLock.RLock()
513+
defer c.schemeLock.RUnlock()
512514
gvr, err := getGVRFromObject(obj, c.scheme)
513515
if err != nil {
514516
return err
@@ -558,6 +560,8 @@ func (c *fakeClient) Watch(ctx context.Context, list client.ObjectList, opts ...
558560
}
559561

560562
func (c *fakeClient) List(ctx context.Context, obj client.ObjectList, opts ...client.ListOption) error {
563+
c.schemeLock.RLock()
564+
defer c.schemeLock.RUnlock()
561565
gvk, err := apiutil.GVKForObject(obj, c.scheme)
562566
if err != nil {
563567
return err
@@ -570,9 +574,11 @@ func (c *fakeClient) List(ctx context.Context, obj client.ObjectList, opts ...cl
570574
if _, isUnstructuredList := obj.(runtime.Unstructured); isUnstructuredList && !c.scheme.Recognizes(gvk) {
571575
// We need to register the ListKind with UnstructuredList:
572576
// https://github.com/kubernetes/kubernetes/blob/7b2776b89fb1be28d4e9203bdeec079be903c103/staging/src/k8s.io/client-go/dynamic/fake/simple.go#L44-L51
573-
c.schemeWriteLock.Lock()
577+
c.schemeLock.RUnlock()
578+
c.schemeLock.Lock()
574579
c.scheme.AddKnownTypeWithName(gvk.GroupVersion().WithKind(gvk.Kind+"List"), &unstructured.UnstructuredList{})
575-
c.schemeWriteLock.Unlock()
580+
c.schemeLock.Unlock()
581+
c.schemeLock.RLock()
576582
}
577583

578584
listOpts := client.ListOptions{}
@@ -712,6 +718,8 @@ func (c *fakeClient) IsObjectNamespaced(obj runtime.Object) (bool, error) {
712718
}
713719

714720
func (c *fakeClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
721+
c.schemeLock.RLock()
722+
defer c.schemeLock.RUnlock()
715723
createOptions := &client.CreateOptions{}
716724
createOptions.ApplyOptions(opts)
717725

@@ -748,6 +756,8 @@ func (c *fakeClient) Create(ctx context.Context, obj client.Object, opts ...clie
748756
}
749757

750758
func (c *fakeClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
759+
c.schemeLock.RLock()
760+
defer c.schemeLock.RUnlock()
751761
gvr, err := getGVRFromObject(obj, c.scheme)
752762
if err != nil {
753763
return err
@@ -793,6 +803,8 @@ func (c *fakeClient) Delete(ctx context.Context, obj client.Object, opts ...clie
793803
}
794804

795805
func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error {
806+
c.schemeLock.RLock()
807+
defer c.schemeLock.RUnlock()
796808
gvk, err := apiutil.GVKForObject(obj, c.scheme)
797809
if err != nil {
798810
return err
@@ -842,6 +854,8 @@ func (c *fakeClient) Update(ctx context.Context, obj client.Object, opts ...clie
842854
}
843855

844856
func (c *fakeClient) update(obj client.Object, isStatus bool, opts ...client.UpdateOption) error {
857+
c.schemeLock.RLock()
858+
defer c.schemeLock.RUnlock()
845859
updateOptions := &client.UpdateOptions{}
846860
updateOptions.ApplyOptions(opts)
847861

@@ -870,6 +884,8 @@ func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.
870884
}
871885

872886
func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
887+
c.schemeLock.RLock()
888+
defer c.schemeLock.RUnlock()
873889
patchOptions := &client.PatchOptions{}
874890
patchOptions.ApplyOptions(opts)
875891

Diff for: pkg/client/fake/client_test.go

+88
Original file line numberDiff line numberDiff line change
@@ -2409,6 +2409,93 @@ var _ = Describe("Fake client", func() {
24092409
Expect(cl.SubResource(subResourceScale).Update(context.Background(), obj, client.WithSubResourceBody(scale)).Error()).To(Equal(expectedErr))
24102410
})
24112411

2412+
It("is threadsafe", func() {
2413+
cl := NewClientBuilder().Build()
2414+
2415+
u := func() *unstructured.Unstructured {
2416+
u := &unstructured.Unstructured{}
2417+
u.SetAPIVersion("custom/v1")
2418+
u.SetKind("Version")
2419+
u.SetName("foo")
2420+
return u
2421+
}
2422+
2423+
uList := func() *unstructured.UnstructuredList {
2424+
u := &unstructured.UnstructuredList{}
2425+
u.SetAPIVersion("custom/v1")
2426+
u.SetKind("Version")
2427+
2428+
return u
2429+
}
2430+
2431+
meta := func() *metav1.PartialObjectMetadata {
2432+
return &metav1.PartialObjectMetadata{
2433+
ObjectMeta: metav1.ObjectMeta{
2434+
Name: "foo",
2435+
Namespace: "default",
2436+
},
2437+
TypeMeta: metav1.TypeMeta{
2438+
APIVersion: "custom/v1",
2439+
Kind: "Version",
2440+
},
2441+
}
2442+
}
2443+
metaList := func() *metav1.PartialObjectMetadataList {
2444+
return &metav1.PartialObjectMetadataList{
2445+
TypeMeta: metav1.TypeMeta{
2446+
2447+
APIVersion: "custom/v1",
2448+
Kind: "Version",
2449+
},
2450+
}
2451+
}
2452+
2453+
pod := func() *corev1.Pod {
2454+
return &corev1.Pod{ObjectMeta: metav1.ObjectMeta{
2455+
Name: "foo",
2456+
Namespace: "default",
2457+
}}
2458+
}
2459+
2460+
ctx := context.Background()
2461+
ops := []func(){
2462+
func() { _ = cl.Create(ctx, u()) },
2463+
func() { _ = cl.Get(ctx, client.ObjectKeyFromObject(u()), u()) },
2464+
func() { _ = cl.Update(ctx, u()) },
2465+
func() { _ = cl.Patch(ctx, u(), client.RawPatch(types.StrategicMergePatchType, []byte("foo"))) },
2466+
func() { _ = cl.Delete(ctx, u()) },
2467+
func() { _ = cl.DeleteAllOf(ctx, u(), client.HasLabels{"foo"}) },
2468+
func() { _ = cl.List(ctx, uList()) },
2469+
2470+
func() { _ = cl.Create(ctx, meta()) },
2471+
func() { _ = cl.Get(ctx, client.ObjectKeyFromObject(meta()), meta()) },
2472+
func() { _ = cl.Update(ctx, meta()) },
2473+
func() { _ = cl.Patch(ctx, meta(), client.RawPatch(types.StrategicMergePatchType, []byte("foo"))) },
2474+
func() { _ = cl.Delete(ctx, meta()) },
2475+
func() { _ = cl.DeleteAllOf(ctx, meta(), client.HasLabels{"foo"}) },
2476+
func() { _ = cl.List(ctx, metaList()) },
2477+
2478+
func() { _ = cl.Create(ctx, pod()) },
2479+
func() { _ = cl.Get(ctx, client.ObjectKeyFromObject(pod()), pod()) },
2480+
func() { _ = cl.Update(ctx, pod()) },
2481+
func() { _ = cl.Patch(ctx, pod(), client.RawPatch(types.StrategicMergePatchType, []byte("foo"))) },
2482+
func() { _ = cl.Delete(ctx, pod()) },
2483+
func() { _ = cl.DeleteAllOf(ctx, pod(), client.HasLabels{"foo"}) },
2484+
func() { _ = cl.List(ctx, &corev1.PodList{}) },
2485+
}
2486+
2487+
wg := sync.WaitGroup{}
2488+
wg.Add(len(ops))
2489+
for _, op := range ops {
2490+
go func() {
2491+
defer wg.Done()
2492+
op()
2493+
}()
2494+
}
2495+
2496+
wg.Wait()
2497+
})
2498+
24122499
scalableObjs := []client.Object{
24132500
&appsv1.Deployment{
24142501
ObjectMeta: metav1.ObjectMeta{
@@ -2497,6 +2584,7 @@ var _ = Describe("Fake client", func() {
24972584
scaleExpected.ResourceVersion = scaleActual.ResourceVersion
24982585
Expect(cmp.Diff(scaleExpected, scaleActual)).To(BeEmpty())
24992586
})
2587+
25002588
}
25012589
})
25022590

0 commit comments

Comments
 (0)