diff --git a/README.md b/README.md index 166f659..9a3cfbc 100644 --- a/README.md +++ b/README.md @@ -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 | | [`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 | +| [`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. diff --git a/gatling-spring-boot/README.md b/gatling-spring-boot/README.md new file mode 100644 index 0000000..3ef1f4f --- /dev/null +++ b/gatling-spring-boot/README.md @@ -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 diff --git a/gatling-spring-boot/k6/knee.js b/gatling-spring-boot/k6/knee.js new file mode 100644 index 0000000..a35514f --- /dev/null +++ b/gatling-spring-boot/k6/knee.js @@ -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 }); +} diff --git a/gatling-spring-boot/k6/threshold-fail.js b/gatling-spring-boot/k6/threshold-fail.js new file mode 100644 index 0000000..e09644f --- /dev/null +++ b/gatling-spring-boot/k6/threshold-fail.js @@ -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)]}`); +} diff --git a/gatling-spring-boot/output/01-saturation-test.txt b/gatling-spring-boot/output/01-saturation-test.txt new file mode 100644 index 0000000..dd22a65 --- /dev/null +++ b/gatling-spring-boot/output/01-saturation-test.txt @@ -0,0 +1,3 @@ +workers=2 queue=3 delay=400ms, 7 simultaneous callers +served=5 rejected=2 +pool stats: completed=5 rejected=2 diff --git a/gatling-spring-boot/output/02-single-request.txt b/gatling-spring-boot/output/02-single-request.txt new file mode 100644 index 0000000..d6abd26 --- /dev/null +++ b/gatling-spring-boot/output/02-single-request.txt @@ -0,0 +1 @@ +{"symbol":"ORCL","price":344.44} diff --git a/gatling-spring-boot/output/03-basic-console.txt b/gatling-spring-boot/output/03-basic-console.txt new file mode 100644 index 0000000..eca00a1 --- /dev/null +++ b/gatling-spring-boot/output/03-basic-console.txt @@ -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] ------------------------------------------------------------------------ diff --git a/gatling-spring-boot/output/04-knee-steps-console.txt b/gatling-spring-boot/output/04-knee-steps-console.txt new file mode 100644 index 0000000..2d2a6be --- /dev/null +++ b/gatling-spring-boot/output/04-knee-steps-console.txt @@ -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] ------------------------------------------------------------------------ diff --git a/gatling-spring-boot/output/05-failing-assertion-console.txt b/gatling-spring-boot/output/05-failing-assertion-console.txt new file mode 100644 index 0000000..f1cf1e4 --- /dev/null +++ b/gatling-spring-boot/output/05-failing-assertion-console.txt @@ -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 diff --git a/gatling-spring-boot/output/05b-failing-assertion-exit.txt b/gatling-spring-boot/output/05b-failing-assertion-exit.txt new file mode 100644 index 0000000..47c3598 --- /dev/null +++ b/gatling-spring-boot/output/05b-failing-assertion-exit.txt @@ -0,0 +1 @@ +maven exit code: 1 diff --git a/gatling-spring-boot/output/06-knee-ramp-console.txt b/gatling-spring-boot/output/06-knee-ramp-console.txt new file mode 100644 index 0000000..dec5ef3 --- /dev/null +++ b/gatling-spring-boot/output/06-knee-ramp-console.txt @@ -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] ------------------------------------------------------------------------ diff --git a/gatling-spring-boot/output/07-closed-model-console.txt b/gatling-spring-boot/output/07-closed-model-console.txt new file mode 100644 index 0000000..ca4cb12 --- /dev/null +++ b/gatling-spring-boot/output/07-closed-model-console.txt @@ -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] ------------------------------------------------------------------------ diff --git a/gatling-spring-boot/output/10-gatling-steps-table.txt b/gatling-spring-boot/output/10-gatling-steps-table.txt new file mode 100644 index 0000000..3fe6fd2 --- /dev/null +++ b/gatling-spring-boot/output/10-gatling-steps-table.txt @@ -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 diff --git a/gatling-spring-boot/output/20-k6-version.txt b/gatling-spring-boot/output/20-k6-version.txt new file mode 100644 index 0000000..bfb820f --- /dev/null +++ b/gatling-spring-boot/output/20-k6-version.txt @@ -0,0 +1 @@ +k6 v2.3.0 (commit/e088784614, go1.26.8, linux/amd64) diff --git a/gatling-spring-boot/output/21-k6-steps-table.txt b/gatling-spring-boot/output/21-k6-steps-table.txt new file mode 100644 index 0000000..81b22b7 --- /dev/null +++ b/gatling-spring-boot/output/21-k6-steps-table.txt @@ -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 diff --git a/gatling-spring-boot/output/22-k6-knee-console.txt b/gatling-spring-boot/output/22-k6-knee-console.txt new file mode 100644 index 0000000..f544539 --- /dev/null +++ b/gatling-spring-boot/output/22-k6-knee-console.txt @@ -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 + + + diff --git a/gatling-spring-boot/output/23-k6-threshold-fail-console.txt b/gatling-spring-boot/output/23-k6-threshold-fail-console.txt new file mode 100644 index 0000000..a34ec9f --- /dev/null +++ b/gatling-spring-boot/output/23-k6-threshold-fail-console.txt @@ -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" diff --git a/gatling-spring-boot/output/23b-k6-threshold-fail-exit.txt b/gatling-spring-boot/output/23b-k6-threshold-fail-exit.txt new file mode 100644 index 0000000..7474890 --- /dev/null +++ b/gatling-spring-boot/output/23b-k6-threshold-fail-exit.txt @@ -0,0 +1 @@ +k6 exit code: 99 diff --git a/gatling-spring-boot/output/30-knee-chart.svg b/gatling-spring-boot/output/30-knee-chart.svg new file mode 100644 index 0000000..29a6790 --- /dev/null +++ b/gatling-spring-boot/output/30-knee-chart.svg @@ -0,0 +1,55 @@ +AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTozZmQ0ZmVhYS0wMzJlLTQyMjUtYWYwZS0zMDFhMmNjNTVlYjcAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaEREDOquEzouzr9R/QcNHMIAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDpjOWZhNjJlOC0yMzZiLTQzYzUtYWM1My1kYmZhNGJlNTllYjdscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNon/0pTFBaclKFM0DEBDPwvAAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggGRqXDMUhQ2PK3/ADf0cDfSCrkfKd/eXF8vlXMx7XztikZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaKmBub5JMEG4teE0ZSfXlUAAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRMAAAAAAAAAAAAAAAAZGhhc2hYIBRFsNLiCVxFj8uEkAFL4FMkgINybSTS9strmLSebbbGZG5hbWVuanVtYmYgbWFuaWZlc3RqZXhjbHVzaW9uc4GiZXN0YXJ0GQHvZmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjNmZDRmZWFhLTAzMmUtNDIyNS1hZjBlLTMwMWEyY2M1NWViNy9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOmQwYTM1NDgzLTgxZDUtNDY2Ni05OGE3LWU5ZTI1YzQ2Y2YyZXJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCAZGpcMxSFDY8rf8AN/RwN9IKuR8p395cXy+VczHtfO2KJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggHlMcssczmXmTy5Q2w1CMiUcj71WCJ6wCM3a0BQXn/2SiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggxx6SXExl1lgzVLPrb1XuoaGoyG3dgfIM1RIuDkYMppt0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQIH37ekHpCPmxz5EkSQRJ0OR4VFTftrE0RXyx5Rf84rv09+1FKPYeIwlkd9JUTIYGhRVb+F372D4odIahVad2tM= + +Response time (ms) vs injected rate (users/s), Gatling 3.16.0 and k6 v2.3.0 +0 +300 +600 +900 + + + + +30 +60 +90 +120 +150 +180 +210 +240 +calculated capacity: 8 workers / 0.050 s = 160 per second +Gatling p50Gatling p95 +k6 p95 +Requests rejected with HTTP 503 (%) +0 +10 +20 +30 +40 + + +30 + + +60 + + +90 + + +120 + + +150 + + +180 + + +210 + + +240 + +injected rate, users per second (blue = Gatling, amber = k6) + diff --git a/gatling-spring-boot/output/knee-gatling.json b/gatling-spring-boot/output/knee-gatling.json new file mode 100644 index 0000000..ab0a0c2 --- /dev/null +++ b/gatling-spring-boot/output/knee-gatling.json @@ -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 + } +] diff --git a/gatling-spring-boot/output/knee-k6.json b/gatling-spring-boot/output/knee-k6.json new file mode 100644 index 0000000..a3bd2ca --- /dev/null +++ b/gatling-spring-boot/output/knee-k6.json @@ -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 + } +] diff --git a/gatling-spring-boot/pom.xml b/gatling-spring-boot/pom.xml new file mode 100644 index 0000000..fad8a83 --- /dev/null +++ b/gatling-spring-boot/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 4.1.2 + + + + com.ankurm.testing + gatling-spring-boot-lab + 1.0.0 + + + 25 + 25 + 3.16.0 + 4.21.12 + + + + + org.springframework.boot + spring-boot-starter-webmvc + + + org.springframework.boot + spring-boot-starter-test + test + + + io.gatling.highcharts + gatling-charts-highcharts + ${gatling.version} + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + io.gatling + gatling-maven-plugin + ${gatling-maven-plugin.version} + + + + diff --git a/gatling-spring-boot/scripts/analyze.py b/gatling-spring-boot/scripts/analyze.py new file mode 100644 index 0000000..a26b9da --- /dev/null +++ b/gatling-spring-boot/scripts/analyze.py @@ -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 -> output/10-gatling-steps-table.txt, output/knee-gatling.json + analyze.py k6 -> 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'(.*?)', 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'' for s in steps if s in series) + return f'{dots}' + + o = ['', + '', + 'Response time (ms) vs injected rate (users/s), Gatling 3.16.0 and k6 v2.3.0'] + for ms in range(0, ymax + 1, 300): + o.append(f'{ms}') + o.append(f'') + 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'{int(s[5:8])}') + # 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'calculated capacity: 8 workers / 0.050 s = 160 per second') + # legend + o.append('Gatling p50' + 'Gatling p95') + if k: o.append('k6 p95') + # 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'Requests rejected with HTTP 503 (%)') + for v in range(0, top + 1, 10): + o.append(f'{v}') + 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'') + o.append(f'{int(s[5:8])}') + o.append(f'') + o.append(f'injected rate, users per second (blue = Gatling, amber = k6)') + o.append('') + (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__) diff --git a/gatling-spring-boot/scripts/run-all.sh b/gatling-spring-boot/scripts/run-all.sh new file mode 100644 index 0000000..148cd38 --- /dev/null +++ b/gatling-spring-boot/scripts/run-all.sh @@ -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() { # + 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/" diff --git a/gatling-spring-boot/src/main/java/lab/gatling/DownstreamPool.java b/gatling-spring-boot/src/main/java/lab/gatling/DownstreamPool.java new file mode 100644 index 0000000..accfff2 --- /dev/null +++ b/gatling-spring-boot/src/main/java/lab/gatling/DownstreamPool.java @@ -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 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 { } +} diff --git a/gatling-spring-boot/src/main/java/lab/gatling/LabApplication.java b/gatling-spring-boot/src/main/java/lab/gatling/LabApplication.java new file mode 100644 index 0000000..f2e9e08 --- /dev/null +++ b/gatling-spring-boot/src/main/java/lab/gatling/LabApplication.java @@ -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); + } +} diff --git a/gatling-spring-boot/src/main/java/lab/gatling/QuoteController.java b/gatling-spring-boot/src/main/java/lab/gatling/QuoteController.java new file mode 100644 index 0000000..1c17784 --- /dev/null +++ b/gatling-spring-boot/src/main/java/lab/gatling/QuoteController.java @@ -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 saturated() { + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).header("Retry-After", "1").body("downstream saturated"); + } +} diff --git a/gatling-spring-boot/src/main/resources/application.properties b/gatling-spring-boot/src/main/resources/application.properties new file mode 100644 index 0000000..c53fd3e --- /dev/null +++ b/gatling-spring-boot/src/main/resources/application.properties @@ -0,0 +1,2 @@ +server.port=${PORT:18300} +spring.application.name=gatling-spring-boot-lab diff --git a/gatling-spring-boot/src/test/java/lab/gatling/BasicSimulation.java b/gatling-spring-boot/src/test/java/lab/gatling/BasicSimulation.java new file mode 100644 index 0000000..afdf750 --- /dev/null +++ b/gatling-spring-boot/src/test/java/lab/gatling/BasicSimulation.java @@ -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 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)); + } +} diff --git a/gatling-spring-boot/src/test/java/lab/gatling/ClosedModelSimulation.java b/gatling-spring-boot/src/test/java/lab/gatling/ClosedModelSimulation.java new file mode 100644 index 0000000..d7a2b83 --- /dev/null +++ b/gatling-spring-boot/src/test/java/lab/gatling/ClosedModelSimulation.java @@ -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()); + } +} diff --git a/gatling-spring-boot/src/test/java/lab/gatling/Config.java b/gatling-spring-boot/src/test/java/lab/gatling/Config.java new file mode 100644 index 0000000..b28023a --- /dev/null +++ b/gatling-spring-boot/src/test/java/lab/gatling/Config.java @@ -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"); + } +} diff --git a/gatling-spring-boot/src/test/java/lab/gatling/FailingAssertionSimulation.java b/gatling-spring-boot/src/test/java/lab/gatling/FailingAssertionSimulation.java new file mode 100644 index 0000000..47fbc80 --- /dev/null +++ b/gatling-spring-boot/src/test/java/lab/gatling/FailingAssertionSimulation.java @@ -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)); + } +} diff --git a/gatling-spring-boot/src/test/java/lab/gatling/KneeRampSimulation.java b/gatling-spring-boot/src/test/java/lab/gatling/KneeRampSimulation.java new file mode 100644 index 0000000..a3da876 --- /dev/null +++ b/gatling-spring-boot/src/test/java/lab/gatling/KneeRampSimulation.java @@ -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()); + } +} diff --git a/gatling-spring-boot/src/test/java/lab/gatling/KneeStepsSimulation.java b/gatling-spring-boot/src/test/java/lab/gatling/KneeStepsSimulation.java new file mode 100644 index 0000000..7f94631 --- /dev/null +++ b/gatling-spring-boot/src/test/java/lab/gatling/KneeStepsSimulation.java @@ -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()); + } +} diff --git a/gatling-spring-boot/src/test/java/lab/gatling/SaturationTest.java b/gatling-spring-boot/src/test/java/lab/gatling/SaturationTest.java new file mode 100644 index 0000000..70a269a --- /dev/null +++ b/gatling-spring-boot/src/test/java/lab/gatling/SaturationTest.java @@ -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> 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 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 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); + } + } +} diff --git a/gatling-spring-boot/src/test/java/lab/gatling/Transcript.java b/gatling-spring-boot/src/test/java/lab/gatling/Transcript.java new file mode 100644 index 0000000..58284e2 --- /dev/null +++ b/gatling-spring-boot/src/test/java/lab/gatling/Transcript.java @@ -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 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); + } + } +} diff --git a/gatling-spring-boot/src/test/resources/data/symbols.csv b/gatling-spring-boot/src/test/resources/data/symbols.csv new file mode 100644 index 0000000..4619cb2 --- /dev/null +++ b/gatling-spring-boot/src/test/resources/data/symbols.csv @@ -0,0 +1,13 @@ +symbol +ORCL +MSFT +GOOG +AMZN +TCS +INFY +HDFC +RELI +SBIN +WIPRO +LT +ITC diff --git a/gatling-spring-boot/src/test/resources/logback-test.xml b/gatling-spring-boot/src/test/resources/logback-test.xml new file mode 100644 index 0000000..40e2c02 --- /dev/null +++ b/gatling-spring-boot/src/test/resources/logback-test.xml @@ -0,0 +1,10 @@ + + + + + + %d{HH:mm:ss.SSS} [%-5level] %logger{15} - %msg%n + + +