scheduler_perf: more useful errors for configuration mistakes

Before, the first error was reported, which typically was the "invalid op code"
error from the createAny operation:

    scheduler_perf.go:900: parsing test cases error: error unmarshaling JSON: while decoding JSON: cannot unmarshal {"collectMetrics":true,"count":10,"duration":"30s","namespace":"test","opcode":"createPods","podTemplatePath":"config/dra/pod-with-claim-template.yaml","steadyState":true} into any known op type: invalid opcode "createPods"; expected "createAny"

Now the opcode is determined first, then decoding into exactly the matching operation is
tried and validated. Unknown fields are an error.

In the case above, decoding a string into time.Duration failed:

    scheduler_test.go:29: parsing test cases error: error unmarshaling JSON: while decoding JSON: decoding {"collectMetrics":true,"count":10,"duration":"30s","namespace":"test","opcode":"createPods","podTemplatePath":"config/dra/pod-with-claim-template.yaml","steadyState":true} into *benchmark.createPodsOp: json: cannot unmarshal string into Go struct field createPodsOp.Duration of type time.Duration

Some typos:

    scheduler_test.go:29: parsing test cases error: error unmarshaling JSON: while decoding JSON: unknown opcode "sleeep" in {"duration":"5s","opcode":"sleeep"}

    scheduler_test.go:29: parsing test cases error: error unmarshaling JSON: while decoding JSON: decoding {"countParram":"$deletingPods","deletePodsPerSecond":50,"opcode":"createPods"} into *benchmark.createPodsOp: json: unknown field "countParram"
This commit is contained in:
Patrick Ohly 2024-09-12 12:59:10 +02:00
parent 7bbb3465e5
commit 51cafb0053
3 changed files with 37 additions and 63 deletions

View File

@ -56,9 +56,6 @@ type createAny struct {
var _ runnableOp = &createAny{} var _ runnableOp = &createAny{}
func (c *createAny) isValid(allowParameterization bool) error { func (c *createAny) isValid(allowParameterization bool) error {
if c.Opcode != createAnyOpcode {
return fmt.Errorf("invalid opcode %q; expected %q", c.Opcode, createAnyOpcode)
}
if c.TemplatePath == "" { if c.TemplatePath == "" {
return fmt.Errorf("TemplatePath must be set") return fmt.Errorf("TemplatePath must be set")
} }

View File

@ -50,9 +50,6 @@ var _ realOp = &createResourceClaimsOp{}
var _ runnableOp = &createResourceClaimsOp{} var _ runnableOp = &createResourceClaimsOp{}
func (op *createResourceClaimsOp) isValid(allowParameterization bool) error { func (op *createResourceClaimsOp) isValid(allowParameterization bool) error {
if op.Opcode != createResourceClaimsOpcode {
return fmt.Errorf("invalid opcode %q; expected %q", op.Opcode, createResourceClaimsOpcode)
}
if !isValidCount(allowParameterization, op.Count, op.CountParam) { if !isValidCount(allowParameterization, op.Count, op.CountParam) {
return fmt.Errorf("invalid Count=%d / CountParam=%q", op.Count, op.CountParam) return fmt.Errorf("invalid Count=%d / CountParam=%q", op.Count, op.CountParam)
} }
@ -139,9 +136,6 @@ var _ realOp = &createResourceDriverOp{}
var _ runnableOp = &createResourceDriverOp{} var _ runnableOp = &createResourceDriverOp{}
func (op *createResourceDriverOp) isValid(allowParameterization bool) error { func (op *createResourceDriverOp) isValid(allowParameterization bool) error {
if op.Opcode != createResourceDriverOpcode {
return fmt.Errorf("invalid opcode %q; expected %q", op.Opcode, createResourceDriverOpcode)
}
if !isValidCount(allowParameterization, op.MaxClaimsPerNode, op.MaxClaimsPerNodeParam) { if !isValidCount(allowParameterization, op.MaxClaimsPerNode, op.MaxClaimsPerNodeParam) {
return fmt.Errorf("invalid MaxClaimsPerNode=%d / MaxClaimsPerNodeParam=%q", op.MaxClaimsPerNode, op.MaxClaimsPerNodeParam) return fmt.Errorf("invalid MaxClaimsPerNode=%d / MaxClaimsPerNodeParam=%q", op.MaxClaimsPerNode, op.MaxClaimsPerNodeParam)
} }

View File

@ -17,6 +17,7 @@ limitations under the License.
package benchmark package benchmark
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
@ -404,36 +405,43 @@ type op struct {
// UnmarshalJSON is a custom unmarshaler for the op struct since we don't know // UnmarshalJSON is a custom unmarshaler for the op struct since we don't know
// which op we're decoding at runtime. // which op we're decoding at runtime.
func (op *op) UnmarshalJSON(b []byte) error { func (op *op) UnmarshalJSON(b []byte) error {
possibleOps := []realOp{ possibleOps := map[operationCode]realOp{
&createAny{}, createAnyOpcode: &createAny{},
&createNodesOp{}, createNodesOpcode: &createNodesOp{},
&createNamespacesOp{}, createNamespacesOpcode: &createNamespacesOp{},
&createPodsOp{}, createPodsOpcode: &createPodsOp{},
&createPodSetsOp{}, createPodSetsOpcode: &createPodSetsOp{},
&deletePodsOp{}, deletePodsOpcode: &deletePodsOp{},
&createResourceClaimsOp{}, createResourceClaimsOpcode: &createResourceClaimsOp{},
&createResourceDriverOp{}, createResourceDriverOpcode: &createResourceDriverOp{},
&churnOp{}, churnOpcode: &churnOp{},
&barrierOp{}, barrierOpcode: &barrierOp{},
&sleepOp{}, sleepOpcode: &sleepOp{},
&startCollectingMetricsOp{}, startCollectingMetricsOpcode: &startCollectingMetricsOp{},
&stopCollectingMetricsOp{}, stopCollectingMetricsOpcode: &stopCollectingMetricsOp{},
// TODO(#94601): add a delete nodes op to simulate scaling behaviour? // TODO(#94601): add a delete nodes op to simulate scaling behaviour?
} }
var firstError error // First determine the opcode using lenient decoding (= ignore extra fields).
for _, possibleOp := range possibleOps { var possibleOp struct {
if err := json.Unmarshal(b, possibleOp); err == nil { Opcode operationCode
if err2 := possibleOp.isValid(true); err2 == nil {
op.realOp = possibleOp
return nil
} else if firstError == nil {
// Don't return an error yet. Even though this op is invalid, it may
// still match other possible ops.
firstError = err2
}
}
} }
return fmt.Errorf("cannot unmarshal %s into any known op type: %w", string(b), firstError) if err := json.Unmarshal(b, &possibleOp); err != nil {
return fmt.Errorf("decoding opcode from %s: %w", string(b), err)
}
realOp, ok := possibleOps[possibleOp.Opcode]
if !ok {
return fmt.Errorf("unknown opcode %q in %s", possibleOp.Opcode, string(b))
}
decoder := json.NewDecoder(bytes.NewReader(b))
decoder.DisallowUnknownFields()
if err := decoder.Decode(realOp); err != nil {
return fmt.Errorf("decoding %s into %T: %w", string(b), realOp, err)
}
if err := realOp.isValid(true); err != nil {
return fmt.Errorf("%s not valid for %T: %w", string(b), realOp, err)
}
op.realOp = realOp
return nil
} }
// realOp is an interface that is implemented by different structs. To evaluate // realOp is an interface that is implemented by different structs. To evaluate
@ -441,6 +449,8 @@ func (op *op) UnmarshalJSON(b []byte) error {
type realOp interface { type realOp interface {
// isValid verifies the validity of the op args such as node/pod count. Note // isValid verifies the validity of the op args such as node/pod count. Note
// that we don't catch undefined parameters at this stage. // that we don't catch undefined parameters at this stage.
//
// This returns errInvalidOp if the configured operation does not match.
isValid(allowParameterization bool) error isValid(allowParameterization bool) error
// collectsMetrics checks if the op collects metrics. // collectsMetrics checks if the op collects metrics.
collectsMetrics() bool collectsMetrics() bool
@ -497,9 +507,6 @@ type createNodesOp struct {
} }
func (cno *createNodesOp) isValid(allowParameterization bool) error { func (cno *createNodesOp) isValid(allowParameterization bool) error {
if cno.Opcode != createNodesOpcode {
return fmt.Errorf("invalid opcode %q", cno.Opcode)
}
if !isValidCount(allowParameterization, cno.Count, cno.CountParam) { if !isValidCount(allowParameterization, cno.Count, cno.CountParam) {
return fmt.Errorf("invalid Count=%d / CountParam=%q", cno.Count, cno.CountParam) return fmt.Errorf("invalid Count=%d / CountParam=%q", cno.Count, cno.CountParam)
} }
@ -538,9 +545,6 @@ type createNamespacesOp struct {
} }
func (cmo *createNamespacesOp) isValid(allowParameterization bool) error { func (cmo *createNamespacesOp) isValid(allowParameterization bool) error {
if cmo.Opcode != createNamespacesOpcode {
return fmt.Errorf("invalid opcode %q", cmo.Opcode)
}
if !isValidCount(allowParameterization, cmo.Count, cmo.CountParam) { if !isValidCount(allowParameterization, cmo.Count, cmo.CountParam) {
return fmt.Errorf("invalid Count=%d / CountParam=%q", cmo.Count, cmo.CountParam) return fmt.Errorf("invalid Count=%d / CountParam=%q", cmo.Count, cmo.CountParam)
} }
@ -595,9 +599,6 @@ type createPodsOp struct {
} }
func (cpo *createPodsOp) isValid(allowParameterization bool) error { func (cpo *createPodsOp) isValid(allowParameterization bool) error {
if cpo.Opcode != createPodsOpcode {
return fmt.Errorf("invalid opcode %q; expected %q", cpo.Opcode, createPodsOpcode)
}
if !isValidCount(allowParameterization, cpo.Count, cpo.CountParam) { if !isValidCount(allowParameterization, cpo.Count, cpo.CountParam) {
return fmt.Errorf("invalid Count=%d / CountParam=%q", cpo.Count, cpo.CountParam) return fmt.Errorf("invalid Count=%d / CountParam=%q", cpo.Count, cpo.CountParam)
} }
@ -641,9 +642,6 @@ type createPodSetsOp struct {
} }
func (cpso *createPodSetsOp) isValid(allowParameterization bool) error { func (cpso *createPodSetsOp) isValid(allowParameterization bool) error {
if cpso.Opcode != createPodSetsOpcode {
return fmt.Errorf("invalid opcode %q; expected %q", cpso.Opcode, createPodSetsOpcode)
}
if !isValidCount(allowParameterization, cpso.Count, cpso.CountParam) { if !isValidCount(allowParameterization, cpso.Count, cpso.CountParam) {
return fmt.Errorf("invalid Count=%d / CountParam=%q", cpso.Count, cpso.CountParam) return fmt.Errorf("invalid Count=%d / CountParam=%q", cpso.Count, cpso.CountParam)
} }
@ -729,9 +727,6 @@ type churnOp struct {
} }
func (co *churnOp) isValid(_ bool) error { func (co *churnOp) isValid(_ bool) error {
if co.Opcode != churnOpcode {
return fmt.Errorf("invalid opcode %q", co.Opcode)
}
if co.Mode != Recreate && co.Mode != Create { if co.Mode != Recreate && co.Mode != Create {
return fmt.Errorf("invalid mode: %v. must be one of %v", co.Mode, []string{Recreate, Create}) return fmt.Errorf("invalid mode: %v. must be one of %v", co.Mode, []string{Recreate, Create})
} }
@ -767,9 +762,6 @@ type barrierOp struct {
} }
func (bo *barrierOp) isValid(allowParameterization bool) error { func (bo *barrierOp) isValid(allowParameterization bool) error {
if bo.Opcode != barrierOpcode {
return fmt.Errorf("invalid opcode %q", bo.Opcode)
}
return nil return nil
} }
@ -805,9 +797,6 @@ func (so *sleepOp) UnmarshalJSON(data []byte) (err error) {
} }
func (so *sleepOp) isValid(_ bool) error { func (so *sleepOp) isValid(_ bool) error {
if so.Opcode != sleepOpcode {
return fmt.Errorf("invalid opcode %q; expected %q", so.Opcode, sleepOpcode)
}
return nil return nil
} }
@ -831,9 +820,6 @@ type startCollectingMetricsOp struct {
} }
func (scm *startCollectingMetricsOp) isValid(_ bool) error { func (scm *startCollectingMetricsOp) isValid(_ bool) error {
if scm.Opcode != startCollectingMetricsOpcode {
return fmt.Errorf("invalid opcode %q; expected %q", scm.Opcode, startCollectingMetricsOpcode)
}
if len(scm.Namespaces) == 0 { if len(scm.Namespaces) == 0 {
return fmt.Errorf("namespaces cannot be empty") return fmt.Errorf("namespaces cannot be empty")
} }
@ -857,9 +843,6 @@ type stopCollectingMetricsOp struct {
} }
func (scm *stopCollectingMetricsOp) isValid(_ bool) error { func (scm *stopCollectingMetricsOp) isValid(_ bool) error {
if scm.Opcode != stopCollectingMetricsOpcode {
return fmt.Errorf("invalid opcode %q; expected %q", scm.Opcode, stopCollectingMetricsOpcode)
}
return nil return nil
} }