Skip to content

Instantly share code, notes, and snippets.

@jeanmarc
Last active February 28, 2022 13:32
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jeanmarc/ab30dd899aea1f8cb914ea2ed1a11e7b to your computer and use it in GitHub Desktop.
Save jeanmarc/ab30dd899aea1f8cb914ea2ed1a11e7b to your computer and use it in GitHub Desktop.
Reusable connections in Gatling tests

Introduction

Gist explaining a way to add long-living connections to Gatling and create scenarios that reuse the connection(s) to put load on the target system.

I have used the gatling-kafka plugin available on GitHub as inspiration.

Note that the code snippets provided here do not complile right out of the box. You will have to add some dependencies, and code yourself to make things work.

Prerequisites

The code snippets are based on my experiments using the following component versions:

  • SBT 0.13.8
  • Scala 2.11.7
  • Gatling 2.1.7

See build.sbt for details

Approach

You store connection information on the Gatling session object, allowing the subsequent steps in the scenario to use the correct connection. This is done by storing a connection object: session.set("someConnectionName", connObject).

Your scenario has to make sure it creates 1 connection as the first step, then uses that connection in its remaining steps, and finally closes the connection as its last step.

The load test is then executed by starting a couple of users, each playing the scenario. One connection is created per user, mimicking the production situation.

The creation action is executed first, and it:

  • opens the connection (results in some connection reference/object/actor)
  • stores the connection reference/object/actor on the session object
  • sends a continuation message (the updated session object) to the next actor

The regular actions that are added to the scenario each perform the following steps:

  • retrieve the connection object from the session object
  • perform some action via the connection
  • send a continuation message (the session object) to the next actor

The termination action is executed last and it:

  • retrieves the connection object from the session
  • closes the connection (and destroys the connection reference/object/actor if needed)
  • removes the connection reference/object/actor from the session object
  • sends a continuation message (the updated session object) to the next actor

If needed you can add timing recording to the actions, see Metrics.scala

Builders vs Actions

Gatling scenarios are not the actual scenarios, but more a template that is used to create action actors whenever they are executed. A DSL is suggested (Predef.scala) that creates statement wrappers around the ActionBuilders. ActionBuilders are objects that implement the build() method, which creates an Action actor whenever it is called. The actors are the ones that open the connection, or call the actions, or close the connection.

Usage example: BasicSimulation.scala

import io.gatling.core.Predef._
import io.gatling.core.action.{Failable, Interruptable}
import io.gatling.core.result.message.OK
import io.gatling.core.result.writer.DataWriterClient
import io.gatling.core.util.TimeHelper._
import io.gatling.core.validation.{Success, Validation}
import nl.ing.sbt.gatling.plugin.aether.client.SimpleAetherClient
object BarAction {
}
case class BarAction (next: ActorRef, connectionName: String)
extends Interruptable
with Failable
with DataWriterClient {
def executeOrFail(session: Session): Validation[Unit] = {
val start = nowMillis
logMessage("performing BarAction")
val conn = session(connectionName).as[ConnectionObject]
val endRequest = nowMillis
val result = conn.barAction
// TODO: maybe evaluate result and reject if it is not what is expected?
val finish = nowMillis
logMessage("BarAction returned", attrs)
writeRequestData(session, "BarAction", start, endRequest, endRequest, finish, OK, Some(result.toString()))
next ! session
Success("OK")
}
def logMessage(text: String): Unit = {
logger.debug(text)
}
}
import io.gatling.core.action.builder.ActionBuilder
import io.gatling.core.config.Protocols
case class BarActionBuilder( connName: String) extends ActionBuilder{
def build(next: ActorRef, registry: Protocols): ActorRef = {
actor(actorName("barAction"))(new BarAction(next, connName))
}
}
class BasicSimulation extends Simulation
with StrictLogging {
val connConf = ??? // Your protocol to create the connection
val sampleScenario = scenario("SampleConnection")
.exec(openConnection("myConn", connConf))
.pause(1 seconds)
.exec(actionBar("myConn"))
.pause(6 seconds)
.exec(actionFooBar("myConn"))
.pause(2 seconds)
.exec(actionFoo("myConn"))
.pause(2 seconds)
.exec(closeConnection("myConn"))
setUp(sampleScenario.inject(constantUsersPerSec(2) during (120 seconds),
??? // add other scenarios when needed)
)
.protocols(connConf)
.assertions(global.successfulRequests.percent.is(100))
}
import sbt._
import sbt.Keys._
scalaVersion := "2.11.7"
libraryDependencies ++= Seq(
"io.gatling.highcharts" % "gatling-charts-highcharts" % "2.1.7" % "provided, test",
"io.gatling" % "gatling-test-framework" % "2.1.7" % "test"
)
lazy val root = (project in file ("."))
.enablePlugins(GatlingPlugin)
import io.gatling.core.action.builder.ActionBuilder
import io.gatling.core.config.Protocols
case class CloseFooConnectionActionBuilder(connName: String) extends ActionBuilder{
def build(next: ActorRef, registry: Protocols) = {
actor(actorName("CloseFooConnection"))(new CloseFooConnectionAction( next, connName))
}
}
import io.gatling.core.Predef._
import io.gatling.core.action.{Failable, Interruptable}
import io.gatling.core.result.message.OK
import io.gatling.core.result.writer.DataWriterClient
import io.gatling.core.util.TimeHelper._
import io.gatling.core.validation.Validation
case class CloseFooConnectionAction ( next: ActorRef, connName: String)
extends Interruptable
with Failable
with DataWriterClient {
def executeOrFail(session: Session): Validation[Unit] = {
val start = nowMillis
val theConnection = session(connName).as[ConnectionObject]
logger.info(s"closing connection ...")
val endRequest = nowMillis
// close the connection
theConnection.close()
logger.info(s"closed connection")
val finish = nowMillis
writeRequestData(session, "CloseFooConnection", start, endRequest, endRequest, finish, OK, None)
next ! newSession.remove(connName)
}
}
import io.gatling.core.Predef._
import io.gatling.core.action.{Failable, Interruptable}
import io.gatling.core.result.message.OK
import io.gatling.core.result.writer.DataWriterClient
import io.gatling.core.util.TimeHelper._
import io.gatling.core.validation.{Success, Validation}
case class FooConnectionAction( next: ActorRef, name: String, protocol: ConnectionProtocol) extends Interruptable
with Failable
with DataWriterClient{
def executeOrFail(session: Session): Validation[Unit] = {
val start = nowMillis
logger.info(s"connecting to ${protocol.connection} ...")
val endRequest = nowMillis
val conn = ??? // create the connection using the informationin the protocol object
logger.info(s"connected to ${protocol.connection}")
val finish = nowMillis
writeRequestData(session, "FooConnection-" + protocol.connection, start, endRequest, endRequest, finish, OK, Some(conn.toString()))
// place the connection object on the session
next ! session.set(name, conn)
}
}
import io.gatling.core.action.builder.ActionBuilder
import io.gatling.core.config.Protocols
case class FooConnectionActionBuilder( connName: String, protocol: ConnectionProtocol) extends ActionBuilder{
def build(next: ActorRef, registry: Protocols) = {
actor(actorName("fooConnection"))(new FooConnectionAction( next, connName, protocol))
}
}
import io.gatling.core.action.{Failable, Interruptable}
import io.gatling.core.result.writer.DataWriterClient
import io.gatling.core.Predef.Session
import io.gatling.core.util.TimeHelper.nowMillis
class SomeAction(attr: Any) extends Interruptable with Failable with DataWriterClient {
def executeOrFail()
def executeOrFail(session: Session): Validation[Unit] = {
val start = nowMillis
val myConnection = session("knownConnectionName").as[YourConnectionObjectClass]
// prepare your action
???
val endRequest = nowMillis
// fire your action through the connection
val result = myConnection.???
// TODO: maybe evaluate result and reject if it is not what is expected?
val finish = nowMillis
writeRequestData(session, "???yourActionIdentifier", start, endRequest, endRequest, finish, OK, Some(result.toString()))
next ! session
}
}
import java.util.concurrent.atomic.AtomicInteger
object Predef {
def openConnection( name: String, protocol: ConnectionProtocol) = new FooConnectionActionBuilder(name, protocol)
// if you need shared state between the actions, consider creating a state object and
// storing it on the session object in the same way that the connection object is stored
def actionBar(connName: String) = new BarActionBuilder( connName)
def actionFoo(connName: String) = new FooActionBuilder( connName)
def actionFooBar(connName: String) = new FooBarActionBuilder( connName)
def closeConnection( connName: String) =
new CloseFooConnectionActionBuilder( connName)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment