Skip to content

Commit f769360

Browse files
authored
fix: data races that occur when functions do not clone pointer object… (#2629)
1 parent 1248818 commit f769360

File tree

5 files changed

+89
-7
lines changed

5 files changed

+89
-7
lines changed

internal/dispatch/caching/caching.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ func (cd *Dispatcher) DispatchCheck(ctx context.Context, req *v1.DispatchCheckRe
196196

197197
// Return both the computed and err in ALL cases: computed contains resolved
198198
// metadata even if there was an error.
199-
return computed, err
199+
return computed.CloneVT(), err
200200
}
201201

202202
// DispatchExpand implements dispatch.Expand interface and does not do any caching yet.

internal/dispatch/caching/cachingdispatch_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package caching
22

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

@@ -149,6 +150,77 @@ func TestMaxDepthCaching(t *testing.T) {
149150
}
150151
}
151152

153+
func TestConcurrentDebugInfoAccess(t *testing.T) {
154+
require := require.New(t)
155+
156+
delegate := delegateDispatchMock{&mock.Mock{}}
157+
158+
originalReq := &v1.DispatchCheckRequest{
159+
ResourceIds: []string{"original"},
160+
Subject: &core.ObjectAndRelation{
161+
Relation: "original",
162+
},
163+
}
164+
165+
// Have the delegate return one request object for all calls
166+
delegate.On("DispatchCheck", mock.Anything).
167+
Return(&v1.DispatchCheckResponse{
168+
Metadata: &v1.ResponseMeta{
169+
DispatchCount: 1,
170+
DebugInfo: &v1.DebugInformation{Check: &v1.CheckDebugTrace{Request: originalReq}},
171+
},
172+
}, nil)
173+
174+
dispatcher, err := NewCachingDispatcher(DispatchTestCache(t), false, "", nil)
175+
require.NoError(err)
176+
dispatcher.SetDelegate(delegate)
177+
t.Cleanup(func() {
178+
_ = dispatcher.Close()
179+
})
180+
181+
// Spawn multiple goroutines that create requests with slice views of the same backing array
182+
// This simulates what happens in ComputeBulkCheck with ForEachChunkUntil
183+
const numGoroutines = 1000
184+
errors := make(chan error, numGoroutines)
185+
186+
var wg sync.WaitGroup
187+
for i := 0; i < numGoroutines; i++ {
188+
wg.Add(1)
189+
go func(goroutineID int) {
190+
defer wg.Done()
191+
192+
request := &v1.DispatchCheckRequest{
193+
ResourceRelation: RR("document", "viewer"),
194+
ResourceIds: []string{"doc1"},
195+
Subject: tuple.MustParseSubjectONR("user:alice").ToCoreONR(),
196+
Metadata: &v1.ResolverMeta{
197+
AtRevision: decimal.Zero.String(),
198+
DepthRemaining: 50,
199+
},
200+
Debug: v1.DispatchCheckRequest_ENABLE_BASIC_DEBUGGING,
201+
}
202+
203+
resp, err := dispatcher.DispatchCheck(context.Background(), request)
204+
if err != nil {
205+
errors <- err
206+
return
207+
}
208+
209+
require.NotNil(resp.GetMetadata().GetDebugInfo().GetCheck().GetRequest())
210+
211+
// we mutate the response to prove that it's not shared across goroutines
212+
resp.GetMetadata().GetDebugInfo().GetCheck().GetRequest().Subject.Relation = "modified"
213+
resp.GetMetadata().GetDebugInfo().GetCheck().GetRequest().ResourceIds = []string{"modified"}
214+
}(i)
215+
}
216+
217+
wg.Wait()
218+
close(errors)
219+
for err := range errors {
220+
require.NoError(err)
221+
}
222+
}
223+
152224
type delegateDispatchMock struct {
153225
*mock.Mock
154226
}

internal/dispatch/dispatch.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type Dispatcher interface {
3737
// Check interface describes just the methods required to dispatch check requests.
3838
type Check interface {
3939
// DispatchCheck submits a single check request and returns its result.
40+
// The result should be safe to access from multiple goroutines.
4041
DispatchCheck(ctx context.Context, req *v1.DispatchCheckRequest) (*v1.DispatchCheckResponse, error)
4142
}
4243

internal/dispatch/singleflight/singleflight.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func (d *Dispatcher) DispatchCheck(ctx context.Context, req *v1.DispatchCheckReq
8888
}
8989

9090
singleFlightCount.WithLabelValues("DispatchCheck", strconv.FormatBool(isShared)).Inc()
91-
return sharedResp, err
91+
return sharedResp.CloneVT(), err
9292
}
9393

9494
func (d *Dispatcher) DispatchExpand(ctx context.Context, req *v1.DispatchExpandRequest) (*v1.DispatchExpandResponse, error) {

internal/dispatch/singleflight/singleflight_test.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,28 @@ func TestSingleFlightDispatcher(t *testing.T) {
4242
wg := sync.WaitGroup{}
4343
wg.Add(4)
4444
go func() {
45-
_, _ = disp.DispatchCheck(t.Context(), req.CloneVT())
45+
resp1, err := disp.DispatchCheck(t.Context(), req.CloneVT())
46+
require.NoError(t, err)
47+
// this goroutine mutates the response; other goroutines that read should be unaffected
48+
resp1.GetMetadata().GetDebugInfo().GetCheck().GetSubProblems()[0].IsCachedResult = false
4649
wg.Done()
4750
}()
4851
go func() {
49-
_, _ = disp.DispatchCheck(t.Context(), req.CloneVT())
52+
resp2, _ := disp.DispatchCheck(t.Context(), req.CloneVT())
53+
// this goroutine reads the response
54+
t.Log(resp2)
5055
wg.Done()
5156
}()
5257
go func() {
53-
_, _ = disp.DispatchCheck(t.Context(), req.CloneVT())
58+
resp3, _ := disp.DispatchCheck(t.Context(), req.CloneVT())
59+
t.Log(resp3)
5460
wg.Done()
5561
}()
5662
go func() {
5763
anotherReq := req.CloneVT()
5864
anotherReq.ResourceIds = []string{"foo", "baz"}
59-
_, _ = disp.DispatchCheck(t.Context(), anotherReq)
65+
resp4, _ := disp.DispatchCheck(t.Context(), anotherReq)
66+
t.Log(resp4)
6067
wg.Done()
6168
}()
6269

@@ -363,7 +370,9 @@ type mockDispatcher struct {
363370

364371
func (m mockDispatcher) DispatchCheck(_ context.Context, _ *v1.DispatchCheckRequest) (*v1.DispatchCheckResponse, error) {
365372
m.f()
366-
return &v1.DispatchCheckResponse{}, nil
373+
return &v1.DispatchCheckResponse{Metadata: &v1.ResponseMeta{DebugInfo: &v1.DebugInformation{Check: &v1.CheckDebugTrace{
374+
SubProblems: []*v1.CheckDebugTrace{{IsCachedResult: true}},
375+
}}}}, nil
367376
}
368377

369378
func (m mockDispatcher) DispatchExpand(_ context.Context, _ *v1.DispatchExpandRequest) (*v1.DispatchExpandResponse, error) {

0 commit comments

Comments
 (0)