Skip to content

Commit 13bf582

Browse files
committed
fixup! fix race condition
Signed-off-by: Bryce Palmer <bpalmer@redhat.com>
1 parent 5d13d1f commit 13bf582

2 files changed

Lines changed: 35 additions & 29 deletions

File tree

pkg/controllers/switchedcontroller/switched_controller.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ func (c *ControllerWithSwitch) sync(ctx context.Context, syncCtx factory.SyncCon
106106
c.mutex.Lock()
107107
c.switchContextCancel()
108108
c.switchContext = nil
109-
c.mutex.Unlock()
110109

111110
// Server-Side-Apply with an empty operator status for the delegate's field manager
112111
// will remove any conditions owned by it since the list type in the API definition
@@ -117,6 +116,8 @@ func (c *ControllerWithSwitch) sync(ctx context.Context, syncCtx factory.SyncCon
117116
return fmt.Errorf("clearing delegate %q conditions: %v", c.delegateName, err)
118117
}
119118

119+
c.mutex.Unlock()
120+
120121
default:
121122
var e error
122123
c.mutex.Lock()

pkg/controllers/switchedcontroller/switched_controller_test.go

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

@@ -394,6 +395,27 @@ type applyStatusCall struct {
394395
applyConfiguration *applyoperatorv1.OperatorStatusApplyConfiguration
395396
}
396397

398+
// synchronizedOperatorClient serializes access to a non-thread-safe OperatorClient
399+
// (e.g. fakeOperatorClient) so it can be used safely from multiple goroutines.
400+
type synchronizedOperatorClient struct {
401+
v1helpers.OperatorClient
402+
mu sync.Mutex
403+
statusApplied chan struct{}
404+
}
405+
406+
func (c *synchronizedOperatorClient) ApplyOperatorStatus(ctx context.Context, fieldManager string, applyConfiguration *applyoperatorv1.OperatorStatusApplyConfiguration) error {
407+
c.mu.Lock()
408+
err := c.OperatorClient.ApplyOperatorStatus(ctx, fieldManager, applyConfiguration)
409+
c.mu.Unlock()
410+
if c.statusApplied != nil {
411+
select {
412+
case c.statusApplied <- struct{}{}:
413+
default:
414+
}
415+
}
416+
return err
417+
}
418+
397419
type recordingOperatorClient struct {
398420
v1helpers.OperatorClient
399421
applyStatusCalls []applyStatusCall
@@ -405,11 +427,13 @@ func (r *recordingOperatorClient) ApplyOperatorStatus(ctx context.Context, field
405427
}
406428

407429
func TestSwitchedControllerClearsDelegateConditionsOnSwitchOff(t *testing.T) {
408-
delegateSynced := make(chan struct{})
409-
410430
recorder := events.NewInMemoryRecorder("switchedcontroller_test", clocktesting.NewFakePassiveClock(time.Now()))
411431
fakeClient := v1helpers.NewFakeOperatorClient(&operatorv1.OperatorSpec{ManagementState: operatorv1.Managed}, &operatorv1.OperatorStatus{}, nil)
412-
operatorClient := &recordingOperatorClient{OperatorClient: fakeClient}
432+
syncClient := &synchronizedOperatorClient{
433+
OperatorClient: fakeClient,
434+
statusApplied: make(chan struct{}, 1),
435+
}
436+
operatorClient := &recordingOperatorClient{OperatorClient: syncClient}
413437

414438
delegateFn := func(_ context.Context) *factory.Factory {
415439
var enqueueSyncHook factory.PostStartHook = func(ctx context.Context, syncContext factory.SyncContext) error {
@@ -418,13 +442,9 @@ func TestSwitchedControllerClearsDelegateConditionsOnSwitchOff(t *testing.T) {
418442
}
419443
return factory.New().
420444
WithSync(func(ctx context.Context, controllerContext factory.SyncContext) error {
421-
select {
422-
case delegateSynced <- struct{}{}:
423-
default:
424-
}
425445
return fmt.Errorf("route.route.openshift.io \"oauth-openshift\" not found")
426446
}).
427-
WithSyncDegradedOnError(fakeClient).
447+
WithSyncDegradedOnError(syncClient).
428448
WithPostStartHooks(enqueueSyncHook)
429449
}
430450

@@ -449,31 +469,16 @@ func TestSwitchedControllerClearsDelegateConditionsOnSwitchOff(t *testing.T) {
449469
t.Fatalf("unexpected error when syncing: %v", err)
450470
}
451471

452-
// Wait for the delegate to sync at least once and set a degraded condition
472+
// Wait for the delegate's reportDegraded to call ApplyOperatorStatus.
473+
// This signal fires after reportDegraded completes (not just after sync returns),
474+
// which avoids a data race on the non-thread-safe fakeOperatorClient.
453475
syncTimeoutCtx, syncTimeoutCancel := context.WithTimeout(t.Context(), 500*time.Millisecond)
454476
defer syncTimeoutCancel()
455477

456478
select {
457479
case <-syncTimeoutCtx.Done():
458-
t.Fatal("timed out waiting for delegate controller to sync")
459-
case <-delegateSynced:
460-
}
461-
462-
// Verify the delegate set a degraded condition
463-
_, status, _, err := operatorClient.GetOperatorState()
464-
if err != nil {
465-
t.Fatalf("unexpected error getting operator state: %v", err)
466-
}
467-
468-
foundDegraded := false
469-
for _, cond := range status.Conditions {
470-
if cond.Type == "test-controllerDegraded" && cond.Status == operatorv1.ConditionTrue {
471-
foundDegraded = true
472-
break
473-
}
474-
}
475-
if !foundDegraded {
476-
t.Fatal("expected test-controllerDegraded condition to be True after delegate sync error")
480+
t.Fatal("timed out waiting for delegate controller to report degraded status")
481+
case <-syncClient.statusApplied:
477482
}
478483

479484
// Switch off and sync the wrapper — this should clear delegate conditions

0 commit comments

Comments
 (0)