diff --git a/pkg/scheduler/framework/status.go b/pkg/scheduler/framework/status.go new file mode 100644 index 000000000..5e63d67a9 --- /dev/null +++ b/pkg/scheduler/framework/status.go @@ -0,0 +1,137 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ + +package framework + +import "strings" + +// StatusCode is the status code of a Status, returned by a plugin. +type StatusCode int + +// Pre-defined status codes. +const ( + // Success signals that a plugin has completed its run successfully. + // Note that a nil *Status is also considered as a Success Status. + Success StatusCode = iota + // internalError signals that a plugin has encountered an internal error. + // Note that this status code is NOT exported; to return an internalError status, use the + // FromError() call. + internalError + // ClusterUnschedulable signals that a plugin has found that a placement should not be bound + // to a specific cluster. + ClusterUnschedulable + // Skip signals that no action is needed for the plugin to take at the stage. + // If this is returned by a plugin at the Pre- stages (PreFilter or PreScore), the associated + // plugin will be skipped at the following stages (Filter or Score) as well. This helps + // reduce the overhead of having to repeatedly call a plugin that is not needed for every + // cluster in the Filter or Score stage. + Skip +) + +var statusCodeNames = []string{"Success", "InternalError", "ClusterUnschedulable", "Skip"} + +// Name returns the name of a status code. +func (sc StatusCode) Name() string { + return statusCodeNames[sc] +} + +// Status is the result yielded by a plugin. +type Status struct { + // statusCode is the status code of a Status. + statusCode StatusCode + // The reasons behind a Status; this should be empty if the Status is of the status code + // Success. + reasons []string + // The error associated with a Status; this is only set when the Status is of the status code + // internalError. + err error + // The name of the plugin which returns the Status. + sourcePlugin string +} + +// code returns the status code of a Status. +func (s *Status) code() StatusCode { + if s == nil { + return Success + } + return s.statusCode +} + +// IsSuccess returns if a Status is of the status code Success. +func (s *Status) IsSuccess() bool { + return s.code() == Success +} + +// IsInternalError returns if a Status is of the status code interalError. +func (s *Status) IsInteralError() bool { + return s.code() == internalError +} + +// IsPreSkip returns if a Status is of the status code Skip. +func (s *Status) IsPreSkip() bool { + return s.code() == Skip +} + +// IsClusterUnschedulable returns if a Status is of the status code ClusterUnschedulable. +func (s *Status) IsClusterUnschedulable() bool { + return s.code() == ClusterUnschedulable +} + +// Reasons returns the reasons of a Status. +func (s *Status) Reasons() []string { + if s == nil { + return []string{} + } + return s.reasons +} + +// SourcePlugin returns the source plugin associated with a Status. +func (s *Status) SourcePlugin() string { + if s == nil { + return "" + } + return s.sourcePlugin +} + +// InternalError returns the error associated with a Status. +func (s *Status) InternalError() error { + if s == nil { + return nil + } + return s.err +} + +// String returns the description of a Status. +func (s *Status) String() string { + if s == nil { + return s.code().Name() + } + desc := []string{s.code().Name()} + if s.err != nil { + desc = append(desc, s.err.Error()) + } + desc = append(desc, s.reasons...) + return strings.Join(desc, ", ") +} + +// NewNonErrorStatus returns a Status with a non-error status code. +// To return a Status of the internalError status code, use FromError() instead. +func NewNonErrorStatus(code StatusCode, sourcePlugin string, reasons ...string) *Status { + return &Status{ + statusCode: code, + reasons: reasons, + sourcePlugin: sourcePlugin, + } +} + +// FromError returns a Status from an error. +func FromError(err error, sourcePlugin string, reasons ...string) *Status { + return &Status{ + statusCode: internalError, + reasons: reasons, + err: err, + sourcePlugin: sourcePlugin, + } +} diff --git a/pkg/scheduler/framework/status_test.go b/pkg/scheduler/framework/status_test.go new file mode 100644 index 000000000..8e50d6f20 --- /dev/null +++ b/pkg/scheduler/framework/status_test.go @@ -0,0 +1,141 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ + +package framework + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +const ( + dummyPlugin = "dummyPlugin" +) + +var ( + dummyReasons = []string{"reason1", "reason2"} +) + +func TestNonNilStatusMethods(t *testing.T) { + testCases := []struct { + name string + statusCode StatusCode + reasons []string + err error + sourcePlugin string + desc string + }{ + { + name: "status success", + statusCode: Success, + reasons: []string{}, + sourcePlugin: dummyPlugin, + }, + { + name: "status error", + statusCode: internalError, + err: fmt.Errorf("an unexpected error has occurred"), + reasons: dummyReasons, + sourcePlugin: dummyPlugin, + }, + { + name: "status unschedulable", + statusCode: ClusterUnschedulable, + reasons: dummyReasons, + sourcePlugin: dummyPlugin, + }, + { + name: "status preskip", + statusCode: Skip, + reasons: dummyReasons, + sourcePlugin: dummyPlugin, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var status *Status + if tc.err != nil { + status = FromError(tc.err, tc.sourcePlugin, tc.reasons...) + } else { + status = NewNonErrorStatus(tc.statusCode, tc.sourcePlugin, tc.reasons...) + } + + wantCheckOutputs := make([]bool, len(statusCodeNames)) + wantCheckOutputs[tc.statusCode] = true + checkFuncs := []func() bool{ + status.IsSuccess, + status.IsInteralError, + status.IsClusterUnschedulable, + status.IsPreSkip, + } + for idx, checkFunc := range checkFuncs { + if wantCheckOutputs[idx] != checkFunc() { + t.Fatalf("check function for %s = %t, want %t", statusCodeNames[idx], checkFunc(), wantCheckOutputs[idx]) + } + } + + if !cmp.Equal(status.Reasons(), tc.reasons) { + t.Fatalf("Reasons() = %v, want %v", status.Reasons(), tc.reasons) + } + + if !cmp.Equal(status.SourcePlugin(), tc.sourcePlugin) { + t.Fatalf("SourcePlugin() = %s, want %s", status.SourcePlugin(), tc.sourcePlugin) + } + + if !cmp.Equal(status.InternalError(), tc.err, cmpopts.EquateErrors()) { + t.Fatalf("InternalError() = %v, want %v", status.InternalError(), tc.err) + } + + descElems := []string{statusCodeNames[tc.statusCode]} + if tc.err != nil { + descElems = append(descElems, tc.err.Error()) + } + descElems = append(descElems, tc.reasons...) + wantDesc := strings.Join(descElems, ", ") + if !cmp.Equal(status.String(), wantDesc) { + t.Fatalf("String() = %s, want %s", status.String(), wantDesc) + } + }) + } +} + +func TestNilStatusMethods(t *testing.T) { + var status *Status + wantCheckOutputs := make([]bool, len(statusCodeNames)) + wantCheckOutputs[Success] = true + checkFuncs := []func() bool{ + status.IsSuccess, + status.IsInteralError, + status.IsClusterUnschedulable, + status.IsPreSkip, + } + for idx, checkFunc := range checkFuncs { + if wantCheckOutputs[idx] != checkFunc() { + t.Fatalf("check function for %s = %t, want %t", statusCodeNames[idx], checkFunc(), wantCheckOutputs[idx]) + } + } + + if !cmp.Equal(status.Reasons(), []string{}) { + t.Fatalf("Reasons() = %v, want %v", status.Reasons(), []string{}) + } + + if !cmp.Equal(status.SourcePlugin(), "") { + t.Fatalf("SourcePlugin() = %s, want %s", status.SourcePlugin(), "") + } + + if !cmp.Equal(status.InternalError(), nil, cmpopts.EquateErrors()) { + t.Fatalf("InternalError() = %v, want %v", status.InternalError(), nil) + } + + wantDesc := statusCodeNames[Success] + if !cmp.Equal(status.String(), wantDesc) { + t.Fatalf("String() = %s, want %s", status.String(), wantDesc) + } +}