Gatling’s power comes not just from its built-in protocols, but from how easily you can extend it to speak any language.
Let’s imagine we need to test a new, obscure RPC protocol we’ve invented. We can’t just send raw bytes and hope for the best; we need Gatling to understand our protocol’s framing, authentication, and request/response structure. This is where custom protocol plugins shine.
Here’s a Gatling simulation using a hypothetical MyRpcProtocol:
import io.gatling.core.Predef._
import io.gatling.core.protocol.{Protocol, ProtocolKey}
import io.gatling.core.session.Session
import io.gatling.core.stats.StatsEngine
import io.gatling.core.structure.ScenarioBuilder
import io.gatling.core.util.Name
import io.netty.buffer.ByteBuf
import java.util.concurrent.atomic.AtomicLong
// --- Our Hypothetical RPC Protocol Definition ---
// A simple request structure
case class RpcRequest(id: Long, method: String, payload: ByteBuf)
// A simple response structure
case class RpcResponse(id: Long, status: Int, payload: ByteBuf)
// The core protocol implementation
class MyRpcProtocol(val name: String) extends Protocol {
type ProtocolKeyType = MyRpcProtocol
// Unique key to identify this protocol type
def key = MyRpcProtocol
// A way to generate unique request IDs
private val nextRequestId = new AtomicLong(0)
def generateRequestId(): Long = nextRequestId.getAndIncrement()
// This is where the magic happens: transforming a Gatling Session into a protocol-specific request
def transform(session: Session): Option[RpcRequest] = {
// In a real scenario, you'd pull data from the session (e.g., user ID, parameters)
val requestId = generateRequestId()
val methodName = session("methodName").as[String]
val requestBody = session("requestBody").as[ByteBuf] // Assume ByteBuf is already prepared
Some(RpcRequest(requestId, methodName, requestBody))
}
// This is the inverse: taking a protocol response and putting relevant info back into the Gatling Session
def transform(response: RpcResponse, session: Session): Session = {
session
.set("responseId", response.id)
.set("responseStatus", response.status)
.set("responsePayload", response.payload) // Store the payload if needed for further processing
}
}
object MyRpcProtocol {
// Define the ProtocolKey
val MyRpcProtocol = ProtocolKey(
// Function to extract the protocol from a scenario
(_: ScenarioBuilder).protocol[MyRpcProtocol],
// Function to register the protocol with a scenario
(_: ScenarioBuilder).addProtocol(_: MyRpcProtocol)
)
// A convenient builder
def myRpcProtocol(name: String = "myRpc"): MyRpcProtocol = new MyRpcProtocol(name)
}
// --- Gatling Simulation ---
class MyRpcSimulation extends Simulation {
// Define the protocol instance
val myRpc = myRpcProtocol("myRpcEndpoint")
// Define the HTTP protocol for potential other services, or if MyRpc needs an HTTP endpoint
// val httpProtocol = http.baseUrl("http://example.com")
val scn = scenario("My RPC Scenario")
.exec(session => {
// Prepare data for our RPC request and put it in the session
val requestBody = io.netty.buffer.Unpooled.copiedBuffer(s"{\"param\": \"value\"}".getBytes)
session.set("methodName", "getUser").set("requestBody", requestBody)
})
.exec(MyRpcAction("Call Get User")) // Our custom action that uses the protocol
.exec(session => {
// Process the response data stored in the session by transform(response, session)
println(s"Received response for ID: ${session("responseId").as[Long]}, Status: ${session("responseStatus").as[Int]}")
session
})
// Configure the simulation
setUp(
scn.inject(atOnceUsers(1)) // Inject 1 user immediately
).protocols(myRpc) // Register our custom protocol
}
// --- Custom Gatling Action ---
// This action will use our custom protocol
case class MyRpcAction(name: String) extends io.gatling.core.action.Action {
def name(configuration: Configuration): String = configuration.name
override def execute(session: Session): Unit = {
// Get the protocol instance
val myRpcProtocolInstance = session.protocol(MyRpcProtocol.MyRpcProtocol)
.getOrElse(throw new IllegalStateException("MyRpcProtocol not found in session"))
// Transform the session into our protocol-specific request
myRpcProtocolInstance.transform(session) match {
case Some(rpcRequest) =>
// Here, in a real plugin, you'd establish a connection (e.g., Netty, custom TCP)
// and send the rpcRequest.
// For this example, we'll simulate a response.
println(s"Simulating sending RPC request: ID=${rpcRequest.id}, Method=${rpcRequest.method}")
// Simulate receiving a response
val simulatedResponse = RpcResponse(rpcRequest.id, 200, io.netty.buffer.Unpooled.copiedBuffer("{\"data\": \"success\"}".getBytes))
println(s"Simulating receiving RPC response: ID=${simulatedResponse.id}, Status=${simulatedResponse.status}")
// Transform the response back into the Gatling session
val updatedSession = myRpcProtocolInstance.transform(simulatedResponse, session)
// Pass the session to the next action in the scenario
next.execute(updatedSession)
case None =>
// Handle cases where the session doesn't have the required data to form a request
println("Could not transform session to RPC request.")
// Decide how to proceed: fail, skip, or try to recover
next.execute(session) // Or potentially end the scenario or retry
}
}
}
The core idea is that a custom protocol plugin provides two main transformation methods:
-
transform(session: Session): Option[RequestType]: This takes the current GatlingSessionand converts it into your protocol’s native request object. This is where you’d pull any necessary data (like authentication tokens, message bodies, target endpoints) from the session and construct your request. It returnsOptionbecause sometimes a session might not be ready to produce a request for this protocol. -
transform(response: ResponseType, session: Session): Session: This is the inverse. When your protocol receives a response, you use this method to parse that response and inject relevant information back into the GatlingSession. This allows subsequent steps in your scenario to access response details (like status codes, extracted data, etc.).
You also need to define a ProtocolKey so Gatling knows how to associate your protocol with a scenario and how to retrieve it from the session. A common pattern is to create an object for your protocol that holds the ProtocolKey and a builder method.
The MyRpcAction is a custom Gatling Action that demonstrates how you’d integrate your protocol. It retrieves the protocol instance from the session, uses transform(session) to get the request, simulates sending and receiving, and then uses transform(response, session) to update the session before passing control to the next action.
The most surprising thing about building custom Gatling protocols is how much of the core logic you’re responsible for, from request generation to response parsing, without Gatling dictating the how, only the what (transforming to/from a session).
When you define your setUp block, you register your custom protocol using .protocols(myRpc). Gatling then makes this protocol instance available within the Session for any actions that follow.
The MyRpcAction is the glue. It’s a standard Gatling Action that knows how to interact with your specific protocol. In a real-world plugin, this action would likely involve low-level network I/O using libraries like Netty, managing connections, handling serialization/deserialization, and reporting metrics back to Gatling’s StatsEngine.
The MyRpcProtocol class itself is the heart of the plugin. It holds the state and logic for your custom protocol. The ProtocolKey is crucial; it’s how Gatling identifies and manages different protocol instances within a simulation. The ProtocolKey defines functions to extract and add your protocol to a ScenarioBuilder.
The real power comes from the transform methods. transform(session) is where you map Gatling’s generic Session data into your protocol’s specific request format. This could involve taking a JSON string from the session and turning it into a ByteBuf with specific framing, or extracting an authentication token. Conversely, transform(response, session) takes the raw response from your protocol and populates the Session with meaningful data, like response.id and response.status, so that subsequent Gatling steps can assert on them or use them in further requests.
When you’re building a custom protocol, remember that Gatling’s StatsEngine is your interface for reporting metrics. Your custom protocol’s actions will need to call methods on the StatsEngine (which is accessible via the Session or passed into the Action execute method) to record request durations, success/failure counts, and other vital performance indicators. This is usually done within the execute method of your custom Action after a request has been sent and a response received.
The next step after building a custom protocol is often integrating it with Gatling’s advanced features like feeders, checks, and custom assertions, which will require further understanding of how your protocol’s data interacts with Gatling’s core components.