Skip to content

Commit 999748d

Browse files
Fix client side race condition (#1124)
1 parent 84c60d8 commit 999748d

File tree

2 files changed

+76
-0
lines changed

2 files changed

+76
-0
lines changed

internal/context.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package internal
2222

2323
import (
2424
"fmt"
25+
"sync"
2526
"time"
2627

2728
"github.com/opentracing/opentracing-go"
@@ -287,6 +288,9 @@ type cancelCtx struct {
287288

288289
done Channel // closed by the first cancel call.
289290

291+
mu sync.Mutex
292+
canceled bool
293+
290294
children map[canceler]bool // set to nil by the first cancel call
291295
err error // set to non-nil by the first cancel call
292296
}
@@ -306,6 +310,16 @@ func (c *cancelCtx) String() string {
306310
// cancel closes c.done, cancels each of c's children, and, if
307311
// removeFromParent is true, removes c from its parent's children.
308312
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
313+
c.mu.Lock()
314+
if c.canceled {
315+
c.mu.Unlock()
316+
// calling cancel from multiple go routines isn't safe
317+
// avoid a data race by only allowing the first call
318+
return
319+
}
320+
c.canceled = true
321+
c.mu.Unlock()
322+
309323
if err == nil {
310324
panic("context: internal error: missing cancel error")
311325
}

internal/context_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) 2017-2021 Uber Technologies Inc.
2+
// Portions of the Software are attributed to Copyright (c) 2020 Temporal Technologies Inc.
3+
//
4+
// Permission is hereby granted, free of charge, to any person obtaining a copy
5+
// of this software and associated documentation files (the "Software"), to deal
6+
// in the Software without restriction, including without limitation the rights
7+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
// copies of the Software, and to permit persons to whom the Software is
9+
// furnished to do so, subject to the following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included in
12+
// all copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
// THE SOFTWARE.
21+
22+
package internal
23+
24+
import (
25+
"testing"
26+
"time"
27+
28+
"github.com/stretchr/testify/assert"
29+
"go.uber.org/zap/zaptest"
30+
)
31+
32+
func TestContext_RaceRegression(t *testing.T) {
33+
/*
34+
A race condition existed due to concurrently ending goroutines on shutdown (i.e. closing their chan without waiting
35+
on them to finish shutdown), which executed... quite a lot of non-concurrency-safe code in a concurrent way. All
36+
decision-sensitive code is assumed to be run strictly sequentially.
37+
38+
Context cancellation was one identified by a customer, and it's fairly easy to test.
39+
In principle this must be safe to do - contexts are supposed to be concurrency-safe. Even if ours are not actually
40+
safe (for valid reasons), our execution model needs to ensure they *act* like it's safe.
41+
*/
42+
s := WorkflowTestSuite{}
43+
s.SetLogger(zaptest.NewLogger(t))
44+
env := s.NewTestWorkflowEnvironment()
45+
wf := func(ctx Context) error {
46+
ctx, cancel := WithCancel(ctx)
47+
racyCancel := func(ctx Context) {
48+
defer cancel() // defer is necessary as Sleep will never return due to Goexit
49+
_ = Sleep(ctx, time.Hour)
50+
}
51+
// start a handful to increase odds of a race being detected
52+
for i := 0; i < 10; i++ {
53+
Go(ctx, racyCancel)
54+
}
55+
56+
_ = Sleep(ctx, time.Minute) // die early
57+
return nil
58+
}
59+
env.RegisterWorkflow(wf)
60+
env.ExecuteWorkflow(wf)
61+
assert.NoError(t, env.GetWorkflowError())
62+
}

0 commit comments

Comments
 (0)