http-add-on/scaler/handlers_test.go

615 lines
16 KiB
Go

package main
import (
context "context"
"fmt"
"net"
"testing"
"time"
"github.com/go-logr/logr"
"github.com/kedacore/http-add-on/pkg/queue"
"github.com/kedacore/http-add-on/pkg/routing"
externalscaler "github.com/kedacore/http-add-on/proto"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/test/bufconn"
)
func standardTarget() routing.Target {
return routing.NewTarget(
"testns",
"testsrv",
8080,
"testdepl",
123,
)
}
func TestStreamIsActive(t *testing.T) {
type testCase struct {
name string
host string
expected bool
expectedErr bool
setup func(*routing.Table, *queuePinger)
}
testCases := []testCase{
{
name: "Simple host inactive",
host: t.Name(),
expected: false,
expectedErr: false,
setup: func(table *routing.Table, q *queuePinger) {
table.AddTarget(t.Name(), standardTarget())
q.pingMut.Lock()
defer q.pingMut.Unlock()
q.allCounts[t.Name()] = 0
},
},
{
name: "Host is 'interceptor'",
host: "interceptor",
expected: true,
expectedErr: false,
setup: func(*routing.Table, *queuePinger) {},
},
{
name: "Simple host active",
host: t.Name(),
expected: true,
expectedErr: false,
setup: func(table *routing.Table, q *queuePinger) {
table.AddTarget(t.Name(), standardTarget())
q.pingMut.Lock()
defer q.pingMut.Unlock()
q.allCounts[t.Name()] = 1
},
},
{
name: "No host present, but host in routing table",
host: t.Name(),
expected: false,
expectedErr: false,
setup: func(table *routing.Table, q *queuePinger) {
table.AddTarget(t.Name(), standardTarget())
},
},
{
name: "Host doesn't exist",
host: t.Name(),
expected: false,
expectedErr: true,
setup: func(*routing.Table, *queuePinger) {},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := require.New(t)
ctx := context.Background()
lggr := logr.Discard()
table := routing.NewTable()
ticker, pinger, err := newFakeQueuePinger(ctx, lggr)
r.NoError(err)
defer ticker.Stop()
tc.setup(table, pinger)
hdl := newImpl(
lggr,
pinger,
table,
123,
200,
)
bufSize := 1024 * 1024
lis := bufconn.Listen(bufSize)
grpcServer := grpc.NewServer()
defer grpcServer.Stop()
externalscaler.RegisterExternalScalerServer(
grpcServer,
hdl,
)
go grpcServer.Serve(lis)
bufDialFunc := func(context.Context, string) (net.Conn, error) {
return lis.Dial()
}
conn, err := grpc.DialContext(ctx, "bufnet", grpc.WithContextDialer(bufDialFunc), grpc.WithInsecure())
if err != nil {
t.Fatalf("Failed to dial bufnet: %v", err)
}
defer conn.Close()
client := externalscaler.NewExternalScalerClient(conn)
testRef := &externalscaler.ScaledObjectRef{
ScalerMetadata: map[string]string{
"host": tc.host,
},
}
// First will see if we can establish the stream and handle this
// error.
streamClient, err := client.StreamIsActive(ctx, testRef)
if err != nil {
t.Fatalf("StreamIsActive failed: %v", err)
}
// Next, as in TestIsActive, we check for any error, expected
// or unexpected, for each table test.
res, err := streamClient.Recv()
if tc.expectedErr && err != nil {
return
} else if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if tc.expected != res.Result {
t.Fatalf("Expected IsActive result %v, got: %v", tc.expected, res.Result)
}
})
}
}
func TestIsActive(t *testing.T) {
type testCase struct {
name string
host string
expected bool
expectedErr bool
setup func(*routing.Table, *queuePinger)
}
testCases := []testCase{
{
name: "Simple host inactive",
host: t.Name(),
expected: false,
expectedErr: false,
setup: func(table *routing.Table, q *queuePinger) {
table.AddTarget(t.Name(), standardTarget())
q.pingMut.Lock()
defer q.pingMut.Unlock()
q.allCounts[t.Name()] = 0
},
},
{
name: "Host is 'interceptor'",
host: "interceptor",
expected: true,
expectedErr: false,
setup: func(*routing.Table, *queuePinger) {},
},
{
name: "Simple host active",
host: t.Name(),
expected: true,
expectedErr: false,
setup: func(table *routing.Table, q *queuePinger) {
table.AddTarget(t.Name(), standardTarget())
q.pingMut.Lock()
defer q.pingMut.Unlock()
q.allCounts[t.Name()] = 1
},
},
{
name: "No host present, but host in routing table",
host: t.Name(),
expected: false,
expectedErr: false,
setup: func(table *routing.Table, q *queuePinger) {
table.AddTarget(t.Name(), standardTarget())
},
},
{
name: "Host doesn't exist",
host: t.Name(),
expected: false,
expectedErr: true,
setup: func(*routing.Table, *queuePinger) {},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := require.New(t)
ctx := context.Background()
lggr := logr.Discard()
table := routing.NewTable()
ticker, pinger, err := newFakeQueuePinger(ctx, lggr)
r.NoError(err)
defer ticker.Stop()
tc.setup(table, pinger)
hdl := newImpl(
lggr,
pinger,
table,
123,
200,
)
res, err := hdl.IsActive(
ctx,
&externalscaler.ScaledObjectRef{
ScalerMetadata: map[string]string{
"host": tc.host,
},
},
)
if tc.expectedErr && err != nil {
return
} else if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if tc.expected != res.Result {
t.Fatalf("Expected IsActive result %v, got: %v", tc.expected, res.Result)
}
})
}
}
func TestGetMetricSpecTable(t *testing.T) {
const ns = "testns"
type testCase struct {
name string
defaultTargetMetric int64
defaultTargetMetricInterceptor int64
scalerMetadata map[string]string
newRoutingTableFn func() *routing.Table
checker func(*testing.T, *externalscaler.GetMetricSpecResponse, error)
}
cases := []testCase{
{
name: "valid host as host value in scaler metadata",
defaultTargetMetric: 0,
defaultTargetMetricInterceptor: 123,
scalerMetadata: map[string]string{
"host": "validHost",
"targetPendingRequests": "123",
},
newRoutingTableFn: func() *routing.Table {
ret := routing.NewTable()
ret.AddTarget("validHost", routing.NewTarget(
ns,
"testsrv",
8080,
"testdepl",
123,
))
return ret
},
checker: func(t *testing.T, res *externalscaler.GetMetricSpecResponse, err error) {
t.Helper()
r := require.New(t)
r.NoError(err)
r.NotNil(res)
r.Equal(1, len(res.MetricSpecs))
spec := res.MetricSpecs[0]
r.Equal("validHost", spec.MetricName)
r.Equal(int64(123), spec.TargetSize)
},
},
{
name: "interceptor as host in scaler metadata",
defaultTargetMetric: 1000,
defaultTargetMetricInterceptor: 2000,
scalerMetadata: map[string]string{
"host": "interceptor",
"targetPendingRequests": "123",
},
newRoutingTableFn: func() *routing.Table {
ret := routing.NewTable()
ret.AddTarget("validHost", routing.NewTarget(
ns,
"testsrv",
8080,
"testdepl",
123,
))
return ret
},
checker: func(t *testing.T, res *externalscaler.GetMetricSpecResponse, err error) {
t.Helper()
r := require.New(t)
r.NoError(err)
r.NotNil(res)
r.Equal(1, len(res.MetricSpecs))
spec := res.MetricSpecs[0]
r.Equal("interceptor", spec.MetricName)
r.Equal(int64(2000), spec.TargetSize)
},
},
}
for i, c := range cases {
testName := fmt.Sprintf("test case #%d: %s", i, c.name)
// capture tc in scope so that we can run the below test
// in parallel
testCase := c
t.Run(testName, func(t *testing.T) {
ctx := context.Background()
t.Parallel()
lggr := logr.Discard()
table := testCase.newRoutingTableFn()
ticker, pinger, err := newFakeQueuePinger(ctx, lggr)
if err != nil {
t.Fatalf(
"error creating new fake queue pinger and related components: %s",
err,
)
}
defer ticker.Stop()
hdl := newImpl(
lggr,
pinger,
table,
testCase.defaultTargetMetric,
testCase.defaultTargetMetricInterceptor,
)
scaledObjectRef := externalscaler.ScaledObjectRef{
ScalerMetadata: testCase.scalerMetadata,
}
ret, err := hdl.GetMetricSpec(ctx, &scaledObjectRef)
testCase.checker(t, ret, err)
})
}
}
func TestGetMetrics(t *testing.T) {
type testCase struct {
name string
scalerMetadata map[string]string
setupFn func(
context.Context,
logr.Logger,
) (*routing.Table, *queuePinger, func(), error)
checkFn func(*testing.T, *externalscaler.GetMetricsResponse, error)
defaultTargetMetric int64
defaultTargetMetricInterceptor int64
}
startFakeInterceptorServer := func(
ctx context.Context,
lggr logr.Logger,
hostMap map[string]int,
queuePingerTickDur time.Duration,
) (*queuePinger, func(), error) {
// create a new fake queue with the host map in it
q := queue.NewFakeCounter()
for host, val := range hostMap {
// NOTE: don't call .Resize here or you'll have to make sure
// to receive on q.ResizedCh
q.RetMap[host] = val
}
// create the HTTP server to encode and serve
// the host map
fakeSrv, fakeSrvURL, endpoints, err := startFakeQueueEndpointServer(
"testns",
"testSvc",
q,
1,
)
if err != nil {
return nil, nil, err
}
// create a fake queue pinger. this is the simulated
// scaler that pings the above fake interceptor
ticker, pinger, err := newFakeQueuePinger(
ctx,
lggr,
func(opts *fakeQueuePingerOpts) { opts.endpoints = endpoints },
func(opts *fakeQueuePingerOpts) { opts.tickDur = queuePingerTickDur },
func(opts *fakeQueuePingerOpts) { opts.port = fakeSrvURL.Port() },
)
if err != nil {
return nil, nil, err
}
// sleep for a bit to ensure the pinger has time to do its first tick
time.Sleep(10 * queuePingerTickDur)
return pinger, func() {
ticker.Stop()
fakeSrv.Close()
}, nil
}
testCases := []testCase{
{
name: "no 'host' field in the scaler metadata field",
scalerMetadata: map[string]string{},
setupFn: func(
ctx context.Context,
lggr logr.Logger,
) (*routing.Table, *queuePinger, func(), error) {
table := routing.NewTable()
ticker, pinger, err := newFakeQueuePinger(ctx, lggr)
if err != nil {
return nil, nil, nil, err
}
return table, pinger, func() { ticker.Stop() }, nil
},
checkFn: func(t *testing.T, res *externalscaler.GetMetricsResponse, err error) {
t.Helper()
r := require.New(t)
r.Error(err)
r.Nil(res)
r.Contains(
err.Error(),
"no 'host' field found in ScaledObject metadata",
)
},
defaultTargetMetric: int64(200),
defaultTargetMetricInterceptor: int64(300),
},
{
name: "missing host value in the queue pinger",
scalerMetadata: map[string]string{
"host": "missingHostInQueue",
},
setupFn: func(
ctx context.Context,
lggr logr.Logger,
) (*routing.Table, *queuePinger, func(), error) {
table := routing.NewTable()
// create queue and ticker without the host in it
ticker, pinger, err := newFakeQueuePinger(ctx, lggr)
if err != nil {
return nil, nil, nil, err
}
return table, pinger, func() { ticker.Stop() }, nil
},
checkFn: func(t *testing.T, res *externalscaler.GetMetricsResponse, err error) {
t.Helper()
r := require.New(t)
r.Error(err)
r.Contains(err.Error(), "host 'missingHostInQueue' not found in counts")
r.Nil(res)
},
defaultTargetMetric: int64(200),
defaultTargetMetricInterceptor: int64(300),
},
{
name: "valid host",
scalerMetadata: map[string]string{
"host": "validHost",
},
setupFn: func(
ctx context.Context,
lggr logr.Logger,
) (*routing.Table, *queuePinger, func(), error) {
table := routing.NewTable()
pinger, done, err := startFakeInterceptorServer(ctx, lggr, map[string]int{
"validHost": 201,
}, 2*time.Millisecond)
if err != nil {
return nil, nil, nil, err
}
return table, pinger, done, nil
},
checkFn: func(t *testing.T, res *externalscaler.GetMetricsResponse, err error) {
t.Helper()
r := require.New(t)
r.NoError(err)
r.NotNil(res)
r.Equal(1, len(res.MetricValues))
metricVal := res.MetricValues[0]
r.Equal("validHost", metricVal.MetricName)
r.Equal(int64(201), metricVal.MetricValue)
},
defaultTargetMetric: int64(200),
defaultTargetMetricInterceptor: int64(300),
},
{
name: "'interceptor' as host",
scalerMetadata: map[string]string{
"host": "interceptor",
},
setupFn: func(
ctx context.Context,
lggr logr.Logger,
) (*routing.Table, *queuePinger, func(), error) {
table := routing.NewTable()
pinger, done, err := startFakeInterceptorServer(ctx, lggr, map[string]int{
"host1": 201,
"host2": 202,
}, 2*time.Millisecond)
if err != nil {
return nil, nil, nil, err
}
return table, pinger, done, nil
},
checkFn: func(t *testing.T, res *externalscaler.GetMetricsResponse, err error) {
t.Helper()
r := require.New(t)
r.NoError(err)
r.NotNil(res)
r.Equal(1, len(res.MetricValues))
metricVal := res.MetricValues[0]
r.Equal("interceptor", metricVal.MetricName)
// the value here needs to be the same thing as
// the sum of the values in the fake queue created
// in the setup function
r.Equal(int64(403), metricVal.MetricValue)
},
defaultTargetMetric: int64(200),
defaultTargetMetricInterceptor: int64(300),
},
{
name: "host in routing table, missing in queue pinger",
scalerMetadata: map[string]string{
"host": "myhost.com",
},
setupFn: func(
ctx context.Context,
lggr logr.Logger,
) (*routing.Table, *queuePinger, func(), error) {
table := routing.NewTable()
table.AddTarget(
"myhost.com",
standardTarget(),
)
pinger, done, err := startFakeInterceptorServer(ctx, lggr, map[string]int{
"host1": 201,
"host2": 202,
}, 2*time.Millisecond)
if err != nil {
return nil, nil, nil, err
}
return table, pinger, done, nil
},
checkFn: func(t *testing.T, res *externalscaler.GetMetricsResponse, err error) {
t.Helper()
r := require.New(t)
r.NoError(err)
r.NotNil(res)
r.Equal(1, len(res.MetricValues))
metricVal := res.MetricValues[0]
r.Equal("myhost.com", metricVal.MetricName)
// the value here needs to be the same thing as
// the sum of the values in the fake queue created
// in the setup function
r.Equal(int64(0), metricVal.MetricValue)
},
defaultTargetMetric: int64(200),
defaultTargetMetricInterceptor: int64(300),
},
}
for i, c := range testCases {
tc := c
name := fmt.Sprintf("test case %d: %s", i, tc.name)
t.Run(name, func(t *testing.T) {
r := require.New(t)
ctx, done := context.WithCancel(
context.Background(),
)
defer done()
lggr := logr.Discard()
table, pinger, cleanup, err := tc.setupFn(ctx, lggr)
r.NoError(err)
defer cleanup()
hdl := newImpl(
lggr,
pinger,
table,
tc.defaultTargetMetric,
tc.defaultTargetMetricInterceptor,
)
res, err := hdl.GetMetrics(ctx, &externalscaler.GetMetricsRequest{
ScaledObjectRef: &externalscaler.ScaledObjectRef{
ScalerMetadata: tc.scalerMetadata,
},
})
tc.checkFn(t, res, err)
})
}
}