stac-api-load-testing 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {stac_api_load_testing-0.3.0/stac_api_load_testing.egg-info → stac_api_load_testing-0.4.0}/PKG-INFO +55 -1
- stac_api_load_testing-0.4.0/README.md +149 -0
- {stac_api_load_testing-0.3.0 → stac_api_load_testing-0.4.0}/pyproject.toml +1 -1
- {stac_api_load_testing-0.3.0 → stac_api_load_testing-0.4.0}/stac_api_load_testing/cli.py +156 -27
- {stac_api_load_testing-0.3.0 → stac_api_load_testing-0.4.0}/stac_api_load_testing/config_files/locustfile.py +297 -30
- stac_api_load_testing-0.4.0/stac_api_load_testing/data_loader/data_loader.py +118 -0
- {stac_api_load_testing-0.3.0 → stac_api_load_testing-0.4.0}/stac_api_load_testing/data_loader/setup_data/collection.json +1 -0
- {stac_api_load_testing-0.3.0 → stac_api_load_testing-0.4.0/stac_api_load_testing.egg-info}/PKG-INFO +55 -1
- stac_api_load_testing-0.3.0/README.md +0 -95
- stac_api_load_testing-0.3.0/stac_api_load_testing/data_loader/data_loader.py +0 -60
- {stac_api_load_testing-0.3.0 → stac_api_load_testing-0.4.0}/LICENSE +0 -0
- {stac_api_load_testing-0.3.0 → stac_api_load_testing-0.4.0}/setup.cfg +0 -0
- {stac_api_load_testing-0.3.0 → stac_api_load_testing-0.4.0}/stac_api_load_testing/__init__.py +0 -0
- {stac_api_load_testing-0.3.0 → stac_api_load_testing-0.4.0}/stac_api_load_testing/config_files/taurus_locust.yml +0 -0
- {stac_api_load_testing-0.3.0 → stac_api_load_testing-0.4.0}/stac_api_load_testing/data_loader/__init__.py +0 -0
- {stac_api_load_testing-0.3.0 → stac_api_load_testing-0.4.0}/stac_api_load_testing/data_loader/setup_data/sentinel-s2-l2a-cogs_0_100.json +0 -0
- {stac_api_load_testing-0.3.0 → stac_api_load_testing-0.4.0}/stac_api_load_testing.egg-info/SOURCES.txt +0 -0
- {stac_api_load_testing-0.3.0 → stac_api_load_testing-0.4.0}/stac_api_load_testing.egg-info/dependency_links.txt +0 -0
- {stac_api_load_testing-0.3.0 → stac_api_load_testing-0.4.0}/stac_api_load_testing.egg-info/entry_points.txt +0 -0
- {stac_api_load_testing-0.3.0 → stac_api_load_testing-0.4.0}/stac_api_load_testing.egg-info/requires.txt +0 -0
- {stac_api_load_testing-0.3.0 → stac_api_load_testing-0.4.0}/stac_api_load_testing.egg-info/top_level.txt +0 -0
{stac_api_load_testing-0.3.0/stac_api_load_testing.egg-info → stac_api_load_testing-0.4.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stac-api-load-testing
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Load testing for stac compliant apis
|
|
5
5
|
Author-email: Jonathan Healy <jonathan.d.healy@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -68,6 +68,11 @@ Usage: stac-api-load-testing [OPTIONS]
|
|
|
68
68
|
|
|
69
69
|
Options:
|
|
70
70
|
-i, --ingest Ingest sample data into the STAC API.
|
|
71
|
+
--collections INTEGER Number of collections to create for --ingest.
|
|
72
|
+
--items-per-collection INTEGER
|
|
73
|
+
Items per collection for --ingest; beyond the
|
|
74
|
+
100 bundled items, duplicates are synthesized
|
|
75
|
+
with new ids and shifted geometry.
|
|
71
76
|
-l, --locust Run Locust load tests against the STAC API.
|
|
72
77
|
-t, --taurus Run the Taurus wrapper for performance testing
|
|
73
78
|
against the STAC API.
|
|
@@ -83,6 +88,8 @@ Options:
|
|
|
83
88
|
--spawn-rate INTEGER Users spawned per second for --benchmark.
|
|
84
89
|
--run-time TEXT Duration of each --benchmark run, in locust
|
|
85
90
|
notation (e.g. '30s', '1m').
|
|
91
|
+
-w, --workers INTEGER Locust worker processes for --benchmark; >1
|
|
92
|
+
distributes load across cores on this machine.
|
|
86
93
|
-o, --output PATH Write aggregated --benchmark results to a CSV
|
|
87
94
|
file.
|
|
88
95
|
--version Show the version and exit.
|
|
@@ -92,6 +99,10 @@ Options:
|
|
|
92
99
|
## Ingest test data - http://localhost:8084 is just an example url
|
|
93
100
|
```$ stac-api-load-testing --ingest --api-address http://localhost:8084```
|
|
94
101
|
|
|
102
|
+
Larger synthetic datasets: `--collections` creates `test-collection`, `test-collection-2`, ..., and `--items-per-collection` controls items per collection. Above the 100 bundled items, duplicates are derived from the sample features with new ids and randomly relocated geometry:
|
|
103
|
+
|
|
104
|
+
```$ stac-api-load-testing --ingest --api-address http://localhost:8084 --collections 2 --items-per-collection 500```
|
|
105
|
+
|
|
95
106
|
## Run Locust Load Testing Ouside of Taurus Wrapper
|
|
96
107
|
```$ stac-api-load-testing --locust --api-address http://localhost:8084```
|
|
97
108
|
- go to ```http://localhost:8089``` and start with desired settings
|
|
@@ -110,5 +121,48 @@ Compare multiple backends in one command (comma-separated addresses):
|
|
|
110
121
|
|
|
111
122
|
```$ stac-api-load-testing --benchmark --api-address http://localhost:8085,http://localhost:8087 --users 10,50,100 --run-time 1m -o results.csv```
|
|
112
123
|
|
|
124
|
+
## Example benchmark: pgstac vs opensearch vs stac-os-rs
|
|
125
|
+
|
|
126
|
+
Methodology used for the comparison below:
|
|
127
|
+
|
|
128
|
+
1. Start all three backends, each with its own datastore on a ~2GB memory budget (OpenSearch 3.8.0 `-Xms2g -Xmx2g`, pgstac `shared_buffers=2GB`):
|
|
129
|
+
|
|
130
|
+
```$ docker compose up -d opensearch opensearch-rs app-opensearch app-stac-os-rs pgstac app-pgstac```
|
|
131
|
+
|
|
132
|
+
2. Ingest the same dataset into each backend — 4 collections with 500 Sentinel-2 L2A items each (2,000 items total):
|
|
133
|
+
|
|
134
|
+
```$ stac-api-load-testing --ingest --api-address http://localhost:8083 --collections 4 --items-per-collection 500```
|
|
135
|
+
```$ stac-api-load-testing --ingest --api-address http://localhost:8085 --collections 4 --items-per-collection 500```
|
|
136
|
+
```$ stac-api-load-testing --ingest --api-address http://localhost:8087 --collections 4 --items-per-collection 500```
|
|
137
|
+
|
|
138
|
+
3. Run the benchmark sweep against all three in one command (10/50/100/150 users, spawn rate 10 users/s, 1 minute per run, 8 locust worker processes):
|
|
139
|
+
|
|
140
|
+
```$ stac-api-load-testing --benchmark --api-address stac-fastapi-pgstac=http://localhost:8083,stac-fastapi-os=http://localhost:8085,stac-os-rs=http://localhost:8087 --users 10,50,100,150 --run-time 1m -w 8 -o results.csv```
|
|
141
|
+
|
|
142
|
+
Environment: AMD Ryzen 7 8845HS (16 threads), 28GB RAM, Docker 29.8.0, locust 2.47.0 (`-w 8`), stac-fastapi.pgstac 7.2.0, stac-fastapi-os v8.1.1, stac-os-rs 0.5.0. The locustfile drives a read/search workload — landing page, collections, collection items, single items across all collections, GET/POST `/search` with bbox, intersects, sortby, datetime, CQL2, and ids queries, one level of `next`-link pagination, and filtered item lists; every search uses `limit=10`; write tasks are disabled.
|
|
143
|
+
|
|
144
|
+
Note on parity: stac-fastapi-os and stac-os-rs compute `numberMatched` (total-hit counting) on every search, while this stac-fastapi-pgstac version ships no context extension and skips count work entirely — so pgstac's reported numbers do not include a cost the OpenSearch-backed APIs pay per query.
|
|
145
|
+
|
|
146
|
+
### Results
|
|
147
|
+
|
|
148
|
+
| api | collections | items | users | requests | failures | median (ms) | p95 (ms) | p99 (ms) | req/s |
|
|
149
|
+
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
150
|
+
| stac-fastapi-pgstac (:8083) | 4 | 2,000 | 10 | 11,252 | 0 | 27 | 140 | 170 | 187 |
|
|
151
|
+
| stac-fastapi-pgstac (:8083) | 4 | 2,000 | 50 | 13,817 | 0 | 54 | 780 | 2,500 | 230 |
|
|
152
|
+
| stac-fastapi-pgstac (:8083) | 4 | 2,000 | 100 | 13,629 | 0 | 120 | 1,300 | 5,900 | 227 |
|
|
153
|
+
| stac-fastapi-pgstac (:8083) | 4 | 2,000 | 150 | 13,619 | 0 | 250 | 1,600 | 7,300 | 227 |
|
|
154
|
+
| stac-fastapi-os (:8085) | 4 | 2,000 | 10 | 49,492 | 0 | 9 | 25 | 32 | 824 |
|
|
155
|
+
| stac-fastapi-os (:8085) | 4 | 2,000 | 50 | 59,998 | 0 | 43 | 98 | 140 | 999 |
|
|
156
|
+
| stac-fastapi-os (:8085) | 4 | 2,000 | 100 | 58,755 | 0 | 90 | 170 | 220 | 978 |
|
|
157
|
+
| stac-fastapi-os (:8085) | 4 | 2,000 | 150 | 51,455 | 0 | 150 | 270 | 330 | 857 |
|
|
158
|
+
| stac-os-rs (:8087) | 4 | 2,000 | 10 | 100,517 | 0 | 5 | 10 | 12 | 1,674 |
|
|
159
|
+
| stac-os-rs (:8087) | 4 | 2,000 | 50 | 131,865 | 0 | 20 | 38 | 47 | 2,196 |
|
|
160
|
+
| stac-os-rs (:8087) | 4 | 2,000 | 100 | 130,389 | 0 | 40 | 68 | 82 | 2,170 |
|
|
161
|
+
| stac-os-rs (:8087) | 4 | 2,000 | 150 | 105,080 | 0 | 72 | 120 | 150 | 1,748 |
|
|
162
|
+
|
|
163
|
+
### Summary
|
|
164
|
+
|
|
165
|
+
stac-os-rs sustained ~2.2x the throughput of stac-fastapi-os and ~8.8x stac-fastapi-pgstac, peaking at ~2,200 req/s with zero failures across all twelve runs. All three backends saturate by ~100 users on this box — throughput knees at ~250 (pgstac), ~1,000 (fastapi-os), and ~2,200 (stac-os-rs) req/s; at 150 users each shows the classic overload curve with declining throughput and rising latency. The OpenSearch-backed APIs degrade without errors, as does pgstac — its earlier failures (CQL2 500s, dropped connections) traced to Docker's 64MB `/dev/shm` default starving Postgres parallel-query workers, fixed by `shm_size: 1gb` on the pgstac container; with that resolved its ~230 req/s ceiling is clean but its p99 still reaches 7.3s at 150 users. Running enough locust workers matters: in single-process runs the same setup measured only ~1,000 req/s because the load generator itself was CPU-bound — with `-w 8` no CPU warnings appeared and reported numbers reflect the backends. These numbers reflect a 2,000-item dataset on a single machine and should be treated as a relative comparison, not absolute capacity.
|
|
166
|
+
|
|
113
167
|
## Debugging Locust tasks using a single simulated user
|
|
114
168
|
```$ python stac_api_load_testing/config_files/locustfile.py```
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# stac-api-load-testing
|
|
2
|
+
Taurus and Locust load testing tools for stac-api backends.
|
|
3
|
+
|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
Requires Python >= 3.11.
|
|
7
|
+
|
|
8
|
+
## Run stac-fastapi backends locally (elasticsearch|opensearch|pgstac|mongo|stac-os-rs)
|
|
9
|
+
```$ docker compose up elasticsearch```
|
|
10
|
+
```$ docker compose up app-elasticsearch```
|
|
11
|
+
|
|
12
|
+
Backend ports: pgstac `:8083`, elasticsearch `:8084`, opensearch `:8085`, mongo `:8086`, stac-os-rs `:8087`
|
|
13
|
+
|
|
14
|
+
| backend | app image | database image |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| pgstac | stac-fastapi.pgstac 7.2.0 (built locally) | ghcr.io/stac-utils/pgstac:v0.10.0 |
|
|
17
|
+
| elasticsearch | ghcr.io/stac-utils/stac-fastapi-es:v8.1.1 | elasticsearch 9.3.2 |
|
|
18
|
+
| opensearch | ghcr.io/stac-utils/stac-fastapi-os:v8.1.1 | opensearch 3.8.0 |
|
|
19
|
+
| mongo | stac-fastapi.mongo 3.2.0 (built locally) | mongo 7.0 |
|
|
20
|
+
| stac-os-rs | ghcr.io/jonhealy1/stac-os-rs:0.2.0 | opensearch 3.8.0 |
|
|
21
|
+
|
|
22
|
+
All datastores are given a ~2GB memory budget for comparable benchmarks: ES/OpenSearch heaps `-Xms2g -Xmx2g` with `bootstrap.memory_lock`, pgstac `shared_buffers=2GB`, mongo `--wiredTigerCacheSizeGB 2`. Ingest the same data into each backend before comparing (`stac-api-load-testing --ingest --api-address <url>`).
|
|
23
|
+
|
|
24
|
+
## Install locally for development
|
|
25
|
+
```$ pip install -e .```
|
|
26
|
+
|
|
27
|
+
## Install from PyPI
|
|
28
|
+
```$ pip install stac-api-load-testing```
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Usage: stac-api-load-testing [OPTIONS]
|
|
32
|
+
|
|
33
|
+
Entry point for the stac-api-load-testing CLI tool.
|
|
34
|
+
|
|
35
|
+
This tool facilitates data ingestion, Locust load testing, and Taurus
|
|
36
|
+
performance testing against a specified STAC API endpoint.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
ingest (bool): If True, ingest sample data into the specified STAC API.
|
|
40
|
+
locust (bool): If True, execute Locust load tests against the
|
|
41
|
+
specified STAC API.
|
|
42
|
+
taurus (bool): If True, perform Taurus performance
|
|
43
|
+
testing with custom settings against the specified STAC API.
|
|
44
|
+
concurrency (int): Specifies the number of concurrent users for Taurus testing. Default is 10.
|
|
45
|
+
ramp_up (str): Specifies the ramp-up period for Taurus testing, in Taurus notation (e.g., '1m' for 1 minute). Default is '1m'.
|
|
46
|
+
iterations (int): Specifies the number of iterations each virtual user will
|
|
47
|
+
execute in Taurus testing. Default is 100.
|
|
48
|
+
api_address (str): The base URL of the STAC API to be tested.
|
|
49
|
+
|
|
50
|
+
Options:
|
|
51
|
+
-i, --ingest Ingest sample data into the STAC API.
|
|
52
|
+
--collections INTEGER Number of collections to create for --ingest.
|
|
53
|
+
--items-per-collection INTEGER
|
|
54
|
+
Items per collection for --ingest; beyond the
|
|
55
|
+
100 bundled items, duplicates are synthesized
|
|
56
|
+
with new ids and shifted geometry.
|
|
57
|
+
-l, --locust Run Locust load tests against the STAC API.
|
|
58
|
+
-t, --taurus Run the Taurus wrapper for performance testing
|
|
59
|
+
against the STAC API.
|
|
60
|
+
-c, --concurrency INTEGER Number of concurrent users for Taurus option.
|
|
61
|
+
-r, --ramp-up TEXT Ramp-up time for Taurus option.
|
|
62
|
+
-n, --iterations INTEGER Number of iterations for Taurus option.
|
|
63
|
+
-a, --api-address TEXT Specify the STAC API URL to test against.
|
|
64
|
+
Accepts a comma-separated list for --benchmark.
|
|
65
|
+
-b, --benchmark Run a headless locust benchmark sweep against the
|
|
66
|
+
STAC API.
|
|
67
|
+
-u, --users TEXT Comma-separated concurrency levels for
|
|
68
|
+
--benchmark.
|
|
69
|
+
--spawn-rate INTEGER Users spawned per second for --benchmark.
|
|
70
|
+
--run-time TEXT Duration of each --benchmark run, in locust
|
|
71
|
+
notation (e.g. '30s', '1m').
|
|
72
|
+
-w, --workers INTEGER Locust worker processes for --benchmark; >1
|
|
73
|
+
distributes load across cores on this machine.
|
|
74
|
+
-o, --output PATH Write aggregated --benchmark results to a CSV
|
|
75
|
+
file.
|
|
76
|
+
--version Show the version and exit.
|
|
77
|
+
--help Show this message and exit.
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Ingest test data - http://localhost:8084 is just an example url
|
|
81
|
+
```$ stac-api-load-testing --ingest --api-address http://localhost:8084```
|
|
82
|
+
|
|
83
|
+
Larger synthetic datasets: `--collections` creates `test-collection`, `test-collection-2`, ..., and `--items-per-collection` controls items per collection. Above the 100 bundled items, duplicates are derived from the sample features with new ids and randomly relocated geometry:
|
|
84
|
+
|
|
85
|
+
```$ stac-api-load-testing --ingest --api-address http://localhost:8084 --collections 2 --items-per-collection 500```
|
|
86
|
+
|
|
87
|
+
## Run Locust Load Testing Ouside of Taurus Wrapper
|
|
88
|
+
```$ stac-api-load-testing --locust --api-address http://localhost:8084```
|
|
89
|
+
- go to ```http://localhost:8089``` and start with desired settings
|
|
90
|
+
- or run headless: ```$ locust --locustfile stac_api_load_testing/config_files/locustfile.py --host http://localhost:8084 --headless -u 10 -r 1 -t 1m```
|
|
91
|
+
|
|
92
|
+
## Inside of Taurus Wrapper
|
|
93
|
+
```$ stac-api-load-testing --taurus --api-address http://localhost:8084```
|
|
94
|
+
|
|
95
|
+
## Benchmark / compare backends
|
|
96
|
+
Runs headless locust sweeps at each concurrency level and prints a comparison
|
|
97
|
+
table. Ingest into each backend first, then run them one at a time:
|
|
98
|
+
|
|
99
|
+
```$ stac-api-load-testing --benchmark --api-address http://localhost:8085 --users 10,50,100 --run-time 1m```
|
|
100
|
+
|
|
101
|
+
Compare multiple backends in one command (comma-separated addresses):
|
|
102
|
+
|
|
103
|
+
```$ stac-api-load-testing --benchmark --api-address http://localhost:8085,http://localhost:8087 --users 10,50,100 --run-time 1m -o results.csv```
|
|
104
|
+
|
|
105
|
+
## Example benchmark: pgstac vs opensearch vs stac-os-rs
|
|
106
|
+
|
|
107
|
+
Methodology used for the comparison below:
|
|
108
|
+
|
|
109
|
+
1. Start all three backends, each with its own datastore on a ~2GB memory budget (OpenSearch 3.8.0 `-Xms2g -Xmx2g`, pgstac `shared_buffers=2GB`):
|
|
110
|
+
|
|
111
|
+
```$ docker compose up -d opensearch opensearch-rs app-opensearch app-stac-os-rs pgstac app-pgstac```
|
|
112
|
+
|
|
113
|
+
2. Ingest the same dataset into each backend — 4 collections with 500 Sentinel-2 L2A items each (2,000 items total):
|
|
114
|
+
|
|
115
|
+
```$ stac-api-load-testing --ingest --api-address http://localhost:8083 --collections 4 --items-per-collection 500```
|
|
116
|
+
```$ stac-api-load-testing --ingest --api-address http://localhost:8085 --collections 4 --items-per-collection 500```
|
|
117
|
+
```$ stac-api-load-testing --ingest --api-address http://localhost:8087 --collections 4 --items-per-collection 500```
|
|
118
|
+
|
|
119
|
+
3. Run the benchmark sweep against all three in one command (10/50/100/150 users, spawn rate 10 users/s, 1 minute per run, 8 locust worker processes):
|
|
120
|
+
|
|
121
|
+
```$ stac-api-load-testing --benchmark --api-address stac-fastapi-pgstac=http://localhost:8083,stac-fastapi-os=http://localhost:8085,stac-os-rs=http://localhost:8087 --users 10,50,100,150 --run-time 1m -w 8 -o results.csv```
|
|
122
|
+
|
|
123
|
+
Environment: AMD Ryzen 7 8845HS (16 threads), 28GB RAM, Docker 29.8.0, locust 2.47.0 (`-w 8`), stac-fastapi.pgstac 7.2.0, stac-fastapi-os v8.1.1, stac-os-rs 0.5.0. The locustfile drives a read/search workload — landing page, collections, collection items, single items across all collections, GET/POST `/search` with bbox, intersects, sortby, datetime, CQL2, and ids queries, one level of `next`-link pagination, and filtered item lists; every search uses `limit=10`; write tasks are disabled.
|
|
124
|
+
|
|
125
|
+
Note on parity: stac-fastapi-os and stac-os-rs compute `numberMatched` (total-hit counting) on every search, while this stac-fastapi-pgstac version ships no context extension and skips count work entirely — so pgstac's reported numbers do not include a cost the OpenSearch-backed APIs pay per query.
|
|
126
|
+
|
|
127
|
+
### Results
|
|
128
|
+
|
|
129
|
+
| api | collections | items | users | requests | failures | median (ms) | p95 (ms) | p99 (ms) | req/s |
|
|
130
|
+
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
131
|
+
| stac-fastapi-pgstac (:8083) | 4 | 2,000 | 10 | 11,252 | 0 | 27 | 140 | 170 | 187 |
|
|
132
|
+
| stac-fastapi-pgstac (:8083) | 4 | 2,000 | 50 | 13,817 | 0 | 54 | 780 | 2,500 | 230 |
|
|
133
|
+
| stac-fastapi-pgstac (:8083) | 4 | 2,000 | 100 | 13,629 | 0 | 120 | 1,300 | 5,900 | 227 |
|
|
134
|
+
| stac-fastapi-pgstac (:8083) | 4 | 2,000 | 150 | 13,619 | 0 | 250 | 1,600 | 7,300 | 227 |
|
|
135
|
+
| stac-fastapi-os (:8085) | 4 | 2,000 | 10 | 49,492 | 0 | 9 | 25 | 32 | 824 |
|
|
136
|
+
| stac-fastapi-os (:8085) | 4 | 2,000 | 50 | 59,998 | 0 | 43 | 98 | 140 | 999 |
|
|
137
|
+
| stac-fastapi-os (:8085) | 4 | 2,000 | 100 | 58,755 | 0 | 90 | 170 | 220 | 978 |
|
|
138
|
+
| stac-fastapi-os (:8085) | 4 | 2,000 | 150 | 51,455 | 0 | 150 | 270 | 330 | 857 |
|
|
139
|
+
| stac-os-rs (:8087) | 4 | 2,000 | 10 | 100,517 | 0 | 5 | 10 | 12 | 1,674 |
|
|
140
|
+
| stac-os-rs (:8087) | 4 | 2,000 | 50 | 131,865 | 0 | 20 | 38 | 47 | 2,196 |
|
|
141
|
+
| stac-os-rs (:8087) | 4 | 2,000 | 100 | 130,389 | 0 | 40 | 68 | 82 | 2,170 |
|
|
142
|
+
| stac-os-rs (:8087) | 4 | 2,000 | 150 | 105,080 | 0 | 72 | 120 | 150 | 1,748 |
|
|
143
|
+
|
|
144
|
+
### Summary
|
|
145
|
+
|
|
146
|
+
stac-os-rs sustained ~2.2x the throughput of stac-fastapi-os and ~8.8x stac-fastapi-pgstac, peaking at ~2,200 req/s with zero failures across all twelve runs. All three backends saturate by ~100 users on this box — throughput knees at ~250 (pgstac), ~1,000 (fastapi-os), and ~2,200 (stac-os-rs) req/s; at 150 users each shows the classic overload curve with declining throughput and rising latency. The OpenSearch-backed APIs degrade without errors, as does pgstac — its earlier failures (CQL2 500s, dropped connections) traced to Docker's 64MB `/dev/shm` default starving Postgres parallel-query workers, fixed by `shm_size: 1gb` on the pgstac container; with that resolved its ~230 req/s ceiling is clean but its p99 still reaches 7.3s at 150 users. Running enough locust workers matters: in single-process runs the same setup measured only ~1,000 req/s because the load generator itself was CPU-bound — with `-w 8` no CPU warnings appeared and reported numbers reflect the backends. These numbers reflect a 2,000-item dataset on a single machine and should be treated as a relative comparison, not absolute capacity.
|
|
147
|
+
|
|
148
|
+
## Debugging Locust tasks using a single simulated user
|
|
149
|
+
```$ python stac_api_load_testing/config_files/locustfile.py```
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""stac-api-load-testing cli tool."""
|
|
2
2
|
|
|
3
3
|
import csv
|
|
4
|
+
import json
|
|
4
5
|
import os
|
|
5
6
|
import re
|
|
6
7
|
import shutil
|
|
@@ -11,6 +12,7 @@ from importlib import resources
|
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
|
|
13
14
|
import click
|
|
15
|
+
import requests
|
|
14
16
|
import yaml
|
|
15
17
|
|
|
16
18
|
from .data_loader import data_loader
|
|
@@ -74,12 +76,81 @@ def _read_aggregated_stats(csv_path: str) -> dict:
|
|
|
74
76
|
return {}
|
|
75
77
|
|
|
76
78
|
|
|
79
|
+
def _collection_ids(base_url: str) -> list:
|
|
80
|
+
"""Return all collection ids served by a STAC API."""
|
|
81
|
+
response = requests.get(f"{base_url}/collections", timeout=30)
|
|
82
|
+
response.raise_for_status()
|
|
83
|
+
return [c["id"] for c in response.json().get("collections", [])]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _collection_item_count(base_url: str, collection_id: str) -> int:
|
|
87
|
+
"""Count items in one collection, preferring the cheap numberMatched field."""
|
|
88
|
+
response = requests.post(
|
|
89
|
+
f"{base_url}/search",
|
|
90
|
+
json={"collections": [collection_id], "limit": 1},
|
|
91
|
+
timeout=30,
|
|
92
|
+
)
|
|
93
|
+
response.raise_for_status()
|
|
94
|
+
matched = response.json().get("numberMatched")
|
|
95
|
+
if matched is not None:
|
|
96
|
+
return matched
|
|
97
|
+
|
|
98
|
+
# APIs without the context extension: count by paging the items endpoint.
|
|
99
|
+
count = 0
|
|
100
|
+
url = f"{base_url}/collections/{collection_id}/items?limit=500"
|
|
101
|
+
for _ in range(100):
|
|
102
|
+
response = requests.get(url, timeout=60)
|
|
103
|
+
response.raise_for_status()
|
|
104
|
+
page = response.json()
|
|
105
|
+
count += len(page.get("features", []))
|
|
106
|
+
url = next(
|
|
107
|
+
(
|
|
108
|
+
link["href"]
|
|
109
|
+
for link in page.get("links", [])
|
|
110
|
+
if link.get("rel") == "next" and link.get("href")
|
|
111
|
+
),
|
|
112
|
+
None,
|
|
113
|
+
)
|
|
114
|
+
if url is None:
|
|
115
|
+
return count
|
|
116
|
+
return count
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _api_metadata(base_url: str) -> dict:
|
|
120
|
+
"""Fetch the catalog title and dataset size from a STAC API.
|
|
121
|
+
|
|
122
|
+
Returns a dict with 'name', 'collections' and 'items' keys; fields are
|
|
123
|
+
empty when the API is unreachable or doesn't report them.
|
|
124
|
+
"""
|
|
125
|
+
meta: dict[str, object] = {"name": "", "collections": "", "items": ""}
|
|
126
|
+
try:
|
|
127
|
+
response = requests.get(base_url, timeout=30)
|
|
128
|
+
response.raise_for_status()
|
|
129
|
+
meta["name"] = response.json().get("title", "")
|
|
130
|
+
except requests.RequestException as exc:
|
|
131
|
+
click.secho(f"could not fetch catalog info from {base_url}: {exc}", fg="yellow")
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
collection_ids = _collection_ids(base_url)
|
|
135
|
+
meta["collections"] = len(collection_ids)
|
|
136
|
+
counts = {
|
|
137
|
+
collection_id: _collection_item_count(base_url, collection_id)
|
|
138
|
+
for collection_id in collection_ids
|
|
139
|
+
}
|
|
140
|
+
meta["items"] = sum(counts.values())
|
|
141
|
+
meta["collection_counts"] = counts
|
|
142
|
+
except requests.RequestException as exc:
|
|
143
|
+
click.secho(f"could not count items in {base_url}: {exc}", fg="yellow")
|
|
144
|
+
return meta
|
|
145
|
+
|
|
146
|
+
|
|
77
147
|
def run_benchmark(
|
|
78
148
|
api_addresses: list,
|
|
79
149
|
concurrency_levels: list,
|
|
80
150
|
spawn_rate: int,
|
|
81
151
|
run_time: str,
|
|
82
152
|
output: str,
|
|
153
|
+
workers: int,
|
|
83
154
|
):
|
|
84
155
|
"""
|
|
85
156
|
Run headless Locust benchmark sweeps against one or more STAC APIs.
|
|
@@ -94,37 +165,56 @@ def run_benchmark(
|
|
|
94
165
|
spawn_rate (int): Users spawned per second in each run.
|
|
95
166
|
run_time (str): Duration of each run, in locust notation (e.g. '30s').
|
|
96
167
|
output (str): Optional path to write aggregated results as CSV.
|
|
168
|
+
workers (int): Locust worker processes (--processes); >1 runs
|
|
169
|
+
distributed on this machine to avoid a single-process CPU ceiling.
|
|
97
170
|
"""
|
|
98
171
|
locustfile_path = _package_resource("config_files", "locustfile.py")
|
|
99
172
|
results = []
|
|
100
173
|
|
|
101
|
-
for
|
|
174
|
+
for target in api_addresses:
|
|
175
|
+
# Targets can be labelled: "name=http://..." overrides the catalog title.
|
|
176
|
+
name, _, address = target.partition("=")
|
|
177
|
+
if not address:
|
|
178
|
+
name, address = "", target
|
|
179
|
+
|
|
180
|
+
meta = _api_metadata(address)
|
|
181
|
+
api_name = name or meta["name"] or address
|
|
182
|
+
# Locust workers derive item ids locally and need only the count.
|
|
183
|
+
os.environ["STAC_COLLECTION_COUNTS"] = json.dumps(
|
|
184
|
+
meta.get("collection_counts") or {}
|
|
185
|
+
)
|
|
186
|
+
click.secho(
|
|
187
|
+
f"\n=== {api_name} ({address}) — "
|
|
188
|
+
f"{meta['collections']} collections, {meta['items']} items ===",
|
|
189
|
+
fg="cyan",
|
|
190
|
+
)
|
|
102
191
|
for users in concurrency_levels:
|
|
103
192
|
click.secho(f"\n>>> {address} @ {users} users for {run_time}", fg="cyan")
|
|
104
193
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
105
194
|
csv_prefix = os.path.join(tmpdir, "locust")
|
|
106
195
|
stats_csv = f"{csv_prefix}_stats.csv"
|
|
196
|
+
cmd = [
|
|
197
|
+
_tool_path("locust"),
|
|
198
|
+
"--locustfile",
|
|
199
|
+
locustfile_path,
|
|
200
|
+
"--host",
|
|
201
|
+
address,
|
|
202
|
+
"--headless",
|
|
203
|
+
"-u",
|
|
204
|
+
str(users),
|
|
205
|
+
"-r",
|
|
206
|
+
str(spawn_rate),
|
|
207
|
+
"-t",
|
|
208
|
+
run_time,
|
|
209
|
+
"--csv",
|
|
210
|
+
csv_prefix,
|
|
211
|
+
"--only-summary",
|
|
212
|
+
]
|
|
213
|
+
if workers > 1:
|
|
214
|
+
cmd += ["--processes", str(workers)]
|
|
107
215
|
# locust exits non-zero when requests fail; under load that is
|
|
108
216
|
# data (backend errors), not a benchmark tool error.
|
|
109
|
-
subprocess.run(
|
|
110
|
-
[
|
|
111
|
-
_tool_path("locust"),
|
|
112
|
-
"--locustfile",
|
|
113
|
-
locustfile_path,
|
|
114
|
-
"--host",
|
|
115
|
-
address,
|
|
116
|
-
"--headless",
|
|
117
|
-
"-u",
|
|
118
|
-
str(users),
|
|
119
|
-
"-r",
|
|
120
|
-
str(spawn_rate),
|
|
121
|
-
"-t",
|
|
122
|
-
run_time,
|
|
123
|
-
"--csv",
|
|
124
|
-
csv_prefix,
|
|
125
|
-
"--only-summary",
|
|
126
|
-
]
|
|
127
|
-
)
|
|
217
|
+
subprocess.run(cmd)
|
|
128
218
|
|
|
129
219
|
if not os.path.exists(stats_csv):
|
|
130
220
|
click.secho(
|
|
@@ -137,7 +227,10 @@ def run_benchmark(
|
|
|
137
227
|
if stats:
|
|
138
228
|
results.append(
|
|
139
229
|
{
|
|
140
|
-
"api":
|
|
230
|
+
"api": api_name,
|
|
231
|
+
"url": address,
|
|
232
|
+
"collections": meta["collections"],
|
|
233
|
+
"items": meta["items"],
|
|
141
234
|
"users": users,
|
|
142
235
|
"requests": stats.get("Request Count"),
|
|
143
236
|
"failures": stats.get("Failure Count"),
|
|
@@ -152,12 +245,16 @@ def run_benchmark(
|
|
|
152
245
|
click.secho("No benchmark results collected.", fg="red")
|
|
153
246
|
return
|
|
154
247
|
|
|
155
|
-
header =
|
|
248
|
+
header = (
|
|
249
|
+
f"{'api':<36}{'colls':>6}{'items':>7}{'users':>7}{'reqs':>9}"
|
|
250
|
+
f"{'fails':>7}{'med':>8}{'p95':>8}{'p99':>8}{'rps':>9}"
|
|
251
|
+
)
|
|
156
252
|
click.echo("\n" + header)
|
|
157
253
|
click.echo("-" * len(header))
|
|
158
254
|
for row in results:
|
|
159
255
|
click.echo(
|
|
160
|
-
f"{row['api']:<
|
|
256
|
+
f"{row['api']:<36}{row['collections']:>6}{row['items']:>7}"
|
|
257
|
+
f"{row['users']:>7}{row['requests']:>9}"
|
|
161
258
|
f"{row['failures']:>7}{row['median_ms']:>8}{row['p95_ms']:>8}"
|
|
162
259
|
f"{row['p99_ms']:>8}{row['rps']:>9}"
|
|
163
260
|
)
|
|
@@ -202,7 +299,19 @@ def run_benchmark(
|
|
|
202
299
|
"-a",
|
|
203
300
|
"--api-address",
|
|
204
301
|
default="http://localhost:8080",
|
|
205
|
-
help="Specify the STAC API URL to test against. Accepts a comma-separated list for --benchmark.",
|
|
302
|
+
help="Specify the STAC API URL to test against. Accepts a comma-separated list for --benchmark; prefix with 'name=' to label a target in results.",
|
|
303
|
+
)
|
|
304
|
+
@click.option(
|
|
305
|
+
"--collections",
|
|
306
|
+
default=1,
|
|
307
|
+
help="Number of collections to create for --ingest (test-collection, test-collection-2, ...).",
|
|
308
|
+
type=int,
|
|
309
|
+
)
|
|
310
|
+
@click.option(
|
|
311
|
+
"--items-per-collection",
|
|
312
|
+
default=100,
|
|
313
|
+
help="Items to load per collection for --ingest; beyond the 100 bundled items, duplicates are synthesized with new ids and shifted geometry.",
|
|
314
|
+
type=int,
|
|
206
315
|
)
|
|
207
316
|
@click.option(
|
|
208
317
|
"-b",
|
|
@@ -227,6 +336,13 @@ def run_benchmark(
|
|
|
227
336
|
default="30s",
|
|
228
337
|
help="Duration of each --benchmark run, in locust notation (e.g. '30s', '1m').",
|
|
229
338
|
)
|
|
339
|
+
@click.option(
|
|
340
|
+
"-w",
|
|
341
|
+
"--workers",
|
|
342
|
+
default=1,
|
|
343
|
+
help="Locust worker processes for --benchmark (--processes); >1 distributes load across cores on this machine.",
|
|
344
|
+
type=int,
|
|
345
|
+
)
|
|
230
346
|
@click.option(
|
|
231
347
|
"-o",
|
|
232
348
|
"--output",
|
|
@@ -234,7 +350,7 @@ def run_benchmark(
|
|
|
234
350
|
help="Write aggregated --benchmark results to a CSV file.",
|
|
235
351
|
type=click.Path(),
|
|
236
352
|
)
|
|
237
|
-
@click.version_option(version="0.
|
|
353
|
+
@click.version_option(version="0.4.0")
|
|
238
354
|
def main(
|
|
239
355
|
ingest: bool,
|
|
240
356
|
locust: bool,
|
|
@@ -243,10 +359,13 @@ def main(
|
|
|
243
359
|
concurrency: int,
|
|
244
360
|
ramp_up: str,
|
|
245
361
|
iterations: int,
|
|
362
|
+
collections: int,
|
|
363
|
+
items_per_collection: int,
|
|
246
364
|
benchmark: bool,
|
|
247
365
|
users: str,
|
|
248
366
|
spawn_rate: int,
|
|
249
367
|
run_time: str,
|
|
368
|
+
workers: int,
|
|
250
369
|
output: str,
|
|
251
370
|
):
|
|
252
371
|
"""
|
|
@@ -262,18 +381,26 @@ def main(
|
|
|
262
381
|
concurrency (int): Specifies the number of concurrent users for Taurus testing. Default is 10.
|
|
263
382
|
ramp_up (str): Specifies the ramp-up period for Taurus testing, in Taurus notation (e.g., '1m' for 1 minute). Default is '1m'.
|
|
264
383
|
iterations (int): Specifies the number of iterations each virtual user will execute in Taurus testing. Default is 100.
|
|
384
|
+
collections (int): Number of collections to create for ingest. Default 1.
|
|
385
|
+
items_per_collection (int): Items per collection for ingest. Default 100;
|
|
386
|
+
above 100, duplicates are synthesized with new ids and shifted geometry.
|
|
265
387
|
api_address (str): The base URL of the STAC API to be tested. Comma-separated list allowed for benchmark.
|
|
266
388
|
benchmark (bool): If True, run headless locust runs sweeping --users levels and print a comparison table.
|
|
267
389
|
users (str): Comma-separated concurrency levels for benchmark. Default '10,50,100'.
|
|
268
390
|
spawn_rate (int): Users spawned per second in each benchmark run. Default 10.
|
|
269
391
|
run_time (str): Duration of each benchmark run in locust notation. Default '30s'.
|
|
392
|
+
workers (int): Locust worker processes per benchmark run. Default 1.
|
|
270
393
|
output (str): Optional CSV file path for aggregated benchmark results.
|
|
271
394
|
"""
|
|
272
395
|
os.environ["LOCUST_HOST"] = api_address
|
|
273
396
|
|
|
274
397
|
if ingest:
|
|
275
398
|
# Load data into the STAC API
|
|
276
|
-
data_loader.load_items(
|
|
399
|
+
data_loader.load_items(
|
|
400
|
+
stac_api_base_url=api_address,
|
|
401
|
+
num_collections=collections,
|
|
402
|
+
items_per_collection=items_per_collection,
|
|
403
|
+
)
|
|
277
404
|
elif locust:
|
|
278
405
|
# Execute Locust load tests
|
|
279
406
|
locust_file_path = _package_resource("config_files", "locustfile.py")
|
|
@@ -301,7 +428,9 @@ def main(
|
|
|
301
428
|
elif benchmark:
|
|
302
429
|
concurrency_levels = [int(x) for x in users.split(",")]
|
|
303
430
|
api_addresses = [a.strip() for a in api_address.split(",")]
|
|
304
|
-
run_benchmark(
|
|
431
|
+
run_benchmark(
|
|
432
|
+
api_addresses, concurrency_levels, spawn_rate, run_time, output, workers
|
|
433
|
+
)
|
|
305
434
|
|
|
306
435
|
|
|
307
436
|
if __name__ == "__main__":
|