Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion .github/workflows/gatling-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
java-version: ${{ matrix.java }}
distribution: "temurin"
cache: "maven"

- name: Start up softwares via Docker Compose
run: |
pwd
Expand All @@ -47,3 +47,49 @@ jobs:

- name: Build and analyze
run: ./mvnw clean gatling:test

- name: Copy Gatling Reports to docs folder
if: always()
run: |
pwd
echo "Current working directory contents:"
ls -la

TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
REPORT_DIR="../docs/gatling-reports/${TIMESTAMP}"
echo "Creating report directory: ${REPORT_DIR}"
mkdir -p "${REPORT_DIR}"

if [ -d "target/gatling" ]; then
echo "Found Gatling reports directory in target/gatling"
ls -la target/gatling/
cp -r target/gatling/* "${REPORT_DIR}/"
echo "Reports copied successfully to ${REPORT_DIR}"
ls -la "${REPORT_DIR}/"
else
echo "ERROR: No Gatling reports found at target/gatling"
ls -la target/ || echo "target directory does not exist"
exit 0
fi
shell: bash

- name: Commit and Push Gatling Reports
if: always() && github.ref == 'refs/heads/main'
run: |
cd ..
pwd
echo "Checking for report files to commit:"
ls -la docs/gatling-reports/ || echo "docs/gatling-reports directory does not exist"

git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add docs/gatling-reports/
if git diff --staged --quiet; then
echo "No changes to commit"
else
git commit -m "Update Gatling performance test reports [skip ci]"
git push
fi
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2 changes: 2 additions & 0 deletions docs/gatling-reports/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# This directory stores Gatling performance test reports
# Reports are automatically generated and committed by GitHub Actions
2 changes: 1 addition & 1 deletion gatling-tests/ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This module contains comprehensive Gatling performance tests for the Spring Boot

1. All microservices must be running (catalog-service, inventory-service, order-service, etc.)
2. API Gateway must be accessible at http://localhost:8765 (configurable)
3. JDK 21 or later
3. JDK 25 or later

## Available Test Suites

Expand Down
9 changes: 7 additions & 2 deletions gatling-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
<version>0.0.1-SNAPSHOT</version>

<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<gatling.version>3.14.9</gatling.version>
<gatling-plugin.version>4.20.8</gatling-plugin.version>
Expand Down Expand Up @@ -57,11 +57,16 @@
<simulationClass>${gatling.simulationClass}</simulationClass>
<jvmArgs>
<jvmArg>-Xmx1g</jvmArg>
<jvmArg>-Xms512m</jvmArg>
</jvmArgs>
<includes>
<include>simulation.*</include>
</includes>
<runMultipleSimulations>true</runMultipleSimulations>
<!-- Enable more detailed reporting -->
<noReports>false</noReports>
<reportsOnly></reportsOnly>
<runDescription>Microservices Performance Test</runDescription>
</configuration>
</plugin>
</plugins>
Expand Down
6 changes: 4 additions & 2 deletions gatling-tests/src/test/java/simulation/BaseSimulation.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public abstract class BaseSimulation extends Simulation {
// Kafka initialization delay in seconds (Kafka takes 5-8 seconds to initialize on first product
// creation)
protected static final int KAFKA_INIT_DELAY_SECONDS =
Integer.parseInt(System.getProperty("kafkaInitDelay", "15"));
Integer.parseInt(System.getProperty("kafkaInitDelay", "10"));

// JSON object mapper for serialization/deserialization
protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
Expand All @@ -45,7 +45,9 @@ public abstract class BaseSimulation extends Simulation {
.maxConnectionsPerHost(100) // Increase max connections
.connectionHeader("keep-alive")
.acceptEncodingHeader("gzip, deflate")
.enableHttp2(); // Enable HTTP/2 for better performance
.enableHttp2() // Enable HTTP/2 for better performance
.inferHtmlResources() // Better resource inference
.silentResources(); // Don't log resource requests separately

// Common data feeder for product data with address information
protected Iterator<Map<String, Object>> enhancedProductFeeder() {
Expand Down
174 changes: 90 additions & 84 deletions gatling-tests/src/test/java/simulation/CreateProductSimulation.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static io.gatling.javaapi.core.CoreDsl.atOnceUsers;
import static io.gatling.javaapi.core.CoreDsl.bodyString;
import static io.gatling.javaapi.core.CoreDsl.constantUsersPerSec;
import static io.gatling.javaapi.core.CoreDsl.details;
import static io.gatling.javaapi.core.CoreDsl.exec;
import static io.gatling.javaapi.core.CoreDsl.global;
import static io.gatling.javaapi.core.CoreDsl.jsonPath;
Expand Down Expand Up @@ -32,33 +33,33 @@ public class CreateProductSimulation extends BaseSimulation {

private static final Logger LOGGER = LoggerFactory.getLogger(CreateProductSimulation.class);

// Configuration parameters - can be externalized to properties
private static final int RAMP_USERS = Integer.parseInt(System.getProperty("rampUsers", "3"));
// Configuration parameters - optimized for sustainable throughput
private static final int RAMP_USERS = Integer.parseInt(System.getProperty("rampUsers", "5"));
private static final int CONSTANT_USERS =
Integer.parseInt(System.getProperty("constantUsers", "15"));
Integer.parseInt(System.getProperty("constantUsers", "5"));
private static final int RAMP_DURATION_SECONDS =
Integer.parseInt(System.getProperty("rampDuration", "30"));
Integer.parseInt(System.getProperty("rampDuration", "10"));
private static final int TEST_DURATION_SECONDS =
Integer.parseInt(System.getProperty("testDuration", "60"));
Integer.parseInt(System.getProperty("testDuration", "30"));

// Breaking down the test flow into reusable components for better readability and
// maintainability
private final ChainBuilder createProduct =
exec(http("Create product")
.post("/catalog-service/api/catalog")
.body(
StringBody(
"""
{
"productCode": "#{productCode}",
"productName": "#{productName}",
"price": #{price},
"description": "Performance test product"
}
"""))
.asJson()
.check(status().is(201))
.check(header("location").saveAs("productLocation")))
.post("/catalog-service/api/catalog")
.body(
StringBody(
"""
{
"productCode": "#{productCode}",
"productName": "#{productName}",
"price": #{price},
"description": "Performance test product"
}
"""))
.asJson()
.check(status().is(201))
.check(header("location").saveAs("productLocation")))
.exec(
session -> {
LOGGER.debug(
Expand All @@ -81,9 +82,9 @@ public class CreateProductSimulation extends BaseSimulation {

private final ChainBuilder getInventory =
exec(http("Get product inventory")
.get("/inventory-service/api/inventory/#{productCode}")
.check(status().is(200))
.check(bodyString().saveAs("inventoryResponseBody")))
.get("/inventory-service/api/inventory/#{productCode}")
.check(status().is(200))
.check(bodyString().saveAs("inventoryResponseBody")))
.pause(1000) // Add a pause to ensure the response is processed
.exec(
session -> {
Expand Down Expand Up @@ -112,20 +113,20 @@ public class CreateProductSimulation extends BaseSimulation {

private final ChainBuilder updateInventory =
exec(http("Update inventory")
.put(
.put(
session ->
"/inventory-service/api/inventory/"
+ getInventoryId(
session.getString(
"inventoryResponseBody")))
.body(
StringBody(
session ->
"/inventory-service/api/inventory/"
+ getInventoryId(
session.getString(
"inventoryResponseBody")))
.body(
StringBody(
session ->
getBodyAsString(
session.getString(
"inventoryResponseBody"))))
.asJson()
.check(status().is(200)))
getBodyAsString(
session.getString(
"inventoryResponseBody"))))
.asJson()
.check(status().is(200)))
.exec(
session -> {
LOGGER.debug(
Expand All @@ -136,32 +137,32 @@ public class CreateProductSimulation extends BaseSimulation {

private final ChainBuilder createOrder =
exec(http("Create order with product")
.post("/order-service/api/orders")
.body(
StringBody(
"""
{
"customerId": #{customerId},
"items": [
{
"productCode": "#{productCode}",
"quantity": #{quantity},
"productPrice": #{price}
}
],
"deliveryAddress": {
"addressLine1": "123 Performance Test St",
"addressLine2": "Suite 456",
"city": "Test City",
"state": "TS",
"zipCode": "12345",
"country": "Test Country"
}
}
"""))
.asJson()
.check(status().is(201))
.check(header("location").saveAs("orderLocation")))
.post("/order-service/api/orders")
.body(
StringBody(
"""
{
"customerId": #{customerId},
"items": [
{
"productCode": "#{productCode}",
"quantity": #{quantity},
"productPrice": #{price}
}
],
"deliveryAddress": {
"addressLine1": "123 Performance Test St",
"addressLine2": "Suite 456",
"city": "Test City",
"state": "TS",
"zipCode": "12345",
"country": "Test Country"
}
}
"""))
.asJson()
.check(status().is(201))
.check(header("location").saveAs("orderLocation")))
.exec(
session -> {
LOGGER.debug(
Expand All @@ -174,21 +175,21 @@ public class CreateProductSimulation extends BaseSimulation {
scenario("E2E Product Creation Workflow")
.feed(enhancedProductFeeder())
.exec(createProduct)
.pause(Duration.ofMillis(10)) // Add pause to reduce load
.pause(Duration.ofMillis(100)) // Small pause for realistic behavior
.exec(getProduct)
.pause(Duration.ofMillis(10)) // Add pause to reduce load
.pause(Duration.ofMillis(100)) // Small pause for realistic behavior
.exec(getInventory)
.pause(Duration.ofMillis(20)) // More pause before the critical update
.pause(Duration.ofMillis(200)) // Small pause before the critical update
.exec(
session -> {
// Add safeguard to skip inventory update if inventory info is
// missing or invalid
if (session.contains("inventoryResponseBody")
&& session.getString("inventoryResponseBody") != null
&& !Objects.requireNonNull(
session.getString("inventoryResponseBody"))
.trim()
.isEmpty()) {
session.getString("inventoryResponseBody"))
.trim()
.isEmpty()) {
return session;
} else {
LOGGER.warn(
Expand All @@ -199,7 +200,7 @@ public class CreateProductSimulation extends BaseSimulation {
}
})
.exec(updateInventory)
.pause(Duration.ofMillis(10)) // Add pause to reduce load
.pause(Duration.ofMillis(100)) // Small pause for realistic behavior
.exec(createOrder);

/**
Expand Down Expand Up @@ -328,28 +329,33 @@ public CreateProductSimulation() {

// Global assertions to validate overall service performance
this.setUp(
productWorkflow
// Small pause between steps to simulate realistic user behavior
.pause(Duration.ofMillis(500))
.injectOpen(
// Initial single user for Kafka initialization
atOnceUsers(1),
// Wait for Kafka initialization to complete
nothingFor(Duration.ofSeconds(KAFKA_INIT_DELAY_SECONDS)),
// Ramp up users phase for gradual load increase
rampUsers(RAMP_USERS)
.during(Duration.ofSeconds(RAMP_DURATION_SECONDS)),
// Constant load phase to test system stability
constantUsersPerSec(CONSTANT_USERS)
.during(Duration.ofSeconds(TEST_DURATION_SECONDS))))
productWorkflow.injectOpen(
// Initial single user for Kafka initialization
atOnceUsers(1),
// Wait for Kafka initialization to complete
nothingFor(Duration.ofSeconds(KAFKA_INIT_DELAY_SECONDS)),
// Ramp up users phase for gradual load increase
rampUsers(RAMP_USERS)
.during(Duration.ofSeconds(RAMP_DURATION_SECONDS)),
// Constant user arrival rate (not constant concurrent users)
constantUsersPerSec(CONSTANT_USERS)
.during(Duration.ofSeconds(TEST_DURATION_SECONDS))))
.protocols(httpProtocol)
.assertions(
// Add global performance SLA assertions
global().responseTime().mean().lt(1500), // Mean response time under 1.5s
global().responseTime()
.percentile(95)
.lt(5000), // 95% of responses under 5s
global().failedRequests().percent().lt(5.0) // Less than 5% failed requests
);
global().responseTime()
.percentile(99)
.lt(8000), // 99% of responses under 8s
global().successfulRequests().percent().gt(95.0), // More than 95% success
global().failedRequests().percent().lt(5.0), // Less than 5% failed requests
// Request-specific assertions for detailed metrics
details("Create product").responseTime().mean().lt(500),
details("Create product").successfulRequests().percent().gt(95.0),
details("Create order with product").responseTime().mean().lt(800),
details("Update inventory").responseTime().mean().lt(400));
}
}
Loading
Loading