Skip to content

Commit c676c7f

Browse files
smyrickdariuszkuc
authored andcommitted
Add perf tests to validate subscription performance (#514)
* Add perf tests to validate subscription performance Add a simple perf test runner, https://artillery.io/. This allows us to easily run the perf tests and debug the server in the same project * Update README
1 parent 17097c8 commit c676c7f

File tree

9 files changed

+2287
-9
lines changed

9 files changed

+2287
-9
lines changed

graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/execution/ApolloSubscriptionProtocolHandler.kt

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class ApolloSubscriptionProtocolHandler(
4949
private val objectMapper: ObjectMapper
5050
) {
5151
// Sessions are saved by web socket session id
52-
private val activeSessions = ConcurrentHashMap<String, Subscription>()
52+
private val activeKeepAliveSessions = ConcurrentHashMap<String, Subscription>()
5353
// Operations are saved by web socket session id, then operation id
5454
private val activeOperations = ConcurrentHashMap<String, ConcurrentHashMap<String, Subscription>>()
5555

@@ -71,7 +71,10 @@ class ApolloSubscriptionProtocolHandler(
7171
// Send the GQL_CONNECTION_KEEP_ALIVE message every interval until the connection is closed or terminated
7272
val keepAliveFlux = Flux.interval(Duration.ofMillis(keepAliveInterval))
7373
.map { keepAliveMessage }
74-
.doOnSubscribe { activeSessions[session.id] = it }
74+
.doOnSubscribe {
75+
logger.debug("GraphQL subscription INIT, sessionId=${session.id} activeSessions=${activeKeepAliveSessions.count()}")
76+
activeKeepAliveSessions[session.id] = it
77+
}
7578

7679
return flux.concatWith(keepAliveFlux)
7780
}
@@ -102,14 +105,14 @@ class ApolloSubscriptionProtocolHandler(
102105
@Suppress("Detekt.TooGenericExceptionCaught")
103106
private fun startSubscription(operationMessage: SubscriptionOperationMessage, session: WebSocketSession): Flux<SubscriptionOperationMessage> {
104107
if (operationMessage.id == null) {
105-
logger.error("Operation id is required")
108+
logger.error("GraphQL subscription operation id is required")
106109
return Flux.just(basicConnectionErrorMessage)
107110
}
108111

109112
val payload = operationMessage.payload
110113

111114
if (payload == null) {
112-
logger.error("Payload was null instead of a GraphQLRequest object")
115+
logger.error("GraphQL subscription payload was null instead of a GraphQLRequest object")
113116
stopSubscription(operationMessage, session)
114117
return Flux.just(SubscriptionOperationMessage(type = GQL_CONNECTION_ERROR.type, id = operationMessage.id))
115118
}
@@ -126,11 +129,11 @@ class ApolloSubscriptionProtocolHandler(
126129
}
127130
.concatWith(Flux.just(SubscriptionOperationMessage(type = GQL_COMPLETE.type, id = operationMessage.id)))
128131
.doOnSubscribe {
129-
logger.trace("WebSocket GraphQL subscription subscribe, WebSocketSessionID=${session.id} OperationMessageID=${operationMessage.id}")
132+
logger.debug("GraphQL subscription START, sessionId=${session.id} operationId=${operationMessage.id}")
130133
activeOperations[session.id]?.put(operationMessage.id, it)
131134
}
132-
.doOnCancel { logger.trace("WebSocket GraphQL subscription cancel, WebSocketSessionID=${session.id} OperationMessageID=${operationMessage.id}") }
133-
.doOnComplete { logger.trace("WebSocket GraphQL subscription complete, WebSocketSessionID=${session.id} OperationMessageID=${operationMessage.id}") }
135+
.doOnCancel { logger.debug("GraphQL subscription CANCEL, sessionId=${session.id} operationId=${operationMessage.id}") }
136+
.doOnComplete { logger.debug("GraphQL subscription COMPELTE, sessionId=${session.id} operationId=${operationMessage.id}") }
134137
} catch (exception: Exception) {
135138
logger.error("Error running graphql subscription", exception)
136139
stopSubscription(operationMessage, session)
@@ -139,6 +142,7 @@ class ApolloSubscriptionProtocolHandler(
139142
}
140143

141144
private fun stopSubscription(operationMessage: SubscriptionOperationMessage, session: WebSocketSession) {
145+
logger.debug("GraphQL subscription STOP, sessionId=${session.id} operationId=${operationMessage.id}")
142146
if (operationMessage.id != null) {
143147
val operationsForSession = activeOperations[session.id]
144148
operationsForSession?.get(operationMessage.id)?.cancel()
@@ -147,10 +151,11 @@ class ApolloSubscriptionProtocolHandler(
147151
}
148152

149153
private fun terminateSession(session: WebSocketSession) {
154+
logger.debug("GraphQL subscription TERMINATE, sessionId=${session.id}")
150155
activeOperations[session.id]?.forEach { _, subscription -> subscription.cancel() }
151156
activeOperations.remove(session.id)
152-
activeSessions[session.id]?.cancel()
153-
activeSessions.remove(session.id)
157+
activeKeepAliveSessions[session.id]?.cancel()
158+
activeKeepAliveSessions.remove(session.id)
154159
session.close()
155160
}
156161
}

perf-tests/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules/

perf-tests/.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
12

perf-tests/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Performance Tests
2+
3+
This is a simple performance test script that runs using [Artillery](https://github.com/artilleryio/artillery)
4+
5+
## Local Setup
6+
7+
### Requirements
8+
9+
* Make sure you are using the correct version of [Node](https://nodejs.org/). You can use [NVM](https://github.com/nvm-sh/nvm) to install the version specified in `.nvmrc`
10+
11+
### Running Tests
12+
13+
* Install the dependencies locally to run the tests
14+
15+
```shell script
16+
$ npm install
17+
```
18+
19+
* There are multiple performance test cases. To see the availabel tests, run the following command.
20+
```shell script
21+
$ npm run
22+
```
23+
* To execute a test simply run the appropiate command through `npm`. Feel free to modify the config for the tests locally as well.
24+
```shell script
25+
$ npm run perf-test-query
26+
```

perf-tests/graphql-mutation.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
config:
2+
target: 'http://localhost:8080'
3+
phases:
4+
- duration: 30
5+
arrivalRate: 20
6+
7+
scenarios:
8+
- name: "Run GraphQL Mutation"
9+
flow:
10+
- post:
11+
url: "/graphql"
12+
json:
13+
query: |-
14+
mutation TestMutation {
15+
addToList(entry: "foo")
16+
}

perf-tests/graphql-query.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
config:
2+
target: 'http://localhost:8080'
3+
phases:
4+
- duration: 30
5+
arrivalRate: 20
6+
7+
scenarios:
8+
- name: "Run GraphQL Query"
9+
flow:
10+
- post:
11+
url: "/graphql"
12+
json:
13+
query: |-
14+
query TestQuery {
15+
generateNumber
16+
}

perf-tests/graphql-subscription.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
config:
2+
target: 'ws://localhost:8080/subscriptions'
3+
ws:
4+
rejectUnauthorized: false
5+
subprotocols:
6+
- graphql-ws
7+
phases:
8+
- duration: 30
9+
arrivalRate: 20
10+
11+
scenarios:
12+
- name: "Run GraphQL Subscription"
13+
engine: "ws"
14+
flow:
15+
- send:
16+
type: "connection_init"
17+
18+
- think: 1
19+
20+
- send:
21+
type: "start"
22+
id: "1"
23+
payload:
24+
query: |-
25+
subscription TestSubscription {
26+
counter
27+
}
28+
29+
- think: 2
30+
31+
- send:
32+
type: "connection_terminate"
33+
34+

0 commit comments

Comments
 (0)