Skip to content

Commit

Permalink
fixed-value strategy plugin (#436)
Browse files Browse the repository at this point in the history
  • Loading branch information
jazzyfresh authored Mar 31, 2021
1 parent 404ff5b commit dc56c42
Show file tree
Hide file tree
Showing 5 changed files with 300 additions and 0 deletions.
16 changes: 16 additions & 0 deletions plugins/builtin/strategy/fixed-value/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package main

import (
hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad-autoscaler/plugins"
fixedvalue "github.com/hashicorp/nomad-autoscaler/plugins/builtin/strategy/fixed-value/plugin"
)

func main() {
plugins.Serve(factory)
}

// factory returns a new instance of the FixedValue Strategy plugin.
func factory(log hclog.Logger) interface{} {
return fixedvalue.NewFixedValuePlugin(log)
}
110 changes: 110 additions & 0 deletions plugins/builtin/strategy/fixed-value/plugin/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package plugin

import (
"fmt"
"strconv"

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad-autoscaler/plugins"
"github.com/hashicorp/nomad-autoscaler/plugins/base"
"github.com/hashicorp/nomad-autoscaler/plugins/strategy"
"github.com/hashicorp/nomad-autoscaler/sdk"
)

const (
// pluginName is the unique name of the this plugin amongst strategy
// plugins.
pluginName = "fixed-value"

// These are the keys read from the RunRequest.Config map.
runConfigKeyValue = "value"
)

var (
PluginID = plugins.PluginID{
Name: pluginName,
PluginType: sdk.PluginTypeStrategy,
}

PluginConfig = &plugins.InternalPluginConfig{
Factory: func(l hclog.Logger) interface{} { return NewFixedValuePlugin(l) },
}

pluginInfo = &base.PluginInfo{
Name: pluginName,
PluginType: sdk.PluginTypeStrategy,
}
)

// Assert that StrategyPlugin meets the strategy.Strategy interface.
var _ strategy.Strategy = (*StrategyPlugin)(nil)

// StrategyPlugin is the FixedValue implementation of the strategy.Strategy
// interface.
type StrategyPlugin struct {
config map[string]string
logger hclog.Logger
}

// NewFixedValuePlugin returns the FixedValue implementation of the
// strategy.Strategy interface.
func NewFixedValuePlugin(log hclog.Logger) strategy.Strategy {
return &StrategyPlugin{
logger: log,
}
}

// SetConfig satisfies the SetConfig function on the base.Base interface.
func (s *StrategyPlugin) SetConfig(config map[string]string) error {
s.config = config
return nil
}

// PluginInfo satisfies the PluginInfo function on the base.Base interface.
func (s *StrategyPlugin) PluginInfo() (*base.PluginInfo, error) {
return pluginInfo, nil
}

// Run satisfies the Run function on the strategy.Strategy interface.
func (s *StrategyPlugin) Run(eval *sdk.ScalingCheckEvaluation, count int64) (*sdk.ScalingCheckEvaluation, error) {

// Read and parse fixed value from req.Config.
t := eval.Check.Strategy.Config[runConfigKeyValue]
if t == "" {
return nil, fmt.Errorf("missing required field `%s`", runConfigKeyValue)
}

value, err := strconv.ParseInt(t, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid value for `%s`: %v (%T)", runConfigKeyValue, t, t)
}

// Identify the direction of scaling, if any.
eval.Action.Direction = s.calculateDirection(count, value)
if eval.Action.Direction == sdk.ScaleDirectionNone {
return eval, nil
}

// Log at trace level the details of the strategy calculation. This is
// helpful in ultra-debugging situations when there is a need to understand
// all the calculations made.
s.logger.Trace("calculated scaling strategy results",
"check_name", eval.Check.Name, "current_count", count, "new_count", value,
"direction", eval.Action.Direction)

eval.Action.Count = value
eval.Action.Reason = fmt.Sprintf("scaling %s because fixed value is %d", eval.Action.Direction, value)

return eval, nil
}

// calculateDirection is used to calculate the direction of scaling that should
// occur, if any at all.
func (s *StrategyPlugin) calculateDirection(count, fixed int64) sdk.ScaleDirection {
if count == fixed {
return sdk.ScaleDirectionNone
} else if count < fixed {
return sdk.ScaleDirectionUp
}
return sdk.ScaleDirectionDown
}
166 changes: 166 additions & 0 deletions plugins/builtin/strategy/fixed-value/plugin/plugin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package plugin

import (
"fmt"
"testing"

hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad-autoscaler/plugins/base"
"github.com/hashicorp/nomad-autoscaler/sdk"
"github.com/stretchr/testify/assert"
)

func TestStrategyPlugin_SetConfig(t *testing.T) {
s := &StrategyPlugin{}
expectedOutput := map[string]string{"example-item": "example-value"}
err := s.SetConfig(expectedOutput)
assert.Nil(t, err)
assert.Equal(t, expectedOutput, s.config)
}

func TestStrategyPlugin_PluginInfo(t *testing.T) {
s := &StrategyPlugin{}
expectedOutput := &base.PluginInfo{Name: "fixed-value", PluginType: "strategy"}
actualOutput, err := s.PluginInfo()
assert.Nil(t, err)
assert.Equal(t, expectedOutput, actualOutput)
}

func TestStrategyPlugin_Run(t *testing.T) {
testCases := []struct {
inputEval *sdk.ScalingCheckEvaluation
inputCount int64
expectedResp *sdk.ScalingCheckEvaluation
expectedError error
name string
}{
{
inputEval: &sdk.ScalingCheckEvaluation{
Check: &sdk.ScalingPolicyCheck{
Strategy: &sdk.ScalingPolicyStrategy{},
},
},
expectedResp: nil,
expectedError: fmt.Errorf("missing required field `value`"),
name: "incorrect strategy input config",
},
{
inputEval: &sdk.ScalingCheckEvaluation{
Check: &sdk.ScalingPolicyCheck{
Strategy: &sdk.ScalingPolicyStrategy{
Config: map[string]string{"value": "not-the-int-you're-looking-for"},
},
},
},
expectedResp: nil,
expectedError: fmt.Errorf("invalid value for `value`: not-the-int-you're-looking-for (string)"),
name: "incorrect input strategy config fixed value - string",
},
{
inputEval: &sdk.ScalingCheckEvaluation{
Metrics: sdk.TimestampedMetrics{sdk.TimestampedMetric{Value: 13}},
Check: &sdk.ScalingPolicyCheck{
Strategy: &sdk.ScalingPolicyStrategy{
Config: map[string]string{"value": "13"},
},
},
Action: &sdk.ScalingAction{},
},
inputCount: 2,
expectedResp: &sdk.ScalingCheckEvaluation{
Metrics: sdk.TimestampedMetrics{sdk.TimestampedMetric{Value: 13}},
Check: &sdk.ScalingPolicyCheck{
Strategy: &sdk.ScalingPolicyStrategy{
Config: map[string]string{"value": "13"},
},
},
Action: &sdk.ScalingAction{
Count: 13,
Reason: "scaling up because fixed value is 13",
Direction: sdk.ScaleDirectionUp,
},
},
expectedError: nil,
name: "fixed scale up",
},
{
inputEval: &sdk.ScalingCheckEvaluation{
Metrics: sdk.TimestampedMetrics{sdk.TimestampedMetric{Value: 26}},
Check: &sdk.ScalingPolicyCheck{
Strategy: &sdk.ScalingPolicyStrategy{
Config: map[string]string{"value": "4"},
},
},
Action: &sdk.ScalingAction{},
},
inputCount: 10,
expectedResp: &sdk.ScalingCheckEvaluation{
Metrics: sdk.TimestampedMetrics{sdk.TimestampedMetric{Value: 26}},
Check: &sdk.ScalingPolicyCheck{
Strategy: &sdk.ScalingPolicyStrategy{
Config: map[string]string{"value": "4"},
},
},
Action: &sdk.ScalingAction{
Count: 4,
Reason: "scaling down because fixed value is 4",
Direction: sdk.ScaleDirectionDown,
},
},
expectedError: nil,
name: "fixed scale down",
},
{
inputEval: &sdk.ScalingCheckEvaluation{
Metrics: sdk.TimestampedMetrics{sdk.TimestampedMetric{Value: 9}},
Check: &sdk.ScalingPolicyCheck{
Strategy: &sdk.ScalingPolicyStrategy{
Config: map[string]string{"value": "10"},
},
},
Action: &sdk.ScalingAction{},
},
inputCount: 10,
expectedResp: &sdk.ScalingCheckEvaluation{
Metrics: sdk.TimestampedMetrics{sdk.TimestampedMetric{Value: 9}},
Check: &sdk.ScalingPolicyCheck{
Strategy: &sdk.ScalingPolicyStrategy{
Config: map[string]string{"value": "10"},
},
},
Action: &sdk.ScalingAction{
Direction: sdk.ScaleDirectionNone,
},
},
expectedError: nil,
name: "no scaling - current count same as fixed",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := &StrategyPlugin{logger: hclog.NewNullLogger()}
actualResp, actualError := s.Run(tc.inputEval, tc.inputCount)
assert.Equal(t, tc.expectedResp, actualResp, tc.name)
assert.Equal(t, tc.expectedError, actualError, tc.name)
})
}
}

func TestStrategyPlugin_calculateDirection(t *testing.T) {
testCases := []struct {
inputCount int64
fixedCount int64
expectedOutput sdk.ScaleDirection
}{
{inputCount: 0, fixedCount: 1, expectedOutput: sdk.ScaleDirectionUp},
{inputCount: 5, fixedCount: 5, expectedOutput: sdk.ScaleDirectionNone},
{inputCount: 4, fixedCount: 0, expectedOutput: sdk.ScaleDirectionDown},
}

s := &StrategyPlugin{}

for _, tc := range testCases {
assert.Equal(t, tc.expectedOutput, s.calculateDirection(tc.inputCount, tc.fixedCount))
}
}
5 changes: 5 additions & 0 deletions plugins/manager/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
datadog "github.com/hashicorp/nomad-autoscaler/plugins/builtin/apm/datadog/plugin"
nomadAPM "github.com/hashicorp/nomad-autoscaler/plugins/builtin/apm/nomad/plugin"
prometheus "github.com/hashicorp/nomad-autoscaler/plugins/builtin/apm/prometheus/plugin"
fixedValue "github.com/hashicorp/nomad-autoscaler/plugins/builtin/strategy/fixed-value/plugin"
passthrough "github.com/hashicorp/nomad-autoscaler/plugins/builtin/strategy/pass-through/plugin"
targetValue "github.com/hashicorp/nomad-autoscaler/plugins/builtin/strategy/target-value/plugin"
awsASG "github.com/hashicorp/nomad-autoscaler/plugins/builtin/target/aws-asg/plugin"
Expand Down Expand Up @@ -36,6 +37,9 @@ func (pm *PluginManager) loadInternalPlugin(cfg *config.Plugin, pluginType strin
case plugins.InternalStrategyTargetValue:
info.factory = targetValue.PluginConfig.Factory
info.driver = "target-value"
case plugins.InternalStrategyFixedValue:
info.factory = fixedValue.PluginConfig.Factory
info.driver = "fixed-value"
case plugins.InternalAPMPrometheus:
info.factory = prometheus.PluginConfig.Factory
info.driver = "prometheus"
Expand Down Expand Up @@ -92,6 +96,7 @@ func (pm *PluginManager) useInternal(plugin string) bool {
plugins.InternalAPMPrometheus,
plugins.InternalStrategyPassThrough,
plugins.InternalStrategyTargetValue,
plugins.InternalStrategyFixedValue,
plugins.InternalTargetAWSASG,
plugins.InternalTargetAzureVMSS,
plugins.InternalTargetGCEMIG,
Expand Down
3 changes: 3 additions & 0 deletions plugins/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ const (
// name.
InternalStrategyTargetValue = "target-value"

// InternalStrategyFixedValue is the Fixed Value Strategy internal plugin name.
InternalStrategyFixedValue = "fixed-value"

// InternalTargetAWSASG is the Amazon Web Services AutoScaling Group target
// plugin.
InternalTargetAWSASG = "aws-asg"
Expand Down

0 comments on commit dc56c42

Please sign in to comment.