Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New config framework for the different schedulers #913

Merged
merged 40 commits into from
Mar 13, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
029db7c
Add the config framework for the different schedulers
na-- Jan 29, 2019
15a4128
Fix minor issues linting and test issues
na-- Jan 30, 2019
4a1c941
Break apart the scheduler config type initializations
na-- Jan 31, 2019
b50ff18
Clean up some commented-out code
na-- Jan 31, 2019
4821c50
Update and test the new scheduler configurations
na-- Feb 25, 2019
99b0994
Merge branch 'master' into scheduler-config-wip
na-- Feb 25, 2019
ad3f6f4
Fix linter issues
na-- Feb 25, 2019
62c910c
Remove the debug output
na-- Feb 26, 2019
8113438
Clean up and add more tests
na-- Feb 26, 2019
ce1c967
Restore the Split() method
na-- Feb 26, 2019
2451ead
Add copyright notices
na-- Feb 28, 2019
507c5c5
Improve the execution config generation from shortcuts
na-- Mar 1, 2019
54eb412
Warn if the user specifies only the new execution option
na-- Mar 1, 2019
9cab261
Improve the scheduler config tests
na-- Mar 1, 2019
977b10e
Refactor some CLI configs and add some basic configuration tests
na-- Mar 1, 2019
d1943ec
Improve the readability of the config test cases
na-- Mar 1, 2019
effbf30
Work arround some strage appveyor environment variable issues
na-- Mar 5, 2019
2120a12
Fix minor issues with the structure of the new config tests
na-- Mar 5, 2019
2c1adf3
Move TestConfigConsolidation() and its helpers to a separate file
na-- Mar 5, 2019
62ff4aa
Fix a typo in a comment
na-- Mar 5, 2019
704dcc3
Fix the duplicate config file path CLI flag
na-- Mar 6, 2019
a7d0b6f
Preserve the trailing spaces in the k6 ASCII banner
na-- Mar 6, 2019
b86d5cd
Automatically create the config file's parent folder
na-- Mar 6, 2019
c8c902a
Add a missing copyright notice
na-- Mar 6, 2019
13e506d
Fix the newline before the banner
na-- Mar 7, 2019
5d8b354
Move the root CLI persistent flags to their own flagset
na-- Mar 7, 2019
588ef66
Fix an env.var/CLI flag conflict and issues with CLI usage messages
na-- Mar 7, 2019
e76972a
Improve the CLI flags test framework in the config consolidation test
na-- Mar 7, 2019
295abd2
Add support for testing the JSON config in the consolidation as well
na-- Mar 7, 2019
9eb4379
Improve the comments on the funcitons that deal with the file config
na-- Mar 7, 2019
2de0923
Add tests and fix a minor bug
na-- Mar 8, 2019
bf84bc0
Merge pull request #935 from loadimpact/config-painful-testing
na-- Mar 8, 2019
93e4eae
Merge branch 'master' into scheduler-config-wip
na-- Mar 8, 2019
227f22c
Merge branch 'master' into scheduler-config-wip
na-- Mar 11, 2019
079282a
Fix or suppress linter errors
na-- Mar 11, 2019
238e224
Use a custom error type for execution conflict errors in the config
na-- Mar 11, 2019
6d19e61
Improve config consolidation, default values and tests
na-- Mar 12, 2019
9404af6
Extend config validation to the cloud and archive subcommands
na-- Mar 12, 2019
2498121
Silence a linter warning... for now!
na-- Mar 12, 2019
d88be34
Override the execution setting when execution shortcuts are used
na-- Mar 13, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import (
"syscall"
"time"

"github.com/davecgh/go-spew/spew"

"github.com/loadimpact/k6/api"
"github.com/loadimpact/k6/core"
"github.com/loadimpact/k6/core/local"
Expand All @@ -61,6 +63,7 @@ const (
teardownTimeoutErrorCode = 101
genericTimeoutErrorCode = 102
genericEngineErrorCode = 103
invalidOptionsErrorCode = 104
)

var (
Expand Down Expand Up @@ -165,6 +168,18 @@ a commandline interface for interacting with it.`,
conf.Duration = types.NullDuration{}
}

//TODO: move a bunch of the logic above to a config "constructor" and to the Validate() method
spew.Dump(conf.Options.Execution)
if errList := conf.Validate(); len(errList) != 0 {
errMsg := []string{
fmt.Sprintf("There were %d errors with the script options:", len(errList)),
}
for _, err := range errList {
errMsg = append(errMsg, fmt.Sprintf("\t- %s", err.Error()))
}
return ExitCode{errors.New(strings.Join(errMsg, "\n")), invalidOptionsErrorCode}
}

// If summary trend stats are defined, update the UI to reflect them
if len(conf.SummaryTrendStats) > 0 {
ui.UpdateTrendColumns(conf.SummaryTrendStats)
Expand Down
2 changes: 1 addition & 1 deletion converter/har/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func Convert(h HAR, options lib.Options, minSleep, maxSleep uint, enableChecks b
}

fprint(w, "\nexport let options = {\n")
options.ForEachValid("json", func(key string, val interface{}) {
options.ForEachSpecified("json", func(key string, val interface{}) {
if valJSON, err := json.MarshalIndent(val, " ", " "); err != nil {
convertErr = err
} else {
Expand Down
2 changes: 1 addition & 1 deletion js/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ func (b *Bundle) Instantiate() (bi *BundleInstance, instErr error) {
} else {
jsOptionsObj = jsOptions.ToObject(rt)
}
b.Options.ForEachValid("json", func(key string, val interface{}) {
b.Options.ForEachSpecified("json", func(key string, val interface{}) {
if err := jsOptionsObj.Set(key, val); err != nil {
instErr = err
}
Expand Down
20 changes: 18 additions & 2 deletions lib/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"net"
"reflect"

"github.com/loadimpact/k6/lib/scheduler"
"github.com/loadimpact/k6/lib/types"
"github.com/loadimpact/k6/stats"
"github.com/pkg/errors"
Expand Down Expand Up @@ -194,6 +195,8 @@ type Options struct {
Iterations null.Int `json:"iterations" envconfig:"iterations"`
Stages []Stage `json:"stages" envconfig:"stages"`

Execution scheduler.ConfigMap `json:"execution" envconfig:"execution"`

// Timeouts for the setup() and teardown() functions
SetupTimeout types.NullDuration `json:"setupTimeout" envconfig:"setup_timeout"`
TeardownTimeout types.NullDuration `json:"teardownTimeout" envconfig:"teardown_timeout"`
Expand Down Expand Up @@ -307,6 +310,12 @@ func (o Options) Apply(opts Options) Options {
}
}
}

//TODO: handle o.Execution overwriting by plain vus/iterations/duration/stages options
if len(opts.Execution) > 0 {
o.Execution = opts.Execution
}

if opts.SetupTimeout.Valid {
o.SetupTimeout = opts.SetupTimeout
}
Expand Down Expand Up @@ -395,10 +404,17 @@ func (o Options) Apply(opts Options) Options {
return o
}

// ForEachValid enumerates all struct fields and calls the supplied function with each
// Validate checks if all of the specified options make sense
func (o Options) Validate() []error {
//TODO: validate all of the other options... that we should have already been validating...
//TODO: maybe integrate an external validation lib: https://github.com/avelino/awesome-go#validation
return o.Execution.Validate()
}

// ForEachSpecified enumerates all struct fields and calls the supplied function with each
// element that is valid. It panics for any unfamiliar or unexpected fields, so make sure
// new fields in Options are accounted for.
func (o Options) ForEachValid(structTag string, callback func(key string, value interface{})) {
func (o Options) ForEachSpecified(structTag string, callback func(key string, value interface{})) {
structType := reflect.TypeOf(o)
structVal := reflect.ValueOf(o)
for i := 0; i < structType.NumField(); i++ {
Expand Down
95 changes: 95 additions & 0 deletions lib/scheduler/base_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package scheduler

import (
"fmt"
"time"

"github.com/loadimpact/k6/lib/types"
null "gopkg.in/guregu/null.v3"
)

const minPercentage = 0.01

// The maximum time k6 will wait after an iteration is supposed to be done
const maxIterationTimeout = 300 * time.Second

// BaseConfig contains the common config fields for all schedulers
type BaseConfig struct {
Name string `json:"-"` // set via the JS object key
Type string `json:"type"`
StartTime types.NullDuration `json:"startTime"`
Interruptible null.Bool `json:"interruptible"`
IterationTimeout types.NullDuration `json:"iterationTimeout"`
Env map[string]string `json:"env"`
Exec string `json:"exec"` // function name, externally validated
Percentage float64 `json:"-"` // 100, unless Split() was called

// Future extensions: tags, distribution, others?
}

// Make sure we implement the Config interface, even with the BaseConfig!
var _ Config = &BaseConfig{}

// NewBaseConfig returns a default base config with the default values
func NewBaseConfig(name, configType string, interruptible bool) BaseConfig {
return BaseConfig{
Name: name,
Type: configType,
Interruptible: null.NewBool(interruptible, false),
IterationTimeout: types.NewNullDuration(30*time.Second, false),
Percentage: 100,
}
}

// Validate checks some basic things like present name, type, and a positive start time
func (bc BaseConfig) Validate() (errors []error) {
// Some just-in-case checks, since those things are likely checked in other places or
// even assigned by us:
if bc.Name == "" {
errors = append(errors, fmt.Errorf("scheduler name shouldn't be empty"))
}
if bc.Type == "" {
errors = append(errors, fmt.Errorf("missing or empty type field"))
}
if bc.Percentage < minPercentage || bc.Percentage > 100 {
errors = append(errors, fmt.Errorf(
"percentage should be between %f and 100, but is %f", minPercentage, bc.Percentage,
))
}
// The actually reasonable checks:
if bc.StartTime.Valid && bc.StartTime.Duration < 0 {
errors = append(errors, fmt.Errorf("scheduler start time should be positive"))
}
iterTimeout := time.Duration(bc.IterationTimeout.Duration)
if iterTimeout < 0 || iterTimeout > maxIterationTimeout {
errors = append(errors, fmt.Errorf(
"the iteration timeout should be between 0 and %s, but is %s", maxIterationTimeout, iterTimeout,
))
}
return errors
}

// GetBaseConfig just returns itself
func (bc BaseConfig) GetBaseConfig() BaseConfig {
return bc
}

// CopyWithPercentage is a helper function that just sets the percentage to
// the specified amount.
func (bc BaseConfig) CopyWithPercentage(percentage float64) *BaseConfig {
c := bc
c.Percentage = percentage
return &c
}

// Split splits the BaseConfig with the accurate percentages
func (bc BaseConfig) Split(percentages []float64) ([]Config, error) {
if err := checkPercentagesSum(percentages); err != nil {
return nil, err
}
configs := make([]Config, len(percentages))
for i, p := range percentages {
configs[i] = bc.CopyWithPercentage(p)
}
return configs, nil
}
92 changes: 92 additions & 0 deletions lib/scheduler/configmap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package scheduler

import (
"encoding/json"
"fmt"
)

// GetParsedConfig returns a struct instance corresponding to the supplied
// config type. It will be fully initialized - with both the default values of
// the type, as well as with whatever the user had specified in the JSON
func GetParsedConfig(name, configType string, rawJSON []byte) (result Config, err error) {
switch configType {
case constantLoopingVUsType:
config := NewConstantLoopingVUsConfig(name)
err = json.Unmarshal(rawJSON, &config)
result = config
case variableLoopingVUsType:
config := NewVariableLoopingVUsConfig(name)
err = json.Unmarshal(rawJSON, &config)
result = config
case sharedIterationsType:
config := NewSharedIterationsConfig(name)
err = json.Unmarshal(rawJSON, &config)
result = config
case perVUIterationsType:
config := NewPerVUIterationsConfig(name)
err = json.Unmarshal(rawJSON, &config)
result = config
case constantArrivalRateType:
config := NewConstantArrivalRateConfig(name)
err = json.Unmarshal(rawJSON, &config)
result = config
case variableArrivalRateType:
config := NewVariableArrivalRateConfig(name)
err = json.Unmarshal(rawJSON, &config)
result = config
default:
return nil, fmt.Errorf("unknown execution scheduler type '%s'", configType)
}
return
}
na-- marked this conversation as resolved.
Show resolved Hide resolved

// ConfigMap can contain mixed scheduler config types
type ConfigMap map[string]Config

// UnmarshalJSON implements the json.Unmarshaler interface in a two-step manner,
// creating the correct type of configs based on the `type` property.
func (scs *ConfigMap) UnmarshalJSON(b []byte) error {
var protoConfigs map[string]protoConfig
if err := json.Unmarshal(b, &protoConfigs); err != nil {
return err
}

result := make(ConfigMap, len(protoConfigs))
for k, v := range protoConfigs {
if v.Type == "" {
return fmt.Errorf("execution config '%s' doesn't have a type value", k)
}
config, err := GetParsedConfig(k, v.Type, v.rawJSON)
if err != nil {
return err
}
result[k] = config
}

*scs = result

return nil
}

// Validate checks if all of the specified scheduler options make sense
func (scs ConfigMap) Validate() (errors []error) {
for name, scheduler := range scs {
if schedErr := scheduler.Validate(); len(schedErr) != 0 {
errors = append(errors,
fmt.Errorf("scheduler %s has errors: %s", name, concatErrors(schedErr, ", ")))
}
}
return errors
}

type protoConfig struct {
BaseConfig
rawJSON json.RawMessage
}

// UnmarshalJSON just reads unmarshals the base config (to get the type), but it also
// stores the unprocessed JSON so we can parse the full config in the next step
func (pc *protoConfig) UnmarshalJSON(b []byte) error {
*pc = protoConfig{BaseConfig{}, b}
return json.Unmarshal(b, &pc.BaseConfig)
}
73 changes: 73 additions & 0 deletions lib/scheduler/constant_arrival_rate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package scheduler

import (
"fmt"
"time"

"github.com/loadimpact/k6/lib/types"
null "gopkg.in/guregu/null.v3"
)

const constantArrivalRateType = "constant-arrival-rate"

// ConstantArrivalRateConfig stores config for the constant arrival-rate scheduler
type ConstantArrivalRateConfig struct {
BaseConfig
Rate null.Int `json:"rate"`
TimeUnit types.NullDuration `json:"timeUnit"` //TODO: rename to something else?
na-- marked this conversation as resolved.
Show resolved Hide resolved
Duration types.NullDuration `json:"duration"`

// Initialize `PreAllocatedVUs` numeber of VUs, and if more than that are needed,
// they will be dynamically allocated, until `MaxVUs` is reached, which is an
// absolutely hard limit on the number of VUs the scheduler will use
PreAllocatedVUs null.Int `json:"preAllocatedVUs"`
MaxVUs null.Int `json:"maxVUs"`
}

// NewConstantArrivalRateConfig returns a ConstantArrivalRateConfig with default values
func NewConstantArrivalRateConfig(name string) ConstantArrivalRateConfig {
return ConstantArrivalRateConfig{
BaseConfig: NewBaseConfig(name, constantArrivalRateType, false),
TimeUnit: types.NewNullDuration(1*time.Second, false),
//TODO: set some default values for PreAllocatedVUs and MaxVUs?
}
}

// Make sure we implement the Config interface
var _ Config = &ConstantArrivalRateConfig{}

// Validate makes sure all options are configured and valid
func (carc ConstantArrivalRateConfig) Validate() []error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am pretty sure we will have to either use a validation library or write ourself one. Even if it means adding code generation. Maybe we can leave it until we actually redo the configuration but ... I don't like code like this that will most definitely miss something

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at several validation libraries, and none of them seemed like a big improvement. The biggest obstacle were the nullable types we use - we'd have to make a bunch of adapters to support them properly, which isn't very worth it...

I think we can consolidate some of the repeating checks we'll have into helper functions, or reuse some of the validator functions (but not struct tags and reflection-based stuff) from some library for things like IP addresses, urls, etc., but I'm not sure if will make sense to do more than that. Code generation for sure feels an extreme response to this problem...

And I doubt any other solution will allow us the current flexibility. Consider this validation code below:

if !carc.Rate.Valid {
	errors = append(errors, fmt.Errorf("the iteration rate isn't specified"))
} else if carc.Rate.Int64 <= 0 {
	errors = append(errors, fmt.Errorf("the iteration rate should be positive"))
}

Notice how only one of the errors will be specified, since it doesn't make sense to show both the not specified and should be positive errors at the same time. Not sure how libraries will handle that correctly. Also, we have the flexibility to write very user-friendly and specific error messages. And we can have subtly different validations between the different configs, for example the validation for the variable arrival-rate doesn't require the value and allows it to be 0, but not negative.

I'm sure we can create something with all of the flexibility, but with less boilerplate, it's just likely going to be very difficult, i.e. I'm not sure if it'd end up worth it.

errors := carc.BaseConfig.Validate()
if !carc.Rate.Valid {
errors = append(errors, fmt.Errorf("the iteration rate isn't specified"))
} else if carc.Rate.Int64 <= 0 {
errors = append(errors, fmt.Errorf("the iteration rate should be positive"))
}

if time.Duration(carc.TimeUnit.Duration) < 0 {
errors = append(errors, fmt.Errorf("the timeUnit should be more than 0"))
}

if !carc.Duration.Valid {
errors = append(errors, fmt.Errorf("the duration is unspecified"))
} else if time.Duration(carc.Duration.Duration) < minDuration {
errors = append(errors, fmt.Errorf(
"the duration should be at least %s, but is %s", minDuration, carc.Duration,
))
}

if !carc.PreAllocatedVUs.Valid {
errors = append(errors, fmt.Errorf("the number of preAllocatedVUs isn't specified"))
} else if carc.PreAllocatedVUs.Int64 < 0 {
errors = append(errors, fmt.Errorf("the number of preAllocatedVUs shouldn't be negative"))
}

if !carc.MaxVUs.Valid {
errors = append(errors, fmt.Errorf("the number of maxVUs isn't specified"))
} else if carc.MaxVUs.Int64 < carc.PreAllocatedVUs.Int64 {
errors = append(errors, fmt.Errorf("maxVUs shouldn't be less than preAllocatedVUs"))
}

return errors
}
Loading