Skip to content

Commit

Permalink
feat(Scaler): Adds Solr Scaler (#4355)
Browse files Browse the repository at this point in the history
Signed-off-by: ithesadson <thesadson@gmail.com>
  • Loading branch information
ithesadson authored Mar 23, 2023
1 parent 43789f2 commit 078c51b
Show file tree
Hide file tree
Showing 5 changed files with 561 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio

- **CPU/Memory scaler**: Add support for scale to zero if there are multiple triggers([#4269](https://github.com/kedacore/keda/issues/4269))
- TODO ([#XXX](https://github.com/kedacore/keda/issue/XXX))
- **General:** Introduce new Solr Scaler ([#4234](https://github.com/kedacore/keda/issues/4234))

### Improvements

Expand Down
186 changes: 186 additions & 0 deletions pkg/scalers/solr_scaler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package scalers

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"

"github.com/go-logr/logr"
v2 "k8s.io/api/autoscaling/v2"
"k8s.io/metrics/pkg/apis/external_metrics"

kedautil "github.com/kedacore/keda/v2/pkg/util"
)

type solrScaler struct {
metricType v2.MetricTargetType
metadata *solrMetadata
httpClient *http.Client
logger logr.Logger
}

type solrMetadata struct {
host string
collection string
targetQueryValue float64
activationTargetQueryValue float64
query string
scalerIndex int

// Authentication
username string
password string
}

type solrResponse struct {
Response struct {
NumFound int `json:"numFound"`
} `json:"response"`
}

// NewSolrScaler creates a new solr Scaler
func NewSolrScaler(config *ScalerConfig) (Scaler, error) {
metricType, err := GetMetricTargetType(config)
if err != nil {
return nil, fmt.Errorf("error getting scaler metric type: %w", err)
}

meta, err := parseSolrMetadata(config)
if err != nil {
return nil, fmt.Errorf("error parsing Solr metadata: %w", err)
}
httpClient := kedautil.CreateHTTPClient(config.GlobalHTTPTimeout, false)

logger := InitializeLogger(config, "solr_scaler")

return &solrScaler{
metricType: metricType,
metadata: meta,
httpClient: httpClient,
logger: logger,
}, nil
}

// parseSolrMetadata parses the metadata and returns a solrMetadata or an error if the ScalerConfig is invalid.
func parseSolrMetadata(config *ScalerConfig) (*solrMetadata, error) {
meta := solrMetadata{}

if val, ok := config.TriggerMetadata["host"]; ok {
meta.host = val
} else {
return nil, fmt.Errorf("no host given")
}

if val, ok := config.TriggerMetadata["collection"]; ok {
meta.collection = val
} else {
return nil, fmt.Errorf("no collection given")
}

if val, ok := config.TriggerMetadata["query"]; ok {
meta.query = val
} else {
meta.query = "*:*"
}

if val, ok := config.TriggerMetadata["targetQueryValue"]; ok {
targetQueryValue, err := strconv.ParseFloat(val, 64)
if err != nil {
return nil, fmt.Errorf("targetQueryValue parsing error %w", err)
}
meta.targetQueryValue = targetQueryValue
} else {
return nil, fmt.Errorf("no targetQueryValue given")
}

meta.activationTargetQueryValue = 0
if val, ok := config.TriggerMetadata["activationTargetQueryValue"]; ok {
activationTargetQueryValue, err := strconv.ParseFloat(val, 64)
if err != nil {
return nil, fmt.Errorf("invalid activationTargetQueryValue - must be an integer")
}
meta.activationTargetQueryValue = activationTargetQueryValue
}
// Parse Authentication
if val, ok := config.AuthParams["username"]; ok {
meta.username = val
} else {
return nil, fmt.Errorf("no username given")
}

if val, ok := config.AuthParams["password"]; ok {
meta.password = val
} else {
return nil, fmt.Errorf("no password given")
}

meta.scalerIndex = config.ScalerIndex
return &meta, nil
}

func (s *solrScaler) getItemCount(ctx context.Context) (float64, error) {
var SolrResponse1 *solrResponse
var itemCount float64

url := fmt.Sprintf("%s/solr/%s/select?q=%s&wt=json",
s.metadata.host, s.metadata.collection, s.metadata.query)

req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return -1, err
}
// Add BasicAuth
req.SetBasicAuth(s.metadata.username, s.metadata.password)

resp, err := s.httpClient.Do(req)
if err != nil {
return -1, fmt.Errorf("error sending request to solr, %w", err)
}

defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return -1, err
}

err = json.Unmarshal(body, &SolrResponse1)
if err != nil {
return -1, fmt.Errorf("%w, make sure you enter username, password and collection values correctly in the yaml file", err)
}
itemCount = float64(SolrResponse1.Response.NumFound)
return itemCount, nil
}

// GetMetricSpecForScaling returns the MetricSpec for the Horizontal Pod Autoscaler
func (s *solrScaler) GetMetricSpecForScaling(context.Context) []v2.MetricSpec {
externalMetric := &v2.ExternalMetricSource{
Metric: v2.MetricIdentifier{
Name: GenerateMetricNameWithIndex(s.metadata.scalerIndex, kedautil.NormalizeString("solr")),
},
Target: GetMetricTargetMili(s.metricType, s.metadata.targetQueryValue),
}
metricSpec := v2.MetricSpec{
External: externalMetric, Type: externalMetricType,
}
return []v2.MetricSpec{metricSpec}
}

// GetMetricsAndActivity query from Solr,and return to external metrics and activity
func (s *solrScaler) GetMetricsAndActivity(ctx context.Context, metricName string) ([]external_metrics.ExternalMetricValue, bool, error) {
result, err := s.getItemCount(ctx)
if err != nil {
return []external_metrics.ExternalMetricValue{}, false, fmt.Errorf("failed to inspect solr, because of %w", err)
}

metric := GenerateMetricInMili(metricName, result)

return append([]external_metrics.ExternalMetricValue{}, metric), result > s.metadata.activationTargetQueryValue, nil
}

// Close closes the http client connection.
func (s *solrScaler) Close(context.Context) error {
return nil
}
77 changes: 77 additions & 0 deletions pkg/scalers/solr_scaler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package scalers

import (
"context"
"net/http"
"testing"
)

type parseSolrMetadataTestData struct {
metadata map[string]string
isError bool
authParams map[string]string
}

type solrMetricIdentifier struct {
metadataTestData *parseSolrMetadataTestData
scalerIndex int
name string
}

var testSolrMetadata = []parseSolrMetadataTestData{
// nothing passed
{map[string]string{}, true, map[string]string{}},
// properly formed metadata
{map[string]string{"host": "http://192.168.49.2:30217", "collection": "my_core", "query": "*:*", "targetQueryValue": "1"}, false, map[string]string{"username": "test_username", "password": "test_password"}},
// no query passed
{map[string]string{"host": "http://192.168.49.2:30217", "collection": "my_core", "targetQueryValue": "1"}, false, map[string]string{"username": "test_username", "password": "test_password"}},
// no host passed
{map[string]string{"collection": "my_core", "query": "*:*", "targetQueryValue": "1"}, true, map[string]string{"username": "test_username", "password": "test_password"}},
// no collection passed
{map[string]string{"host": "http://192.168.49.2:30217", "query": "*:*", "targetQueryValue": "1"}, true, map[string]string{"username": "test_username", "password": "test_password"}},
// no targetQueryValue passed
{map[string]string{"host": "http://192.168.49.2:30217", "collection": "my_core", "query": "*:*"}, true, map[string]string{"username": "test_username", "password": "test_password"}},
// no username passed
{map[string]string{"host": "http://192.168.49.2:30217", "collection": "my_core", "query": "*:*", "targetQueryValue": "1"}, true, map[string]string{"password": "test_password"}},
// no password passed
{map[string]string{"host": "http://192.168.49.2:30217", "collection": "my_core", "query": "*:*", "targetQueryValue": "1"}, true, map[string]string{"username": "test_username"}},
}

var solrMetricIdentifiers = []solrMetricIdentifier{
{&testSolrMetadata[1], 0, "s0-solr"},
{&testSolrMetadata[2], 1, "s1-solr"},
}

func TestSolrParseMetadata(t *testing.T) {
testCaseNum := 1
for _, testData := range testSolrMetadata {
_, err := parseSolrMetadata(&ScalerConfig{TriggerMetadata: testData.metadata, AuthParams: testData.authParams})
if err != nil && !testData.isError {
t.Errorf("Expected success but got error for unit test # %v", testCaseNum)
}
if testData.isError && err == nil {
t.Errorf("Expected error but got success for unit test # %v", testCaseNum)
}
testCaseNum++
}
}

func TestSolrGetMetricSpecForScaling(t *testing.T) {
for _, testData := range solrMetricIdentifiers {
ctx := context.Background()
meta, err := parseSolrMetadata(&ScalerConfig{TriggerMetadata: testData.metadataTestData.metadata, ScalerIndex: testData.scalerIndex, AuthParams: testData.metadataTestData.authParams})
if err != nil {
t.Fatal("Could not parse metadata:", err)
}
mockSolrScaler := solrScaler{
metadata: meta,
httpClient: http.DefaultClient,
}

metricSpec := mockSolrScaler.GetMetricSpecForScaling(ctx)
metricName := metricSpec[0].External.Metric.Name
if metricName != testData.name {
t.Errorf("Wrong External metric source name: %s, expected: %s", metricName, testData.name)
}
}
}
2 changes: 2 additions & 0 deletions pkg/scaling/scalers_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ func buildScaler(ctx context.Context, client client.Client, triggerType string,
return scalers.NewSeleniumGridScaler(config)
case "solace-event-queue":
return scalers.NewSolaceScaler(config)
case "solr":
return scalers.NewSolrScaler(config)
case "stan":
return scalers.NewStanScaler(config)
default:
Expand Down
Loading

0 comments on commit 078c51b

Please sign in to comment.