Skip to content

Commit 27f99ed

Browse files
[server] concurrent execution for batched queries (#1301)
This adds the logic to execute Queries in a concurrent instead of a sequential way, this will only apply for batched operations that are only queries, if there is a mutation, the batch will still be executed in a sequential way.
1 parent eb3a870 commit 27f99ed

File tree

11 files changed

+222
-12
lines changed

11 files changed

+222
-12
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ subprojects {
9797
// even though we don't have any Java code, since we are building using Java LTS version,
9898
// this is required for Gradle to set the correct JVM versions in the module metadata
9999
targetCompatibility = JavaVersion.VERSION_1_8
100+
sourceCompatibility = JavaVersion.VERSION_1_8
100101
}
101102

102103
// published artifacts

gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ mockkVersion = 1.12.0
4040
mustacheVersion = 0.9.10
4141
rxjavaVersion = 3.1.0
4242
wireMockVersion = 2.30.1
43+
kotlinxBenchmarkVersion = 0.4.0
4344

4445
# plugin versions
4546
detektVersion = 1.18.0

servers/graphql-kotlin-server/build.gradle.kts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,40 @@
1+
import kotlinx.benchmark.gradle.JvmBenchmarkTarget
2+
13
description = "Common code for running a GraphQL server in any HTTP server framework"
24

35
val kotlinCoroutinesVersion: String by project
6+
val kotlinxBenchmarkVersion: String by project
7+
8+
plugins {
9+
id("org.jetbrains.kotlinx.benchmark")
10+
}
411

512
dependencies {
613
api(project(path = ":graphql-kotlin-schema-generator"))
714
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutinesVersion")
815
}
916

17+
// Benchmarks
18+
19+
// Create a separate source set for benchmarks.
20+
sourceSets.create("benchmarks")
21+
22+
kotlin.sourceSets.getByName("benchmarks") {
23+
dependencies {
24+
implementation("org.jetbrains.kotlinx:kotlinx-benchmark-runtime:$kotlinxBenchmarkVersion")
25+
implementation(sourceSets.main.get().output)
26+
implementation(sourceSets.main.get().runtimeClasspath)
27+
}
28+
}
29+
30+
benchmark {
31+
targets {
32+
register("benchmarks") {
33+
this as JvmBenchmarkTarget
34+
}
35+
}
36+
}
37+
1038
tasks {
1139
jacocoTestCoverageVerification {
1240
violationRules {
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2022 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.server
18+
19+
import com.expediagroup.graphql.server.extensions.isMutation
20+
import com.expediagroup.graphql.server.types.GraphQLRequest
21+
import org.openjdk.jmh.annotations.Benchmark
22+
import org.openjdk.jmh.annotations.Setup
23+
import org.openjdk.jmh.annotations.State
24+
import org.openjdk.jmh.annotations.Scope
25+
import org.openjdk.jmh.annotations.Fork
26+
import org.openjdk.jmh.annotations.Warmup
27+
import org.openjdk.jmh.annotations.Measurement
28+
import java.util.concurrent.TimeUnit
29+
import kotlin.random.Random
30+
31+
@State(Scope.Benchmark)
32+
@Fork(1)
33+
@Warmup(iterations = 2)
34+
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
35+
open class GraphQLRequestBenchmark {
36+
private val requests = mutableListOf<GraphQLRequest>()
37+
38+
@Setup
39+
fun setUp() {
40+
val charPool = ('a'..'z') + ('A'..'Z') + ('0'..'9')
41+
val range = (1..3072)
42+
repeat(50) {
43+
val randomStringForQuery = range
44+
.map { Random.nextInt(0, charPool.size) }
45+
.map(charPool::get)
46+
.joinToString("")
47+
val query = """$randomStringForQuery query HeroNameAndFriends(${"$"}episode: Episode) {
48+
hero(episode: ${"$"}episode) {
49+
name
50+
friends {
51+
name
52+
}
53+
}
54+
}"""
55+
requests.add(GraphQLRequest(query))
56+
}
57+
val randomStringForMutation = range
58+
.map { Random.nextInt(0, charPool.size) }
59+
.map(charPool::get)
60+
.joinToString("")
61+
62+
val mutation = """$randomStringForMutation mutation AddNewPet (${"$"}name: String!,${"$"}petType: PetType) {
63+
addPet(name:${"$"}name,petType:${"$"}petType) {
64+
name
65+
petType
66+
}
67+
}"""
68+
requests.add(GraphQLRequest(mutation))
69+
}
70+
71+
@Benchmark
72+
fun isMutationBenchmark(): Boolean {
73+
return requests.any(GraphQLRequest::isMutation)
74+
}
75+
}

servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/execution/GraphQLServer.kt

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,21 @@
1616

1717
package com.expediagroup.graphql.server.execution
1818

19+
import com.expediagroup.graphql.generator.execution.GraphQLContext
20+
import com.expediagroup.graphql.server.extensions.isMutation
21+
import com.expediagroup.graphql.server.extensions.toGraphQLError
22+
import com.expediagroup.graphql.server.extensions.toGraphQLKotlinType
1923
import com.expediagroup.graphql.server.types.GraphQLBatchRequest
2024
import com.expediagroup.graphql.server.types.GraphQLBatchResponse
2125
import com.expediagroup.graphql.server.types.GraphQLRequest
26+
import com.expediagroup.graphql.server.types.GraphQLResponse
2227
import com.expediagroup.graphql.server.types.GraphQLServerResponse
2328
import kotlinx.coroutines.CoroutineScope
2429
import kotlinx.coroutines.SupervisorJob
30+
import kotlinx.coroutines.async
31+
import kotlinx.coroutines.awaitAll
2532
import kotlinx.coroutines.coroutineScope
33+
import kotlinx.coroutines.supervisorScope
2634
import kotlin.coroutines.CoroutineContext
2735
import kotlin.coroutines.EmptyCoroutineContext
2836

@@ -58,14 +66,44 @@ open class GraphQLServer<Request>(
5866

5967
when (graphQLRequest) {
6068
is GraphQLRequest -> requestHandler.executeRequest(graphQLRequest, deprecatedContext, graphQLContext)
61-
is GraphQLBatchRequest -> GraphQLBatchResponse(
62-
graphQLRequest.requests.map {
63-
requestHandler.executeRequest(it, deprecatedContext, graphQLContext)
69+
is GraphQLBatchRequest -> when {
70+
graphQLRequest.requests.any(GraphQLRequest::isMutation) -> GraphQLBatchResponse(
71+
graphQLRequest.requests.map {
72+
requestHandler.executeRequest(it, deprecatedContext, graphQLContext)
73+
}
74+
)
75+
else -> {
76+
GraphQLBatchResponse(
77+
handleConcurrently(graphQLRequest, deprecatedContext, graphQLContext)
78+
)
6479
}
65-
)
80+
}
6681
}
6782
} else {
6883
null
6984
}
7085
}
86+
87+
/**
88+
* Concurrently execute a [batchRequest], a failure in an specific request will not cause the scope
89+
* to fail and does not affect the other requests. The total execution time will be the time of the slowest
90+
* request
91+
*/
92+
private suspend fun handleConcurrently(
93+
batchRequest: GraphQLBatchRequest,
94+
context: GraphQLContext?,
95+
graphQLContext: Map<*, Any>
96+
): List<GraphQLResponse<*>> = supervisorScope {
97+
batchRequest.requests.map { request ->
98+
async {
99+
try {
100+
requestHandler.executeRequest(request, context, graphQLContext)
101+
} catch (e: Exception) {
102+
GraphQLResponse<Any?>(
103+
errors = listOf(e.toGraphQLError().toGraphQLKotlinType())
104+
)
105+
}
106+
}
107+
}.awaitAll()
108+
}
71109
}

servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/extensions/requestExtensions.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ import org.dataloader.DataLoaderRegistry
2323
/**
2424
* Convert the common [GraphQLRequest] to the execution input used by graphql-java
2525
*/
26-
fun GraphQLRequest.toExecutionInput(graphQLContext: Any? = null, dataLoaderRegistry: DataLoaderRegistry? = null, graphQLContextMap: Map<*, Any>? = null): ExecutionInput =
26+
fun GraphQLRequest.toExecutionInput(
27+
graphQLContext: Any? = null,
28+
dataLoaderRegistry: DataLoaderRegistry? = null,
29+
graphQLContextMap: Map<*, Any>? = null
30+
): ExecutionInput =
2731
ExecutionInput.newExecutionInput()
2832
.query(this.query)
2933
.operationName(this.operationName)
@@ -34,3 +38,5 @@ fun GraphQLRequest.toExecutionInput(graphQLContext: Any? = null, dataLoaderRegis
3438
}
3539
.dataLoaderRegistry(dataLoaderRegistry ?: DataLoaderRegistry())
3640
.build()
41+
42+
fun GraphQLRequest.isMutation(): Boolean = query.contains("mutation ")

servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/execution/GraphQLServerTest.kt

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.expediagroup.graphql.server.types.GraphQLBatchRequest
2121
import com.expediagroup.graphql.server.types.GraphQLRequest
2222
import io.mockk.coEvery
2323
import io.mockk.coVerify
24+
import io.mockk.every
2425
import io.mockk.mockk
2526
import kotlinx.coroutines.ExperimentalCoroutinesApi
2627
import kotlinx.coroutines.test.runBlockingTest
@@ -36,7 +37,13 @@ class GraphQLServerTest {
3637
@Test
3738
fun `the request handler and parser are called`() {
3839
val mockParser = mockk<GraphQLRequestParser<MockHttpRequest>> {
39-
coEvery { parseRequest(any()) } returns GraphQLBatchRequest(requests = listOf(mockk()))
40+
coEvery { parseRequest(any()) } returns GraphQLBatchRequest(
41+
requests = listOf(
42+
mockk {
43+
every { query } returns "query OperationName { parent { child } }"
44+
}
45+
)
46+
)
4047
}
4148
val mockContextFactory = mockk<GraphQLContextFactory<MockContext, MockHttpRequest>> {
4249
coEvery { generateContext(any()) } returns MockContext()
@@ -58,10 +65,48 @@ class GraphQLServerTest {
5865
}
5966
}
6067

68+
@Test
69+
fun `the request handler and parser are called for a batch with a mutation`() {
70+
val mockParser = mockk<GraphQLRequestParser<MockHttpRequest>> {
71+
coEvery { parseRequest(any()) } returns GraphQLBatchRequest(
72+
requests = listOf(
73+
mockk {
74+
every { query } returns "query OperationName { parent { child } }"
75+
},
76+
mockk {
77+
every { query } returns "mutation OperationName { field { to { mutate } } }"
78+
}
79+
)
80+
)
81+
}
82+
val mockContextFactory = mockk<GraphQLContextFactory<MockContext, MockHttpRequest>> {
83+
coEvery { generateContext(any()) } returns MockContext()
84+
coEvery { generateContextMap(any()) } returns mapOf("foo" to 1)
85+
}
86+
val mockHandler = mockk<GraphQLRequestHandler> {
87+
coEvery { executeRequest(any(), any(), any()) } returns mockk()
88+
}
89+
90+
val server = GraphQLServer(mockParser, mockContextFactory, mockHandler)
91+
92+
runBlockingTest { server.execute(mockk()) }
93+
94+
coVerify(exactly = 1) {
95+
mockParser.parseRequest(any())
96+
mockContextFactory.generateContext(any())
97+
mockContextFactory.generateContextMap(any())
98+
}
99+
coVerify(exactly = 2) {
100+
mockHandler.executeRequest(any(), any(), any())
101+
}
102+
}
103+
61104
@Test
62105
fun `null context is used and passed to the request handler`() {
63106
val mockParser = mockk<GraphQLRequestParser<MockHttpRequest>> {
64-
coEvery { parseRequest(any()) } returns mockk<GraphQLRequest>()
107+
coEvery { parseRequest(any()) } returns mockk<GraphQLRequest> {
108+
every { query } returns "query OperationName { parent { child } }"
109+
}
65110
}
66111
val mockContextFactory = mockk<GraphQLContextFactory<MockContext, MockHttpRequest>> {
67112
coEvery { generateContext(any()) } returns null

servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/extensions/RequestExtensionsKtTest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import org.dataloader.DataLoaderRegistry
2222
import org.junit.jupiter.api.Test
2323
import kotlin.test.assertEquals
2424
import kotlin.test.assertNotNull
25+
import kotlin.test.assertTrue
2526

2627
class RequestExtensionsKtTest {
2728

@@ -75,4 +76,17 @@ class RequestExtensionsKtTest {
7576
val executionInput = request.toExecutionInput(graphQLContextMap = context)
7677
assertEquals(1, executionInput.graphQLContext.get("foo"))
7778
}
79+
80+
@Test
81+
fun `verify graphQLRequest is a mutation`() {
82+
val request = GraphQLRequest(
83+
query = """
84+
mutation addPet(name: "name", petType: "type") {
85+
name
86+
petType
87+
}
88+
""".trimIndent()
89+
)
90+
assertTrue(request.isMutation())
91+
}
7892
}

servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLSchemaConfiguration.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ const val DEFAULT_INSTRUMENTATION_ORDER = 0
4747

4848
/**
4949
* Configuration class that loads both the federated and non-federation
50-
* configuraiton and creates the GraphQL schema object and request handler.
50+
* configuration and creates the GraphQL schema object and request handler.
5151
*
5252
* This config can then be used by all Spring specific configuration classes
5353
* to handle incoming requests from HTTP routes or subscriptions and send them

servers/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/server/spring/subscriptions/SubscriptionWebSocketHandlerIT.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class SubscriptionWebSocketHandlerIT(
6666
val dataOutput = TestPublisher.create<String>()
6767

6868
val response = client.execute(uri) { session ->
69-
executeSubsciption(session, startMessage, dataOutput)
69+
executeSubscription(session, startMessage, dataOutput)
7070
}.subscribe()
7171

7272
StepVerifier.create(dataOutput)
@@ -90,7 +90,7 @@ class SubscriptionWebSocketHandlerIT(
9090
val dataOutput = TestPublisher.create<String>()
9191

9292
val response = client.execute(uri) { session ->
93-
executeSubsciption(session, startMessage, dataOutput)
93+
executeSubscription(session, startMessage, dataOutput)
9494
}.subscribe()
9595

9696
StepVerifier.create(dataOutput)
@@ -116,7 +116,7 @@ class SubscriptionWebSocketHandlerIT(
116116
headers.set("X-Custom-Header", "junit")
117117

118118
val response = client.execute(uri, headers) { session ->
119-
executeSubsciption(session, startMessage, dataOutput)
119+
executeSubscription(session, startMessage, dataOutput)
120120
}.subscribe()
121121

122122
StepVerifier.create(dataOutput)
@@ -127,7 +127,7 @@ class SubscriptionWebSocketHandlerIT(
127127
response.dispose()
128128
}
129129

130-
private fun executeSubsciption(
130+
private fun executeSubscription(
131131
session: WebSocketSession,
132132
startMessage: String,
133133
dataOutput: TestPublisher<String>

settings.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pluginManagement {
22
val detektVersion: String by settings
33
val dokkaVersion: String by settings
44
val kotlinVersion: String by settings
5+
val kotlinxBenchmarkVersion: String by settings
56
val ktlintPluginVersion: String by settings
67
val mavenPluginDevelopmentVersion: String by settings
78
val nexusPublishPluginVersion: String by settings
@@ -18,6 +19,7 @@ pluginManagement {
1819
id("io.github.gradle-nexus.publish-plugin") version nexusPublishPluginVersion
1920
id("io.gitlab.arturbosch.detekt") version detektVersion
2021
id("org.jetbrains.dokka") version dokkaVersion
22+
id("org.jetbrains.kotlinx.benchmark") version kotlinxBenchmarkVersion
2123
id("org.jlleitschuh.gradle.ktlint") version ktlintPluginVersion
2224
id("org.springframework.boot") version springBootVersion
2325
}

0 commit comments

Comments
 (0)