Skip to content

Commit 05ffed2

Browse files
Fix race condition in storing observed status conditions (#63)
1 parent 0fad555 commit 05ffed2

File tree

2 files changed

+65
-22
lines changed

2 files changed

+65
-22
lines changed

status/controller.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package status
33
import (
44
"context"
55
"fmt"
6+
"sync"
67
"time"
78

89
"github.com/awslabs/operatorpkg/object"
@@ -37,14 +38,13 @@ const (
3738
type Controller[T Object] struct {
3839
kubeClient client.Client
3940
eventRecorder record.EventRecorder
40-
observedConditions map[reconcile.Request]ConditionSet
41+
observedConditions sync.Map // map[reconcile.Request]ConditionSet
4142
}
4243

4344
func NewController[T Object](client client.Client, eventRecorder record.EventRecorder) *Controller[T] {
4445
return &Controller[T]{
45-
kubeClient: client,
46-
eventRecorder: eventRecorder,
47-
observedConditions: map[reconcile.Request]ConditionSet{},
46+
kubeClient: client,
47+
eventRecorder: eventRecorder,
4848
}
4949
}
5050

@@ -80,8 +80,11 @@ func (c *Controller[T]) Reconcile(ctx context.Context, req reconcile.Request) (r
8080
}
8181

8282
currentConditions := o.StatusConditions()
83-
observedConditions := c.observedConditions[req]
84-
c.observedConditions[req] = currentConditions
83+
observedConditions := ConditionSet{}
84+
if v, ok := c.observedConditions.Load(req); ok {
85+
observedConditions = v.(ConditionSet)
86+
}
87+
c.observedConditions.Store(req, currentConditions)
8588

8689
// Detect and record condition counts
8790
for _, condition := range o.GetConditions() {

status/controller_test.go

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package status_test
22

33
import (
44
"context"
5+
"sync"
56
"time"
67

78
"github.com/awslabs/operatorpkg/status"
@@ -25,11 +26,11 @@ var _ = Describe("Controller", func() {
2526
var ctx context.Context
2627
var recorder *record.FakeRecorder
2728
var controller *status.Controller[*TestObject]
28-
var client client.Client
29+
var kubeClient client.Client
2930
BeforeEach(func() {
3031
recorder = record.NewFakeRecorder(10)
31-
client = fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
32-
controller = status.NewController[*TestObject](client, recorder)
32+
kubeClient = fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
33+
controller = status.NewController[*TestObject](kubeClient, recorder)
3334
ctx = log.IntoContext(context.Background(), ginkgo.GinkgoLogr)
3435
})
3536

@@ -38,7 +39,7 @@ var _ = Describe("Controller", func() {
3839
testObject.StatusConditions() // initialize conditions
3940

4041
// conditions not set
41-
ExpectApplied(ctx, client, testObject)
42+
ExpectApplied(ctx, kubeClient, testObject)
4243
ExpectReconciled(ctx, controller, testObject)
4344

4445
// Ready Condition
@@ -73,9 +74,9 @@ var _ = Describe("Controller", func() {
7374
// Transition Foo
7475
time.Sleep(time.Second * 1)
7576
testObject.StatusConditions().SetTrue(ConditionTypeFoo)
76-
ExpectApplied(ctx, client, testObject)
77+
ExpectApplied(ctx, kubeClient, testObject)
7778
ExpectReconciled(ctx, controller, testObject)
78-
ExpectStatusConditions(ctx, client, FastTimeout, testObject, status.Condition{Type: ConditionTypeFoo, Status: metav1.ConditionTrue})
79+
ExpectStatusConditions(ctx, kubeClient, FastTimeout, testObject, status.Condition{Type: ConditionTypeFoo, Status: metav1.ConditionTrue})
7980

8081
// Ready Condition
8182
Expect(GetMetric("operator_status_condition_count", conditionLabels(status.ConditionReady, metav1.ConditionTrue))).To(BeNil())
@@ -125,9 +126,9 @@ var _ = Describe("Controller", func() {
125126

126127
// Transition Bar, root condition should also flip
127128
testObject.StatusConditions().SetTrueWithReason(ConditionTypeBar, "reason", "message")
128-
ExpectApplied(ctx, client, testObject)
129+
ExpectApplied(ctx, kubeClient, testObject)
129130
ExpectReconciled(ctx, controller, testObject)
130-
ExpectStatusConditions(ctx, client, FastTimeout, testObject, status.Condition{Type: ConditionTypeBar, Status: metav1.ConditionTrue, Reason: "reason", Message: "message"})
131+
ExpectStatusConditions(ctx, kubeClient, FastTimeout, testObject, status.Condition{Type: ConditionTypeBar, Status: metav1.ConditionTrue, Reason: "reason", Message: "message"})
131132

132133
// Ready Condition
133134
Expect(GetMetric("operator_status_condition_count", conditionLabels(status.ConditionReady, metav1.ConditionTrue)).GetGauge().GetValue()).To(BeEquivalentTo(1))
@@ -201,7 +202,7 @@ var _ = Describe("Controller", func() {
201202
Expect(recorder.Events).To(Receive(Equal("Normal Ready Status condition transitioned, Type: Ready, Status: Unknown -> True, Reason: Ready")))
202203

203204
// Delete the object, state should clear
204-
ExpectDeleted(ctx, client, testObject)
205+
ExpectDeleted(ctx, kubeClient, testObject)
205206
ExpectReconciled(ctx, controller, testObject)
206207

207208
// Ready Condition
@@ -233,15 +234,15 @@ var _ = Describe("Controller", func() {
233234
testObject.StatusConditions() // initialize conditions
234235

235236
// conditions not set
236-
ExpectApplied(ctx, client, testObject)
237+
ExpectApplied(ctx, kubeClient, testObject)
237238
ExpectReconciled(ctx, controller, testObject)
238239

239-
// set the bar condition and transition it to true
240+
// set the baz condition and transition it to true
240241
testObject.StatusConditions().SetTrue(ConditionTypeBaz)
241242

242-
ExpectApplied(ctx, client, testObject)
243+
ExpectApplied(ctx, kubeClient, testObject)
243244
ExpectReconciled(ctx, controller, testObject)
244-
ExpectStatusConditions(ctx, client, FastTimeout, testObject, status.Condition{Type: ConditionTypeBaz, Status: metav1.ConditionTrue, Reason: ConditionTypeBaz, Message: ""})
245+
ExpectStatusConditions(ctx, kubeClient, FastTimeout, testObject, status.Condition{Type: ConditionTypeBaz, Status: metav1.ConditionTrue, Reason: ConditionTypeBaz, Message: ""})
245246

246247
Expect(GetMetric("operator_status_condition_transitions_total", conditionLabels(ConditionTypeBaz, metav1.ConditionTrue)).GetCounter().GetValue()).To(BeEquivalentTo(1))
247248
Expect(GetMetric("operator_status_condition_transitions_total", conditionLabels(ConditionTypeBaz, metav1.ConditionFalse))).To(BeNil())
@@ -250,9 +251,9 @@ var _ = Describe("Controller", func() {
250251
// set the bar condition and transition it to false
251252
testObject.StatusConditions().SetFalse(ConditionTypeBaz, "reason", "message")
252253

253-
ExpectApplied(ctx, client, testObject)
254+
ExpectApplied(ctx, kubeClient, testObject)
254255
ExpectReconciled(ctx, controller, testObject)
255-
ExpectStatusConditions(ctx, client, FastTimeout, testObject, status.Condition{Type: ConditionTypeBaz, Status: metav1.ConditionFalse, Reason: "reason", Message: "message"})
256+
ExpectStatusConditions(ctx, kubeClient, FastTimeout, testObject, status.Condition{Type: ConditionTypeBaz, Status: metav1.ConditionFalse, Reason: "reason", Message: "message"})
256257

257258
Expect(GetMetric("operator_status_condition_transitions_total", conditionLabels(ConditionTypeBaz, metav1.ConditionTrue)).GetCounter().GetValue()).To(BeEquivalentTo(1))
258259
Expect(GetMetric("operator_status_condition_transitions_total", conditionLabels(ConditionTypeBaz, metav1.ConditionFalse)).GetCounter().GetValue()).To(BeEquivalentTo(1))
@@ -261,13 +262,52 @@ var _ = Describe("Controller", func() {
261262
// clear the condition and don't expect the metrics to change
262263
_ = testObject.StatusConditions().Clear(ConditionTypeBaz)
263264

264-
ExpectApplied(ctx, client, testObject)
265+
ExpectApplied(ctx, kubeClient, testObject)
265266
ExpectReconciled(ctx, controller, testObject)
266267

267268
Expect(GetMetric("operator_status_condition_transitions_total", conditionLabels(ConditionTypeBaz, metav1.ConditionTrue)).GetCounter().GetValue()).To(BeEquivalentTo(1))
268269
Expect(GetMetric("operator_status_condition_transitions_total", conditionLabels(ConditionTypeBaz, metav1.ConditionFalse)).GetCounter().GetValue()).To(BeEquivalentTo(1))
269270
Expect(GetMetric("operator_status_condition_transitions_total", conditionLabels(ConditionTypeBaz, metav1.ConditionUnknown))).To(BeNil())
270271
})
272+
It("should not race when reconciling status conditions simultaneously", func() {
273+
var objs []*TestObject
274+
for range 100 {
275+
testObject := test.Object(&TestObject{})
276+
testObject.StatusConditions() // initialize conditions
277+
// conditions not set
278+
ExpectApplied(ctx, kubeClient, testObject)
279+
objs = append(objs, testObject)
280+
}
281+
282+
// Run 100 object reconciles at once to attempt to trigger a data raceg
283+
var wg sync.WaitGroup
284+
for _, obj := range objs {
285+
wg.Add(1)
286+
go func() {
287+
defer wg.Done()
288+
defer GinkgoRecover()
289+
290+
ExpectReconciled(ctx, controller, obj)
291+
}()
292+
}
293+
294+
for _, obj := range objs {
295+
// set the baz condition and transition it to true
296+
obj.StatusConditions().SetTrue(ConditionTypeBaz)
297+
ExpectApplied(ctx, kubeClient, obj)
298+
}
299+
300+
// Run 100 object reconciles at once to attempt to trigger a data race
301+
for _, obj := range objs {
302+
wg.Add(1)
303+
go func() {
304+
defer wg.Done()
305+
defer GinkgoRecover()
306+
307+
ExpectReconciled(ctx, controller, obj)
308+
}()
309+
}
310+
})
271311
})
272312

273313
// GetMetric attempts to find a metric given name and labels

0 commit comments

Comments
 (0)