Skip to content

Commit 6c50ebc

Browse files
authored
fix: [TKC-3433] fix fatal error: concurrent map writes (#6268)
1 parent 512e4b9 commit 6c50ebc

File tree

2 files changed

+136
-2
lines changed

2 files changed

+136
-2
lines changed

pkg/testworkflows/testworkflowexecutor/testworkflowtemplatefetcher.go

+14-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type testWorkflowTemplateFetcher struct {
2020
client testworkflowtemplateclient.TestWorkflowTemplateClient
2121
environmentId string
2222
cache map[string]*testkube.TestWorkflowTemplate
23+
cacheMu sync.RWMutex
2324
}
2425

2526
func NewTestWorkflowTemplateFetcher(
@@ -34,6 +35,8 @@ func NewTestWorkflowTemplateFetcher(
3435
}
3536

3637
func (r *testWorkflowTemplateFetcher) SetCache(name string, tpl *testkube.TestWorkflowTemplate) {
38+
r.cacheMu.Lock()
39+
defer r.cacheMu.Unlock()
3740
if tpl == nil {
3841
delete(r.cache, name)
3942
} else {
@@ -43,14 +46,18 @@ func (r *testWorkflowTemplateFetcher) SetCache(name string, tpl *testkube.TestWo
4346

4447
func (r *testWorkflowTemplateFetcher) Prefetch(name string) error {
4548
name = testworkflowresolver.GetInternalTemplateName(name)
49+
r.cacheMu.RLock()
4650
if _, ok := r.cache[name]; ok {
51+
r.cacheMu.RUnlock()
4752
return nil
4853
}
54+
r.cacheMu.RUnlock()
55+
4956
workflow, err := r.client.Get(context.Background(), r.environmentId, name)
5057
if err != nil {
5158
return errors.Wrapf(err, "cannot fetch Test Workflow Template by name: %s", name)
5259
}
53-
r.cache[name] = workflow
60+
r.SetCache(name, workflow)
5461
return nil
5562
}
5663

@@ -75,13 +82,18 @@ func (r *testWorkflowTemplateFetcher) PrefetchMany(namesSet map[string]struct{})
7582
}
7683

7784
func (r *testWorkflowTemplateFetcher) Get(name string) (*testkube.TestWorkflowTemplate, error) {
85+
r.cacheMu.RLock()
7886
if r.cache[name] == nil {
87+
r.cacheMu.RUnlock()
7988
err := r.Prefetch(name)
8089
if err != nil {
8190
return nil, err
8291
}
92+
r.cacheMu.RLock()
8393
}
84-
return r.cache[name], nil
94+
tmpl := r.cache[name]
95+
r.cacheMu.RUnlock()
96+
return tmpl, nil
8597
}
8698

8799
func (r *testWorkflowTemplateFetcher) GetMany(names map[string]struct{}) (map[string]*testkube.TestWorkflowTemplate, error) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package testworkflowexecutor
2+
3+
import (
4+
"testing"
5+
6+
"github.com/golang/mock/gomock"
7+
"github.com/stretchr/testify/assert"
8+
9+
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
10+
"github.com/kubeshop/testkube/pkg/newclients/testworkflowtemplateclient"
11+
)
12+
13+
func TestTestWorkflowTemplateFetcher_ConcurrentAccess(t *testing.T) {
14+
// Create mock controller
15+
ctrl := gomock.NewController(t)
16+
defer ctrl.Finish()
17+
18+
// Create mock client
19+
mockClient := testworkflowtemplateclient.NewMockTestWorkflowTemplateClient(ctrl)
20+
21+
// Create test templates
22+
templates := make([]*testkube.TestWorkflowTemplate, 10)
23+
for i := 0; i < 10; i++ {
24+
templates[i] = &testkube.TestWorkflowTemplate{
25+
Name: "template-" + string(rune('A'+i)),
26+
Description: "description-" + string(rune('A'+i)),
27+
Labels: map[string]string{"key": "value"},
28+
Spec: &testkube.TestWorkflowTemplateSpec{},
29+
}
30+
}
31+
32+
// Setup mock expectations
33+
for _, tmpl := range templates {
34+
mockClient.EXPECT().
35+
Get(gomock.Any(), "test-env", tmpl.Name).
36+
Return(tmpl, nil).
37+
AnyTimes()
38+
}
39+
40+
// Create fetcher
41+
fetcher := NewTestWorkflowTemplateFetcher(mockClient, "test-env")
42+
43+
// Test concurrent prefetch
44+
t.Run("concurrent prefetch", func(t *testing.T) {
45+
names := make(map[string]struct{})
46+
for _, tmpl := range templates {
47+
names[tmpl.Name] = struct{}{}
48+
}
49+
50+
// Launch multiple goroutines to prefetch templates
51+
err := fetcher.PrefetchMany(names)
52+
assert.NoError(t, err)
53+
54+
// Verify all templates were fetched
55+
for _, tmpl := range templates {
56+
fetched, err := fetcher.Get(tmpl.Name)
57+
assert.NoError(t, err)
58+
assert.Equal(t, tmpl.Name, fetched.Name)
59+
assert.Equal(t, tmpl.Description, fetched.Description)
60+
assert.Equal(t, tmpl.Labels, fetched.Labels)
61+
}
62+
})
63+
64+
// Test concurrent get
65+
t.Run("concurrent get", func(t *testing.T) {
66+
names := make(map[string]struct{})
67+
for _, tmpl := range templates {
68+
names[tmpl.Name] = struct{}{}
69+
}
70+
71+
// Launch multiple goroutines to get templates
72+
results, err := fetcher.GetMany(names)
73+
assert.NoError(t, err)
74+
assert.Len(t, results, len(templates))
75+
76+
// Verify all templates were retrieved correctly
77+
for _, tmpl := range templates {
78+
fetched, ok := results[tmpl.Name]
79+
assert.True(t, ok)
80+
assert.Equal(t, tmpl.Name, fetched.Name)
81+
assert.Equal(t, tmpl.Description, fetched.Description)
82+
assert.Equal(t, tmpl.Labels, fetched.Labels)
83+
}
84+
})
85+
86+
// Test concurrent set and get
87+
t.Run("concurrent set and get", func(t *testing.T) {
88+
// Create a new template
89+
newTmpl := &testkube.TestWorkflowTemplate{
90+
Name: "new-template",
91+
Description: "new-description",
92+
Labels: map[string]string{"key": "value"},
93+
Spec: &testkube.TestWorkflowTemplateSpec{},
94+
}
95+
96+
// Setup mock expectations for the new template
97+
mockClient.EXPECT().
98+
Get(gomock.Any(), "test-env", newTmpl.Name).
99+
Return(newTmpl, nil).
100+
AnyTimes()
101+
102+
// Launch goroutines to set and get the template concurrently
103+
done := make(chan struct{})
104+
go func() {
105+
for i := 0; i < 100; i++ {
106+
fetcher.SetCache(newTmpl.Name, newTmpl)
107+
}
108+
close(done)
109+
}()
110+
111+
// Try to get the template while it's being set
112+
for i := 0; i < 100; i++ {
113+
fetched, err := fetcher.Get(newTmpl.Name)
114+
assert.NoError(t, err)
115+
assert.Equal(t, newTmpl.Name, fetched.Name)
116+
assert.Equal(t, newTmpl.Description, fetched.Description)
117+
assert.Equal(t, newTmpl.Labels, fetched.Labels)
118+
}
119+
120+
<-done // Wait for set operations to complete
121+
})
122+
}

0 commit comments

Comments
 (0)