go.uber.org/cadence@v1.2.9/internal/context_test.go (about) 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 "context" 26 "testing" 27 "time" 28 29 "github.com/stretchr/testify/assert" 30 "github.com/stretchr/testify/mock" 31 "github.com/stretchr/testify/require" 32 ) 33 34 func TestContextChildParentCancelRace(t *testing.T) { 35 /* 36 Testing previous race happened while child and parent cancelling at the same time 37 While child is trying to remove itself from the parent, parent tries to iterate 38 its children and cancel them at the same time. 39 */ 40 env := newTestWorkflowEnv(t) 41 42 wf := func(ctx Context) error { 43 parentCtx, parentCancel := WithCancel(ctx) 44 defer parentCancel() 45 46 type cancelerContext struct { 47 ctx Context 48 canceler func() 49 } 50 51 children := []cancelerContext{} 52 numChildren := 100 53 54 for i := 0; i < numChildren; i++ { 55 c, canceler := WithCancel(parentCtx) 56 children = append(children, cancelerContext{ 57 ctx: c, 58 canceler: canceler, 59 }) 60 } 61 62 for i := 0; i < numChildren; i++ { 63 go children[i].canceler() 64 if i == numChildren/2 { 65 go parentCancel() 66 } 67 } 68 69 return nil 70 } 71 env.RegisterWorkflow(wf) 72 env.ExecuteWorkflow(wf) 73 assert.NoError(t, env.GetWorkflowError()) 74 } 75 76 func TestContextConcurrentCancelRace(t *testing.T) { 77 /* 78 A race condition existed due to concurrently ending goroutines on shutdown (i.e. closing their chan without waiting 79 on them to finish shutdown), which executed... quite a lot of non-concurrency-safe code in a concurrent way. All 80 decision-sensitive code is assumed to be run strictly sequentially. 81 82 Context cancellation was one identified by a customer, and it's fairly easy to test. 83 In principle this must be safe to do - contexts are supposed to be concurrency-safe. Even if ours are not actually 84 safe (for valid reasons), our execution model needs to ensure they *act* like it's safe. 85 */ 86 env := newTestWorkflowEnv(t) 87 wf := func(ctx Context) error { 88 ctx, cancel := WithCancel(ctx) 89 racyCancel := func(ctx Context) { 90 defer cancel() // defer is necessary as Sleep will never return due to Goexit 91 _ = Sleep(ctx, time.Hour) 92 } 93 // start a handful to increase odds of a race being detected 94 for i := 0; i < 10; i++ { 95 Go(ctx, racyCancel) 96 } 97 98 _ = Sleep(ctx, time.Minute) // die early 99 return nil 100 } 101 env.RegisterWorkflow(wf) 102 env.ExecuteWorkflow(wf) 103 assert.NoError(t, env.GetWorkflowError()) 104 } 105 106 func TestContextAddChildCancelParentRace(t *testing.T) { 107 /* 108 It's apparently also possible to race on adding children while propagating the cancel to children. 109 */ 110 env := newTestWorkflowEnv(t) 111 wf := func(ctx Context) error { 112 ctx, cancel := WithCancel(ctx) 113 racyCancel := func(ctx Context) { 114 defer cancel() // defer is necessary as Sleep will never return due to Goexit 115 defer func() { 116 _, ccancel := WithCancel(ctx) 117 cancel() 118 ccancel() 119 }() 120 _ = Sleep(ctx, time.Hour) 121 } 122 // start a handful to increase odds of a race being detected 123 for i := 0; i < 10; i++ { 124 Go(ctx, racyCancel) 125 } 126 127 _ = Sleep(ctx, time.Minute) // die early 128 return nil 129 } 130 env.RegisterWorkflow(wf) 131 env.ExecuteWorkflow(wf) 132 assert.NoError(t, env.GetWorkflowError()) 133 } 134 135 func TestContextCancellationOrderDeterminism(t *testing.T) { 136 /* 137 Previously, child-contexts were stored in a map, preventing deterministic order when propagating cancellation. 138 The order of branches being selected in this test was random, both for the first event and in following ones. 139 140 In principle this should be fine, but it's possible for the effects of cancellation to trigger a selector's 141 future-done callback, which currently records the *real-time*-first event as the branch to unblock, rather than 142 doing something more safe by design (e.g. choosing based on state when the selector's goroutine is unblocked). 143 144 Unfortunately, we cannot change the selector's behavior without introducing non-backwards-compatible changes to 145 currently-working workflows. 146 147 So the workaround for now is to maintain child-context order, so they are canceled in a consistent order. 148 As this order was not controlled before, and Go does a pretty good job at randomizing map iteration order, 149 converting non-determinism to determinism should be strictly no worse for backwards compatibility, and it 150 fixes the issue for future executions. 151 */ 152 check := func(t *testing.T, separateStart, separateSelect bool) { 153 env := newTestWorkflowEnv(t) 154 act := func(ctx context.Context) error { 155 return nil // will be mocked 156 } 157 wf := func(ctx Context) ([]int, error) { 158 ctx, cancel := WithCancel(ctx) 159 Go(ctx, func(ctx Context) { 160 _ = Sleep(ctx, time.Minute) 161 cancel() 162 }) 163 164 // start some activities, which will not complete before the timeout cancels them 165 ctx = WithActivityOptions(ctx, ActivityOptions{ 166 TaskList: "", 167 ScheduleToCloseTimeout: time.Hour, 168 ScheduleToStartTimeout: time.Hour, 169 StartToCloseTimeout: time.Hour, 170 }) 171 s := NewSelector(ctx) 172 var result []int 173 for i := 0; i < 10; i++ { 174 i := i 175 // need a child context, a future alone is not enough as it does not become a child 176 cctx, ccancel := WithCancel(ctx) 177 178 s.AddFuture(ExecuteActivity(cctx, act), func(f Future) { 179 ccancel() // TODO: is this necessary to prevent leaks? if it is, how can we make it not? 180 err := f.Get(ctx, nil) 181 if err == nil || !IsCanceledError(err) { 182 // fail the test, this should not happen - activities must be canceled or it's not valid. 183 t.Errorf("activity completion or failure for some reason other than cancel: %v", err) 184 } 185 result = append(result, i) 186 }) 187 188 if separateStart { 189 // yield so they are submitted one at a time, in case that matters 190 _ = Sleep(ctx, time.Second) 191 } 192 } 193 for i := 0; i < 10; i++ { 194 if separateSelect { 195 // yield so they are selected one at a time, in case that matters 196 _ = Sleep(ctx, time.Second) 197 } 198 s.Select(ctx) 199 } 200 201 return result, nil 202 } 203 env.RegisterWorkflow(wf) 204 env.RegisterActivity(act) 205 206 // activities must not complete in time 207 env.OnActivity(act, mock.Anything).After(5 * time.Minute).Return(nil) 208 209 env.ExecuteWorkflow(wf) 210 require.NoError(t, env.GetWorkflowError()) 211 var result []int 212 require.NoError(t, env.GetWorkflowResult(&result)) 213 require.NotEmpty(t, result) 214 assert.Equal(t, 0, result[0], "first activity to be created should be the first one canceled") 215 assert.Equal(t, []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, result[1:], "other activities should finish in a consistent (but undefined) order") 216 } 217 218 type variant struct { 219 name string 220 separateStart bool 221 separateSelect bool 222 } 223 // all variants expose this behavior, but being a bit more exhaustive in the face 224 // of decision-scheduling differences seems good. 225 for _, test := range []variant{ 226 {"many in one decision", false, false}, 227 {"many started at once, selected slowly", false, true}, 228 {"started slowly, selected quickly", true, false}, 229 {"started and selected slowly", true, true}, 230 } { 231 t.Run(test.name, func(t *testing.T) { 232 check(t, test.separateStart, test.separateSelect) 233 }) 234 } 235 } 236 237 func BenchmarkSliceMaintenance(b *testing.B) { 238 // all essentially identical 239 b.Run("append", func(b *testing.B) { 240 data := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 241 for i := 0; i < b.N; i++ { 242 data = append(data[:5], data[6:]...) 243 data = append(data, i) // keep the slice the same size for all iterations 244 } 245 }) 246 b.Run("copy", func(b *testing.B) { 247 data := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 248 for i := 0; i < b.N; i++ { 249 copy(data[5:], data[6:]) 250 data = data[:9] // trim to actual size, as the last value is now duplicated. capacity is still 10. 251 data = append(data, i) // keep the slice the same size for all iterations 252 } 253 }) 254 b.Run("copy explicit capacity", func(b *testing.B) { 255 data := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 256 for i := 0; i < b.N; i++ { 257 copy(data[5:], data[6:]) 258 data = data[:9:10] // trim to actual size, as the last value is now duplicated. explicitly reserve 10 cap. 259 data = append(data, i) // keep the slice the same size for all iterations 260 } 261 }) 262 }