stac-api-load-testing 0.3.1__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.
Files changed (20) hide show
  1. {stac_api_load_testing-0.3.1/stac_api_load_testing.egg-info → stac_api_load_testing-0.4.0}/PKG-INFO +39 -19
  2. {stac_api_load_testing-0.3.1 → stac_api_load_testing-0.4.0}/README.md +38 -18
  3. {stac_api_load_testing-0.3.1 → stac_api_load_testing-0.4.0}/pyproject.toml +1 -1
  4. {stac_api_load_testing-0.3.1 → stac_api_load_testing-0.4.0}/stac_api_load_testing/cli.py +156 -27
  5. {stac_api_load_testing-0.3.1 → stac_api_load_testing-0.4.0}/stac_api_load_testing/config_files/locustfile.py +297 -30
  6. stac_api_load_testing-0.4.0/stac_api_load_testing/data_loader/data_loader.py +118 -0
  7. {stac_api_load_testing-0.3.1 → stac_api_load_testing-0.4.0/stac_api_load_testing.egg-info}/PKG-INFO +39 -19
  8. stac_api_load_testing-0.3.1/stac_api_load_testing/data_loader/data_loader.py +0 -60
  9. {stac_api_load_testing-0.3.1 → stac_api_load_testing-0.4.0}/LICENSE +0 -0
  10. {stac_api_load_testing-0.3.1 → stac_api_load_testing-0.4.0}/setup.cfg +0 -0
  11. {stac_api_load_testing-0.3.1 → stac_api_load_testing-0.4.0}/stac_api_load_testing/__init__.py +0 -0
  12. {stac_api_load_testing-0.3.1 → stac_api_load_testing-0.4.0}/stac_api_load_testing/config_files/taurus_locust.yml +0 -0
  13. {stac_api_load_testing-0.3.1 → stac_api_load_testing-0.4.0}/stac_api_load_testing/data_loader/__init__.py +0 -0
  14. {stac_api_load_testing-0.3.1 → stac_api_load_testing-0.4.0}/stac_api_load_testing/data_loader/setup_data/collection.json +0 -0
  15. {stac_api_load_testing-0.3.1 → stac_api_load_testing-0.4.0}/stac_api_load_testing/data_loader/setup_data/sentinel-s2-l2a-cogs_0_100.json +0 -0
  16. {stac_api_load_testing-0.3.1 → stac_api_load_testing-0.4.0}/stac_api_load_testing.egg-info/SOURCES.txt +0 -0
  17. {stac_api_load_testing-0.3.1 → stac_api_load_testing-0.4.0}/stac_api_load_testing.egg-info/dependency_links.txt +0 -0
  18. {stac_api_load_testing-0.3.1 → stac_api_load_testing-0.4.0}/stac_api_load_testing.egg-info/entry_points.txt +0 -0
  19. {stac_api_load_testing-0.3.1 → stac_api_load_testing-0.4.0}/stac_api_load_testing.egg-info/requires.txt +0 -0
  20. {stac_api_load_testing-0.3.1 → stac_api_load_testing-0.4.0}/stac_api_load_testing.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stac-api-load-testing
3
- Version: 0.3.1
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,39 +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
 
113
- ## Example benchmark: stac-fastapi-os vs stac-os-rs
124
+ ## Example benchmark: pgstac vs opensearch vs stac-os-rs
114
125
 
115
126
  Methodology used for the comparison below:
116
127
 
117
- 1. Start each backend with its own dedicated OpenSearch 3.8.0 instance (2GB heap each):
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```
118
131
 
119
- ```$ docker compose up -d opensearch opensearch-rs app-opensearch app-stac-os-rs```
132
+ 2. Ingest the same dataset into each backend — 4 collections with 500 Sentinel-2 L2A items each (2,000 items total):
120
133
 
121
- 2. Ingest the same dataset into both backends — 1 collection (`test-collection`) with 100 Sentinel-2 L2A items:
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```
122
137
 
123
- ```$ stac-api-load-testing --ingest --api-address http://localhost:8085```
124
- ```$ stac-api-load-testing --ingest --api-address http://localhost:8087```
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):
125
139
 
126
- 3. Run the benchmark sweep against both in one command (10/50/100 users, spawn rate 10 users/s, 1 minute per run):
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```
127
141
 
128
- ```$ stac-api-load-testing --benchmark --api-address http://localhost:8085,http://localhost:8087 --users 10,50,100 --run-time 1m -o results.csv```
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.
129
143
 
130
- Environment: AMD Ryzen 7 8845HS (16 threads), 28GB RAM, Docker 29.8.0, locust 2.47.0, stac-fastapi-os v8.1.1, stac-os-rs 0.2.0. The locustfile drives a read/search workload — landing page, collections, collection items, single items, and GET/POST `/search` with bbox, intersects, and sortby queries; write tasks are disabled.
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.
131
145
 
132
146
  ### Results
133
147
 
134
- | api | users | requests | failures | median (ms) | p95 (ms) | p99 (ms) | req/s |
135
- | --- | --- | --- | --- | --- | --- | --- | --- |
136
- | stac-fastapi-os (:8085) | 10 | 48,672 | 0 | 9 | 24 | 32 | 822 |
137
- | stac-fastapi-os (:8085) | 50 | 51,659 | 0 | 48 | 88 | 110 | 864 |
138
- | stac-fastapi-os (:8085) | 100 | 47,758 | 0 | 100 | 170 | 210 | 800 |
139
- | stac-os-rs (:8087) | 10 | 75,601 | 0 | 6 | 19 | 36 | 1,276 |
140
- | stac-os-rs (:8087) | 50 | 75,769 | 0 | 33 | 62 | 190 | 1,264 |
141
- | stac-os-rs (:8087) | 100 | 72,621 | 0 | 64 | 120 | 360 | 1,220 |
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 |
142
162
 
143
163
  ### Summary
144
164
 
145
- stac-os-rs sustained ~45-55% higher throughput than stac-fastapi-os at every concurrency level (peaks of ~1,270 vs ~860 req/s) with lower median latency, and zero failures on either backend. stac-fastapi-os saturates near 50 users — median latency climbs 9→48→100ms while throughput stays flat — whereas stac-os-rs plateaus at a higher request rate. Caveat: locust reported >90% CPU on the load generator during all stac-os-rs runs, so its true ceiling is likely higher than measured; distributed locust workers would give a more precise figure. These numbers reflect a small (100-item) dataset on a single machine and should be treated as relative comparison, not absolute capacity.
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.
146
166
 
147
167
  ## Debugging Locust tasks using a single simulated user
148
168
  ```$ python stac_api_load_testing/config_files/locustfile.py```
@@ -49,6 +49,11 @@ Usage: stac-api-load-testing [OPTIONS]
49
49
 
50
50
  Options:
51
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.
52
57
  -l, --locust Run Locust load tests against the STAC API.
53
58
  -t, --taurus Run the Taurus wrapper for performance testing
54
59
  against the STAC API.
@@ -64,6 +69,8 @@ Options:
64
69
  --spawn-rate INTEGER Users spawned per second for --benchmark.
65
70
  --run-time TEXT Duration of each --benchmark run, in locust
66
71
  notation (e.g. '30s', '1m').
72
+ -w, --workers INTEGER Locust worker processes for --benchmark; >1
73
+ distributes load across cores on this machine.
67
74
  -o, --output PATH Write aggregated --benchmark results to a CSV
68
75
  file.
69
76
  --version Show the version and exit.
@@ -73,6 +80,10 @@ Options:
73
80
  ## Ingest test data - http://localhost:8084 is just an example url
74
81
  ```$ stac-api-load-testing --ingest --api-address http://localhost:8084```
75
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
+
76
87
  ## Run Locust Load Testing Ouside of Taurus Wrapper
77
88
  ```$ stac-api-load-testing --locust --api-address http://localhost:8084```
78
89
  - go to ```http://localhost:8089``` and start with desired settings
@@ -91,39 +102,48 @@ Compare multiple backends in one command (comma-separated addresses):
91
102
 
92
103
  ```$ stac-api-load-testing --benchmark --api-address http://localhost:8085,http://localhost:8087 --users 10,50,100 --run-time 1m -o results.csv```
93
104
 
94
- ## Example benchmark: stac-fastapi-os vs stac-os-rs
105
+ ## Example benchmark: pgstac vs opensearch vs stac-os-rs
95
106
 
96
107
  Methodology used for the comparison below:
97
108
 
98
- 1. Start each backend with its own dedicated OpenSearch 3.8.0 instance (2GB heap each):
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```
99
112
 
100
- ```$ docker compose up -d opensearch opensearch-rs app-opensearch app-stac-os-rs```
113
+ 2. Ingest the same dataset into each backend — 4 collections with 500 Sentinel-2 L2A items each (2,000 items total):
101
114
 
102
- 2. Ingest the same dataset into both backends — 1 collection (`test-collection`) with 100 Sentinel-2 L2A items:
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```
103
118
 
104
- ```$ stac-api-load-testing --ingest --api-address http://localhost:8085```
105
- ```$ stac-api-load-testing --ingest --api-address http://localhost:8087```
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):
106
120
 
107
- 3. Run the benchmark sweep against both in one command (10/50/100 users, spawn rate 10 users/s, 1 minute per run):
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```
108
122
 
109
- ```$ stac-api-load-testing --benchmark --api-address http://localhost:8085,http://localhost:8087 --users 10,50,100 --run-time 1m -o results.csv```
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.
110
124
 
111
- Environment: AMD Ryzen 7 8845HS (16 threads), 28GB RAM, Docker 29.8.0, locust 2.47.0, stac-fastapi-os v8.1.1, stac-os-rs 0.2.0. The locustfile drives a read/search workload — landing page, collections, collection items, single items, and GET/POST `/search` with bbox, intersects, and sortby queries; write tasks are disabled.
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.
112
126
 
113
127
  ### Results
114
128
 
115
- | api | users | requests | failures | median (ms) | p95 (ms) | p99 (ms) | req/s |
116
- | --- | --- | --- | --- | --- | --- | --- | --- |
117
- | stac-fastapi-os (:8085) | 10 | 48,672 | 0 | 9 | 24 | 32 | 822 |
118
- | stac-fastapi-os (:8085) | 50 | 51,659 | 0 | 48 | 88 | 110 | 864 |
119
- | stac-fastapi-os (:8085) | 100 | 47,758 | 0 | 100 | 170 | 210 | 800 |
120
- | stac-os-rs (:8087) | 10 | 75,601 | 0 | 6 | 19 | 36 | 1,276 |
121
- | stac-os-rs (:8087) | 50 | 75,769 | 0 | 33 | 62 | 190 | 1,264 |
122
- | stac-os-rs (:8087) | 100 | 72,621 | 0 | 64 | 120 | 360 | 1,220 |
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 |
123
143
 
124
144
  ### Summary
125
145
 
126
- stac-os-rs sustained ~45-55% higher throughput than stac-fastapi-os at every concurrency level (peaks of ~1,270 vs ~860 req/s) with lower median latency, and zero failures on either backend. stac-fastapi-os saturates near 50 users — median latency climbs 9→48→100ms while throughput stays flat — whereas stac-os-rs plateaus at a higher request rate. Caveat: locust reported >90% CPU on the load generator during all stac-os-rs runs, so its true ceiling is likely higher than measured; distributed locust workers would give a more precise figure. These numbers reflect a small (100-item) dataset on a single machine and should be treated as relative comparison, not absolute capacity.
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.
127
147
 
128
148
  ## Debugging Locust tasks using a single simulated user
129
149
  ```$ python stac_api_load_testing/config_files/locustfile.py```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "stac-api-load-testing"
7
- version = "0.3.1"
7
+ version = "0.4.0"
8
8
  description = "Load testing for stac compliant apis"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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 address in api_addresses:
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": address,
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 = f"{'api':<26}{'users':>7}{'reqs':>9}{'fails':>7}{'med':>8}{'p95':>8}{'p99':>8}{'rps':>9}"
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']:<26}{row['users']:>7}{row['requests']:>9}"
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.3.1")
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(stac_api_base_url=api_address)
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(api_addresses, concurrency_levels, spawn_rate, run_time, output)
431
+ run_benchmark(
432
+ api_addresses, concurrency_levels, spawn_rate, run_time, output, workers
433
+ )
305
434
 
306
435
 
307
436
  if __name__ == "__main__":