New posts

This commit is contained in:
2026-11-06 21:43:41 +05:30
parent 11782a7ec3
commit c92977b23d
38 changed files with 2006 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
# gatling-spring-boot
Companion code for the ankurm.com post *Load Testing Spring Boot with Gatling's Java DSL (and How It Compares to k6)*.
A tiny Spring Boot app whose `/api/quote/{symbol}` endpoint calls a simulated slow dependency with limited capacity
(8 workers x 50 ms = about 160 requests/s, queue of 100, then HTTP 503), plus Gatling Java DSL simulations that
find the knee, and the same test written for k6.
| Tool | Version | Source |
|---|---|---|
| Spring Boot | 4.1.2 | `maven-metadata.xml` (latest GA; 4.2.0-RC2 is not GA) |
| Gatling (`gatling-charts-highcharts`) | 3.16.0 | `maven-metadata.xml`; the report itself says released 2026-09-29 |
| `gatling-maven-plugin` | 4.21.12 | `maven-metadata.xml` |
| k6 | v2.3.0 | `dl.k6.io` deb package, unpacked without installing |
| JDK | 25 | |
## Run
```bash
./scripts/run-all.sh # about 6 minutes; regenerates everything in output/
K6=/path/to/k6 ./scripts/run-all.sh # include the k6 steps (skipped when k6 is not found)
```
Load numbers depend on the machine: treat them as indicative. Single runs: `mvn test-compile gatling:test -Dgatling.simulationClass=lab.gatling.KneeStepsSimulation`
(start the app first: `mvn package -DskipTests && java -jar target/gatling-spring-boot-lab-1.0.0.jar`).
## Files
- `src/main/java/lab/gatling/` the app: `DownstreamPool` (the bounded pool), `QuoteController`
- `src/test/java/lab/gatling/` simulations `BasicSimulation`, `KneeStepsSimulation`, `KneeRampSimulation`, `ClosedModelSimulation`, `FailingAssertionSimulation`, and `SaturationTest` (JUnit)
- `k6/knee.js`, `k6/threshold-fail.js` the k6 equivalents
- `scripts/run-all.sh`, `scripts/analyze.py` run everything, turn reports into tables and the chart
- `output/` captured real output of the last run
+49
View File
@@ -0,0 +1,49 @@
// k6 equivalent of KneeStepsSimulation: open model, 30 -> 240 requests/s in 8 steps,
// each held 6 s and separated by 2 s ramps. Every request is tagged with the step it was sent in.
import http from 'k6/http';
import exec from 'k6/execution';
import { check } from 'k6';
import { SharedArray } from 'k6/data';
const BASE = __ENV.APP_URL || 'http://localhost:18300';
const STEPS = 8, RATE_STEP = 30, HOLD_S = 6, RAMP_S = 2;
// the feeder: same CSV Gatling reads
const symbols = new SharedArray('symbols', () =>
open('../src/test/resources/data/symbols.csv').trim().split('\n').slice(1));
// k6 treats only these statuses as success, so a 503 counts in http_req_failed
http.setResponseCallback(http.expectedStatuses(200));
const stages = [];
for (let i = 0; i < STEPS; i++) {
if (i > 0) stages.push({ duration: `${RAMP_S}s`, target: RATE_STEP * (i + 1) });
stages.push({ duration: `${HOLD_S}s`, target: RATE_STEP * (i + 1) });
}
export const options = {
scenarios: {
knee: {
executor: 'ramping-arrival-rate', // open model: iterations start on a schedule
startRate: RATE_STEP,
timeUnit: '1s',
preAllocatedVUs: 200,
maxVUs: 1000,
stages,
},
},
};
function stepLabel() {
const elapsed = Date.now() - exec.scenario.startTime;
const slot = Math.floor(elapsed / ((HOLD_S + RAMP_S) * 1000));
const into = elapsed % ((HOLD_S + RAMP_S) * 1000);
if (into >= HOLD_S * 1000 || slot >= STEPS) return 'ramp';
return `rate ${String(RATE_STEP * (slot + 1)).padStart(3, '0')}/s`;
}
export default function () {
const symbol = symbols[Math.floor(Math.random() * symbols.length)];
const res = http.get(`${BASE}/api/quote/${symbol}`, { tags: { step: stepLabel() } });
check(res, { 'status is 200': (r) => r.status === 200 });
}
+31
View File
@@ -0,0 +1,31 @@
// k6 equivalent of FailingAssertionSimulation: 220 requests/s against ~160/s of capacity,
// with thresholds that cannot hold. k6 exits with code 99 when a threshold fails.
import http from 'k6/http';
import { SharedArray } from 'k6/data';
const BASE = __ENV.APP_URL || 'http://localhost:18300';
const symbols = new SharedArray('symbols', () =>
open('../src/test/resources/data/symbols.csv').trim().split('\n').slice(1));
http.setResponseCallback(http.expectedStatuses(200));
export const options = {
scenarios: {
overload: {
executor: 'constant-arrival-rate',
rate: 220,
timeUnit: '1s',
duration: '15s',
preAllocatedVUs: 200,
maxVUs: 1000,
},
},
thresholds: {
http_req_duration: ['p(95)<200'],
http_req_failed: ['rate<0.01'],
},
};
export default function () {
http.get(`${BASE}/api/quote/${symbols[Math.floor(Math.random() * symbols.length)]}`);
}
@@ -0,0 +1,3 @@
workers=2 queue=3 delay=400ms, 7 simultaneous callers
served=5 rejected=2
pool stats: completed=5 rejected=2
@@ -0,0 +1 @@
{"symbol":"ORCL","price":344.44}
@@ -0,0 +1,124 @@
[INFO] Scanning for projects...
[INFO]
[INFO] -------------< com.ankurm.testing:gatling-spring-boot-lab >-------------
[INFO] Building gatling-spring-boot-lab 1.0.0
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- resources:3.5.0:resources (default-resources) @ gatling-spring-boot-lab ---
[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO] Copying 0 resource from src/main/resources to target/classes
[INFO]
[INFO] --- compiler:3.15.0:compile (default-compile) @ gatling-spring-boot-lab ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] --- resources:3.5.0:testResources (default-testResources) @ gatling-spring-boot-lab ---
[INFO] Copying 2 resources from src/test/resources to target/test-classes
[INFO]
[INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ gatling-spring-boot-lab ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] >>> gatling:4.21.12:test (default-cli) > test-compile @ gatling-spring-boot-lab >>>
[INFO]
[INFO] --- resources:3.5.0:resources (default-resources) @ gatling-spring-boot-lab ---
[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO] Copying 0 resource from src/main/resources to target/classes
[INFO]
[INFO] --- compiler:3.15.0:compile (default-compile) @ gatling-spring-boot-lab ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] --- resources:3.5.0:testResources (default-testResources) @ gatling-spring-boot-lab ---
[INFO] Copying 2 resources from src/test/resources to target/test-classes
[INFO]
[INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ gatling-spring-boot-lab ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] <<< gatling:4.21.12:test (default-cli) < test-compile @ gatling-spring-boot-lab <<<
[INFO]
[INFO]
[INFO] --- gatling:4.21.12:test (default-cli) @ gatling-spring-boot-lab ---
[INFO] Running simulation lab.gatling.BasicSimulation.
========================================================================================================================
2026-11-06 13:22:03 GMT 5s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 32 | 32 | 0
> get quote | 17 | 17 | 0
> get another quote | 15 | 15 | 0
---- Browse quotes -----------------------------------------------------------------------------------------------------
[#############|| ] 12%
waiting: 108 / active: 2 / done: 15
========================================================================================================================
========================================================================================================================
2026-11-06 13:22:08 GMT 10s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 107 | 107 | 0
> get quote | 55 | 55 | 0
> get another quote | 52 | 52 | 0
---- Browse quotes -----------------------------------------------------------------------------------------------------
[##############################################||| ] 41.6%
waiting: 70 / active: 3 / done: 52
========================================================================================================================
========================================================================================================================
2026-11-06 13:22:13 GMT 15s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 206 | 206 | 0
> get quote | 105 | 105 | 0
> get another quote | 101 | 101 | 0
---- Browse quotes -----------------------------------------------------------------------------------------------------
[#########################################################################################|||| ] 80.8%
waiting: 20 / active: 4 / done: 101
========================================================================================================================
========================================================================================================================
2026-11-06 13:22:15 GMT 17s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 250 | 250 | 0
> get quote | 125 | 125 | 0
> get another quote | 125 | 125 | 0
---- Browse quotes -----------------------------------------------------------------------------------------------------
[###############################################################################################################] 100%
waiting: 0 / active: 0 / done: 125
========================================================================================================================
Parsing log file(s)...
Parsing log file(s) done in 0s.
Generating reports...
========================================================================================================================
---- Global Information -------------------------------------------------------------|---Total---|-----OK----|----KO----
> request count | 250 | 250 | -
> min response time (ms) | 48 | 48 | -
> max response time (ms) | 161 | 161 | -
> mean response time (ms) | 57 | 57 | -
> response time std deviation (ms) | 8 | 8 | -
> response time 50th percentile (ms) | 56 | 56 | -
> response time 75th percentile (ms) | 58 | 58 | -
> response time 95th percentile (ms) | 62 | 62 | -
> response time 99th percentile (ms) | 87 | 87 | -
> mean throughput (rps) | 13.89 | 13.89 | -
---- Response Time Distribution ----------------------------------------------------------------------------------------
> OK: t < 800 ms 250 (100%)
> OK: 800 ms <= t < 1200 ms 0 (0%)
> OK: t >= 1200 ms 0 (0%)
> KO 0 (0%)
========================================================================================================================
Reports generated, please open the following file: file:///tmp/work/3/repos/asmhatre/java-testing-lab/gatling-spring-boot/target/gatling/basicsimulation-20261106132157606/index.html
Global: 95th percentile of response time is less than 200.0 : true (actual : 62.0)
Global: percentage of successful events is greater than 99.0 : true (actual : 100.0)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 22.242 s
[INFO] Finished at: 2026-11-06T18:52:15+05:30
[INFO] ------------------------------------------------------------------------
@@ -0,0 +1,299 @@
[INFO] Scanning for projects...
[INFO]
[INFO] -------------< com.ankurm.testing:gatling-spring-boot-lab >-------------
[INFO] Building gatling-spring-boot-lab 1.0.0
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- resources:3.5.0:resources (default-resources) @ gatling-spring-boot-lab ---
[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO] Copying 0 resource from src/main/resources to target/classes
[INFO]
[INFO] --- compiler:3.15.0:compile (default-compile) @ gatling-spring-boot-lab ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] --- resources:3.5.0:testResources (default-testResources) @ gatling-spring-boot-lab ---
[INFO] Copying 2 resources from src/test/resources to target/test-classes
[INFO]
[INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ gatling-spring-boot-lab ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] >>> gatling:4.21.12:test (default-cli) > test-compile @ gatling-spring-boot-lab >>>
[INFO]
[INFO] --- resources:3.5.0:resources (default-resources) @ gatling-spring-boot-lab ---
[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO] Copying 0 resource from src/main/resources to target/classes
[INFO]
[INFO] --- compiler:3.15.0:compile (default-compile) @ gatling-spring-boot-lab ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] --- resources:3.5.0:testResources (default-testResources) @ gatling-spring-boot-lab ---
[INFO] Copying 2 resources from src/test/resources to target/test-classes
[INFO]
[INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ gatling-spring-boot-lab ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] <<< gatling:4.21.12:test (default-cli) < test-compile @ gatling-spring-boot-lab <<<
[INFO]
[INFO]
[INFO] --- gatling:4.21.12:test (default-cli) @ gatling-spring-boot-lab ---
[INFO] Running simulation lab.gatling.KneeStepsSimulation.
========================================================================================================================
2026-11-06 13:22:26 GMT 5s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 149 | 149 | 0
> quote rate 030/s | 149 | 149 | 0
---- One quote per user ------------------------------------------------------------------------------------------------
[#| ] 1.78%
waiting: 8,220 / active: 1 / done: 149
========================================================================================================================
========================================================================================================================
2026-11-06 13:22:31 GMT 10s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 387 | 387 | 0
> quote rate 030/s | 182 | 182 | 0
> quote ramp | 91 | 91 | 0
> quote rate 060/s | 114 | 114 | 0
---- One quote per user ------------------------------------------------------------------------------------------------
[#####| ] 4.62%
waiting: 7,980 / active: 3 / done: 387
========================================================================================================================
========================================================================================================================
2026-11-06 13:22:36 GMT 15s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 694 | 694 | 0
> quote rate 030/s | 182 | 182 | 0
> quote ramp | 152 | 152 | 0
> quote rate 060/s | 360 | 360 | 0
---- One quote per user ------------------------------------------------------------------------------------------------
[#########| ] 8.29%
waiting: 7,673 / active: 3 / done: 694
========================================================================================================================
========================================================================================================================
2026-11-06 13:22:41 GMT 20s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 1,135 | 1,135 | 0
> quote rate 030/s | 182 | 182 | 0
> quote ramp | 243 | 243 | 0
> quote rate 060/s | 360 | 360 | 0
> quote rate 090/s | 350 | 350 | 0
---- One quote per user ------------------------------------------------------------------------------------------------
[###############| ] 13.56%
waiting: 7,230 / active: 5 / done: 1,135
========================================================================================================================
========================================================================================================================
2026-11-06 13:22:46 GMT 25s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 1,643 | 1,643 | 0
> quote rate 030/s | 182 | 182 | 0
> quote ramp | 454 | 454 | 0
> quote rate 060/s | 360 | 360 | 0
> quote rate 090/s | 540 | 540 | 0
> quote rate 120/s | 107 | 107 | 0
---- One quote per user ------------------------------------------------------------------------------------------------
[#####################| ] 19.63%
waiting: 6,721 / active: 6 / done: 1,643
========================================================================================================================
========================================================================================================================
2026-11-06 13:22:51 GMT 30s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 2,244 | 2,244 | 0
> quote rate 030/s | 182 | 182 | 0
> quote ramp | 454 | 454 | 0
> quote rate 060/s | 360 | 360 | 0
> quote rate 090/s | 540 | 540 | 0
> quote rate 120/s | 708 | 708 | 0
---- One quote per user ------------------------------------------------------------------------------------------------
[#############################| ] 26.81%
waiting: 6,121 / active: 5 / done: 2,244
========================================================================================================================
========================================================================================================================
2026-11-06 13:22:56 GMT 35s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 2,961 | 2,961 | 0
> quote rate 030/s | 182 | 182 | 0
> quote ramp | 725 | 725 | 0
> quote rate 060/s | 360 | 360 | 0
> quote rate 090/s | 540 | 540 | 0
> quote rate 120/s | 720 | 720 | 0
> quote rate 150/s | 434 | 434 | 0
---- One quote per user ------------------------------------------------------------------------------------------------
[#######################################| ] 35.38%
waiting: 5,401 / active: 8 / done: 2,961
========================================================================================================================
========================================================================================================================
2026-11-06 13:23:01 GMT 40s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 3,723 | 3,723 | 0
> quote rate 030/s | 182 | 182 | 0
> quote ramp | 1,021 | 1,021 | 0
> quote rate 060/s | 360 | 360 | 0
> quote rate 090/s | 540 | 540 | 0
> quote rate 120/s | 720 | 720 | 0
> quote rate 150/s | 900 | 900 | 0
---- One quote per user ------------------------------------------------------------------------------------------------
[#################################################| ] 44.48%
waiting: 4,621 / active: 26 / done: 3,723
========================================================================================================================
========================================================================================================================
2026-11-06 13:23:06 GMT 45s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 4,542 | 4,515 | 27
> quote rate 030/s | 182 | 182 | 0
> quote ramp | 1,057 | 1,057 | 0
> quote rate 060/s | 360 | 360 | 0
> quote rate 090/s | 540 | 540 | 0
> quote rate 120/s | 720 | 720 | 0
> quote rate 150/s | 900 | 900 | 0
> quote rate 180/s | 783 | 756 | 27
---- Errors ------------------------------------------------------------------------------------------------------------
> status.find.is(200), found 503 27 (100%)
---- One quote per user ------------------------------------------------------------------------------------------------
[############################################################|| ] 54.27%
waiting: 3,721 / active: 107 / done: 4,542
========================================================================================================================
========================================================================================================================
2026-11-06 13:23:11 GMT 50s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 5,531 | 5,303 | 228
> quote rate 030/s | 182 | 182 | 0
> quote ramp | 1,448 | 1,374 | 74
> quote rate 060/s | 360 | 360 | 0
> quote rate 090/s | 540 | 540 | 0
> quote rate 120/s | 720 | 720 | 0
> quote rate 150/s | 900 | 900 | 0
> quote rate 180/s | 1,080 | 1,029 | 51
> quote rate 210/s | 301 | 198 | 103
---- Errors ------------------------------------------------------------------------------------------------------------
> status.find.is(200), found 503 228 (100%)
---- One quote per user ------------------------------------------------------------------------------------------------
[#########################################################################|| ] 66.08%
waiting: 2,731 / active: 108 / done: 5,531
========================================================================================================================
========================================================================================================================
2026-11-06 13:23:16 GMT 55s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 6,588 | 6,091 | 497
> quote rate 030/s | 182 | 182 | 0
> quote ramp | 1,546 | 1,414 | 132
> quote rate 060/s | 360 | 360 | 0
> quote rate 090/s | 540 | 540 | 0
> quote rate 120/s | 720 | 720 | 0
> quote rate 150/s | 900 | 900 | 0
> quote rate 180/s | 1,080 | 1,029 | 51
> quote rate 210/s | 1,260 | 946 | 314
---- Errors ------------------------------------------------------------------------------------------------------------
> status.find.is(200), found 503 497 (100%)
---- One quote per user ------------------------------------------------------------------------------------------------
[#######################################################################################|| ] 78.71%
waiting: 1,675 / active: 107 / done: 6,588
========================================================================================================================
========================================================================================================================
2026-11-06 13:23:21 GMT 60s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 7,780 | 6,882 | 898
> quote rate 030/s | 182 | 182 | 0
> quote ramp | 1,899 | 1,688 | 211
> quote rate 060/s | 360 | 360 | 0
> quote rate 090/s | 540 | 540 | 0
> quote rate 120/s | 720 | 720 | 0
> quote rate 150/s | 900 | 900 | 0
> quote rate 180/s | 1,080 | 1,029 | 51
> quote rate 210/s | 1,260 | 946 | 314
> quote rate 240/s | 839 | 517 | 322
---- Errors ------------------------------------------------------------------------------------------------------------
> status.find.is(200), found 503 898 (100%)
---- One quote per user ------------------------------------------------------------------------------------------------
[#######################################################################################################|| ] 92.95%
waiting: 482 / active: 108 / done: 7,780
========================================================================================================================
========================================================================================================================
2026-11-06 13:23:24 GMT 62s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 8,370 | 7,308 | 1,062
> quote rate 030/s | 182 | 182 | 0
> quote ramp | 1,899 | 1,688 | 211
> quote rate 060/s | 360 | 360 | 0
> quote rate 090/s | 540 | 540 | 0
> quote rate 120/s | 720 | 720 | 0
> quote rate 150/s | 900 | 900 | 0
> quote rate 180/s | 1,080 | 1,029 | 51
> quote rate 210/s | 1,260 | 946 | 314
> quote rate 240/s | 1,429 | 943 | 486
---- Errors ------------------------------------------------------------------------------------------------------------
> status.find.is(200), found 503 1,062 (100%)
---- One quote per user ------------------------------------------------------------------------------------------------
[###############################################################################################################] 100%
waiting: 0 / active: 0 / done: 8,370
========================================================================================================================
Parsing log file(s)...
Parsing log file(s) done in 0s.
Generating reports...
========================================================================================================================
---- Global Information -------------------------------------------------------------|---Total---|-----OK----|----KO----
> request count | 8,370 | 7,308 | 1,062
> min response time (ms) | 0 | 46 | 0
> max response time (ms) | 711 | 711 | 27
> mean response time (ms) | 293 | 335 | 1
> response time std deviation (ms) | 301 | 300 | 1
> response time 50th percentile (ms) | 57 | 105 | 1
> response time 75th percentile (ms) | 674 | 676 | 1
> response time 95th percentile (ms) | 688 | 689 | 3
> response time 99th percentile (ms) | 695 | 696 | 5
> mean throughput (rps) | 132.86 | 116 | 16.86
---- Response Time Distribution ----------------------------------------------------------------------------------------
> OK: t < 800 ms 7,308 (87.31%)
> OK: 800 ms <= t < 1200 ms 0 (0%)
> OK: t >= 1200 ms 0 (0%)
> KO 1,062 (12.69%)
---- Errors ------------------------------------------------------------------------------------------------------------
> status.find.is(200), found 503 1,062 (100%)
========================================================================================================================
Reports generated, please open the following file: file:///tmp/work/3/repos/asmhatre/java-testing-lab/gatling-spring-boot/target/gatling/kneestepssimulation-20261106132221044/index.html
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:07 min
[INFO] Finished at: 2026-11-06T18:53:24+05:30
[INFO] ------------------------------------------------------------------------
@@ -0,0 +1,138 @@
[INFO] Scanning for projects...
[INFO]
[INFO] -------------< com.ankurm.testing:gatling-spring-boot-lab >-------------
[INFO] Building gatling-spring-boot-lab 1.0.0
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- resources:3.5.0:resources (default-resources) @ gatling-spring-boot-lab ---
[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO] Copying 0 resource from src/main/resources to target/classes
[INFO]
[INFO] --- compiler:3.15.0:compile (default-compile) @ gatling-spring-boot-lab ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] --- resources:3.5.0:testResources (default-testResources) @ gatling-spring-boot-lab ---
[INFO] Copying 2 resources from src/test/resources to target/test-classes
[INFO]
[INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ gatling-spring-boot-lab ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] >>> gatling:4.21.12:test (default-cli) > test-compile @ gatling-spring-boot-lab >>>
[INFO]
[INFO] --- resources:3.5.0:resources (default-resources) @ gatling-spring-boot-lab ---
[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO] Copying 0 resource from src/main/resources to target/classes
[INFO]
[INFO] --- compiler:3.15.0:compile (default-compile) @ gatling-spring-boot-lab ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] --- resources:3.5.0:testResources (default-testResources) @ gatling-spring-boot-lab ---
[INFO] Copying 2 resources from src/test/resources to target/test-classes
[INFO]
[INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ gatling-spring-boot-lab ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] <<< gatling:4.21.12:test (default-cli) < test-compile @ gatling-spring-boot-lab <<<
[INFO]
[INFO]
[INFO] --- gatling:4.21.12:test (default-cli) @ gatling-spring-boot-lab ---
[INFO] Running simulation lab.gatling.FailingAssertionSimulation.
========================================================================================================================
2026-11-06 13:24:47 GMT 5s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 993 | 769 | 224
> get quote | 993 | 769 | 224
---- Errors ------------------------------------------------------------------------------------------------------------
> status.find.is(200), found 503 224 (100%)
---- One quote per user ------------------------------------------------------------------------------------------------
[#################################|||| ] 30.06%
waiting: 2,200 / active: 108 / done: 992
========================================================================================================================
========================================================================================================================
2026-11-06 13:24:52 GMT 10s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 2,093 | 1,563 | 530
> get quote | 2,093 | 1,563 | 530
---- Errors ------------------------------------------------------------------------------------------------------------
> status.find.is(200), found 503 530 (100%)
---- One quote per user ------------------------------------------------------------------------------------------------
[######################################################################|||| ] 63.42%
waiting: 1,100 / active: 107 / done: 2,093
========================================================================================================================
========================================================================================================================
2026-11-06 13:24:57 GMT 15s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 3,191 | 2,356 | 835
> get quote | 3,191 | 2,356 | 835
---- Errors ------------------------------------------------------------------------------------------------------------
> status.find.is(200), found 503 835 (100%)
---- One quote per user ------------------------------------------------------------------------------------------------
[###########################################################################################################||||] 96.7%
waiting: 1 / active: 108 / done: 3,191
========================================================================================================================
========================================================================================================================
2026-11-06 13:24:58 GMT 15s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 3,300 | 2,464 | 836
> get quote | 3,300 | 2,464 | 836
---- Errors ------------------------------------------------------------------------------------------------------------
> status.find.is(200), found 503 836 (100%)
---- One quote per user ------------------------------------------------------------------------------------------------
[###############################################################################################################] 100%
waiting: 0 / active: 0 / done: 3,300
========================================================================================================================
Parsing log file(s)...
Parsing log file(s) done in 0s.
Generating reports...
========================================================================================================================
---- Global Information -------------------------------------------------------------|---Total---|-----OK----|----KO----
> request count | 3,300 | 2,464 | 836
> min response time (ms) | 0 | 64 | 0
> max response time (ms) | 706 | 706 | 9
> mean response time (ms) | 479 | 641 | 1
> response time std deviation (ms) | 295 | 112 | 1
> response time 50th percentile (ms) | 671 | 676 | 1
> response time 75th percentile (ms) | 680 | 682 | 1
> response time 95th percentile (ms) | 690 | 691 | 2
> response time 99th percentile (ms) | 696 | 696 | 4
> mean throughput (rps) | 206.25 | 154 | 52.25
---- Response Time Distribution ----------------------------------------------------------------------------------------
> OK: t < 800 ms 2,464 (74.67%)
> OK: 800 ms <= t < 1200 ms 0 (0%)
> OK: t >= 1200 ms 0 (0%)
> KO 836 (25.33%)
---- Errors ------------------------------------------------------------------------------------------------------------
> status.find.is(200), found 503 836 (100%)
========================================================================================================================
Reports generated, please open the following file: file:///tmp/work/3/repos/asmhatre/java-testing-lab/gatling-spring-boot/target/gatling/failingassertionsimulation-20261106132442427/index.html
Global: 95th percentile of response time is less than 200.0 : false (actual : 690.0)
Global: percentage of successful events is greater than 99.0 : false (actual : 74.66666666666667)
get quote: max of response time is less than 1000.0 : true (actual : 706.0)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 20.175 s
[INFO] Finished at: 2026-11-06T18:54:58+05:30
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal io.gatling:gatling-maven-plugin:4.21.12:test (default-cli) on project gatling-spring-boot-lab: Gatling simulation assertions failed ! ForkException -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
@@ -0,0 +1 @@
maven exit code: 1
@@ -0,0 +1,186 @@
[INFO] Scanning for projects...
[INFO]
[INFO] -------------< com.ankurm.testing:gatling-spring-boot-lab >-------------
[INFO] Building gatling-spring-boot-lab 1.0.0
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- resources:3.5.0:resources (default-resources) @ gatling-spring-boot-lab ---
[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO] Copying 0 resource from src/main/resources to target/classes
[INFO]
[INFO] --- compiler:3.15.0:compile (default-compile) @ gatling-spring-boot-lab ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] --- resources:3.5.0:testResources (default-testResources) @ gatling-spring-boot-lab ---
[INFO] Copying 2 resources from src/test/resources to target/test-classes
[INFO]
[INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ gatling-spring-boot-lab ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] >>> gatling:4.21.12:test (default-cli) > test-compile @ gatling-spring-boot-lab >>>
[INFO]
[INFO] --- resources:3.5.0:resources (default-resources) @ gatling-spring-boot-lab ---
[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO] Copying 0 resource from src/main/resources to target/classes
[INFO]
[INFO] --- compiler:3.15.0:compile (default-compile) @ gatling-spring-boot-lab ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] --- resources:3.5.0:testResources (default-testResources) @ gatling-spring-boot-lab ---
[INFO] Copying 2 resources from src/test/resources to target/test-classes
[INFO]
[INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ gatling-spring-boot-lab ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] <<< gatling:4.21.12:test (default-cli) < test-compile @ gatling-spring-boot-lab <<<
[INFO]
[INFO]
[INFO] --- gatling:4.21.12:test (default-cli) @ gatling-spring-boot-lab ---
[INFO] Running simulation lab.gatling.KneeRampSimulation.
========================================================================================================================
2026-11-06 13:23:36 GMT 5s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 123 | 123 | 0
> get quote | 123 | 123 | 0
---- One quote per user ------------------------------------------------------------------------------------------------
[##| ] 2.37%
waiting: 5,075 / active: 2 / done: 123
========================================================================================================================
========================================================================================================================
2026-11-06 13:23:41 GMT 10s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 396 | 396 | 0
> get quote | 396 | 396 | 0
---- One quote per user ------------------------------------------------------------------------------------------------
[########| ] 7.62%
waiting: 4,800 / active: 4 / done: 396
========================================================================================================================
========================================================================================================================
2026-11-06 13:23:46 GMT 15s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 819 | 819 | 0
> get quote | 819 | 819 | 0
---- One quote per user ------------------------------------------------------------------------------------------------
[#################| ] 15.75%
waiting: 4,376 / active: 5 / done: 819
========================================================================================================================
========================================================================================================================
2026-11-06 13:23:51 GMT 20s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 1,391 | 1,391 | 0
> get quote | 1,391 | 1,391 | 0
---- One quote per user ------------------------------------------------------------------------------------------------
[#############################| ] 26.75%
waiting: 3,801 / active: 8 / done: 1,391
========================================================================================================================
========================================================================================================================
2026-11-06 13:23:56 GMT 25s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 2,115 | 2,115 | 0
> get quote | 2,115 | 2,115 | 0
---- One quote per user ------------------------------------------------------------------------------------------------
[#############################################| ] 40.67%
waiting: 3,076 / active: 9 / done: 2,115
========================================================================================================================
========================================================================================================================
2026-11-06 13:24:01 GMT 30s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 2,904 | 2,904 | 0
> get quote | 2,904 | 2,904 | 0
---- One quote per user ------------------------------------------------------------------------------------------------
[#############################################################||| ] 55.85%
waiting: 2,202 / active: 94 / done: 2,904
========================================================================================================================
========================================================================================================================
2026-11-06 13:24:06 GMT 35s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 3,915 | 3,695 | 220
> get quote | 3,915 | 3,695 | 220
---- Errors ------------------------------------------------------------------------------------------------------------
> status.find.is(200), found 503 220 (100%)
---- One quote per user ------------------------------------------------------------------------------------------------
[###################################################################################||| ] 75.29%
waiting: 1,178 / active: 107 / done: 3,915
========================================================================================================================
========================================================================================================================
2026-11-06 13:24:11 GMT 40s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 5,091 | 4,488 | 603
> get quote | 5,091 | 4,488 | 603
---- Errors ------------------------------------------------------------------------------------------------------------
> status.find.is(200), found 503 603 (100%)
---- One quote per user ------------------------------------------------------------------------------------------------
[############################################################################################################|||] 97.9%
waiting: 2 / active: 107 / done: 5,091
========================================================================================================================
========================================================================================================================
2026-11-06 13:24:12 GMT 40s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 5,200 | 4,596 | 604
> get quote | 5,200 | 4,596 | 604
---- Errors ------------------------------------------------------------------------------------------------------------
> status.find.is(200), found 503 604 (100%)
---- One quote per user ------------------------------------------------------------------------------------------------
[###############################################################################################################] 100%
waiting: 0 / active: 0 / done: 5,200
========================================================================================================================
Parsing log file(s)...
Parsing log file(s) done in 0s.
Generating reports...
========================================================================================================================
---- Global Information -------------------------------------------------------------|---Total---|-----OK----|----KO----
> request count | 5,200 | 4,596 | 604
> min response time (ms) | 0 | 46 | 0
> max response time (ms) | 706 | 706 | 22
> mean response time (ms) | 272 | 308 | 1
> response time std deviation (ms) | 288 | 288 | 1
> response time 50th percentile (ms) | 58 | 96 | 1
> response time 75th percentile (ms) | 670 | 673 | 1
> response time 95th percentile (ms) | 686 | 686 | 2
> response time 99th percentile (ms) | 693 | 693 | 5
> mean throughput (rps) | 126.83 | 112.1 | 14.73
---- Response Time Distribution ----------------------------------------------------------------------------------------
> OK: t < 800 ms 4,596 (88.38%)
> OK: 800 ms <= t < 1200 ms 0 (0%)
> OK: t >= 1200 ms 0 (0%)
> KO 604 (11.62%)
---- Errors ------------------------------------------------------------------------------------------------------------
> status.find.is(200), found 503 604 (100%)
========================================================================================================================
Reports generated, please open the following file: file:///tmp/work/3/repos/asmhatre/java-testing-lab/gatling-spring-boot/target/gatling/kneerampsimulation-20261106132330421/index.html
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 47.002 s
[INFO] Finished at: 2026-11-06T18:54:13+05:30
[INFO] ------------------------------------------------------------------------
@@ -0,0 +1,114 @@
[INFO] Scanning for projects...
[INFO]
[INFO] -------------< com.ankurm.testing:gatling-spring-boot-lab >-------------
[INFO] Building gatling-spring-boot-lab 1.0.0
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- resources:3.5.0:resources (default-resources) @ gatling-spring-boot-lab ---
[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO] Copying 0 resource from src/main/resources to target/classes
[INFO]
[INFO] --- compiler:3.15.0:compile (default-compile) @ gatling-spring-boot-lab ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] --- resources:3.5.0:testResources (default-testResources) @ gatling-spring-boot-lab ---
[INFO] Copying 2 resources from src/test/resources to target/test-classes
[INFO]
[INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ gatling-spring-boot-lab ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] >>> gatling:4.21.12:test (default-cli) > test-compile @ gatling-spring-boot-lab >>>
[INFO]
[INFO] --- resources:3.5.0:resources (default-resources) @ gatling-spring-boot-lab ---
[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO] Copying 0 resource from src/main/resources to target/classes
[INFO]
[INFO] --- compiler:3.15.0:compile (default-compile) @ gatling-spring-boot-lab ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] --- resources:3.5.0:testResources (default-testResources) @ gatling-spring-boot-lab ---
[INFO] Copying 2 resources from src/test/resources to target/test-classes
[INFO]
[INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ gatling-spring-boot-lab ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] <<< gatling:4.21.12:test (default-cli) < test-compile @ gatling-spring-boot-lab <<<
[INFO]
[INFO]
[INFO] --- gatling:4.21.12:test (default-cli) @ gatling-spring-boot-lab ---
[INFO] Running simulation lab.gatling.ClosedModelSimulation.
========================================================================================================================
2026-11-06 13:24:26 GMT 5s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 767 | 767 | 0
> get quote | 767 | 767 | 0
---- Closed loop -------------------------------------------------------------------------------------------------------
active: 50 / done: 0
========================================================================================================================
========================================================================================================================
2026-11-06 13:24:31 GMT 10s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 1,556 | 1,556 | 0
> get quote | 1,556 | 1,556 | 0
---- Closed loop -------------------------------------------------------------------------------------------------------
active: 50 / done: 0
========================================================================================================================
========================================================================================================================
2026-11-06 13:24:36 GMT 15s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 2,347 | 2,347 | 0
> get quote | 2,347 | 2,347 | 0
---- Closed loop -------------------------------------------------------------------------------------------------------
active: 50 / done: 0
========================================================================================================================
========================================================================================================================
2026-11-06 13:24:37 GMT 16s elapsed
---- Requests -----------------------------------------------------------------------|---Total---|-----OK----|----KO----
> Global | 2,506 | 2,506 | 0
> get quote | 2,506 | 2,506 | 0
---- Closed loop -------------------------------------------------------------------------------------------------------
active: 0 / done: 50
========================================================================================================================
Parsing log file(s)...
Parsing log file(s) done in 0s.
Generating reports...
========================================================================================================================
---- Global Information -------------------------------------------------------------|---Total---|-----OK----|----KO----
> request count | 2,506 | 2,506 | -
> min response time (ms) | 47 | 47 | -
> max response time (ms) | 343 | 343 | -
> mean response time (ms) | 301 | 301 | -
> response time std deviation (ms) | 51 | 51 | -
> response time 50th percentile (ms) | 314 | 314 | -
> response time 75th percentile (ms) | 320 | 320 | -
> response time 95th percentile (ms) | 329 | 329 | -
> response time 99th percentile (ms) | 334 | 334 | -
> mean throughput (rps) | 147.41 | 147.41 | -
---- Response Time Distribution ----------------------------------------------------------------------------------------
> OK: t < 800 ms 2,506 (100%)
> OK: 800 ms <= t < 1200 ms 0 (0%)
> OK: t >= 1200 ms 0 (0%)
> KO 0 (0%)
========================================================================================================================
Reports generated, please open the following file: file:///tmp/work/3/repos/asmhatre/java-testing-lab/gatling-spring-boot/target/gatling/closedmodelsimulation-20261106132420420/index.html
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 22.031 s
[INFO] Finished at: 2026-11-06T18:54:37+05:30
[INFO] ------------------------------------------------------------------------
@@ -0,0 +1,10 @@
step requests ok ko ko % p50 p95 p99 max (times in ms)
rate 030/s 182 182 0 0.0 55 60 69 82
rate 060/s 360 360 0 0.0 53 58 62 66
rate 090/s 540 540 0 0.0 52 57 59 61
rate 120/s 720 720 0 0.0 51 57 58 61
rate 150/s 900 900 0 0.0 52 57 60 64
rate 180/s 1080 1029 51 4.7 543 685 689 692
rate 210/s 1260 946 314 24.9 676 693 699 711
rate 240/s 1429 943 486 34.0 672 689 695 702
ramp 1899 1688 211 11.1 56 690 699 708
@@ -0,0 +1 @@
k6 v2.3.0 (commit/e088784614, go1.26.8, linux/amd64)
@@ -0,0 +1,10 @@
step requests ok ko ko % p50 p95 p99 max (times in ms)
rate 030/s 179 179 0 0.0 51 56 57 59
rate 060/s 360 360 0 0.0 51 56 56 57
rate 090/s 540 540 0 0.0 51 56 56 64
rate 120/s 720 720 0 0.0 51 56 56 58
rate 150/s 900 900 0 0.0 52 56 58 60
rate 180/s 1080 1037 43 4.0 506 682 688 693
rate 210/s 1260 945 315 25.0 675 691 698 702
rate 240/s 1440 948 492 34.2 674 693 698 706
ramp 1885 1678 207 11.0 55 687 694 701
@@ -0,0 +1,29 @@
█ TOTAL RESULTS
checks_total.......: 8364 133.443081/s
checks_succeeded...: 87.36% 7307 out of 8364
checks_failed......: 12.63% 1057 out of 8364
✗ status is 200
↳ 87% — ✓ 7307 / ✗ 1057
HTTP
http_req_duration..............: avg=289.82ms min=334.47µs med=55.3ms max=705.87ms p(90)=684.17ms p(95)=688.07ms
{ expected_response:true }...: avg=331.64ms min=46.44ms med=92.48ms max=705.87ms p(90)=685ms p(95)=688.76ms
http_req_failed................: 12.63% 1057 out of 8364
http_reqs......................: 8364 133.443081/s
EXECUTION
iteration_duration.............: avg=290.06ms min=566.23µs med=55.68ms max=706.04ms p(90)=684.43ms p(95)=688.3ms
iterations.....................: 8364 133.443081/s
vus............................: 109 min=2 max=109
vus_max........................: 200 min=200 max=200
NETWORK
data_received..................: 1.2 MB 19 kB/s
data_sent......................: 709 kB 11 kB/s
@@ -0,0 +1,32 @@
█ THRESHOLDS
http_req_duration
✗ 'p(95)<200' p(95)=687.37ms
http_req_failed
✗ 'rate<0.01' rate=24.81%
█ TOTAL RESULTS
HTTP
http_req_duration..............: avg=474.35ms min=391.27µs med=669.21ms max=702.82ms p(90)=683.72ms p(95)=687.37ms
{ expected_response:true }...: avg=630.67ms min=49.56ms med=673.86ms max=702.82ms p(90)=685.57ms p(95)=689.1ms
http_req_failed................: 24.81% 819 out of 3301
http_reqs......................: 3301 210.547036/s
EXECUTION
iteration_duration.............: avg=474.54ms min=477.48µs med=669.34ms max=703.17ms p(90)=683.89ms p(95)=687.51ms
iterations.....................: 3301 210.547036/s
vus............................: 107 min=68 max=108
vus_max........................: 200 min=200 max=200
NETWORK
data_received..................: 478 kB 31 kB/s
data_sent......................: 280 kB 18 kB/s
time="2026-11-06T13:26:18Z" level=error msg="thresholds on metrics 'http_req_duration, http_req_failed' have been crossed"
@@ -0,0 +1 @@
k6 exit code: 99
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,101 @@
[
{
"step": "rate 030/s",
"requests": 182,
"ok": 182,
"ko": 0,
"ko_pct": 0.0,
"p50": 55,
"p95": 60,
"p99": 69,
"max": 82
},
{
"step": "ramp",
"requests": 1899,
"ok": 1688,
"ko": 211,
"ko_pct": 11.11,
"p50": 56,
"p95": 690,
"p99": 699,
"max": 708
},
{
"step": "rate 060/s",
"requests": 360,
"ok": 360,
"ko": 0,
"ko_pct": 0.0,
"p50": 53,
"p95": 58,
"p99": 62,
"max": 66
},
{
"step": "rate 090/s",
"requests": 540,
"ok": 540,
"ko": 0,
"ko_pct": 0.0,
"p50": 52,
"p95": 57,
"p99": 59,
"max": 61
},
{
"step": "rate 120/s",
"requests": 720,
"ok": 720,
"ko": 0,
"ko_pct": 0.0,
"p50": 51,
"p95": 57,
"p99": 58,
"max": 61
},
{
"step": "rate 150/s",
"requests": 900,
"ok": 900,
"ko": 0,
"ko_pct": 0.0,
"p50": 52,
"p95": 57,
"p99": 60,
"max": 64
},
{
"step": "rate 180/s",
"requests": 1080,
"ok": 1029,
"ko": 51,
"ko_pct": 4.72,
"p50": 543,
"p95": 685,
"p99": 689,
"max": 692
},
{
"step": "rate 210/s",
"requests": 1260,
"ok": 946,
"ko": 314,
"ko_pct": 24.92,
"p50": 676,
"p95": 693,
"p99": 699,
"max": 711
},
{
"step": "rate 240/s",
"requests": 1429,
"ok": 943,
"ko": 486,
"ko_pct": 34.01,
"p50": 672,
"p95": 689,
"p99": 695,
"max": 702
}
]
+101
View File
@@ -0,0 +1,101 @@
[
{
"step": "rate 030/s",
"requests": 179,
"ok": 179,
"ko": 0,
"ko_pct": 0.0,
"p50": 51,
"p95": 56,
"p99": 57,
"max": 59
},
{
"step": "rate 060/s",
"requests": 360,
"ok": 360,
"ko": 0,
"ko_pct": 0.0,
"p50": 51,
"p95": 56,
"p99": 56,
"max": 57
},
{
"step": "rate 090/s",
"requests": 540,
"ok": 540,
"ko": 0,
"ko_pct": 0.0,
"p50": 51,
"p95": 56,
"p99": 56,
"max": 64
},
{
"step": "rate 120/s",
"requests": 720,
"ok": 720,
"ko": 0,
"ko_pct": 0.0,
"p50": 51,
"p95": 56,
"p99": 56,
"max": 58
},
{
"step": "rate 150/s",
"requests": 900,
"ok": 900,
"ko": 0,
"ko_pct": 0.0,
"p50": 52,
"p95": 56,
"p99": 58,
"max": 60
},
{
"step": "rate 180/s",
"requests": 1080,
"ok": 1037,
"ko": 43,
"ko_pct": 4.0,
"p50": 506,
"p95": 682,
"p99": 688,
"max": 693
},
{
"step": "rate 210/s",
"requests": 1260,
"ok": 945,
"ko": 315,
"ko_pct": 25.0,
"p50": 675,
"p95": 691,
"p99": 698,
"max": 702
},
{
"step": "rate 240/s",
"requests": 1440,
"ok": 948,
"ko": 492,
"ko_pct": 34.2,
"p50": 674,
"p95": 693,
"p99": 698,
"max": 706
},
{
"step": "ramp",
"requests": 1885,
"ok": 1678,
"ko": 207,
"ko_pct": 11.0,
"p50": 55,
"p95": 687,
"p99": 694,
"max": 701
}
]
+56
View File
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.2</version>
<relativePath/>
</parent>
<groupId>com.ankurm.testing</groupId>
<artifactId>gatling-spring-boot-lab</artifactId>
<version>1.0.0</version>
<properties>
<java.version>25</java.version>
<maven.compiler.release>25</maven.compiler.release>
<gatling.version>3.16.0</gatling.version>
<gatling-maven-plugin.version>4.21.12</gatling-maven-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.gatling.highcharts</groupId>
<artifactId>gatling-charts-highcharts</artifactId>
<version>${gatling.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- Gatling simulations live in src/test/java. Pick one with -Dgatling.simulationClass=... -->
<plugin>
<groupId>io.gatling</groupId>
<artifactId>gatling-maven-plugin</artifactId>
<version>${gatling-maven-plugin.version}</version>
</plugin>
</plugins>
</build>
</project>
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""Turns the raw load-test artefacts into the tables and chart quoted in the post.
analyze.py gatling <report-dir> -> output/10-gatling-steps-table.txt, output/knee-gatling.json
analyze.py k6 <k6-json> -> output/21-k6-steps-table.txt, output/knee-k6.json
analyze.py chart -> output/30-knee-chart.svg
Gatling's open-source report has no stats.json, so the per-step rows are read from the
statistics table in index.html (columns: total, ok, ko, ko%, req/s, min, p50, p75, p95, p99, max, mean, stddev).
Standard library only.
"""
import html, json, math, re, sys
from pathlib import Path
OUT = Path(__file__).resolve().parent.parent / "output"
KEYS = ["step", "requests", "ok", "ko", "ko_pct", "p50", "p95", "p99", "max"]
def gatling(report_dir):
page = (Path(report_dir) / "index.html").read_text(encoding="utf-8")
rows = []
for tr in re.findall(r'<tr id="req_[^"]+" data-parent="ROOT">(.*?)</tr>', page, re.S):
name = html.unescape(re.search(r'class="ellipsed-name">([^<]+)<', tr).group(1)).replace("quote ", "")
c = {int(n): v for n, v in re.findall(r'class="value [a-z ]*col-(\d+)">([^<]*)<', tr)}
rows.append(dict(step=name, requests=int(c[2]), ok=int(c[3]), ko=int(c[4]), ko_pct=float(c[5]),
p50=int(c[8]), p95=int(c[10]), p99=int(c[11]), max=int(c[12])))
return rows
def pct(values, p):
"""k6's own method: linear interpolation between closest ranks."""
if not values:
return 0
s = sorted(values)
idx = p / 100 * (len(s) - 1)
lo, hi = math.floor(idx), math.ceil(idx)
return s[lo] + (s[hi] - s[lo]) * (idx - lo)
def k6(json_file):
dur, failed = {}, {}
with open(json_file, encoding="utf-8") as f:
for line in f:
if '"http_req_duration"' not in line and '"http_req_failed"' not in line:
continue
o = json.loads(line)
if o.get("type") != "Point":
continue
tags = o["data"]["tags"]
if "step" not in tags:
continue
if o["metric"] == "http_req_duration":
dur.setdefault(tags["step"], []).append(o["data"]["value"])
elif o["metric"] == "http_req_failed":
failed.setdefault(tags["step"], []).append(o["data"]["value"])
rows = []
for step in sorted(dur, key=lambda s: (s == "ramp", s)):
n = len(dur[step]); ko = int(sum(failed.get(step, [])))
rows.append(dict(step=step, requests=n, ok=n - ko, ko=ko, ko_pct=round(100 * ko / n, 1),
p50=round(pct(dur[step], 50)), p95=round(pct(dur[step], 95)),
p99=round(pct(dur[step], 99)), max=round(max(dur[step]))))
return rows
def table(rows):
# Gatling lists the ramp row where it was first seen; show steps in rate order, ramp last.
rows = sorted(rows, key=lambda r: (r["step"] == "ramp", r["step"]))
lines = [f'{"step":<13}{"requests":>9}{"ok":>7}{"ko":>6}{"ko %":>7}{"p50":>6}{"p95":>6}{"p99":>6}{"max":>6} (times in ms)']
for r in rows:
lines.append(f'{r["step"]:<13}{r["requests"]:>9}{r["ok"]:>7}{r["ko"]:>6}{r["ko_pct"]:>7.1f}'
f'{r["p50"]:>6}{r["p95"]:>6}{r["p99"]:>6}{r["max"]:>6}')
return "\n".join(lines) + "\n"
def chart():
g = {r["step"]: r for r in json.loads((OUT / "knee-gatling.json").read_text())}
k = {}
if (OUT / "knee-k6.json").exists():
k = {r["step"]: r for r in json.loads((OUT / "knee-k6.json").read_text())}
steps = [f"rate {r:03d}/s" for r in range(30, 241, 30)]
X0, X1, Y0, Y1 = 70, 700, 210, 40 # plot box (y grows downwards)
xs = {s: X0 + (X1 - X0) * i / (len(steps) - 1) for i, s in enumerate(steps)}
ymax = 900
y = lambda ms: Y0 - (Y0 - Y1) * min(ms, ymax) / ymax
def line(series, key, cls):
pts = " ".join(f"{xs[s]:.0f},{y(series[s][key]):.0f}" for s in steps if s in series)
dots = "".join(f'<circle class="{cls}d" cx="{xs[s]:.0f}" cy="{y(series[s][key]):.0f}" r="3"/>' for s in steps if s in series)
return f'<polyline class="{cls}" points="{pts}"/>{dots}'
o = ['<svg viewBox="0 0 740 490" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Two panels over eight injected request rates from 30 to 240 per second. Top: Gatling p50 and p95 response time, flat near 50 to 60 milliseconds at low rates and climbing steeply from 150 per second, with k6 p95 shown for comparison. Bottom: percentage of requests rejected with HTTP 503, zero up to 150 per second and rising from 180 per second.">',
'<style>.t{font:600 14px sans-serif;fill:#222}.c{font:12px sans-serif;fill:#444}.m{font:11px monospace;fill:#555}.ax{stroke:#b7bec9;stroke-width:1}.gr{stroke:#e2e5ea;stroke-width:1}'
'.p95{fill:none;stroke:#5b7fc7;stroke-width:2.5}.p95d{fill:#5b7fc7}.p50{fill:none;stroke:#4a9d63;stroke-width:2.5}.p50d{fill:#4a9d63}'
'.k95{fill:none;stroke:#c9973f;stroke-width:2;stroke-dasharray:5 3}.k95d{fill:#c9973f}.kneeline{stroke:#c56a54;stroke-width:1.5;stroke-dasharray:3 3}'
'.gb{fill:#5b7fc7}.kb{fill:#c9973f}</style>',
'<text class="t" x="70" y="22">Response time (ms) vs injected rate (users/s), Gatling 3.16.0 and k6 v2.3.0</text>']
for ms in range(0, ymax + 1, 300):
o.append(f'<line class="gr" x1="{X0}" y1="{y(ms):.0f}" x2="{X1}" y2="{y(ms):.0f}"/><text class="m" x="{X0-8}" y="{y(ms)+4:.0f}" text-anchor="end">{ms}</text>')
o.append(f'<line class="ax" x1="{X0}" y1="{Y0}" x2="{X1}" y2="{Y0}"/><line class="ax" x1="{X0}" y1="{Y1}" x2="{X0}" y2="{Y0}"/>')
o.append(line(g, "p50", "p50")); o.append(line(g, "p95", "p95"))
if k: o.append(line(k, "p95", "k95"))
for s in steps:
o.append(f'<text class="m" x="{xs[s]:.0f}" y="{Y0+16}" text-anchor="middle">{int(s[5:8])}</text>')
# the capacity worked out from the app's settings: 8 workers / 0.050 s = 160 per second
kx = xs["rate 150/s"] + (xs["rate 180/s"] - xs["rate 150/s"]) / 3
o.append(f'<line class="kneeline" x1="{kx:.0f}" y1="{Y1}" x2="{kx:.0f}" y2="{Y0}"/><text class="m" x="{kx-6:.0f}" y="{Y1+110}" text-anchor="end">calculated capacity: 8 workers / 0.050 s = 160 per second</text>')
# legend
o.append('<line class="p50" x1="90" y1="58" x2="120" y2="58"/><text class="c" x="126" y="62">Gatling p50</text>'
'<line class="p95" x1="90" y1="76" x2="120" y2="76"/><text class="c" x="126" y="80">Gatling p95</text>')
if k: o.append('<line class="k95" x1="90" y1="94" x2="120" y2="94"/><text class="c" x="126" y="98">k6 p95</text>')
# bottom panel: KO %
B0, B1 = 440, 300
kmax = max([r["ko_pct"] for r in g.values()] + [r["ko_pct"] for r in k.values()] + [1])
top = math.ceil(kmax / 10) * 10
by = lambda v: B0 - (B0 - B1) * v / top
o.append(f'<text class="t" x="70" y="276">Requests rejected with HTTP 503 (%)</text>')
for v in range(0, top + 1, 10):
o.append(f'<line class="gr" x1="{X0}" y1="{by(v):.0f}" x2="{X1}" y2="{by(v):.0f}"/><text class="m" x="{X0-8}" y="{by(v)+4:.0f}" text-anchor="end">{v}</text>')
for s in steps:
for off, series, cls in ((-9, g, "gb"), (3, k, "kb")):
if s in series:
h = B0 - by(series[s]["ko_pct"])
o.append(f'<rect class="{cls}" x="{xs[s]+off:.0f}" y="{by(series[s]["ko_pct"]):.1f}" width="8" height="{h:.1f}"/>')
o.append(f'<text class="m" x="{xs[s]:.0f}" y="{B0+16}" text-anchor="middle">{int(s[5:8])}</text>')
o.append(f'<line class="ax" x1="{X0}" y1="{B0}" x2="{X1}" y2="{B0}"/>')
o.append(f'<text class="c" x="{(X0+X1)//2}" y="{B0+34}" text-anchor="middle">injected rate, users per second (blue = Gatling, amber = k6)</text>')
o.append('</svg>')
(OUT / "30-knee-chart.svg").write_text("\n".join(o) + "\n", encoding="utf-8")
if __name__ == "__main__":
OUT.mkdir(exist_ok=True)
cmd = sys.argv[1]
if cmd == "gatling":
rows = gatling(sys.argv[2])
(OUT / "knee-gatling.json").write_text(json.dumps(rows, indent=1) + "\n")
(OUT / "10-gatling-steps-table.txt").write_text(table(rows))
elif cmd == "k6":
rows = k6(sys.argv[2])
(OUT / "knee-k6.json").write_text(json.dumps(rows, indent=1) + "\n")
(OUT / "21-k6-steps-table.txt").write_text(table(rows))
elif cmd == "chart":
chart()
else:
sys.exit(__doc__)
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Regenerates every file in gatling-spring-boot/output/.
# Needs JDK 25 and Maven on the PATH and python3 (standard library only).
# k6 is optional: set K6=/path/to/k6 or have it on the PATH; without it the k6 steps are skipped.
# Takes about 6 minutes: no single load run is longer than ~70 seconds.
set -euo pipefail
cd "$(dirname "$0")/.."
PORT="${PORT:-18300}"
export APP_URL="http://localhost:${PORT}"
K6="${K6:-$(command -v k6 || true)}"
OUT=output
JAR=target/gatling-spring-boot-lab-1.0.0.jar
# keep transcripts free of JVM launcher noise that varies per machine
filter() { grep -vE '^(Picked up JAVA_TOOL_OPTIONS|WARNING: )' || true; }
rm -f "$OUT"/*.txt "$OUT"/*.json "$OUT"/*.svg
mkdir -p "$OUT"
# 1. unit test: the capacity limit the whole post relies on (writes output/01-saturation-test.txt)
mvn -B -q clean test 2>&1 | filter
# 2. build and start the app under test
mvn -B -q package -DskipTests 2>&1 | filter
mkdir -p target/k6
PORT="$PORT" java -jar "$JAR" > target/app.log 2>&1 &
APP_PID=$!
trap 'kill $APP_PID 2>/dev/null || true' EXIT
for _ in $(seq 1 60); do
curl -fs "$APP_URL/api/stats" > /dev/null 2>&1 && break
sleep 1
done
curl -fs "$APP_URL/api/quote/ORCL" > "$OUT/02-single-request.txt"; echo >> "$OUT/02-single-request.txt"
# 3. Gatling runs, one simulation per mvn invocation
gatling() { # <output file> <simulation class>
mvn -B test-compile gatling:test -Dgatling.simulationClass="lab.gatling.$2" 2>&1 | filter > "$OUT/$1"
}
gatling 03-basic-console.txt BasicSimulation
gatling 04-knee-steps-console.txt KneeStepsSimulation
python3 scripts/analyze.py gatling "$(ls -dt target/gatling/kneestepssimulation-* | head -1)"
gatling 06-knee-ramp-console.txt KneeRampSimulation
gatling 07-closed-model-console.txt ClosedModelSimulation
# 4. the run that must fail the build; keep going, record Maven's exit code
set +e
gatling 05-failing-assertion-console.txt FailingAssertionSimulation
echo "maven exit code: ${PIPESTATUS[0]}" > "$OUT/05b-failing-assertion-exit.txt"
set -e
# 5. k6, same app, same profile
if [ -n "$K6" ]; then
"$K6" version > "$OUT/20-k6-version.txt"
"$K6" run --quiet --out json=target/k6/knee.json k6/knee.js > "$OUT/22-k6-knee-console.txt" 2>&1
python3 scripts/analyze.py k6 target/k6/knee.json
set +e
"$K6" run --quiet k6/threshold-fail.js > "$OUT/23-k6-threshold-fail-console.txt" 2>&1
echo "k6 exit code: $?" > "$OUT/23b-k6-threshold-fail-exit.txt"
set -e
else
echo "k6 not found: skipping k6 steps" >&2
fi
python3 scripts/analyze.py chart
echo "done: $(ls "$OUT" | wc -l) files in $OUT/"
@@ -0,0 +1,64 @@
package lab.gatling;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* A stand-in for a slow dependency with limited capacity (a legacy pricing service, a
* connection-limited database). {@code workers} requests are served at once, each taking
* about {@code delayMs}; up to {@code queueSize} more wait; everything beyond is rejected.
*
* Capacity is therefore workers / delay: 8 / 0.050 s = 160 requests per second.
* Below that the service answers in ~50 ms. Above it the queue fills, latency climbs, and
* once the queue is full requests are rejected. That is the knee the load test looks for.
*/
@Component
public class DownstreamPool {
private final ThreadPoolExecutor executor;
private final int delayMs;
private final AtomicLong rejected = new AtomicLong();
public DownstreamPool(@Value("${lab.downstream.workers:8}") int workers,
@Value("${lab.downstream.queue:100}") int queueSize,
@Value("${lab.downstream.delay-ms:50}") int delayMs) {
this.delayMs = delayMs;
this.executor = new ThreadPoolExecutor(workers, workers, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(queueSize), new ThreadPoolExecutor.AbortPolicy());
}
/** Runs the lookup on the bounded pool and waits for it. Throws {@link Saturated} when full. */
public double price(String symbol) throws InterruptedException {
Callable<Double> lookup = () -> {
// 0.9x - 1.1x of the nominal delay, derived from the symbol so runs are repeatable
long ms = delayMs - delayMs / 10 + Math.floorMod(symbol.hashCode(), Math.max(1, delayMs / 5 + 1));
Thread.sleep(ms);
return 10 + Math.floorMod(symbol.hashCode(), 49000) / 100.0;
};
try {
return executor.submit(lookup).get();
} catch (RejectedExecutionException e) {
rejected.incrementAndGet();
throw new Saturated();
} catch (ExecutionException e) {
throw new IllegalStateException(e.getCause());
}
}
public Stats stats() {
return new Stats(executor.getActiveCount(), executor.getQueue().size(),
executor.getCompletedTaskCount(), rejected.get());
}
public record Stats(int active, int queued, long completed, long rejected) { }
public static class Saturated extends RuntimeException { }
}
@@ -0,0 +1,11 @@
package lab.gatling;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LabApplication {
public static void main(String[] args) {
SpringApplication.run(LabApplication.class, args);
}
}
@@ -0,0 +1,36 @@
package lab.gatling;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class QuoteController {
private final DownstreamPool pool;
public QuoteController(DownstreamPool pool) {
this.pool = pool;
}
public record Quote(String symbol, double price) { }
@GetMapping("/api/quote/{symbol}")
public Quote quote(@PathVariable String symbol) throws InterruptedException {
return new Quote(symbol, pool.price(symbol));
}
/** Diagnostic endpoint: delete before shipping. */
@GetMapping("/api/stats")
public DownstreamPool.Stats stats() {
return pool.stats();
}
@ExceptionHandler(DownstreamPool.Saturated.class)
ResponseEntity<String> saturated() {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).header("Retry-After", "1").body("downstream saturated");
}
}
@@ -0,0 +1,2 @@
server.port=${PORT:18300}
spring.application.name=gatling-spring-boot-lab
@@ -0,0 +1,44 @@
package lab.gatling;
import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;
import io.gatling.javaapi.core.FeederBuilder;
import io.gatling.javaapi.core.ScenarioBuilder;
import io.gatling.javaapi.core.Simulation;
import java.time.Duration;
/**
* A scenario with a feeder, checks and a mix of injection steps.
* Every user looks up two random symbols with a short think time in between.
*/
public class BasicSimulation extends Simulation {
FeederBuilder<String> symbols = csv("data/symbols.csv").random();
ScenarioBuilder browse = scenario("Browse quotes")
.feed(symbols)
.exec(http("get quote")
.get("/api/quote/#{symbol}")
.check(
status().is(200),
jmesPath("symbol").isEL("#{symbol}"),
jmesPath("price").ofDouble().gt(0.0)))
.pause(Duration.ofMillis(200), Duration.ofMillis(500))
.feed(symbols)
.exec(http("get another quote")
.get("/api/quote/#{symbol}")
.check(status().is(200)));
{
setUp(browse.injectOpen(
atOnceUsers(5),
nothingFor(Duration.ofSeconds(2)),
rampUsers(20).during(Duration.ofSeconds(5)),
constantUsersPerSec(10).during(Duration.ofSeconds(10))))
.protocols(Config.protocol())
.assertions(
global().responseTime().percentile(95).lt(200),
global().successfulRequests().percent().gt(99.0));
}
}
@@ -0,0 +1,26 @@
package lab.gatling;
import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;
import io.gatling.javaapi.core.ScenarioBuilder;
import io.gatling.javaapi.core.Simulation;
import java.time.Duration;
/**
* CLOSED model: 50 virtual users, each sends its next request only after the previous one returned.
* The server can never be asked for more than 50 requests at once, so it is never overloaded
* the way an open arrival rate can overload it.
*/
public class ClosedModelSimulation extends Simulation {
ScenarioBuilder loop = scenario("Closed loop")
.feed(csv("data/symbols.csv").random())
.during(Duration.ofSeconds(15)).on(
exec(http("get quote").get("/api/quote/#{symbol}").check(status().is(200))));
{
setUp(loop.injectClosed(constantConcurrentUsers(50).during(Duration.ofSeconds(15))))
.protocols(Config.protocol());
}
}
@@ -0,0 +1,21 @@
package lab.gatling;
import static io.gatling.javaapi.http.HttpDsl.http;
import io.gatling.javaapi.http.HttpProtocolBuilder;
/** Shared protocol so every simulation targets the same app. */
final class Config {
private Config() { }
static String baseUrl() {
String url = System.getenv("APP_URL");
return url != null ? url : "http://localhost:18300";
}
static HttpProtocolBuilder protocol() {
return http.baseUrl(baseUrl())
.acceptHeader("application/json")
.userAgentHeader("gatling-lab");
}
}
@@ -0,0 +1,28 @@
package lab.gatling;
import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;
import io.gatling.javaapi.core.ScenarioBuilder;
import io.gatling.javaapi.core.Simulation;
import java.time.Duration;
/**
* Deliberately fails: 220 users/s is above the ~160/s capacity, but the assertions below
* demand a p95 under 200 ms and at least 99% success. Gatling exits non-zero, so Maven fails the build.
*/
public class FailingAssertionSimulation extends Simulation {
ScenarioBuilder oneRequest = scenario("One quote per user")
.feed(csv("data/symbols.csv").random())
.exec(http("get quote").get("/api/quote/#{symbol}").check(status().is(200)));
{
setUp(oneRequest.injectOpen(constantUsersPerSec(220).during(Duration.ofSeconds(15))))
.protocols(Config.protocol())
.assertions(
global().responseTime().percentile(95).lt(200),
global().successfulRequests().percent().gt(99.0),
details("get quote").responseTime().max().lt(1000));
}
}
@@ -0,0 +1,22 @@
package lab.gatling;
import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;
import io.gatling.javaapi.core.ScenarioBuilder;
import io.gatling.javaapi.core.Simulation;
import java.time.Duration;
/** The stress profile: one continuous ramp from 10 to 250 users/s over 40 seconds. */
public class KneeRampSimulation extends Simulation {
ScenarioBuilder oneRequest = scenario("One quote per user")
.feed(csv("data/symbols.csv").random())
.exec(http("get quote").get("/api/quote/#{symbol}").check(status().is(200)));
{
setUp(oneRequest.injectOpen(
rampUsersPerSec(10).to(250).during(Duration.ofSeconds(40))))
.protocols(Config.protocol());
}
}
@@ -0,0 +1,52 @@
package lab.gatling;
import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;
import io.gatling.javaapi.core.ScenarioBuilder;
import io.gatling.javaapi.core.Simulation;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicLong;
/**
* Finds the knee. OPEN model: one request per arriving user, so the arrival rate IS the request rate.
* Rates 30, 60, ... 240 users/s, each held 6 s, separated by 2 s ramps.
*
* Gatling's open-source report aggregates everything into one table, which would hide the knee.
* So each request is NAMED after the step it was sent in ("rate 150/s"); the report then has one
* row per step. Requests sent during a ramp between two steps are named "ramp".
*/
public class KneeStepsSimulation extends Simulation {
static final int STEPS = 8, RATE_STEP = 30, HOLD_S = 6, RAMP_S = 2;
// Set by the first user to arrive; every later user derives its step from the time elapsed since then.
static final AtomicLong startedAt = new AtomicLong();
static String stepLabel() {
long now = System.currentTimeMillis();
startedAt.compareAndSet(0, now);
long elapsedMs = now - startedAt.get();
long slot = Math.max(0, elapsedMs) / ((HOLD_S + RAMP_S) * 1000L);
long into = Math.max(0, elapsedMs) % ((HOLD_S + RAMP_S) * 1000L);
if (into >= HOLD_S * 1000L || slot >= STEPS) {
return "ramp";
}
return String.format("rate %03d/s", RATE_STEP * (slot + 1));
}
ScenarioBuilder oneRequest = scenario("One quote per user")
.feed(csv("data/symbols.csv").random())
.exec(session -> session.set("step", stepLabel()))
.exec(http("quote #{step}").get("/api/quote/#{symbol}").check(status().is(200)));
{
setUp(oneRequest.injectOpen(
incrementUsersPerSec(RATE_STEP)
.times(STEPS)
.eachLevelLasting(Duration.ofSeconds(HOLD_S))
.separatedByRampsLasting(Duration.ofSeconds(RAMP_S))
.startingFrom(RATE_STEP)))
.protocols(Config.protocol());
}
}
@@ -0,0 +1,57 @@
package lab.gatling;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
/**
* Proves the lab app has the capacity limit the post depends on, without any load tool:
* 2 workers + a queue of 3 can hold 5 requests; the 6th and 7th are rejected.
*/
class SaturationTest {
@Test
void poolRejectsOnceWorkersAndQueueAreFull() throws Exception {
DownstreamPool pool = new DownstreamPool(2, 3, 400);
List<Future<String>> results = new ArrayList<>();
CountDownLatch go = new CountDownLatch(1);
try (ExecutorService callers = Executors.newFixedThreadPool(7)) {
for (int i = 0; i < 7; i++) {
final int n = i;
Callable<String> call = () -> {
go.await();
Thread.sleep(n * 15L); // stagger so the first five are admitted in order
try {
pool.price("ORCL");
return "ok";
} catch (DownstreamPool.Saturated e) {
return "rejected";
}
};
results.add(callers.submit(call));
}
go.countDown();
long ok = 0;
long rejected = 0;
for (Future<String> f : results) {
if (f.get().equals("ok")) ok++; else rejected++;
}
DownstreamPool.Stats stats = pool.stats();
new Transcript("01-saturation-test.txt")
.line("workers=2 queue=3 delay=400ms, 7 simultaneous callers")
.line("served=%d rejected=%d", ok, rejected)
.line("pool stats: completed=%d rejected=%d", stats.completed(), stats.rejected())
.write();
assertThat(ok).isEqualTo(5);
assertThat(rejected).isEqualTo(2);
assertThat(stats.rejected()).isEqualTo(2);
}
}
}
@@ -0,0 +1,33 @@
package lab.gatling;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/** Collects lines during a test and writes them to output/NN-name.txt. */
final class Transcript {
private final String file;
private final List<String> lines = new ArrayList<>();
Transcript(String file) {
this.file = file;
}
Transcript line(String format, Object... args) {
lines.add(args.length == 0 ? format : String.format(format, args));
return this;
}
void write() {
try {
Path dir = Path.of("output");
Files.createDirectories(dir);
Files.writeString(dir.resolve(file), String.join("\n", lines) + "\n");
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
@@ -0,0 +1,13 @@
symbol
ORCL
MSFT
GOOG
AMZN
TCS
INFY
HDFC
RELI
SBIN
WIPRO
LT
ITC
1 symbol
2 ORCL
3 MSFT
4 GOOG
5 AMZN
6 TCS
7 INFY
8 HDFC
9 RELI
10 SBIN
11 WIPRO
12 LT
13 ITC
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Gatling logs through logback, and Spring Boot's logback support is also on the test classpath.
Without this file logback prints a screenful of its own status lines before every simulation. -->
<configuration>
<statusListener class="ch.qos.logback.core.status.NopStatusListener"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder><pattern>%d{HH:mm:ss.SSS} [%-5level] %logger{15} - %msg%n</pattern></encoder>
</appender>
<root level="WARN"><appender-ref ref="CONSOLE"/></root>
</configuration>