GraphQL Subscriptions

Learn how to run GraphQL subscriptions over WebSocket, and check the messages they push.

Gatling supports GraphQL subscriptions over WebSocket with the graphql-transport-ws protocol.

A virtual user has one WebSocket, over which it can multiplex several subscriptions. Each subscription carries an id and messages are matched on it.

Prerequisites

Gatling GraphQL SDK is not imported by default.

You have to manually add the following imports:

     
import static io.gatling.javaapi.graphql.GraphQlDsl.*;
import io.gatling.javaapi.graphql.GraphQlDsl.*
import io.gatling.graphql.Predef._

You must register the GraphQL protocol and configure wsBaseUrl on the HTTP protocol. The WebSocket path is the protocol wsEndpoint.

Connect

Use graphqlWs.connect to open the WebSocket and perform the connection_init / connection_ack handshake. You must connect before you subscribe.

     
exec(graphqlWs.connect());
exec(graphqlWs.connect()
  .requestName("Open GraphQL WebSocket")
  .endpoint("/other/graphql")
  .connectionInitPayload("authorization", "Bearer #{token}")
  .connectionInitPayload(Map.of("locale", "en"))
  .ackTimeout(Duration.ofSeconds(5)));
exec(graphqlWs.connect()
  .connectionInitPayloadJson("{\"authorization\": \"Bearer #{token.jsonStringify()}\"}"));
exec(graphqlWs.connect())
exec(graphqlWs.connect()
  .requestName("Open GraphQL WebSocket")
  .endpoint("/other/graphql")
  .connectionInitPayload("authorization", "Bearer #{token}")
  .connectionInitPayload(mapOf("locale" to "en"))
  .ackTimeout(Duration.ofSeconds(5)))
exec(graphqlWs.connect()
  .connectionInitPayloadJson("""{"authorization": "Bearer #{token.jsonStringify()}"}"""))
exec(graphqlWs.connect)
exec(
  graphqlWs.connect
    .requestName("Open GraphQL WebSocket")
    .endpoint("/other/graphql")
    .connectionInitPayload("authorization", "Bearer #{token}")
    .connectionInitPayload(Map("locale" -> "en"))
    .ackTimeout(5.seconds)
)
exec(
  graphqlWs.connect
    .connectionInitPayloadJson("""{"authorization": "Bearer #{token.jsonStringify()}"}""")
)
  • requestName: override the request name.
  • endpoint: override the protocol wsEndpoint.
  • connectionInitPayload: set entries of the connection_init message payload, typically where the server expects credentials.
  • connectionInitPayloadJson: set the whole payload at once as a JSON object that can contain Gatling Expression Language placeholders. It can’t be combined with connectionInitPayload.
  • ackTimeout: override how long to wait for the connection_ack message (default: 10 seconds).

Gatling reports the handshake under the names graphql-ws connect, connection_init and connection_ack.

Subscribe

Use subscribe to subscribe with an inline document, or subscribeFile to load the document from a classpath resource. The document must declare a subscription and be known when the Simulation is built.

Like requests, a subscription supports requestName, operationName, variable, variables and variablesJson.

     
exec(graphqlWs
  .subscribe("subscription OrderEvents($id: ID!) { orderCreated(customerId: $id) { id } }")
  .variable("id", "#{customerId}")
  .subscriptionName("orders"));
exec(graphqlWs.subscribeFile("graphql/orderEvents.graphql"));
exec(graphqlWs
  .subscribe("subscription OrderEvents(\$id: ID!) { orderCreated(customerId: \$id) { id } }")
  .variable("id", "#{customerId}")
  .subscriptionName("orders"))
exec(graphqlWs.subscribeFile("graphql/orderEvents.graphql"))
exec(
  graphqlWs
    .subscribe("subscription OrderEvents($id: ID!) { orderCreated(customerId: $id) { id } }")
    .variable("id", "#{customerId}")
    .subscriptionName("orders")
)
exec(graphqlWs.subscribeFile("graphql/orderEvents.graphql"))

Use subscriptionName to set the id the subscription is registered under, which unsubscribe needs. It defaults to the operation name. A virtual user can’t have two active subscriptions with the same id: give each concurrent subscription its own subscriptionName.

Wait for messages

By default, subscribe doesn’t wait for any message. Use await to wait for messages and checkNext to define the checks to perform on each of them. Each checkNext expects one single next message, and messages are reported on their own, under the name <request name> next. Gatling measures the response time of each message from the message that came before it. It’s the inter message latency rather than the time since the subscription started.

     
exec(graphqlWs
  .subscribeFile("graphql/orderEvents.graphql")
  .await(Duration.ofSeconds(10)).on(
    graphqlWs.checkNext()
      .check(
        graphqlWs.data.jsonPath("$.orderCreated.id").saveAs("orderId"),
        graphqlWs.errors.jsonPath("$[0].message").optional()
      )
  ));
exec(graphqlWs
  .subscribeFile("graphql/orderEvents.graphql")
  .await(Duration.ofSeconds(10)).on(
    graphqlWs.checkNext()
      .check(
        graphqlWs.data.jsonPath("$.orderCreated.id").saveAs("orderId"),
        graphqlWs.errors.jsonPath("$[0].message").optional()
      )
  ))
exec(
  graphqlWs
    .subscribeFile("graphql/orderEvents.graphql")
    .await(10.seconds)(
      graphqlWs.checkNext
        .check(
          graphqlWs.data.jsonPath("$.orderCreated.id").saveAs("orderId"),
          graphqlWs.errors.jsonPath("$[0].message").optional
        )
    )
)
  • checkNext accepts a name to report the message under.
  • check accepts the same criteria as GraphQL requests, except that you use graphqlWs.data and graphqlWs.errors in place of graphqlData and graphqlErrors.
  • postCheck applies a function on the Session resulting from the checks.
  • silent doesn’t report the message in the statistics.

The error policy of the protocol applies to the messages.

Use awaitNext to wait for some more messages, with a timeout for each of them.

     
exec(graphqlWs
  .subscribeFile("graphql/orderEvents.graphql")
  .await(Duration.ofSeconds(10)).on(graphqlWs.checkNext("first event"))
  // wait for 5 more messages
  .awaitNext(Duration.ofSeconds(30), 5));
exec(graphqlWs
  .subscribeFile("graphql/orderEvents.graphql")
  .await(Duration.ofSeconds(10)).on(graphqlWs.checkNext("first event"))
  // wait for 5 more messages
  .awaitNext(Duration.ofSeconds(30), 5))
exec(
  graphqlWs
    .subscribeFile("graphql/orderEvents.graphql")
    .await(10.seconds)(graphqlWs.checkNext("first event"))
    // wait for 5 more messages
    .awaitNext(30.seconds, 5)
)

Unsubscribe

Use unsubscribe with the subscription name to tell the server to stop the subscription. By default, Gatling reports it under the name subscription <subscription name> complete.

     
exec(graphqlWs.unsubscribe("orders"));
exec(graphqlWs.unsubscribe("orders").requestName("Stop orders"));
exec(graphqlWs.unsubscribe("orders"))
exec(graphqlWs.unsubscribe("orders").requestName("Stop orders"))
exec(graphqlWs.unsubscribe("orders"))
exec(graphqlWs.unsubscribe("orders").requestName("Stop orders"))

Close

Use close to close the WebSocket. Gatling reports it under the name graphql-ws close.

     
exec(graphqlWs.close());
exec(graphqlWs.close())
exec(graphqlWs.close)

Reconnection

When Gatling reconnects the WebSocket, it performs the handshake again and subscribes again to the subscriptions that were active, the way real clients do, as the server forgot about them.

Example

     
scenario("Subscriptions").exec(
  graphqlWs.connect().connectionInitPayload("authorization", "Bearer #{token}"),
  graphqlWs
    .subscribe("subscription OrderEvents($id: ID!) { orderCreated(customerId: $id) { id } }")
    .variable("id", "#{customerId}")
    .subscriptionName("orders")
    .await(Duration.ofSeconds(10)).on(
      graphqlWs.checkNext().check(graphqlWs.data.jsonPath("$.orderCreated.id").saveAs("orderId"))
    )
    .awaitNext(Duration.ofSeconds(30), 3),
  graphqlWs.unsubscribe("orders"),
  graphqlWs.close()
);
scenario("Subscriptions").exec(
  graphqlWs.connect().connectionInitPayload("authorization", "Bearer #{token}"),
  graphqlWs
    .subscribe("subscription OrderEvents(\$id: ID!) { orderCreated(customerId: \$id) { id } }")
    .variable("id", "#{customerId}")
    .subscriptionName("orders")
    .await(Duration.ofSeconds(10)).on(
      graphqlWs.checkNext().check(graphqlWs.data.jsonPath("$.orderCreated.id").saveAs("orderId"))
    )
    .awaitNext(Duration.ofSeconds(30), 3),
  graphqlWs.unsubscribe("orders"),
  graphqlWs.close()
)
scenario("Subscriptions").exec(
  graphqlWs.connect.connectionInitPayload("authorization", "Bearer #{token}"),
  graphqlWs
    .subscribe("subscription OrderEvents($id: ID!) { orderCreated(customerId: $id) { id } }")
    .variable("id", "#{customerId}")
    .subscriptionName("orders")
    .await(10.seconds)(
      graphqlWs.checkNext.check(graphqlWs.data.jsonPath("$.orderCreated.id").saveAs("orderId"))
    )
    .awaitNext(30.seconds, 3),
  graphqlWs.unsubscribe("orders"),
  graphqlWs.close
)

Edit this page on GitHub