Hi,
I am writing a system which exposes many endpoints which clients access. One use case we have is of an application starting up. This app calls a number of http endpoints sequentially. I want to time how long it takes to complete all of these calls under various loads.
The application uses a java client which is wrapper around the http API, which handles retries in the event receiving a 429 - too many requests.
I have attempted to plug the java client into Gatling. It was a bit awkward and I have some concerns that what I am attempting is not really possible. For example, some of the results are a bit dubious.
My code looks like this:
import static io.gatling.javaapi.core.CoreDsl.atOnce;
import static java.util.Arrays.asList;
import com.company.horizon.clientsdk.Horizon;
import com.company.horizon.clientsdk.HorizonConfig;
import com.company.horizon.clientsdk.criteria.StreamQuery;
import com.company.horizon.clientsdk.subscription.Port;
import com.company.horizon.performance.client.HorizonClientDsl;
import io.gatling.javaapi.core.CoreDsl;
import io.gatling.javaapi.core.ScenarioBuilder;
import io.gatling.javaapi.core.Simulation;
public class ExampleSimulation extends Simulation {
{
Horizon horizon = Horizon.create(HorizonConfig.ephemeral(1035), Port.random());
var productQuery = new StreamQuery("product");
var queryActionBuilder = HorizonClientDsl.horizon(horizon)
.name("StockPricingAppStartup")
.queries(asList(productQuery.toSearchQuery()));
ScenarioBuilder scn = CoreDsl.scenario("StockPricingAppStartupTest")
.exec(queryActionBuilder);
setUp(
scn.injectOpen(atOnce(1))
);
}
}
import com.company.horizon.clientsdk.Horizon;
public class HorizonClientDsl {
public static ClientQueryActionBuilder horizon(Horizon horizon) {
return new ClientQueryActionBuilder(horizon);
}
}
import com.company.horizon.clientsdk.Horizon;
import com.company.horizon.clientsdk.document.SearchQuery;
import io.gatling.javaapi.core.ActionBuilder;
import io.gatling.javaapi.core.ChainBuilder;
import java.util.List;
public class ClientQueryActionBuilder implements ActionBuilder {
private ScalaClientQueryActionBuilder scalaClientQueryActionBuilder;
public ClientQueryActionBuilder(Horizon horizon) {
this.scalaClientQueryActionBuilder = new ScalaClientQueryActionBuilder();
scalaClientQueryActionBuilder.setHorizon(horizon);
}
@Override
public io.gatling.core.action.builder.ActionBuilder asScala() {
return scalaClientQueryActionBuilder;
}
@Override
public ChainBuilder toChainBuilder() {
return ActionBuilder.super.toChainBuilder();
}
public ClientQueryActionBuilder queries(List<SearchQuery> searchQueries) {
scalaClientQueryActionBuilder.setSearchQueries(searchQueries);
return this;
}
public ClientQueryActionBuilder name(String name) {
scalaClientQueryActionBuilder.setName(name);
return this;
}
}
import com.company.horizon.clientsdk.Horizon;
import com.company.horizon.clientsdk.document.SearchQuery;
import io.gatling.core.action.Action;
import io.gatling.core.action.builder.ActionBuilder;
import io.gatling.core.structure.ScenarioContext;
import java.util.List;
public class ScalaClientQueryActionBuilder implements ActionBuilder {
private Horizon horizon;
private List<SearchQuery> searchQueries;
private String name;
@Override
public Action build(ScenarioContext ctx, Action next) {
var statsEngine = ctx.coreComponents().statsEngine();
return new QueryAction(next, name, horizon, searchQueries, statsEngine);
}
public void setHorizon(Horizon horizon) {
this.horizon = horizon;
}
public void setSearchQueries(List<SearchQuery> searchQueries) {
this.searchQueries = searchQueries;
}
public void setName(String name) {
this.name = name;
}
}
import com.company.horizon.clientsdk.Horizon;
import com.company.horizon.clientsdk.document.MapItemParser;
import com.company.horizon.clientsdk.document.SearchQuery;
import com.typesafe.scalalogging.Logger;
import io.gatling.commons.stats.KO$;
import io.gatling.commons.stats.OK$;
import io.gatling.core.action.Action;
import io.gatling.core.session.Session;
import io.gatling.core.stats.StatsEngine;
import scala.Option;
import scala.collection.immutable.List;
public class QueryAction implements Action {
private final Action next;
private final String name;
private final Horizon horizon;
private final java.util.List<SearchQuery> queries;
private StatsEngine statsEngine;
private Logger logger;
public QueryAction(Action next, String name, Horizon horizon, java.util.List<SearchQuery> queries, StatsEngine statsEngine) {
this.next = next;
this.name = name;
this.horizon = horizon;
this.queries = queries;
this.statsEngine = statsEngine;
}
@Override
public String name() {
return name;
}
@Override
public void execute(Session session) {
List<String> groups = scala.collection.immutable.List.<String>newBuilder().result();
var parser = new MapItemParser();
long start = System.currentTimeMillis();
try {
for (SearchQuery query : queries) {
var mapItems = horizon.dao(parser).find(query);
}
long end = System.currentTimeMillis();
statsEngine.logResponse(name, groups, name, start, end, OK$.MODULE$, Option.empty(), Option.empty());
} catch (Exception e) {
long end = System.currentTimeMillis();
statsEngine.logResponse(name, groups, name, start, end, KO$.MODULE$, Option.empty(), Option.apply(e.getMessage()));
}
next.execute(session);
}
@Override
public void com$typesafe$scalalogging$StrictLogging$_setter_$logger_$eq(Logger logger) {
this.logger = logger;
}
@Override
public Logger logger() {
return logger;
}
}
This seems to mostly work. However I’ve noticed a few odd things.
a) the minimum time for one of these action is an order magnitude smaller than it should be.
b) the simulation log looks weird. The users start after the requests happen. e.g. this run with two users
RUN com.company.horizon.performance.ExampleSimulation examplesimulation 1707583474903 3.10.3
REQUEST StockPricingAppStartup 1707583475002 1707583481456 OK
REQUEST StockPricingAppStartup 1707583475002 1707583481457 OK
USER StockPricingAppStartupTest START 1707583481483
USER StockPricingAppStartupTest START 1707583481483
USER StockPricingAppStartupTest END 1707583481484
USER StockPricingAppStartupTest END 1707583481484
I’m worried that I am using in a Gatling in a way that is just not right. Or maybe there are a few things I can tweak to make this work.
TL DR; Can I use Gatling to test the performance of custom bits of java code.
If you made to here, thanks for reading
Tom