diff --git a/pkg/reftracker/ref_tracker.go b/pkg/reftracker/ref_tracker.go index 26a2c0101..e75160553 100644 --- a/pkg/reftracker/ref_tracker.go +++ b/pkg/reftracker/ref_tracker.go @@ -28,18 +28,27 @@ func (a *AppRefTracker) AppsForRef(refKey RefKey) (map[RefKey]struct{}, error) { return nil, fmt.Errorf("could not find ref %s", refKey.Description()) } - return apps, nil + return copyRefKeySet(apps), nil } func (a *AppRefTracker) RefsForApp(appKey RefKey) (map[RefKey]struct{}, error) { a.lock.Lock() defer a.lock.Unlock() - if a.appsToRefs[appKey] == nil { + refs := a.appsToRefs[appKey] + if refs == nil { return nil, fmt.Errorf("could not find refs for App %s", appKey.RefName()) } - return a.appsToRefs[appKey], nil + return copyRefKeySet(refs), nil +} + +func copyRefKeySet(in map[RefKey]struct{}) map[RefKey]struct{} { + out := make(map[RefKey]struct{}, len(in)) + for k := range in { + out[k] = struct{}{} + } + return out } func (a *AppRefTracker) RemoveRef(refKey RefKey) { diff --git a/pkg/reftracker/ref_tracker_test.go b/pkg/reftracker/ref_tracker_test.go index 9ff9c9b6b..72bf8ec9f 100644 --- a/pkg/reftracker/ref_tracker_test.go +++ b/pkg/reftracker/ref_tracker_test.go @@ -4,6 +4,7 @@ package reftracker_test import ( + "sync" "testing" "carvel.dev/kapp-controller/pkg/reftracker" @@ -59,3 +60,54 @@ func Test_RemoveAppFromAllRefs_RemovesApp(t *testing.T) { t.Fatalf("expected app to be removed from appRefTracker after deletion") } } + +func Test_AppsForRef_SafeForConcurrentIteration(t *testing.T) { + appRefTracker := reftracker.NewAppRefTracker() + + refKey := reftracker.NewSecretKey("secretName", "default") + refKeyMap := map[reftracker.RefKey]struct{}{refKey: {}} + + for i := 0; i < 20; i++ { + appKey := reftracker.NewAppKey("seed-app", "default") + appKey = reftracker.NewAppKey(appKey.RefName()+string(rune('a'+i)), "default") + appRefTracker.ReconcileRefs(refKeyMap, appKey) + } + + stop := make(chan struct{}) + var wg sync.WaitGroup + + wg.Add(1) + go func() { + defer wg.Done() + i := 0 + for { + select { + case <-stop: + return + default: + appKey := reftracker.NewAppKey("mutator-app", "default") + appKey = reftracker.NewAppKey(appKey.RefName()+string(rune('a'+(i%26))), "default") + appRefTracker.ReconcileRefs(refKeyMap, appKey) + appRefTracker.RemoveAppFromAllRefs(appKey) + i++ + } + } + }() + + iterDone := make(chan struct{}) + go func() { + defer close(iterDone) + for i := 0; i < 200; i++ { + apps, err := appRefTracker.AppsForRef(refKey) + if err != nil { + continue + } + for range apps { + } + } + }() + + <-iterDone + close(stop) + wg.Wait() +}