Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions .keda/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ The stable implementation will be merged to the upstream KEDA repository frequen
Replace the image registry and tag of these KEDA components with the patched image tag:

```bash
docker pull selenium/keda:2.17.2-selenium-grid-20250717
docker pull selenium/keda-metrics-apiserver:2.17.2-selenium-grid-20250717
docker pull selenium/keda-admission-webhooks:2.17.2-selenium-grid-20250717
docker pull selenium/keda:2.17.2-selenium-grid-20250721
docker pull selenium/keda-metrics-apiserver:2.17.2-selenium-grid-20250721
docker pull selenium/keda-admission-webhooks:2.17.2-selenium-grid-20250721
```

Besides that, you also can use image tag `latest` or `nightly`.
Expand All @@ -27,15 +27,15 @@ If you are deploying KEDA core using their official Helm [chart](https://github.
keda:
registry: selenium
repository: keda
tag: "2.17.2-selenium-grid-20250717"
tag: "2.17.2-selenium-grid-20250721"
metricsApiServer:
registry: selenium
repository: keda-metrics-apiserver
tag: "2.17.2-selenium-grid-20250717"
tag: "2.17.2-selenium-grid-20250721"
webhooks:
registry: selenium
repository: keda-admission-webhooks
tag: "2.17.2-selenium-grid-20250717"
tag: "2.17.2-selenium-grid-20250721"
```

If you are deployment Selenium Grid chart with `autoscaling.enabled` is `true` (implies installing KEDA sub-chart), KEDA images registry and tag already set in the `values.yaml`. Refer to list [configuration](../charts/selenium-grid/CONFIGURATION.md).
Expand All @@ -49,6 +49,8 @@ You can involve to review and discuss the pull requests to help us early detect

[kedacore/keda](https://github.com/kedacore/keda)

- https://github.com/kedacore/keda/pull/6920 (planned, v2.18.0)

- ~~https://github.com/kedacore/keda/pull/6772 (merged, v2.17.1)~~

- ~~https://github.com/kedacore/keda/pull/6684 (merged, v2.17.0)~~
Expand Down
4 changes: 4 additions & 0 deletions .keda/scalers/selenium-grid-scaler.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ triggers:
nodeMaxSessions: 1 # Optional.
enableManagedDownloads: true # Optional.
capabilities: '' # Optional.
overProvisionRatio: '' # Optional.
```

**Parameter list:**
Expand All @@ -42,13 +43,16 @@ triggers:
- `nodeMaxSessions` - Number of maximum sessions that can run in parallel on a Node. Update this parameter align with node config `--max-sessions` (`SE_NODE_MAX_SESSIONS`) to have the correct scaling behavior. (Default: `1`, Optional).
- `enableManagedDownloads`- Set this for Node enabled to auto manage files downloaded for a given session on the Node. When the client requests enabling this feature, it can only be assigned to the Node that also enabled it. Otherwise, the request will wait until it timed out. (Default: `true`, Optional).
- `capabilities` - Add more custom capabilities for matching specific Nodes. It should be in JSON string, see [example](https://www.selenium.dev/documentation/grid/configuration/toml_options/#setting-custom-capabilities-for-matching-specific-nodes) (Optional)
- `overProvisionRatio` - The number of overprovisioning ratio to scale more than the actual number of requests. For example, if there are 20 requests for the browser instead of scaling to 20 Nodes, it is able to scale 20% more than the requested, in this case is 24, in this case input value is `0.2` (Optional)

**Trigger Authentication**
- `username` - Username for basic authentication in GraphQL endpoint instead of embedding in the URL. (Optional)
- `password` - Password for basic authentication in GraphQL endpoint instead of embedding in the URL. (Optional)
- `authType` - Type of authentication to be used. This can be set to `Bearer` or `OAuth2` in case Selenium Grid behind an Ingress proxy with other authentication types. (Optional)
- `accessToken` - Access token. This is required when `authType` is set a value. (Optional)

Noted that trigger authentication parameters are able to set in either trigger metadata or trigger authentication. However, if both are set, the trigger authentication will take precedence.

### Example

---
Expand Down
52 changes: 30 additions & 22 deletions .keda/scalers/selenium_grid_scaler.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,22 @@ type seleniumGridScaler struct {
type seleniumGridScalerMetadata struct {
triggerIndex int

URL string `keda:"name=url, order=authParams;triggerMetadata"`
AuthType string `keda:"name=authType, order=authParams;resolvedEnv, optional"`
Username string `keda:"name=username, order=authParams;resolvedEnv, optional"`
Password string `keda:"name=password, order=authParams;resolvedEnv, optional"`
AccessToken string `keda:"name=accessToken, order=authParams;resolvedEnv, optional"`
BrowserName string `keda:"name=browserName, order=triggerMetadata, optional"`
SessionBrowserName string `keda:"name=sessionBrowserName, order=triggerMetadata, optional"`
BrowserVersion string `keda:"name=browserVersion, order=triggerMetadata, optional"`
PlatformName string `keda:"name=platformName, order=triggerMetadata, optional"`
ActivationThreshold int64 `keda:"name=activationThreshold, order=triggerMetadata, optional"`
UnsafeSsl bool `keda:"name=unsafeSsl, order=triggerMetadata, default=false"`
NodeMaxSessions int64 `keda:"name=nodeMaxSessions, order=triggerMetadata, default=1"`
EnableManagedDownloads bool `keda:"name=enableManagedDownloads, order=triggerMetadata, default=true"`
Capabilities string `keda:"name=capabilities, order=triggerMetadata, optional"`

TargetValue int64
URL string `keda:"name=url, order=authParams;triggerMetadata;resolvedEnv"`
AuthType string `keda:"name=authType, order=authParams;triggerMetadata;resolvedEnv, optional"`
Username string `keda:"name=username, order=authParams;triggerMetadata;resolvedEnv, optional"`
Password string `keda:"name=password, order=authParams;triggerMetadata;resolvedEnv, optional"`
AccessToken string `keda:"name=accessToken, order=authParams;triggerMetadata;resolvedEnv, optional"`
BrowserName string `keda:"name=browserName, order=triggerMetadata, optional"`
SessionBrowserName string `keda:"name=sessionBrowserName, order=triggerMetadata, optional"`
BrowserVersion string `keda:"name=browserVersion, order=triggerMetadata, optional"`
PlatformName string `keda:"name=platformName, order=triggerMetadata, optional"`
NodeMaxSessions int64 `keda:"name=nodeMaxSessions, order=triggerMetadata, default=1"`
EnableManagedDownloads bool `keda:"name=enableManagedDownloads, order=triggerMetadata, default=true"`
Capabilities string `keda:"name=capabilities, order=triggerMetadata, optional"`
OverProvisionRatio float64 `keda:"name=overProvisionRatio, order=triggerMetadata, optional"`
UnsafeSsl bool `keda:"name=unsafeSsl, order=triggerMetadata, optional"`
TargetValue int64 `keda:"name=targetValue, order=triggerMetadata, default=1"`
ActivationThreshold float64 `keda:"name=activationThreshold, order=triggerMetadata, optional"`
}

type Platform struct {
Expand Down Expand Up @@ -173,12 +173,10 @@ func parseCapabilitiesToMap(_capabilities string) (map[string]interface{}, error
}

func parseSeleniumGridScalerMetadata(config *scalersconfig.ScalerConfig) (*seleniumGridScalerMetadata, error) {
meta := &seleniumGridScalerMetadata{
TargetValue: 1,
}
meta := &seleniumGridScalerMetadata{}

if err := config.TypedConfig(meta); err != nil {
return nil, fmt.Errorf("error parsing prometheus metadata: %w", err)
return nil, fmt.Errorf("error parsing Selenium Grid GraphQL response: %w", err)
}

meta.triggerIndex = config.TriggerIndex
Expand All @@ -204,9 +202,19 @@ func (s *seleniumGridScaler) GetMetricsAndActivity(ctx context.Context, metricNa
return []external_metrics.ExternalMetricValue{}, false, fmt.Errorf("error requesting selenium grid endpoint: %w", err)
}

metric := GenerateMetricInMili(metricName, float64(newRequestNodes+onGoingSessions))
scaledCount := getScaledCount(newRequestNodes, onGoingSessions, s.metadata.OverProvisionRatio)
metric := GenerateMetricInMili(metricName, scaledCount)

return []external_metrics.ExternalMetricValue{metric}, scaledCount > s.metadata.ActivationThreshold, nil
}

return []external_metrics.ExternalMetricValue{metric}, (newRequestNodes + onGoingSessions) > s.metadata.ActivationThreshold, nil
func getScaledCount(newRequestNodes int64, onGoingSession int64, overProvisionRatio float64) float64 {
scaledCount := float64(newRequestNodes + onGoingSession)
if overProvisionRatio > 0 {
// Apply over-provision ratio to the scaled count
scaledCount += scaledCount * overProvisionRatio
}
return scaledCount
}

func buildSeleniumGridMetricName(meta *seleniumGridScalerMetadata) string {
Expand Down
59 changes: 58 additions & 1 deletion .keda/scalers/selenium_grid_scaler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ func Test_getCountFromSeleniumResponse(t *testing.T) {
nodeMaxSessions int64
enableManagedDownloads bool
capabilities string
overProvisionRatio float64
}
tests := []struct {
name string
args args
wantNewRequestNodes int64
wantOnGoingSessions int64
wantOverProvisioned float64
wantErr bool
}{
{
Expand Down Expand Up @@ -437,6 +439,57 @@ func Test_getCountFromSeleniumResponse(t *testing.T) {
wantOnGoingSessions: 0,
wantErr: false,
},
{
name: "Scaled_number_when_set_param_over_provisioned",
args: args{
b: []byte(`{
"data": {
"grid": {
"sessionCount": 0,
"maxSession": 0,
"totalSlots": 0
},
"nodesInfo": {
"nodes": [
{
"id": "node-1",
"status": "UP",
"sessionCount": 0,
"maxSession": 1,
"slotCount": 1,
"stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"\", \"platformName\": \"linux\"}}]",
"sessions": []
},
{
"id": "node-2",
"status": "UP",
"sessionCount": 0,
"maxSession": 1,
"slotCount": 1,
"stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"\", \"platformName\": \"Windows 11\"}}]",
"sessions": []
}
]
},
"sessionsInfo": {
"sessionQueueRequests": [
"{\"browserName\": \"chrome\", \"platformName\": \"linux\"}",
"{\"browserName\": \"chrome\", \"platformName\": \"linux\"}"
]
}
}
}`),
browserName: "chrome",
sessionBrowserName: "chrome",
browserVersion: "",
platformName: "linux",
overProvisionRatio: 1.2,
},
wantNewRequestNodes: 1,
wantOnGoingSessions: 0,
wantOverProvisioned: 2.2,
wantErr: false,
},
{
name: "scaler_browserVersion_is_latest,_5_sessionQueueRequests_wihtout_browserVersion_also_1_different_platformName,_1_available_nodeStereotypes_with_3_slots_Linux_and_1_node_Windows,_should_return_count_as_1",
args: args{
Expand Down Expand Up @@ -1048,7 +1101,7 @@ func Test_getCountFromSeleniumResponse(t *testing.T) {
wantErr: false,
},
{
name: "1 queue request without platformName and scaler metadata without platfromName should return 1 new node and 1 ongoing session",
name: "1 queue request without platformName and scaler metadata without platformName should return 1 new node and 1 ongoing session",
args: args{
b: []byte(`{
"data": {
Expand Down Expand Up @@ -3154,13 +3207,17 @@ func Test_getCountFromSeleniumResponse(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
newRequestNodes, onGoingSessions, err := getCountFromSeleniumResponse(tt.args.b, tt.args.browserName, tt.args.browserVersion, tt.args.sessionBrowserName, tt.args.platformName, tt.args.nodeMaxSessions, tt.args.enableManagedDownloads, tt.args.capabilities, logr.Discard())
scaledCount := getScaledCount(newRequestNodes, onGoingSessions, tt.args.overProvisionRatio)
if (err != nil) != tt.wantErr {
t.Errorf("getCountFromSeleniumResponse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(newRequestNodes, tt.wantNewRequestNodes) || !reflect.DeepEqual(onGoingSessions, tt.wantOnGoingSessions) {
t.Errorf("getCountFromSeleniumResponse() = [%v, %v], want [%v, %v]", newRequestNodes, onGoingSessions, tt.wantNewRequestNodes, tt.wantOnGoingSessions)
}
if tt.args.overProvisionRatio > 0 && !reflect.DeepEqual(scaledCount, tt.wantOverProvisioned) {
t.Errorf("getCountFromSeleniumResponse() = %v, want over-provisioned %v", scaledCount, tt.wantOverProvisioned)
}
})
}
}
Expand Down
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ KEDA_TAG_PREV_VERSION := $(or $(KEDA_TAG_PREV_VERSION),$(KEDA_TAG_PREV_VERSION),
KEDA_CORE_VERSION := $(or $(KEDA_CORE_VERSION),$(KEDA_CORE_VERSION),2.17.2)
KEDA_TAG_VERSION := $(or $(KEDA_TAG_VERSION),$(KEDA_TAG_VERSION),2.17.2-selenium-grid)
KEDA_BASED_NAME := $(or $(KEDA_BASED_NAME),$(KEDA_BASED_NAME),ndviet)
KEDA_BASED_TAG := $(or $(KEDA_BASED_TAG),$(KEDA_BASED_TAG),2.17.2-selenium-grid-20250705)
TEST_PATCHED_KEDA := $(or $(TEST_PATCHED_KEDA),$(TEST_PATCHED_KEDA),false)
KEDA_BASED_TAG := $(or $(KEDA_BASED_TAG),$(KEDA_BASED_TAG),2.17.2-selenium-grid-20250721)
TEST_PATCHED_KEDA := $(or $(TEST_PATCHED_KEDA),$(TEST_PATCHED_KEDA),true)

all: hub \
distributor \
Expand Down Expand Up @@ -284,7 +284,7 @@ fetch_grid_scaler_resources:
&& cd ./.keda/scalers \
&& curl -L https://raw.githubusercontent.com/$(KEDA_BASED_NAME)/keda/v$(KEDA_BASED_TAG)/pkg/scalers/selenium_grid_scaler.go -o selenium_grid_scaler.go \
&& curl -L https://raw.githubusercontent.com/$(KEDA_BASED_NAME)/keda/v$(KEDA_BASED_TAG)/pkg/scalers/selenium_grid_scaler_test.go -o selenium_grid_scaler_test.go \
&& curl -L https://raw.githubusercontent.com/$(KEDA_BASED_NAME)/keda-docs/main/content/docs/2.17/scalers/selenium-grid-scaler.md -o selenium-grid-scaler.md
&& curl -L https://raw.githubusercontent.com/$(KEDA_BASED_NAME)/keda-docs/main/content/docs/2.18/scalers/selenium-grid-scaler.md -o selenium-grid-scaler.md

fetch_grid_scaler_images:
docker pull --platform linux/amd64 --platform linux/arm64 $(KEDA_BASED_NAME)/keda:$(KEDA_BASED_TAG)
Expand Down
Loading