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  }