go.uber.org/cadence@v1.2.9/internal/internal_poller_autoscaler_test.go (about) 1 // Copyright (c) 2017-2021 Uber Technologies Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package internal 22 23 import ( 24 "math/rand" 25 "sync" 26 "testing" 27 "time" 28 29 "github.com/stretchr/testify/assert" 30 "go.uber.org/atomic" 31 "go.uber.org/zap/zaptest" 32 33 s "go.uber.org/cadence/.gen/go/shared" 34 "go.uber.org/cadence/internal/common/autoscaler" 35 ) 36 37 func Test_pollerAutoscaler(t *testing.T) { 38 type args struct { 39 disabled bool 40 noTaskPoll, taskPoll, unrelated int 41 initialPollerCount int 42 minPollerCount int 43 maxPollerCount int 44 targetMilliUsage uint64 45 cooldownTime time.Duration 46 autoScalerEpoch int 47 isDryRun bool 48 } 49 50 coolDownTime := time.Millisecond * 50 51 52 tests := []struct { 53 name string 54 args args 55 want int 56 }{ 57 { 58 name: "dry run doesn't change anything", 59 args: args{ 60 noTaskPoll: 100, 61 taskPoll: 0, 62 unrelated: 0, 63 initialPollerCount: 10, 64 minPollerCount: 1, 65 maxPollerCount: 10, 66 targetMilliUsage: 500, 67 cooldownTime: coolDownTime, 68 autoScalerEpoch: 1, 69 isDryRun: true, 70 }, 71 want: 10, 72 }, 73 { 74 name: "no utilization, scale to min", 75 args: args{ 76 noTaskPoll: 100, 77 taskPoll: 0, 78 unrelated: 0, 79 initialPollerCount: 10, 80 minPollerCount: 1, 81 maxPollerCount: 10, 82 targetMilliUsage: 500, 83 cooldownTime: coolDownTime, 84 autoScalerEpoch: 1, 85 isDryRun: false, 86 }, 87 want: 1, 88 }, 89 { 90 name: "low utilization, scale down", 91 args: args{ 92 noTaskPoll: 75, 93 taskPoll: 25, 94 unrelated: 0, 95 initialPollerCount: 10, 96 minPollerCount: 1, 97 maxPollerCount: 10, 98 targetMilliUsage: 500, 99 cooldownTime: coolDownTime, 100 autoScalerEpoch: 1, 101 isDryRun: false, 102 }, 103 want: 5, 104 }, 105 { 106 name: "over utilized, scale up", 107 args: args{ 108 noTaskPoll: 0, 109 taskPoll: 100, 110 unrelated: 0, 111 initialPollerCount: 2, 112 minPollerCount: 1, 113 maxPollerCount: 10, 114 targetMilliUsage: 500, 115 cooldownTime: coolDownTime, 116 autoScalerEpoch: 1, 117 isDryRun: false, 118 }, 119 want: 10, 120 }, 121 { 122 name: "over utilized, scale up to max", 123 args: args{ 124 noTaskPoll: 0, 125 taskPoll: 100, 126 unrelated: 0, 127 initialPollerCount: 6, 128 minPollerCount: 1, 129 maxPollerCount: 10, 130 targetMilliUsage: 500, 131 cooldownTime: coolDownTime, 132 autoScalerEpoch: 1, 133 isDryRun: false, 134 }, 135 want: 10, 136 }, 137 { 138 name: "over utilized, but wait time less than cooldown time", 139 args: args{ 140 noTaskPoll: 0, 141 taskPoll: 100, 142 unrelated: 0, 143 initialPollerCount: 6, 144 minPollerCount: 1, 145 maxPollerCount: 10, 146 targetMilliUsage: 500, 147 cooldownTime: coolDownTime, 148 autoScalerEpoch: 0, 149 isDryRun: false, 150 }, 151 want: 6, 152 }, 153 { 154 name: "disabled", 155 args: args{disabled: true}, 156 }, 157 } 158 159 for _, tt := range tests { 160 t.Run(tt.name, func(t *testing.T) { 161 autoscalerEpoch := atomic.NewUint64(0) 162 pollerScaler := newPollerScaler( 163 pollerAutoScalerOptions{ 164 Enabled: !tt.args.disabled, 165 InitCount: tt.args.initialPollerCount, 166 MinCount: tt.args.minPollerCount, 167 MaxCount: tt.args.maxPollerCount, 168 Cooldown: tt.args.cooldownTime, 169 DryRun: tt.args.isDryRun, 170 TargetUtilization: float64(tt.args.targetMilliUsage) / 1000, 171 }, 172 zaptest.NewLogger(t), 173 // hook function that collects number of iterations 174 func() { 175 autoscalerEpoch.Add(1) 176 }) 177 if tt.args.disabled { 178 assert.Nil(t, pollerScaler) 179 return 180 } 181 182 pollerScaler.Start() 183 184 // simulate concurrent polling 185 pollChan := generateRandomPollResults(tt.args.noTaskPoll, tt.args.taskPoll, tt.args.unrelated) 186 wg := &sync.WaitGroup{} 187 for i := 0; i < tt.args.maxPollerCount; i++ { 188 wg.Add(1) 189 go func() { 190 defer wg.Done() 191 for pollResult := range pollChan { 192 pollerScaler.Acquire(1) 193 pollerScaler.CollectUsage(pollResult) 194 pollerScaler.Release(1) 195 } 196 }() 197 } 198 199 assert.Eventually(t, func() bool { 200 return autoscalerEpoch.Load() == uint64(tt.args.autoScalerEpoch) 201 }, tt.args.cooldownTime+20*time.Millisecond, 10*time.Millisecond) 202 pollerScaler.Stop() 203 res := pollerScaler.GetCurrent() 204 assert.Equal(t, tt.want, int(res)) 205 }) 206 } 207 } 208 209 func Test_pollerUsageEstimator(t *testing.T) { 210 type args struct { 211 noTaskPoll, taskPoll, unrelated int 212 pollerCount int 213 } 214 tests := []struct { 215 name string 216 args args 217 want autoscaler.Usages 218 wantErr bool 219 }{ 220 { 221 name: "400 no-task, 100 task, 100 unrelated", 222 args: args{ 223 noTaskPoll: 400, 224 taskPoll: 100, 225 unrelated: 100, 226 pollerCount: 5, 227 }, 228 want: autoscaler.Usages{ 229 autoscaler.PollerUtilizationRate: 200, 230 }, 231 }, 232 { 233 name: "0 no-task, 0 task, 100 unrelated", 234 args: args{ 235 noTaskPoll: 0, 236 taskPoll: 0, 237 unrelated: 100, 238 pollerCount: 5, 239 }, 240 wantErr: true, 241 }, 242 } 243 for _, tt := range tests { 244 t.Run(tt.name, func(t *testing.T) { 245 estimator := &pollerUsageEstimator{atomicBits: atomic.NewUint64(0)} 246 pollChan := generateRandomPollResults(tt.args.noTaskPoll, tt.args.taskPoll, tt.args.unrelated) 247 wg := &sync.WaitGroup{} 248 for i := 0; i < tt.args.pollerCount; i++ { 249 wg.Add(1) 250 go func() { 251 defer wg.Done() 252 for pollResult := range pollChan { 253 estimator.CollectUsage(pollResult) 254 } 255 }() 256 } 257 wg.Wait() 258 259 res, err := estimator.Estimate() 260 if tt.wantErr { 261 assert.Error(t, err) 262 assert.Nil(t, res) 263 } else { 264 assert.NoError(t, err) 265 assert.Equal(t, tt.want, res) 266 } 267 }) 268 } 269 } 270 271 type unrelatedPolledTask struct{} 272 273 func generateRandomPollResults(noTaskPoll, taskPoll, unrelated int) <-chan interface{} { 274 var result []interface{} 275 for i := 0; i < noTaskPoll; i++ { 276 result = append(result, &activityTask{}) 277 } 278 for i := 0; i < taskPoll; i++ { 279 result = append(result, &activityTask{task: &s.PollForActivityTaskResponse{}}) 280 } 281 for i := 0; i < unrelated; i++ { 282 result = append(result, &unrelatedPolledTask{}) 283 } 284 rand.Seed(time.Now().UnixNano()) 285 rand.Shuffle(len(result), func(i, j int) { 286 result[i], result[j] = result[j], result[i] 287 }) 288 289 pollChan := make(chan interface{}, len(result)) 290 defer close(pollChan) 291 for i := range result { 292 pollChan <- result[i] 293 } 294 return pollChan 295 }