Skip to content

Commit eb3a870

Browse files
authored
standardize coroutine context propagation in the execution (#1349)
📝 Description Attempts to standardize how we can propagate the coroutine/reactor context created by the server when processing incoming requests to the data fetchers that would resolve the underlying fields. `GraphQLContext` map was introduced in `graphql-java` 17 as means to standardize usage of GraphQL context (previously it could be any object). We can store the original GraphQL request context in the map and then use it from within the data fetchers. This is a breaking change as we are changing signatures of `GraphQLContextFactory` and `FunctionDataFetcher`. 🔗 Related Issues * #1336 * #1318 * #1300 * #1257
1 parent 0253109 commit eb3a870

File tree

43 files changed

+381
-335
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+381
-335
lines changed

examples/server/ktor-server/src/main/kotlin/com/expediagroup/graphql/examples/server/ktor/AuthorizedContext.kt

Lines changed: 0 additions & 29 deletions
This file was deleted.

examples/server/ktor-server/src/main/kotlin/com/expediagroup/graphql/examples/server/ktor/KtorGraphQLContextFactory.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,25 +17,25 @@
1717
package com.expediagroup.graphql.examples.server.ktor
1818

1919
import com.expediagroup.graphql.examples.server.ktor.schema.models.User
20+
import com.expediagroup.graphql.generator.execution.GraphQLContext
2021
import com.expediagroup.graphql.server.execution.GraphQLContextFactory
2122
import io.ktor.request.ApplicationRequest
2223

2324
/**
2425
* Custom logic for how this example app should create its context given the [ApplicationRequest]
2526
*/
26-
class KtorGraphQLContextFactory : GraphQLContextFactory<AuthorizedContext, ApplicationRequest> {
27+
class KtorGraphQLContextFactory : GraphQLContextFactory<GraphQLContext, ApplicationRequest> {
2728

28-
override suspend fun generateContext(request: ApplicationRequest): AuthorizedContext {
29-
val loggedInUser = User(
29+
override suspend fun generateContextMap(request: ApplicationRequest): Map<Any, Any> = mutableMapOf<Any, Any>(
30+
"user" to User(
3031
email = "[email protected]",
3132
firstName = "Someone",
3233
lastName = "You Don't know",
3334
universityId = 4
3435
)
35-
36-
// Parse any headers from the Ktor request
37-
val customHeader: String? = request.headers["my-custom-header"]
38-
39-
return AuthorizedContext(loggedInUser, customHeader = customHeader)
36+
).also { map ->
37+
request.headers["my-custom-header"]?.let { customHeader ->
38+
map["customHeader"] = customHeader
39+
}
4040
}
4141
}

examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/context/MyGraphQLContext.kt

Lines changed: 0 additions & 38 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,32 +17,16 @@
1717
package com.expediagroup.graphql.examples.server.spring.context
1818

1919
import com.expediagroup.graphql.server.execution.GraphQLContextFactory
20-
import com.expediagroup.graphql.server.spring.execution.SpringGraphQLContextFactory
21-
import com.expediagroup.graphql.server.spring.subscriptions.SpringSubscriptionGraphQLContextFactory
20+
import com.expediagroup.graphql.server.spring.execution.DefaultSpringGraphQLContextFactory
2221
import org.springframework.stereotype.Component
2322
import org.springframework.web.reactive.function.server.ServerRequest
24-
import org.springframework.web.reactive.socket.WebSocketSession
2523

2624
/**
27-
* [GraphQLContextFactory] that generates [MyGraphQLContext] that will be available when processing GraphQL requests.
25+
* [GraphQLContextFactory] that populates GraphQL context map that will be available when processing GraphQL requests.
2826
*/
2927
@Component
30-
class MyGraphQLContextFactory : SpringGraphQLContextFactory<MyGraphQLContext>() {
31-
32-
override suspend fun generateContext(request: ServerRequest): MyGraphQLContext = MyGraphQLContext(
33-
request = request,
34-
myCustomValue = request.headers().firstHeader("MyHeader") ?: "defaultContext"
35-
)
36-
}
37-
38-
/**
39-
* [GraphQLContextFactory] that generates [MySubscriptionGraphQLContext] that will be available when processing subscription operations.
40-
*/
41-
@Component
42-
class MySubscriptionGraphQLContextFactory : SpringSubscriptionGraphQLContextFactory<MySubscriptionGraphQLContext>() {
43-
44-
override suspend fun generateContext(request: WebSocketSession): MySubscriptionGraphQLContext = MySubscriptionGraphQLContext(
45-
request = request,
46-
auth = null
28+
class MyGraphQLContextFactory : DefaultSpringGraphQLContextFactory() {
29+
override suspend fun generateContextMap(request: ServerRequest): Map<*, Any> = super.generateContextMap(request) + mapOf(
30+
"myCustomValue" to (request.headers().firstHeader("MyHeader") ?: "defaultContext")
4731
)
4832
}

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

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,8 +16,6 @@
1616

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

19-
import com.expediagroup.graphql.examples.server.spring.context.MySubscriptionGraphQLContext
20-
import com.expediagroup.graphql.generator.execution.GraphQLContext
2119
import com.expediagroup.graphql.server.spring.subscriptions.ApolloSubscriptionHooks
2220
import org.springframework.web.reactive.socket.WebSocketSession
2321

@@ -26,14 +24,13 @@ import org.springframework.web.reactive.socket.WebSocketSession
2624
*/
2725
class MySubscriptionHooks : ApolloSubscriptionHooks {
2826

29-
override fun onConnect(
27+
override fun onConnectWithContext(
3028
connectionParams: Map<String, String>,
3129
session: WebSocketSession,
32-
graphQLContext: GraphQLContext?
33-
): GraphQLContext? {
34-
if (graphQLContext != null && graphQLContext is MySubscriptionGraphQLContext) {
35-
graphQLContext.auth = connectionParams["Authorization"]
30+
graphQLContext: Map<*, Any>
31+
): Map<*, Any> = mutableMapOf<Any, Any>().also { contextMap ->
32+
connectionParams["Authorization"]?.let { authValue ->
33+
contextMap["auth"] = authValue
3634
}
37-
return graphQLContext
3835
}
3936
}

examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/query/ContextualQuery.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,15 +16,15 @@
1616

1717
package com.expediagroup.graphql.examples.server.spring.query
1818

19-
import com.expediagroup.graphql.examples.server.spring.context.MyGraphQLContext
2019
import com.expediagroup.graphql.examples.server.spring.model.ContextualResponse
2120
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
2221
import com.expediagroup.graphql.server.operations.Query
22+
import graphql.schema.DataFetchingEnvironment
2323
import org.springframework.stereotype.Component
2424

2525
/**
26-
* Example usage of GraphQLContext. Since the argument [ContextualQuery.contextualQuery] implements
27-
* the GraphQLContext interface it will not appear in the schema and be populated at runtime.
26+
* Example usage of GraphQLContext. Since `DataFetchingEnvironment` is passed as the argument
27+
* of [ContextualQuery.contextualQuery], it will not appear in the schema and be populated at runtime.
2828
*/
2929
@Component
3030
class ContextualQuery : Query {
@@ -33,6 +33,6 @@ class ContextualQuery : Query {
3333
fun contextualQuery(
3434
@GraphQLDescription("some value that will be returned to the user")
3535
value: Int,
36-
context: MyGraphQLContext
37-
): ContextualResponse = ContextualResponse(value, context.myCustomValue)
36+
env: DataFetchingEnvironment
37+
): ContextualResponse = ContextualResponse(value, env.graphQlContext.getOrDefault("myCustomValue", "defaultValue"))
3838
}

examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/subscriptions/SimpleSubscription.kt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
1616

1717
package com.expediagroup.graphql.examples.server.spring.subscriptions
1818

19-
import com.expediagroup.graphql.examples.server.spring.context.MySubscriptionGraphQLContext
2019
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
2120
import com.expediagroup.graphql.server.operations.Subscription
2221
import graphql.GraphqlErrorException
@@ -79,8 +78,4 @@ class SimpleSubscription : Subscription {
7978

8079
return flowOf(dfr, dfr).asPublisher()
8180
}
82-
83-
@GraphQLDescription("Returns a value from the subscription context")
84-
fun subscriptionContext(myGraphQLContext: MySubscriptionGraphQLContext): Flux<String> =
85-
Flux.just(myGraphQLContext.auth ?: "no-auth")
8681
}

generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/execution/EntityResolver.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,12 +20,13 @@ import graphql.GraphQLError
2020
import graphql.execution.DataFetcherResult
2121
import graphql.schema.DataFetcher
2222
import graphql.schema.DataFetchingEnvironment
23-
import kotlinx.coroutines.GlobalScope
23+
import kotlinx.coroutines.CoroutineScope
2424
import kotlinx.coroutines.async
2525
import kotlinx.coroutines.awaitAll
2626
import kotlinx.coroutines.coroutineScope
2727
import kotlinx.coroutines.future.future
2828
import java.util.concurrent.CompletableFuture
29+
import kotlin.coroutines.EmptyCoroutineContext
2930

3031
private const val TYPENAME_FIELD = "__typename"
3132
private const val REPRESENTATIONS = "representations"
@@ -54,7 +55,8 @@ open class EntityResolver(resolvers: List<FederatedTypeResolver<*>>) : DataFetch
5455
val representations: List<Map<String, Any>> = env.getArgument(REPRESENTATIONS)
5556
val indexedBatchRequestsByType = representations.withIndex().groupBy { it.value[TYPENAME_FIELD].toString() }
5657

57-
return GlobalScope.future {
58+
val scope = env.graphQlContext.getOrDefault(CoroutineScope::class, CoroutineScope(EmptyCoroutineContext))
59+
return scope.future {
5860
val data = mutableListOf<Any?>()
5961
val errors = mutableListOf<GraphQLError>()
6062

generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/execution/FederatedGraphQLContext.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,4 +24,5 @@ import com.expediagroup.graphql.generator.execution.GraphQLContext
2424
* request came from the Apollo Gateway. That means we need a special interface
2525
* for the federation context.
2626
*/
27+
@Deprecated(message = "The generic context object is deprecated in favor of the context map", ReplaceWith("graphql.GraphQLContext"))
2728
interface FederatedGraphQLContext : GraphQLContext, HTTPRequestHeaders

generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/execution/EntityQueryResolverTest.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import com.expediagroup.graphql.generator.federation.data.BookResolver
2020
import com.expediagroup.graphql.generator.federation.data.UserResolver
2121
import com.expediagroup.graphql.generator.federation.data.queries.federated.Book
2222
import com.expediagroup.graphql.generator.federation.data.queries.federated.User
23+
import graphql.GraphQLContext
2324
import graphql.GraphQLError
2425
import graphql.schema.DataFetchingEnvironment
2526
import io.mockk.coEvery
@@ -38,6 +39,7 @@ class EntityQueryResolverTest {
3839
val representations = listOf(mapOf<String, Any>("__typename" to "User", "userId" to 123, "name" to "testName"))
3940
val env = mockk<DataFetchingEnvironment> {
4041
every { getArgument<Any>(any()) } returns representations
42+
every { graphQlContext } returns GraphQLContext.newContext().build()
4143
}
4244

4345
val result = resolver.get(env).get()
@@ -56,6 +58,7 @@ class EntityQueryResolverTest {
5658
val resolver = EntityResolver(listOf(mockBookResolver, mockUserResolver))
5759
val env = mockk<DataFetchingEnvironment> {
5860
every { getArgument<Any>(any()) } returns listOf(emptyMap<String, Any>())
61+
every { graphQlContext } returns GraphQLContext.newContext().build()
5962
}
6063

6164
val result = resolver.get(env).get()
@@ -69,6 +72,7 @@ class EntityQueryResolverTest {
6972
val representations = listOf(mapOf<String, Any>("__typename" to "User", "userId" to 123, "name" to "testName"))
7073
val env = mockk<DataFetchingEnvironment> {
7174
every { getArgument<Any>(any()) } returns representations
75+
every { graphQlContext } returns GraphQLContext.newContext().build()
7276
}
7377

7478
val result = resolver.get(env).get()
@@ -86,6 +90,7 @@ class EntityQueryResolverTest {
8690
val representations = listOf(mapOf<String, Any>("__typename" to "User", "userId" to 123, "name" to "testName"))
8791
val env: DataFetchingEnvironment = mockk {
8892
every { getArgument<Any>(any()) } returns representations
93+
every { graphQlContext } returns GraphQLContext.newContext().build()
8994
}
9095

9196
val result = resolver.get(env).get()
@@ -103,6 +108,7 @@ class EntityQueryResolverTest {
103108
val representations = listOf(user1.toRepresentation(), book.toRepresentation(), user2.toRepresentation())
104109
val env = mockk<DataFetchingEnvironment> {
105110
every { getArgument<Any>(any()) } returns representations
111+
every { graphQlContext } returns GraphQLContext.newContext().build()
106112
}
107113

108114
val spyUserResolver = spyk(UserResolver())
@@ -128,6 +134,7 @@ class EntityQueryResolverTest {
128134
val representations = listOf(user.toRepresentation(), book.toRepresentation())
129135
val env = mockk<DataFetchingEnvironment> {
130136
every { getArgument<Any>(any()) } returns representations
137+
every { graphQlContext } returns GraphQLContext.newContext().build()
131138
}
132139

133140
val spyUserResolver: UserResolver = spyk(UserResolver())
@@ -153,6 +160,7 @@ class EntityQueryResolverTest {
153160
val representations = listOf(user.toRepresentation(), user.toRepresentation())
154161
val env = mockk<DataFetchingEnvironment> {
155162
every { getArgument<Any>(any()) } returns representations
163+
every { graphQlContext } returns GraphQLContext.newContext().build()
156164
}
157165

158166
val mockUserResolver: UserResolver = mockk {

0 commit comments

Comments
 (0)