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
+1
View File
@@ -8,6 +8,7 @@ Runnable companion code for the testing posts on [ankurm.com](https://ankurm.com
| [`junit-6-migration/`](junit-6-migration) | JUnit 5 to 6 Migration: What Was Removed, What Changed and How to Upgrade | JUnit 5.14.4 and 6.1.3, JDK 25 | | [`junit-6-migration/`](junit-6-migration) | JUnit 5 to 6 Migration: What Was Removed, What Changed and How to Upgrade | JUnit 5.14.4 and 6.1.3, JDK 25 |
| [`assertj/`](assertj) | AssertJ Complete Guide: Soft Assertions, Recursive Comparison and Custom Assertions | AssertJ 3.27.7, JUnit 6.1.3, JDK 25 | | [`assertj/`](assertj) | AssertJ Complete Guide: Soft Assertions, Recursive Comparison and Custom Assertions | AssertJ 3.27.7, JUnit 6.1.3, JDK 25 |
| [`instancio/`](instancio) | Instancio: Generate Realistic Test Data Automatically in Java | Instancio 6.1.0, JUnit 6.1.3, JDK 25 | | [`instancio/`](instancio) | Instancio: Generate Realistic Test Data Automatically in Java | Instancio 6.1.0, JUnit 6.1.3, JDK 25 |
| [`gatling-spring-boot/`](gatling-spring-boot) | Load Testing Spring Boot with Gatling's Java DSL (and How It Compares to k6) | Gatling 3.16.0, Spring Boot 4.1.2, k6 v2.3.0, JDK 25 |
Every module keeps the real output of its tests in `output/`. Each test writes the transcript and asserts the same facts, so the numbers in the posts fail the build if they stop being true. Every module keeps the real output of its tests in `output/`. Each test writes the transcript and asserts the same facts, so the numbers in the posts fail the build if they stop being true.
+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>