Skip to content

Commit 9c26f9b

Browse files
committed
feat: added performance testing
1 parent 2a0c83e commit 9c26f9b

File tree

7 files changed

+173
-13
lines changed

7 files changed

+173
-13
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ celerybeat.pid
128128
*.sage.py
129129

130130
# Environments
131-
.env
131+
.env*
132132
.venv
133133
env/
134134
venv/

docker-compose.perf.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
version: "3.9"
2+
3+
volumes:
4+
db-data-perf:
5+
6+
services:
7+
db:
8+
image: postgres:15
9+
restart: unless-stopped
10+
environment:
11+
POSTGRES_USER: perf_user
12+
POSTGRES_PASSWORD: perf_pass
13+
POSTGRES_DB: perf_db
14+
ports:
15+
- "5432:5432"
16+
volumes:
17+
- db-data-perf:/var/lib/postgresql/data
18+
healthcheck:
19+
test: ["CMD-SHELL", "pg_isready -U perf_user -d perf_db"]
20+
interval: 5s
21+
timeout: 5s
22+
retries: 10
23+
24+
app:
25+
build:
26+
context: .
27+
dockerfile: dockerfile
28+
image: apex-dispatch-api:perf
29+
restart: unless-stopped
30+
environment:
31+
APP_ENV: development
32+
KEYCLOAK_HOST: ${KEYCLOAK_HOST}
33+
KEYCLOAK_REALM: ${KEYCLOAK_REALM}
34+
KEYCLOAK_CLIENT_ID: ${KEYCLOAK_CLIENT_ID}
35+
KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET}
36+
DATABASE_URL: postgresql+psycopg2://perf_user:perf_pass@db:5432/perf_db
37+
OPENEO_BACKENDS: ${OPENEO_BACKENDS_PERFOMANCE}
38+
depends_on:
39+
db:
40+
condition: service_healthy
41+
ports:
42+
- "${APP_PORT:-8000}:${APP_PORT:-8000}"
43+
healthcheck:
44+
# adjust path if your health endpoint differs
45+
test: ["CMD-SHELL", "curl -f http://localhost:${APP_PORT:-8000}/health || exit 1"]
46+
interval: 5s
47+
timeout: 3s
48+
retries: 12
49+
50+
migrate:
51+
image: apex-dispatch-api:perf
52+
environment:
53+
DATABASE_URL: postgresql+psycopg2://perf_user:perf_pass@db:5432/perf_db
54+
depends_on:
55+
db:
56+
condition: service_healthy
57+
# run the migration only after the app is reachable, then exit
58+
entrypoint: ["/bin/sh", "-c"]
59+
command: >
60+
"alembic upgrade head"

env.example

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
1-
# Keycloak
2-
KEYCLOAK_HOST=
3-
KEYCLOAK_REALM=
4-
51
# App
6-
APP_NAME="APEx Dispatch API"
7-
APP_DESCRIPTION="APEx Dispatch Service API to run jobs and upscaling tasks"
8-
APP_HOST=0.0.0.0
9-
APP_PORT=8000
10-
APP_ENV=development
2+
APP_NAME=
3+
APP_DESCRIPTION=
4+
APP_ENV=
115

126
# CORS
13-
CORS_ALLOWED_ORIGINS=http://localhost:5173
7+
CORS_ALLOWED_ORIGINS=
148

9+
# Keycloak
10+
KEYCLOAK_HOST=
11+
KEYCLOAK_REALM=
12+
KEYCLOAK_CLIENT_ID=
13+
KEYCLOAK_CLIENT_SECRET=
1514

1615
# Database
1716
DATABASE_URL=
1817

19-
# AUTH
20-
OPENEO_AUTH_CLIENT_CREDENTIALS_CDSEFED=
18+
# OPENEO
19+
OPENEO_BACKENDS=

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
alembic
12
cryptography
23
fastapi
34
flake8
45
geojson_pydantic
56
httpx
7+
locust
68
loguru
79
mkdocs
810
mkdocs-jupyter

tests/performance/auth.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import os
2+
3+
from dotenv import load_dotenv
4+
import requests
5+
6+
load_dotenv()
7+
8+
URL = os.getenv("KEYCLOAK_HOST")
9+
REALM = os.getenv("KEYCLOAK_REALM")
10+
CLIENT_ID = os.getenv("KEYCLOAK_CLIENT_PERFORMANCE_ID")
11+
CLIENT_SECRET = os.getenv("KEYCLOAK_CLIENT_PERFORMANCE_SECRET")
12+
13+
14+
def get_token_client_credentials():
15+
url = f"https://{URL}/realms/{REALM}/protocol/openid-connect/token"
16+
data = {
17+
"grant_type": "client_credentials",
18+
"client_id": CLIENT_ID,
19+
"client_secret": CLIENT_SECRET,
20+
}
21+
r = requests.post(url, data=data, timeout=10)
22+
r.raise_for_status()
23+
return r.json()["access_token"]

tests/performance/locustfile.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""
2+
Run locally:
3+
pip install -r requirements.txt
4+
locust -f locust.py --headless -u 50 -r 5 --run-time 2m --host http://localhost:8000
5+
"""
6+
7+
import logging
8+
from locust import HttpUser, task, between
9+
from locust.exception import StopUser
10+
from tests.performance.auth import get_token_client_credentials
11+
from tests.performance.utils import random_temporal_extent
12+
13+
14+
class DispatchUser(HttpUser):
15+
16+
logger = logging.getLogger("user")
17+
wait_time = between(1.0, 3.0) # user "think" time
18+
19+
def on_start(self):
20+
token = get_token_client_credentials()
21+
self.headers = {
22+
"Authorization": f"Bearer {token}",
23+
"Content-Type": "application/json",
24+
}
25+
26+
# simple health check at start; stop user if service unreachable
27+
with self.client.get("/health", name="health", catch_response=True) as r:
28+
if r.status_code != 200:
29+
r.failure(f"Health check failed: {r.status_code}")
30+
# stop this virtual user if service down
31+
raise StopUser()
32+
33+
@task
34+
def execute_statistics(self):
35+
extent = random_temporal_extent(2024)
36+
payload = {
37+
"title": "Test Processing Job",
38+
"label": "openeo",
39+
"service": {
40+
"endpoint": "https://openeo.vito.be",
41+
"application": "https://openeo.vito.be/openeo/1.2/processes/u:ff5c137fbbbf409d14a99875b97109874058056a9a02193e6fde8217d2f1f3e8@egi.eu/timeseries_graph",
42+
},
43+
"format": "json",
44+
"parameters": {
45+
"spatial_extent": {
46+
"type": "Point",
47+
"coordinates": [5.196363779293476, 51.25007554845948],
48+
},
49+
"collection": "CGLS_NDVI300_V2_GLOBAL",
50+
"band": "NDVI",
51+
"temporal_extent": extent,
52+
},
53+
}
54+
self.logger.info(f"Requesting statistics for extent: {extent}")
55+
self.client.post(
56+
"/sync_jobs",
57+
json=payload,
58+
name="statistics",
59+
timeout=30,
60+
headers=self.headers,
61+
)

tests/performance/utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from datetime import datetime, timedelta
2+
import random
3+
4+
5+
def random_temporal_extent(start_year):
6+
# Random start date in 2024
7+
start_date = datetime.strptime(f"{start_year}-01-01", "%Y-%m-%d") + timedelta(
8+
days=random.randint(0, 364)
9+
)
10+
11+
duration_days = random.randint(30, 365)
12+
13+
end_date = start_date + timedelta(days=duration_days)
14+
15+
return [start_date.strftime("%Y-%m-%d"), end_date.strftime("%Y-%m-%d")]

0 commit comments

Comments
 (0)