diff --git a/lib/executor/helpers.go b/lib/executor/helpers.go index 701d8a3e10a..80699407dab 100644 --- a/lib/executor/helpers.go +++ b/lib/executor/helpers.go @@ -16,6 +16,12 @@ import ( "go.k6.io/k6/ui/pb" ) +const ( + // maxConcurrentVUs is an arbitrary limit for sanity checks. + // It prevents running an exaggeratedly large number of concurrent VUs which may lead to an out-of-memory. + maxConcurrentVUs int = 100_000_000 +) + func sumStagesDuration(stages []Stage) (result time.Duration) { for _, s := range stages { result += s.Duration.TimeDuration() @@ -33,6 +39,27 @@ func getStagesUnscaledMaxTarget(unscaledStartValue int64, stages []Stage) int64 return max } +// validateTargetShifts validates the VU Target shifts. +// It will append an error for any VU target that is larger than the maximum value allowed. +// Each Stage needs a Target value. The stages array can be empty. The Targes could be negative. +func validateTargetShifts(startVUs int64, stages []Stage) []error { + var errors []error + + if startVUs > int64(maxConcurrentVUs) { + errors = append(errors, fmt.Errorf( + "the startVUs exceed max limit of %d", maxConcurrentVUs)) + } + + for i := 0; i < len(stages); i++ { + if stages[i].Target.Int64 > int64(maxConcurrentVUs) { + errors = append(errors, fmt.Errorf( + "target for stage %d exceeds max limit of %d", i+1, maxConcurrentVUs)) + } + } + + return errors +} + // A helper function to avoid code duplication func validateStages(stages []Stage) []error { var errors []error diff --git a/lib/executor/ramping_vus.go b/lib/executor/ramping_vus.go index 58afcfe8c64..9acdc4e1773 100644 --- a/lib/executor/ramping_vus.go +++ b/lib/executor/ramping_vus.go @@ -86,7 +86,10 @@ func (vlvc RampingVUsConfig) Validate() []error { errors = append(errors, fmt.Errorf("either startVUs or one of the stages' target values must be greater than 0")) } - return append(errors, validateStages(vlvc.Stages)...) + errors = append(errors, validateStages(vlvc.Stages)...) + errors = append(errors, validateTargetShifts(vlvc.StartVUs.Int64, vlvc.Stages)...) + + return errors } // getRawExecutionSteps calculates and returns as execution steps the number of diff --git a/lib/executor/ramping_vus_test.go b/lib/executor/ramping_vus_test.go index 522aa220865..a8a0ca1334e 100644 --- a/lib/executor/ramping_vus_test.go +++ b/lib/executor/ramping_vus_test.go @@ -36,6 +36,51 @@ func TestRampingVUsConfigValidation(t *testing.T) { errs = c.Validate() require.NotEmpty(t, errs) assert.Contains(t, errs[0].Error(), "greater than 0") + + const maxConcurrentVUs = 100_000_000 + + t.Run("If startVUs are larger than maxConcurrentVUs, the validation should return an error", func(t *testing.T) { + t.Parallel() + + c = NewRampingVUsConfig("stage") + c.StartVUs = null.IntFrom(maxConcurrentVUs + 1) + c.Stages = []Stage{ + {Target: null.IntFrom(0), Duration: types.NullDurationFrom(1 * time.Second)}, + } + + errs = c.Validate() + require.NotEmpty(t, errs) + assert.Contains(t, errs[0].Error(), "the startVUs exceed max limit of") + }) + + t.Run("For multiple VU values larger than maxConcurrentVUs, multiple errors are returned", func(t *testing.T) { + t.Parallel() + + c = NewRampingVUsConfig("stage") + c.StartVUs = null.IntFrom(maxConcurrentVUs + 1) + c.Stages = []Stage{ + {Target: null.IntFrom(maxConcurrentVUs + 2), Duration: types.NullDurationFrom(1 * time.Second)}, + } + + errs = c.Validate() + require.Equal(t, 2, len(errs)) + assert.Contains(t, errs[0].Error(), "the startVUs exceed max limit of") + + assert.Contains(t, errs[1].Error(), "target for stage 1 exceeds max limit of") + }) + + t.Run("VU values below maxConcurrentVUs will pass validation", func(t *testing.T) { + t.Parallel() + + c = NewRampingVUsConfig("stage") + c.StartVUs = null.IntFrom(0) + c.Stages = []Stage{ + {Target: null.IntFrom(maxConcurrentVUs - 1), Duration: types.NullDurationFrom(1 * time.Second)}, + } + + errs = c.Validate() + require.Empty(t, errs) + }) } func TestRampingVUsRun(t *testing.T) {