I have this Java Gatling simulation:
package com.mycompany.performancetests;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mycompany.openapi.Notification;
import io.gatling.javaapi.core.ChainBuilder;
import io.gatling.javaapi.core.ScenarioBuilder;
import io.gatling.javaapi.core.Simulation;
import io.gatling.javaapi.http.HttpProtocolBuilder;
import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;
public class MySimulation extends Simulation {
private final HttpProtocolBuilder httpProtocol = http
.disableFollowRedirect()
.baseUrl(BASE_URL)
.acceptHeader("application/json");
private final ScenarioBuilder mainScn = scenario("MainScenario")
.exec(getStatus())
.exec(postStatus())
);
private static ChainBuilder getStatus() {
return exec(http("GetStatus")
.get(session -> API_PATH + "/notifications/00000000-0000-6000-7000-000000000000/status")
.check(status().is(200))
.check(jsonPath("$[0].id").notNull())
.check(jsonPath("$[0].modelVersion").notNull().saveAs("modelVersion"))
);
}
private static ChainBuilder postStatus() {
return exec(session -> {
// Retrieve the current modelVersion from the session
String currentModelVersion = session.getString("modelVersion");
// Increment the modelVersion by 1
int incrementedModelVersion = Integer.parseInt(currentModelVersion) + 1;
// Update the session with the incremented modelVersion
session.set("modelVersion", String.valueOf(incrementedModelVersion));
return session;
}).exec(http("PostStatus")
.post(session -> API_PATH + "/notifications/00000000-0000-6000-7000-000000000000/status")
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.body(StringBody(session -> """
{
"comment":"",
"fiFlaggedStatus":"FLAGGED",
"investigationAssessment":"WORTHY",
"investigationStatus":"COMPLETED",
"panRiskScore":986,
"modelVersion":"%s",
"created":"2026-07-02T08:20:38.814Z",
"id":""
}
""".formatted(session.getString("modelVersion"))))
.check(status().is(200))
.check(jsonPath("$.modelVersion").saveAs("modelVersion")) // Add this line
.check(jsonPath("$.id").notNull()));
}
{
setUp(mainScn.injectOpen(atOnceUsers(2))).protocols(httpProtocol);
}
}
which generates the following sequence of curl requests:
curl -X GET "https://myurl/00000000-0000-6000-7000-000000000000/status" \
-H "accept: application/json" \
curl -X POST "https://myurl/00000000-0000-6000-7000-000000000000/status" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
--data-binary '{"comment":"","fiFlaggedStatus":"FLAGGED","investigationAssessment":"WORTHY","investigationStatus":"COMPLETED","panRiskScore":986,"modelVersion":"1","created":"2026-07-02T08:20:38.814Z","id":""}'
curl -X POST "https://myurl/00000000-0000-6000-7000-000000000000/status" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
--data-binary '{"comment":"","fiFlaggedStatus":"FLAGGED","investigationAssessment":"WORTHY","investigationStatus":"COMPLETED","panRiskScore":986,"modelVersion":"1","created":"2026-07-02T08:20:38.814Z","id":""}'
However, I need the post status to be executed with different model versions (incremented by 1 integer between requests), so that it won't fail with a 409 conflict. For the initial value, it should add 1 on top of what is already in the database, which is retrieved as part of the get status api. This value is subsequently also part of the response for the post status request. However, as it can be seen by the curl requests, the two post requests are being executed both with the same model version and I'm not sure what to do to bypass that.
Thank you.
Update
Tried the below simulation using Atomic increment, but am still facing the issue as the requests don't seem to be executed in the expected order (post status with model version as 1 being called after post status with model version as 2)
import io.gatling.javaapi.core.ChainBuilder;
import io.gatling.javaapi.core.ScenarioBuilder;
import io.gatling.javaapi.core.Simulation;
import io.gatling.javaapi.http.HttpProtocolBuilder;
import java.util.concurrent.atomic.AtomicInteger;
import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.http;
import static io.gatling.javaapi.http.HttpDsl.status;
public class MySimulation extends Simulation {
private static final String BASE_URL = "myurl";
private static final String CID = "125283";
private static final String ICAS = "31931,5655,20929,9529,31627";
private static final String API_PATH = "/notifications/00000000-0000-6000-7000-000000000000/status";
// Initialize atomic counter
private static final AtomicInteger atomicModelVersion = new AtomicInteger();
private final HttpProtocolBuilder httpProtocol = http
.disableFollowRedirect()
.baseUrl(BASE_URL)
.acceptHeader("application/json");
private final ScenarioBuilder mainScn = scenario("MainScenario")
.exec(getStatus())
.exec(pause(1))
.exec(postStatus());
private static ChainBuilder getStatus() {
return exec(http("GetStatus")
.get(API_PATH)
.check(status().is(200))
.check(jsonPath("$[0].id").notNull())
.check(jsonPath("$[0].modelVersion").optional().saveAs("modelVersion"))
).exec(session -> {
// Retrieve the modelVersion from the session
String modelVersionStr = session.getString("modelVersion");
// Set modelVersion to 1 if it is null or empty
int initialModelVersion = (modelVersionStr == null || modelVersionStr.isEmpty()) ? 0 : Integer.parseInt(modelVersionStr);
// Set the atomic counter to the initial model version
atomicModelVersion.set(initialModelVersion);
return session.set("modelVersion", String.valueOf(initialModelVersion));
});
}
private static ChainBuilder postStatus() {
return exec(session -> {
// Get the current modelVersion
int currentModelVersion = atomicModelVersion.get();
// Update the session with the current modelVersion
session = session.set("modelVersion", String.valueOf(currentModelVersion));
// Increment the atomic counter for the next request
atomicModelVersion.incrementAndGet();
return session;
}).exec(http("PostStatus")
.post(API_PATH)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.body(StringBody(session -> """
{
"comment":"",
"fiFlaggedStatus":"FLAGGED",
"investigationAssessment":"WORTHY",
"investigationStatus":"COMPLETED",
"panRiskScore":986,
"modelVersion":"%s",
"id":""
}
""".formatted(session.getString("modelVersion"))))
.check(status().is(200))
.check(jsonPath("$.modelVersion").saveAs("modelVersion"))
.check(jsonPath("$.id").notNull()));
}
{
setUp(mainScn.injectOpen(atOnceUsers(2))).protocols(httpProtocol);
}
}
curl -X GET "https://myurl/00000000-0000-6000-7000-000000000000/status" \
-H "accept: application/json" \
curl -X GET "https://myurl/00000000-0000-6000-7000-000000000000/status" \
-H "accept: application/json" \
curl -X POST "https://myurl/00000000-0000-6000-7000-000000000000/status" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
--data-binary '{"comment":"","fiFlaggedStatus":"FLAGGED","investigationAssessment":"WORTHY","investigationStatus":"COMPLETED","panRiskScore":986,"modelVersion":"2","id":""}'
curl -X POST "https://myurl/00000000-0000-6000-7000-000000000000/status" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
--data-binary '{"comment":"","fiFlaggedStatus":"FLAGGED","investigationAssessment":"WORTHY","investigationStatus":"COMPLETED","panRiskScore":986,"modelVersion":"1","id":""}'
We got the below simulation working for our case. Not sure if this is the ideal solution, but we are now using an atomic integer to count and update the increase for model version for each notification, then we set up a repeat block to force the post and get requests to be triggered one after the other and in the correct sequence. Prior to this, when using multiple atOnceUsers, such as atOnceUsers(3), we would get all the get requests done one after the other, and only then all the post requests also done one after the other, so the retrieval for the modal version wasn't properly updating and the 409 error kept happening. Due to this same error, we also tried to add the synchronization between the requests and more than one notification, to perhaps avoid some race condition between the values retrieved by the api and the proper saving to the db. As we are now using the repeat block, this may not be needed, but we have yet to assert that and test a bit more to be sure, but for the moment just sharing the working version we currently have.
import io.gatling.javaapi.core.ChainBuilder;
import io.gatling.javaapi.core.ScenarioBuilder;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import static com.mycompany.config.Constants.Notifications.NOTIFICATION_ID;
import static com.mycompany.config.Constants.Notifications.NOTIFICATION_ID_2;
import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.http;
import static io.gatling.javaapi.http.HttpDsl.status;
public class MySimulation extends BaseSimulation {
private static final int REPETITION_AMOUNT = 10;
private static final int PAUSE_TIME = 100;
// Pre-existing hardcoded notification IDs
private static final String[] NOTIFICATION_IDS = {NOTIFICATION_ID.toString(), NOTIFICATION_ID_2.toString()};
// Map to store an AtomicInteger for each notification ID
private static final Map<String, AtomicInteger> modelVersionMap = new ConcurrentHashMap<>();
static {
for (String id : NOTIFICATION_IDS) {
modelVersionMap.put(id, new AtomicInteger(0));
}
}
private final ScenarioBuilder mainScn = scenario("MainScenario")
.exec(session -> {
// Assign a notification ID from the array in a round-robin fashion
long index = session.userId() % NOTIFICATION_IDS.length;
return session.set("notificationId", NOTIFICATION_IDS[(int) index]);
})
.exec(addAuthTokenCookie())
.repeat(REPETITION_AMOUNT, "counter").on(
exec(getStatus())
.pause(Duration.ofMillis(PAUSE_TIME))
.exec(postStatus())
);
private static ChainBuilder getStatus() {
return exec(http("GetStatus")
.get(session -> API_PATH + "/notifications/" + session.getString("notificationId") + "/status")
.check(status().is(200))
.check(jsonPath("$[0].modelVersion").optional().saveAs("modelVersion"))
).exec(session -> {
String notificationId = session.getString("notificationId");
String modelVersionStr = session.getString("modelVersion");
int initialModelVersion = (modelVersionStr == null || modelVersionStr.isEmpty()) ? 0 : Integer.parseInt(modelVersionStr);
synchronized (modelVersionMap) {
modelVersionMap.get(notificationId).set(initialModelVersion);
}
return session.set("modelVersion", String.valueOf(initialModelVersion));
});
}
private static ChainBuilder postStatus() {
return exec(session -> {
String notificationId = session.getString("notificationId");
int currentModelVersion;
synchronized (modelVersionMap) {
currentModelVersion = modelVersionMap.get(notificationId).getAndIncrement();
}
session = session.set("modelVersion", String.valueOf(currentModelVersion));
return session;
}).exec(http("PostStatus")
.post(session -> API_PATH + "/notifications/" + session.getString("notificationId") + "/status")
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.body(StringBody(session -> """
{
"comment":"",
"fiFlaggedStatus":"FLAGGED",
"investigationAssessment":"WORTHY",
"investigationStatus":"COMPLETED",
"panRiskScore":986,
"modelVersion":"%s",
"id":""
}
""".formatted(session.getString("modelVersion"))))
.check(status().is(200))
.check(jsonPath("$.modelVersion").saveAs("modelVersion"))
.check(jsonPath("$.id").notNull())
);
}
{
SetUp testSetup;
if (ENV.equals("stage")) {
testSetup = setUp(loginScn.injectOpen(atOnceUsers(1)).andThen(mainScn.injectOpen(atOnceUsers(1))))
.protocols(httpProtocol);
} else {
testSetup = setUp(mainScn.injectOpen(rampUsers(1).during(Duration.ofSeconds(1))))
.protocols(httpProtocol);
}
addAssertions(testSetup);
}
}