Select Page

 

At Million Victories, we have reached a point where we want our game to be put under heavy stress, to test its resistance to a high load of user calls. There are many tools providing such features : we chose Gatling, a popular open source framework for load testing. Our targets here are the REST API endpoints of Million Lords.

Gatling gathers 3 different tools :

  • a recorder, acting like a proxy between a client and a server to record requests in-between, and generate the corresponding scenario;
  • a gatling, a heavy machine gun to tear apart your server, replaying the scenario a LOT of times;
  • a reporting tool, outputing nice graphs on response times and on how your app died.

We will not be using the recorder, since we want to make a custom scenario to try and understand the guts of the machine gun. It can however be incredibly helpful to output a scenario in a matter of minutes.
Gatling was created in order to write tests in the Scala programming language (a functionnal and refined version of Java). It requires a JVM, and is quite hard to learn, but can also grow very powerful.

 

Writing a scenario to fuel your machine gun

 

One pro of Gatling is the pretty straight forward way of creating a scenario. Even if you don’t know a lot about Scala, you can still build a quite robust scenario. The current scenario will send both GET and POST requests, feeding them with different parameters each time it is run, and reusing the answers provided by the server.

The skeleton of the scenario will be as follow :

  • create a player;
  • get its basic settings (gold, main city);
  • navigate on the map, load new tiles;
  • launch an attack;
  • get the new state of the player;
  • level up its main city.

Initializing the artillery

 

Gatling provides predefined classes that you can just extend to build your scenario. A basic start would be :

package computerdatabase

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._

class BasicSimulation extends Simulation {

  val httpConf = http
    .baseURL("http://my-api-url")
    .acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") // Here are the common headers
    .doNotTrackHeader("1")
    .acceptLanguageHeader("en-US,en;q=0.5")
    .acceptEncodingHeader("gzip, deflate")
    .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0")
    .header("CustomHeader", "CustomValue")

  val scn = scenario("Million Lords Crashtest")

  setUp(scn.inject(rampUsers(100) over (10 seconds)).protocols(httpConf))
}

Let’s analyze this chunk of code. First we import the common packages to get the tools to build the scenario. We then create our class extending the Gatling Simulation class.
We now have two objects to build : httpConf is to configure the global parameters of the scenario. It sets up things like the URL of our api, the common headers, some custom headers as well. Those headers will be used in each following request.

The second object to define is the scenario scn. In this example this scenario does nothing, except being named “Million Lords Crashtest”.
Ultimately, we load our gun with those two objects and get ready to fire : the last line injects a scenario with the given configuration to be played 100 times over 10 seconds.

Let’s now build some ammunition to make it even deadlier.

 

Building requests

 

The process of writing requests is straight forward. For the first request, we initialize a “feeder” that will feed different values to each play of the scenario. In our case the feeder take its values from a huge list (over 400k) of english words to generate random usernames.

 

 val feeder = csv("users.csv").random

  val scn = scenario("Million Lords Crashtest")
    .feed(feeder)
    .pause(3)
    .exec(http("Request_0")
    .get("/getVersion/")
    )

 

This first request is a basic GET. Note the pause preceding the call: Galting waits 3 seconds before the request, this way, you can set the pace of the scenario to better simulate a real use case.
Let’s now create a new user: we will check if the username is available before creating it.

 

   .pause(7677 milliseconds)
    .exec(http("Request_4")
    .post("/getPlayer/IsAvailable")
    .header("content-length", "18")
    .formParam("u", "${username}")
    )
    .pause(21 milliseconds)
    .exec(http("Request_5")
    .post("/getPlayer")
    .body(StringBody(
        """{"u": "${username}", "CustomKey":"CustomValue"}"""
        )).asJSON
    .check(jsonPath("$..id").findAll
        .transform(_.mkString(","))
        .saveAs("idPlayer"))
    .check(jsonPath("$..cities[0]").findAll
        .transform(_.mkString(","))
        .saveAs("idCity"))
    )

 

Pauses can be set in milliseconds as well.

We first build a POST request with a custom header and a custom form parameter. The syntax “${username}” comes from the feeder, because we named the only column of our csv “username”. This username will be the same during the whole process.
An assert method can be used to check if the the server is answering. However, Gatling have a default assert that will output the HTTP status against expected values.

Second is the creation of the player itself. This is another way of building a POST request. I prefer this one because it allows you to provide a custom body with anything you want. The triple quotes ensure everything is escaped, before being sent as JSON.

The interesting part now: we check the returned value from the server: we access JSON answer with XPath syntax.
Here, “$.._id” means we are looking for a child whose key is “_id”.  “$..c[0]” means we are looking for the first element of a list whose key is “c”. We get those two values and then save them as “idPlayer” and “idCity”.

Now that’s already some nice work done here. We can then reuse the defined objects for following requests:

 

   .pause(1941 milliseconds)
    .exec(http("Request_16")
    .get("/getPlayer/${idPlayer}")
    )

The “body” attribute allows any string you want. Here we are loading some new tiles based on coordinates :

 

   .pause(2053 milliseconds)
    .exec(http("Request_17")
    .post("/getTilesByMultibox")
    .header("content-length", "99")
    .body(StringBody(
        """[[[-67, 23], [-53, 37]], [[-67, 38], [-53, 52]]]"""
        )).asJSON
    )

Launching an attack follows the same structure (some parameters are omitted to make it more understandable):

 

   .pause(798 milliseconds)
    .exec(http("Request_63")
    .post("/setMovements/")
    .header("content-length", "640")
    .body(StringBody(
        """{"playerId": "${idPlayer}", "username": "Lerpz", "armyAmount": 20, "trace": [{"array": [4, 50]}, ..]}""")).asJSON
    )

Finally, leveling up the city is just another basic POST request, we provide idCity and the number of levels to add :

 

   .pause(662 milliseconds)
    .exec(http("Request_78")
    .post("/levelUpCity")
    .header("content-length", "67")
    .body(StringBody("""{"cityId": "${idCity}"}""")).asJSON
    )

Now the job is to take all those requests, to mix them up until you have a heavy dirty scenario, the closest you can get to a real use case. Each play will slightly differ thanks to the initial feeder. Our machine gun is now ready to fire.

 

Results

 

Gatling outputs nice graphs to analyze in a quick look the choke points of your api. Here is a sample output :

 

I like the two scalable graphs showing requests and responses. This is a good way to start to see where everything went wrong.

 

You can access the details of every request on the other tab, to see a precise analysis of its response time over the duration of the load testing.

To sum it up, Gatling is a powerful and beautiful way to break your app. Highly customizable, you can provide very detailed scenarii, to match real use cases and make sure your app is ready for its hour of glory.

Check out our other devlogs !

Author : Florentin Vallée