Skip to content

Commit 04ac6f9

Browse files
committed
Add test for autoscaling count
[skip ci] Signed-off-by: Viet Nguyen Duc <[email protected]>
1 parent 31fba3c commit 04ac6f9

File tree

12 files changed

+318
-57
lines changed

12 files changed

+318
-57
lines changed

.github/workflows/k8s-scaling-test.yml

Lines changed: 40 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,43 @@ permissions:
1616
jobs:
1717
build-and-test:
1818
name: Test K8s
19-
runs-on: blacksmith-16vcpu-ubuntu-2204
19+
runs-on: ubuntu-latest
2020
strategy:
2121
fail-fast: false
2222
matrix:
2323
include:
2424
- k8s-version: 'v1.31.2'
25+
test-strategy: chart_test_autoscaling_job_count_chaos
26+
cluster: 'minikube'
27+
helm-version: 'v3.16.3'
28+
docker-version: '27.3.1'
29+
python-version: '3.13'
30+
- k8s-version: 'v1.31.2'
31+
test-strategy: chart_test_autoscaling_job_count_max_sessions
32+
cluster: 'minikube'
33+
helm-version: 'v3.16.3'
34+
docker-version: '27.3.1'
35+
python-version: '3.13'
36+
- k8s-version: 'v1.31.2'
37+
test-strategy: chart_test_autoscaling_job_count
38+
cluster: 'minikube'
39+
helm-version: 'v3.16.3'
40+
docker-version: '27.3.1'
41+
python-version: '3.13'
42+
- k8s-version: 'v1.31.2'
43+
test-strategy: chart_test_autoscaling_deployment_count_chaos
44+
cluster: 'minikube'
45+
helm-version: 'v3.16.3'
46+
docker-version: '27.3.1'
47+
python-version: '3.13'
48+
- k8s-version: 'v1.31.2'
49+
test-strategy: chart_test_autoscaling_deployment_count_max_sessions
50+
cluster: 'minikube'
51+
helm-version: 'v3.16.3'
52+
docker-version: '27.3.1'
53+
python-version: '3.13'
54+
- k8s-version: 'v1.31.2'
55+
test-strategy: chart_test_autoscaling_deployment_count
2556
cluster: 'minikube'
2657
helm-version: 'v3.16.3'
2758
docker-version: '27.3.1'
@@ -79,11 +110,6 @@ jobs:
79110
echo "AUTHORS=${AUTHORS}" >> $GITHUB_ENV
80111
env:
81112
AUTHORS: ${{ vars.AUTHORS || 'SeleniumHQ' }}
82-
- name: Build Helm charts
83-
run: |
84-
BUILD_DATE=${BUILD_DATE} make chart_build
85-
echo "CHART_PACKAGE_PATH=$(cat /tmp/selenium_chart_version)" >> $GITHUB_ENV
86-
echo "CHART_FILE_NAME=$(basename $(cat /tmp/selenium_chart_version))" >> $GITHUB_ENV
87113
- name: Build Docker images
88114
uses: nick-invision/retry@master
89115
with:
@@ -97,61 +123,24 @@ jobs:
97123
timeout_minutes: 10
98124
max_attempts: 3
99125
command: CLUSTER=${CLUSTER} SERVICE_MESH=${SERVICE_MESH} KUBERNETES_VERSION=${KUBERNETES_VERSION} NAME=${IMAGE_REGISTRY} VERSION=${BRANCH} BUILD_DATE=${BUILD_DATE} make chart_cluster_setup
126+
- name: Build Helm charts
127+
run: |
128+
BUILD_DATE=${BUILD_DATE} make chart_build
129+
echo "CHART_PACKAGE_PATH=$(cat /tmp/selenium_chart_version)" >> $GITHUB_ENV
130+
echo "CHART_FILE_NAME=$(basename $(cat /tmp/selenium_chart_version))" >> $GITHUB_ENV
100131
- name: Test Selenium Grid on Kubernetes with Autoscaling
101132
uses: nick-invision/retry@master
102133
with:
103134
timeout_minutes: 30
104135
max_attempts: 3
105136
command: |
106-
NAME=${IMAGE_REGISTRY} VERSION=${BRANCH} BUILD_DATE=${BUILD_DATE} TEST_UPGRADE_CHART=false make chart_test_autoscaling_job_count_chaos
107-
- name: Upload results
108-
if: always()
109-
uses: actions/upload-artifact@main
110-
with:
111-
name: chart_test_autoscaling_job_count_chaos
112-
path: ./tests/tests/*.md
113-
if-no-files-found: ignore
114-
- name: Test Selenium Grid on Kubernetes with Autoscaling
115-
uses: nick-invision/retry@master
116-
with:
117-
timeout_minutes: 30
118-
max_attempts: 3
119-
command: |
120-
NAME=${IMAGE_REGISTRY} VERSION=${BRANCH} BUILD_DATE=${BUILD_DATE} TEST_UPGRADE_CHART=false make chart_test_autoscaling_job_count_max_sessions
121-
- name: Upload results
122-
if: always()
123-
uses: actions/upload-artifact@main
124-
with:
125-
name: chart_test_autoscaling_job_count_max_sessions
126-
path: ./tests/tests/*.md
127-
if-no-files-found: ignore
128-
- name: Test Selenium Grid on Kubernetes with Autoscaling
129-
uses: nick-invision/retry@master
130-
with:
131-
timeout_minutes: 30
132-
max_attempts: 3
133-
command: |
134-
NAME=${IMAGE_REGISTRY} VERSION=${BRANCH} BUILD_DATE=${BUILD_DATE} TEST_UPGRADE_CHART=false make chart_test_autoscaling_job_count_strategy_accurate
135-
- name: Upload results
136-
if: always()
137-
uses: actions/upload-artifact@main
138-
with:
139-
name: chart_test_autoscaling_job_count_strategy_accurate
140-
path: ./tests/tests/*.md
141-
if-no-files-found: ignore
142-
- name: Test Selenium Grid on Kubernetes with Autoscaling
143-
uses: nick-invision/retry@master
144-
with:
145-
timeout_minutes: 30
146-
max_attempts: 3
147-
command: |
148-
NAME=${IMAGE_REGISTRY} VERSION=${BRANCH} BUILD_DATE=${BUILD_DATE} TEST_UPGRADE_CHART=false make chart_test_autoscaling_job_count
137+
NAME=${IMAGE_REGISTRY} VERSION=${BRANCH} BUILD_DATE=${BUILD_DATE} TEST_UPGRADE_CHART=false make ${{ matrix.test-strategy }}
149138
- name: Upload results
150139
if: always()
151140
uses: actions/upload-artifact@main
152141
with:
153-
name: chart_test_autoscaling_job_count
154-
path: ./tests/tests/*.md
142+
name: ${{ matrix.test-strategy }}.md
143+
path: ./tests/tests/scale_up_results.md
155144
if-no-files-found: ignore
156145
- name: Cleanup Kubernetes cluster
157146
if: always()

Makefile

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ SBOM_OUTPUT := $(or $(SBOM_OUTPUT),$(SBOM_OUTPUT),package_versions.txt)
2929
KEDA_TAG_PREV_VERSION := $(or $(KEDA_TAG_PREV_VERSION),$(KEDA_TAG_PREV_VERSION),2.16.0-selenium-grid)
3030
KEDA_TAG_VERSION := $(or $(KEDA_TAG_VERSION),$(KEDA_TAG_VERSION),2.16.0-selenium-grid)
3131
KEDA_BASED_NAME := $(or $(KEDA_BASED_NAME),$(KEDA_BASED_NAME),ndviet)
32-
KEDA_BASED_TAG := $(or $(KEDA_BASED_TAG),$(KEDA_BASED_TAG),2.16.0-selenium-grid-20241127)
32+
KEDA_BASED_TAG := $(or $(KEDA_BASED_TAG),$(KEDA_BASED_TAG),2.16.0-selenium-grid-20241128)
3333

3434
all: hub \
3535
distributor \
@@ -961,6 +961,36 @@ chart_test_autoscaling_playwright_connect_grid:
961961
TEMPLATE_OUTPUT_FILENAME="k8s_playwright_connect_grid_basicAuth_secureIngress_ingressPublicIP_autoScaling_patchKEDA.yaml" \
962962
./tests/charts/make/chart_test.sh JobAutoscaling
963963

964+
chart_test_autoscaling_job_count_chaos:
965+
MATRIX_TESTS=AutoScalingTestsScaleChaos \
966+
make chart_test_autoscaling_job_count
967+
968+
chart_test_autoscaling_job_count_max_sessions:
969+
MAX_SESSIONS_FIREFOX=2 MAX_SESSIONS_EDGE=2 MAX_SESSIONS_CHROME=2 \
970+
make chart_test_autoscaling_job_count
971+
972+
chart_test_autoscaling_job_count:
973+
MATRIX_TESTS=$(or $(MATRIX_TESTS), "AutoscalingTestsScaleUp") SCALING_STRATEGY=$(or $(SCALING_STRATEGY), "default") \
974+
PLATFORMS=$(PLATFORMS) RELEASE_NAME=selenium TEST_PATCHED_KEDA=true SELENIUM_GRID_PROTOCOL=http SELENIUM_GRID_HOST=localhost SELENIUM_GRID_PORT=80 \
975+
SELENIUM_GRID_MONITORING=false CLEAR_POD_HISTORY=true SET_MAX_REPLICAS=100 ENABLE_VIDEO_RECORDER=false \
976+
VERSION=$(TAG_VERSION) VIDEO_TAG=$(FFMPEG_TAG_VERSION)-$(BUILD_DATE) KEDA_BASED_NAME=$(KEDA_BASED_NAME) KEDA_BASED_TAG=$(KEDA_BASED_TAG) NAMESPACE=$(NAMESPACE) BINDING_VERSION=$(BINDING_VERSION) BASE_VERSION=$(BASE_VERSION) \
977+
./tests/charts/make/chart_test.sh JobAutoscaling
978+
979+
chart_test_autoscaling_deployment_count_chaos:
980+
MATRIX_TESTS=AutoScalingTestsScaleChaos \
981+
make chart_test_autoscaling_deployment_count
982+
983+
chart_test_autoscaling_deployment_count_max_sessions:
984+
MAX_SESSIONS_FIREFOX=2 MAX_SESSIONS_EDGE=2 MAX_SESSIONS_CHROME=2 \
985+
make chart_test_autoscaling_deployment_count
986+
987+
chart_test_autoscaling_deployment_count:
988+
MATRIX_TESTS=$(or $(MATRIX_TESTS), "AutoscalingTestsScaleUp") \
989+
PLATFORMS=$(PLATFORMS) RELEASE_NAME=selenium TEST_PATCHED_KEDA=true SELENIUM_GRID_PROTOCOL=http SELENIUM_GRID_HOST=localhost SELENIUM_GRID_PORT=80 \
990+
SELENIUM_GRID_MONITORING=false CLEAR_POD_HISTORY=true SET_MAX_REPLICAS=100 ENABLE_VIDEO_RECORDER=false \
991+
VERSION=$(TAG_VERSION) VIDEO_TAG=$(FFMPEG_TAG_VERSION)-$(BUILD_DATE) KEDA_BASED_NAME=$(KEDA_BASED_NAME) KEDA_BASED_TAG=$(KEDA_BASED_TAG) NAMESPACE=$(NAMESPACE) BINDING_VERSION=$(BINDING_VERSION) BASE_VERSION=$(BASE_VERSION) \
992+
./tests/charts/make/chart_test.sh DeploymentAutoscaling
993+
964994
chart_test_delete:
965995
helm del test -n selenium || true
966996
helm del selenium -n selenium || true

tests/AutoscalingTests/__init__.py

Whitespace-only changes.

tests/AutoscalingTests/common.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import unittest
2+
import random
3+
import time
4+
import subprocess
5+
import signal
6+
import concurrent.futures
7+
import csv
8+
import os
9+
from selenium import webdriver
10+
from selenium.webdriver.firefox.options import Options as FirefoxOptions
11+
from selenium.webdriver.edge.options import Options as EdgeOptions
12+
from selenium.webdriver.chrome.options import Options as ChromeOptions
13+
from selenium.webdriver.remote.client_config import ClientConfig
14+
from csv2md.table import Table
15+
16+
BROWSER = {
17+
"chrome": ChromeOptions(),
18+
"firefox": FirefoxOptions(),
19+
"edge": EdgeOptions(),
20+
}
21+
22+
CLIENT_CONFIG = ClientConfig(
23+
remote_server_addr=f"http://localhost/selenium/wd/hub",
24+
keep_alive=True,
25+
timeout=3600,
26+
)
27+
28+
FIELD_NAMES = ["Iteration", "New request sessions", "Requests accepted time", "Sessions failed", "New scaled pods", "Total sessions", "Total pods", "Gaps"]
29+
30+
def get_pod_count():
31+
result = subprocess.run(["kubectl", "get", "pods", "-A", "--no-headers"], capture_output=True, text=True)
32+
return len([line for line in result.stdout.splitlines() if "selenium-node-" in line and "Running" in line])
33+
34+
def create_session(browser_name):
35+
return webdriver.Remote(command_executor=CLIENT_CONFIG.remote_server_addr, options=BROWSER[browser_name], client_config=CLIENT_CONFIG)
36+
37+
def wait_for_count_matches(sessions, timeout=10, interval=5):
38+
elapsed = 0
39+
while elapsed < timeout:
40+
pod_count = get_pod_count()
41+
if pod_count == len(sessions):
42+
break
43+
print(f"VALIDATING: Waiting for pods to match sessions... ({elapsed}/{timeout} seconds elapsed)")
44+
time.sleep(interval)
45+
elapsed += interval
46+
if pod_count != len(sessions):
47+
print(f"WARN: Mismatch between pod count and session count after {timeout} seconds. Gaps: {pod_count - len(sessions)}")
48+
else:
49+
print(f"PASS: Pod count matches session count after {elapsed} seconds.")
50+
51+
def close_all_sessions(sessions):
52+
for session in sessions:
53+
session.quit()
54+
sessions.clear()
55+
return sessions
56+
57+
def create_sessions_in_parallel(new_request_sessions):
58+
failed_jobs = 0
59+
with concurrent.futures.ThreadPoolExecutor() as executor:
60+
futures = [executor.submit(create_session, random.choice(list(BROWSER.keys()))) for _ in range(new_request_sessions)]
61+
sessions = []
62+
for future in concurrent.futures.as_completed(futures):
63+
try:
64+
sessions.append(future.result())
65+
except Exception as e:
66+
print(f"ERROR: Failed to create session: {e}")
67+
failed_jobs += 1
68+
print(f"Total failed jobs: {failed_jobs}")
69+
return sessions
70+
71+
def randomly_quit_sessions(sessions, sublist_size):
72+
if sessions:
73+
sessions_to_quit = random.sample(sessions, min(sublist_size, len(sessions)))
74+
for session in sessions_to_quit:
75+
session.quit()
76+
sessions.remove(session)
77+
print(f"QUIT: {len(sessions_to_quit)} sessions have been randomly quit.")
78+
return sessions
79+
80+
def export_results_to_csv(output_file, field_names, results):
81+
with open(output_file, mode="w") as csvfile:
82+
writer = csv.DictWriter(csvfile, fieldnames=field_names)
83+
writer.writeheader()
84+
writer.writerows(results)
85+
86+
def export_results_csv_to_md(csv_file, md_file):
87+
with open(csv_file) as f:
88+
table = Table.parse_csv(f)
89+
with open(md_file, mode="w") as f:
90+
f.write(table.markdown())
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import unittest
2+
import random
3+
import time
4+
import signal
5+
import csv
6+
from csv2md.table import Table
7+
from .common import *
8+
9+
SESSIONS = []
10+
RESULTS = []
11+
12+
def signal_handler(signum, frame):
13+
print("Signal received, quitting all sessions...")
14+
close_all_sessions(SESSIONS)
15+
16+
signal.signal(signal.SIGTERM, signal_handler)
17+
signal.signal(signal.SIGINT, signal_handler)
18+
19+
class SeleniumAutoscalingTests(unittest.TestCase):
20+
def test_run_tests(self):
21+
try:
22+
for iteration in range(20):
23+
new_request_sessions = random.randint(3, 6)
24+
start_time = time.time()
25+
start_pods = get_pod_count()
26+
new_sessions = create_sessions_in_parallel(new_request_sessions)
27+
failed_sessions = new_request_sessions - len(new_sessions)
28+
end_time = time.time()
29+
stop_pods = get_pod_count()
30+
SESSIONS.extend(new_sessions)
31+
elapsed_time = end_time - start_time
32+
new_scaled_pods = stop_pods - start_pods
33+
total_sessions = len(SESSIONS)
34+
total_pods = get_pod_count()
35+
RESULTS.append({
36+
FIELD_NAMES[0]: iteration + 1,
37+
FIELD_NAMES[1]: new_request_sessions,
38+
FIELD_NAMES[2]: f"{elapsed_time:.2f} s",
39+
FIELD_NAMES[3]: failed_sessions,
40+
FIELD_NAMES[4]: new_scaled_pods,
41+
FIELD_NAMES[5]: total_sessions,
42+
FIELD_NAMES[6]: total_pods,
43+
FIELD_NAMES[7]: total_pods - total_sessions,
44+
})
45+
print(f"ADDING: Created {new_request_sessions} new sessions in {elapsed_time:.2f} seconds.")
46+
print(f"INFO: Total sessions: {total_sessions}")
47+
print(f"INFO: Total pods: {total_pods}")
48+
randomly_quit_sessions(SESSIONS, random.randint(3, 12))
49+
time.sleep(15)
50+
finally:
51+
print(f"FINISH: Closing {len(SESSIONS)} sessions.")
52+
close_all_sessions(SESSIONS)
53+
output_file = f"tests/scale_up_results"
54+
export_results_to_csv(f"{output_file}.csv", FIELD_NAMES, RESULTS)
55+
export_results_csv_to_md(f"{output_file}.csv", f"{output_file}.md")
56+
57+
if __name__ == "__main__":
58+
unittest.main()
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import unittest
2+
import random
3+
import time
4+
import signal
5+
import csv
6+
from csv2md.table import Table
7+
from .common import *
8+
9+
SESSIONS = []
10+
RESULTS = []
11+
12+
def signal_handler(signum, frame):
13+
print("Signal received, quitting all sessions...")
14+
close_all_sessions(SESSIONS)
15+
16+
signal.signal(signal.SIGTERM, signal_handler)
17+
signal.signal(signal.SIGINT, signal_handler)
18+
19+
class SeleniumAutoscalingTests(unittest.TestCase):
20+
def test_run_tests(self):
21+
try:
22+
for iteration in range(20):
23+
new_request_sessions = random.randint(1, 3)
24+
start_time = time.time()
25+
start_pods = get_pod_count()
26+
new_sessions = create_sessions_in_parallel(new_request_sessions)
27+
failed_sessions = new_request_sessions - len(new_sessions)
28+
end_time = time.time()
29+
stop_pods = get_pod_count()
30+
SESSIONS.extend(new_sessions)
31+
elapsed_time = end_time - start_time
32+
new_scaled_pods = stop_pods - start_pods
33+
total_sessions = len(SESSIONS)
34+
total_pods = get_pod_count()
35+
RESULTS.append({
36+
FIELD_NAMES[0]: iteration + 1,
37+
FIELD_NAMES[1]: new_request_sessions,
38+
FIELD_NAMES[2]: f"{elapsed_time:.2f} s",
39+
FIELD_NAMES[3]: failed_sessions,
40+
FIELD_NAMES[4]: new_scaled_pods,
41+
FIELD_NAMES[5]: total_sessions,
42+
FIELD_NAMES[6]: total_pods,
43+
FIELD_NAMES[7]: total_pods - total_sessions,
44+
})
45+
print(f"ADDING: Created {new_request_sessions} new sessions in {elapsed_time:.2f} seconds.")
46+
print(f"INFO: Total sessions: {total_sessions}")
47+
print(f"INFO: Total pods: {total_pods}")
48+
if iteration % 5 == 0:
49+
randomly_quit_sessions(SESSIONS, 20)
50+
time.sleep(15)
51+
finally:
52+
print(f"FINISH: Closing {len(SESSIONS)} sessions.")
53+
close_all_sessions(SESSIONS)
54+
output_file = f"tests/scale_up_results"
55+
export_results_to_csv(f"{output_file}.csv", FIELD_NAMES, RESULTS)
56+
export_results_csv_to_md(f"{output_file}.csv", f"{output_file}.md")
57+
58+
if __name__ == "__main__":
59+
unittest.main()

0 commit comments

Comments
 (0)