Skip to content

Commit 817d81c

Browse files
✨ Add metamodeling load tests (#6014)
1 parent 71e5513 commit 817d81c

File tree

9 files changed

+224
-11
lines changed

9 files changed

+224
-11
lines changed

tests/environment-setup/test_used_docker_compose.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ def ensure_env_file(env_devel_file: Path) -> Iterable[Path]:
7676

7777

7878
def _skip_not_useful_docker_composes(p) -> bool:
79-
return "osparc-gateway-server" not in f"{p}" and "manual" not in f"{p}"
79+
result = "osparc-gateway-server" not in f"{p}" and "manual" not in f"{p}"
80+
result &= "tests/performance" not in f"{p}"
81+
return result
8082

8183

8284
compose_paths = filter(

tests/performance/Dockerfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ FROM locustio/locust:${LOCUST_VERSION}
66
RUN pip3 --version && \
77
pip3 install \
88
faker \
9+
locust-plugins==2.1.1 \
10+
pydantic \
11+
pydantic-settings \
912
python-dotenv \
10-
locust-plugins==2.1.1 &&\
13+
tenacity && \
1114
pip3 freeze --verbose

tests/performance/Makefile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#
44
include ../../scripts/common.Makefile
55

6-
LOCUST_VERSION=2.5.1
6+
LOCUST_VERSION=2.29.1
77
export LOCUST_VERSION
88

99
# UTILS
@@ -42,9 +42,9 @@ down: ## stops and removes osparc locust containers
4242
docker compose --file docker-compose.yml down
4343

4444
.PHONY: test
45-
test: ## runs osparc locust with target=locust_test_file.py in headless mode for a minute. Will fail if 5% more fail requests or average response time is above 50ms, optional host can be set
45+
test: ## runs osparc locust. locust and test confiuration are specified in .env file next to target file
4646
@$(call check_defined, target, please define target file when calling $@ - e.g. ```make $@ target=MY_LOCUST_FILE.py```)
4747
@export LOCUST_FILE=$(target); \
48-
export TARGET_URL=$(if $(host),$(host),"http://$(get_my_ip):9081"); \
49-
export LOCUST_OPTIONS="--headless --print-stats --users=100 --spawn-rate=20 --run-time=1m --check-fail-ratio=0.01 --check-avg-response-time=$(if $(resp_time),$(resp_time),200)"; \
48+
export ENV_FILE=$$(dirname $$(realpath locust_files/$${LOCUST_FILE}))/.env; \
49+
if [ ! -f $${ENV_FILE} ]; then cp $$(dirname $${ENV_FILE})/.env-devel $${ENV_FILE}; fi; \
5050
docker compose --file docker-compose.yml up --scale worker=4 --exit-code-from=master

tests/performance/docker-compose.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ services:
77
- ./locust_files:/mnt/locust
88
- ./locust_report:/reporting
99
command: >
10-
-f /mnt/locust/${LOCUST_FILE} --host ${TARGET_URL} --html
11-
/reporting/locust_html.html ${LOCUST_OPTIONS} --master
10+
-f /mnt/locust/${LOCUST_FILE} --html /reporting/locust_html.html --master
11+
env_file:
12+
- ${ENV_FILE}
1213

1314
worker:
1415
image: itisfoundation/locust:${LOCUST_VERSION}
1516
volumes:
1617
- ./locust_files:/mnt/locust
1718
command: -f /mnt/locust/${LOCUST_FILE} --worker --master-host master
18-
environment:
19-
- SC_USER_NAME=${SC_USER_NAME}
20-
- SC_PASSWORD=${SC_PASSWORD}
19+
env_file:
20+
- ${ENV_FILE}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# user
2+
SC_USER_NAME=<username>
3+
SC_PASSWORD=<password>
4+
5+
# locust settings
6+
LOCUST_HOST=<host (eg https://api.osparc-master.speag.com/)>
7+
LOCUST_USERS=100
8+
LOCUST_HEADLESS=true
9+
LOCUST_PRINT_STATS=true
10+
LOCUST_SPAWN_RATE=20
11+
LOCUST_RUN_TIME=1m
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# user
2+
OSPARC_API_KEY=<osparc api key>
3+
OSPARC_API_SECRET=<osparc api secret>
4+
TEMPLATE_UUID=<template uuid>
5+
6+
# locust settings
7+
LOCUST_HOST=<api server url>
8+
LOCUST_USERS=100
9+
LOCUST_HEADLESS=true
10+
LOCUST_PRINT_STATS=true
11+
LOCUST_SPAWN_RATE=20
12+
LOCUST_RUN_TIME=1m
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import json
2+
import os
3+
from pathlib import Path
4+
5+
6+
def main():
7+
8+
input_path = Path(os.environ["INPUT_FOLDER"])
9+
output_path = Path(os.environ["OUTPUT_FOLDER"])
10+
11+
input_file_path = input_path / "input.json"
12+
output_file_path = output_path / "output.json"
13+
14+
input_content = json.loads(input_file_path.read_text())
15+
16+
print(input_content)
17+
18+
output_file_path.write_text(json.dumps(input_content))
19+
20+
21+
if __name__ == "__main__":
22+
main()
96.9 KB
Loading
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
from datetime import timedelta
2+
from pathlib import Path
3+
from tempfile import TemporaryDirectory
4+
from typing import Final
5+
from uuid import UUID
6+
7+
from locust import HttpUser, task
8+
from pydantic import Field
9+
from pydantic_settings import BaseSettings
10+
from requests.auth import HTTPBasicAuth
11+
from tenacity import (
12+
Retrying,
13+
retry_if_exception_type,
14+
stop_after_delay,
15+
wait_exponential,
16+
)
17+
from urllib3 import PoolManager, Retry
18+
19+
_MAX_WAIT_SECONDS: Final[int] = 60
20+
21+
22+
# Perform the following setup in order to run this load test:
23+
# 1. Copy .env-devel to .env in this directory and add your osparc keys to .env.
24+
# 2. Construct a study **template** according to study_template.png. passer.py is the file next to this file.
25+
# 3. Setup the locust settings in the .env file (see https://docs.locust.io/en/stable/configuration.html#all-available-configuration-options)
26+
# run 'make test target=metamodeling/workflow.py' in your terminal and watch the magic happen 🤩
27+
28+
29+
class UserSettings(BaseSettings):
30+
osparc_api_key: str = Field(default=...)
31+
osparc_api_secret: str = Field(default=...)
32+
33+
template_uuid: UUID = Field(default=...)
34+
35+
36+
class MetaModelingUser(HttpUser):
37+
def __init__(self, *args, **kwargs):
38+
self._user_settings = UserSettings()
39+
self._auth = HTTPBasicAuth(
40+
username=self._user_settings.osparc_api_key,
41+
password=self._user_settings.osparc_api_secret,
42+
)
43+
retry_strategy = Retry(
44+
total=4,
45+
backoff_factor=4.0,
46+
status_forcelist={429, 503, 504},
47+
allowed_methods={
48+
"DELETE",
49+
"GET",
50+
"HEAD",
51+
"OPTIONS",
52+
"PUT",
53+
"TRACE",
54+
"POST",
55+
"PATCH",
56+
"CONNECT",
57+
},
58+
respect_retry_after_header=True,
59+
raise_on_status=True,
60+
)
61+
self.pool_manager = PoolManager(retries=retry_strategy)
62+
63+
self._input_json_uuid = None
64+
self._job_uuid = None
65+
66+
super().__init__(*args, **kwargs)
67+
68+
def on_start(self) -> None:
69+
self.client.get("/v0/me", auth=self._auth) # fail fast
70+
71+
def on_stop(self) -> None:
72+
if self._input_json_uuid is not None:
73+
response = self.client.delete(
74+
f"/v0/files/{self._input_json_uuid}", name="/v0/files/[file_id]"
75+
)
76+
response.raise_for_status()
77+
if self._job_uuid is not None:
78+
response = self.client.delete(
79+
f"/v0/studies/{self._user_settings.template_uuid}/jobs/{self._job_uuid}",
80+
name="/v0/studies/[study_id]/jobs/[job_id]",
81+
)
82+
response.raise_for_status()
83+
84+
@task
85+
def create_and_run_job(self):
86+
# upload file
87+
with TemporaryDirectory() as tmp_dir:
88+
file = Path(tmp_dir) / "input.json"
89+
file.write_text(
90+
"""
91+
{
92+
"f1": 3
93+
}
94+
"""
95+
)
96+
self._input_json_uuid = self.upload_file(file)
97+
98+
# create job
99+
response = self.client.post(
100+
f"/v0/studies/{self._user_settings.template_uuid}/jobs",
101+
json={
102+
"values": {"InputFile1": f"{self._input_json_uuid}"},
103+
},
104+
auth=self._auth,
105+
name="/v0/studies/[study_id]/jobs",
106+
)
107+
response.raise_for_status()
108+
job_uuid = response.json().get("id")
109+
assert job_uuid is not None
110+
self._job_uuid = UUID(job_uuid)
111+
112+
# start job
113+
response = self.client.post(
114+
f"/v0/studies/{self._user_settings.template_uuid}/jobs/{self._job_uuid}:start",
115+
auth=self._auth,
116+
name="/v0/studies/[study_id]/jobs/[job_id]:start",
117+
)
118+
response.raise_for_status()
119+
state = response.json().get("state")
120+
for attempt in Retrying(
121+
stop=stop_after_delay(timedelta(seconds=_MAX_WAIT_SECONDS)),
122+
wait=wait_exponential(),
123+
retry=retry_if_exception_type(RuntimeError),
124+
):
125+
with attempt:
126+
response = self.client.post(
127+
f"/v0/studies/{self._user_settings.template_uuid}/jobs/{self._job_uuid}:inspect",
128+
auth=self._auth,
129+
name="/v0/studies/[study_id]/jobs/[job_id]:inspect",
130+
)
131+
response.raise_for_status()
132+
state = response.json().get("state")
133+
if not state in {"SUCCESS", "FAILED"}:
134+
raise RuntimeError(
135+
f"Computation not finished after attempt {attempt.retry_state.attempt_number}"
136+
)
137+
138+
assert state == "SUCCESS"
139+
140+
response = self.client.post(
141+
f"/v0/studies/{self._user_settings.template_uuid}/jobs/{self._job_uuid}/outputs",
142+
auth=self._auth,
143+
name="/v0/studies/[study_id]/jobs/[job_id]/outputs",
144+
)
145+
response.raise_for_status()
146+
results = response.json()
147+
assert results is not None
148+
output_file = results.get("OutputFile1")
149+
assert output_file is not None
150+
output_file_uuid = output_file.get("id")
151+
assert output_file_uuid is not None
152+
153+
def upload_file(self, file: Path) -> UUID:
154+
assert file.is_file()
155+
with open(f"{file.resolve()}", "rb") as f:
156+
files = {"file": f}
157+
response = self.client.put(
158+
"/v0/files/content", files=files, auth=self._auth
159+
)
160+
response.raise_for_status()
161+
file_uuid = response.json().get("id")
162+
assert file_uuid is not None
163+
return UUID(file_uuid)

0 commit comments

Comments
 (0)