Skip to content

Commit 14d3b1d

Browse files
dariuszkucsmyrick
authored and
smyrick
committed
feature: batch support for resolving federation requests (ExpediaGroup#306)
* feature: batch support for resolving federation requests NOTE: This is a non-backwards compatible change as FederatedTypeResolver resolve signature is changed to process list of representations at once. This allows resolver to either instantiate entities one by one or use some batch logic. Update _entities query resolver logic to resolve federation requests in batch mode. Specified federated object representations are grouped by the underlying typename and asynchronously resolved in batches. Batch results are then merged into a single list that preserves the original order of entities. If entity cannot be resolved, NULL will be returned instead. Any exceptions thrown while attempting to resolve the federated types will be returned together with partially resolved data. * additional test for invalid result size
1 parent fe6947f commit 14d3b1d

20 files changed

+466
-156
lines changed

graphql-kotlin-federation/src/main/kotlin/com/expedia/graphql/federation/FederatedSchemaGeneratorHooks.kt

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package com.expedia.graphql.federation
33
import com.expedia.graphql.annotations.GraphQLName
44
import com.expedia.graphql.extensions.print
55
import com.expedia.graphql.federation.directives.FieldSet
6+
import com.expedia.graphql.federation.execution.EntityResolver
7+
import com.expedia.graphql.federation.execution.FederatedTypeRegistry
68
import com.expedia.graphql.federation.types.ANY_SCALAR_TYPE
79
import com.expedia.graphql.federation.types.FIELD_SET_SCALAR_TYPE
810
import com.expedia.graphql.federation.types.SERVICE_FIELD_DEFINITION
@@ -71,14 +73,7 @@ open class FederatedSchemaGeneratorHooks(private val federatedTypeRegistry: Fede
7173
.replace("type ${originalQuery.name}", "type ${originalQuery.name} @extends")
7274
.trim()
7375
federatedCodeRegistry.dataFetcher(FieldCoordinates.coordinates(originalQuery.name, SERVICE_FIELD_DEFINITION.name), DataFetcher { _Service(sdl) })
74-
federatedCodeRegistry.dataFetcher(FieldCoordinates.coordinates(originalQuery.name, entityField.name), DataFetcher {
75-
val representations: List<Map<String, Any>> = it.getArgument("representations")
76-
representations.map { representation ->
77-
val type = representation["__typename"]?.toString() ?: throw FederationException("invalid _entity query - missing __typename in the representation, representation=$representation")
78-
val resolver = federatedTypeRegistry.getFederatedResolver(type) ?: throw FederationException("Federation exception - cannot find resolver for $type")
79-
resolver.resolve(representation)
80-
}.toList()
81-
})
76+
federatedCodeRegistry.dataFetcher(FieldCoordinates.coordinates(originalQuery.name, entityField.name), EntityResolver(federatedTypeRegistry))
8277
federatedCodeRegistry.typeResolver("_Entity") { env: TypeResolutionEnvironment -> env.schema.getObjectType(env.getObjectName()) }
8378
federatedSchema.additionalType(ANY_SCALAR_TYPE)
8479
}

graphql-kotlin-federation/src/main/kotlin/com/expedia/graphql/federation/FederatedSchemaValidator.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.expedia.graphql.federation
22

33
import com.expedia.graphql.federation.directives.FieldSet
4+
import com.expedia.graphql.federation.exception.InvalidFederatedSchema
45
import graphql.schema.GraphQLDirective
56
import graphql.schema.GraphQLDirectiveContainer
67
import graphql.schema.GraphQLFieldDefinition

graphql-kotlin-federation/src/main/kotlin/com/expedia/graphql/federation/FederatedTypeResolver.kt

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

graphql-kotlin-federation/src/main/kotlin/com/expedia/graphql/federation/FederationException.kt

Lines changed: 0 additions & 8 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.expedia.graphql.federation.exception
2+
3+
import graphql.ErrorClassification
4+
import graphql.ErrorType
5+
import graphql.GraphQLError
6+
import graphql.language.SourceLocation
7+
8+
/**
9+
* GraphQLError returned whenever exception occurs while trying to resolve federated entity representation.
10+
*/
11+
class FederatedRequestFailure(private val errorMessage: String, private val error: Exception? = null) : GraphQLError {
12+
override fun getMessage(): String = errorMessage
13+
14+
override fun getErrorType(): ErrorClassification = ErrorType.DataFetchingException
15+
16+
override fun getLocations(): List<SourceLocation> = listOf(SourceLocation(-1, -1))
17+
18+
override fun getExtensions(): Map<String, Any>? =
19+
if (error != null) {
20+
mapOf("error" to error)
21+
} else {
22+
null
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.expedia.graphql.federation.exception
2+
3+
import graphql.ErrorClassification
4+
import graphql.ErrorType
5+
import graphql.GraphQLError
6+
import graphql.language.SourceLocation
7+
8+
/**
9+
* GraphQLError returned when federated representation cannot be resolved by any type resolvers.
10+
*/
11+
class InvalidFederatedRequest(private val errorMessage: String) : GraphQLError {
12+
override fun getMessage(): String = errorMessage
13+
14+
override fun getErrorType(): ErrorClassification = ErrorType.ValidationError
15+
16+
override fun getLocations(): List<SourceLocation> = listOf(SourceLocation(-1, -1))
17+
}

graphql-kotlin-federation/src/main/kotlin/com/expedia/graphql/federation/InvalidFederatedSchema.kt renamed to graphql-kotlin-federation/src/main/kotlin/com/expedia/graphql/federation/exception/InvalidFederatedSchema.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.expedia.graphql.federation
1+
package com.expedia.graphql.federation.exception
22

33
import com.expedia.graphql.exceptions.GraphQLKotlinException
44

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package com.expedia.graphql.federation.execution
2+
3+
import com.expedia.graphql.federation.exception.FederatedRequestFailure
4+
import com.expedia.graphql.federation.exception.InvalidFederatedRequest
5+
import graphql.GraphQLError
6+
import graphql.execution.DataFetcherResult
7+
import graphql.schema.DataFetcher
8+
import graphql.schema.DataFetchingEnvironment
9+
import kotlinx.coroutines.GlobalScope
10+
import kotlinx.coroutines.async
11+
import kotlinx.coroutines.awaitAll
12+
import kotlinx.coroutines.future.asCompletableFuture
13+
import java.util.concurrent.CompletableFuture
14+
15+
/**
16+
* Federated _entities query resolver.
17+
*/
18+
open class EntityResolver(private val federatedTypeRegistry: FederatedTypeRegistry) : DataFetcher<CompletableFuture<DataFetcherResult<List<Any?>>>> {
19+
20+
/**
21+
* Resolves entities based on the passed in representations argument. Entities are resolved in the same order
22+
* they are specified in the list of representations. If target representation cannot be resolved, NULL will
23+
* be returned instead.
24+
*
25+
* Representations are grouped by the underlying typename and each batch is resolved asynchronously before merging
26+
* the results back into a single list that preserves the original order.
27+
*
28+
* @return list of resolved nullable entities
29+
*/
30+
override fun get(env: DataFetchingEnvironment): CompletableFuture<DataFetcherResult<List<Any?>>> {
31+
val representations: List<Map<String, Any>> = env.getArgument("representations")
32+
33+
val indexedBatchRequestsByType = representations.withIndex().groupBy { it.value["__typename"].toString() }
34+
return GlobalScope.async {
35+
val data = mutableListOf<Any?>()
36+
val errors = mutableListOf<GraphQLError>()
37+
indexedBatchRequestsByType.map { (typeName, indexedRequests) ->
38+
async {
39+
resolveType(typeName, indexedRequests)
40+
}
41+
}.awaitAll()
42+
.flatten()
43+
.sortedBy { it.first }
44+
.forEach {
45+
val result = it.second
46+
if (result is GraphQLError) {
47+
data.add(null)
48+
errors.add(result)
49+
} else {
50+
data.add(result)
51+
}
52+
}
53+
DataFetcherResult.newResult<List<Any?>>()
54+
.data(data)
55+
.errors(errors)
56+
.build()
57+
}.asCompletableFuture()
58+
}
59+
60+
private suspend fun resolveType(typeName: String, indexedRequests: List<IndexedValue<Map<String, Any>>>): List<Pair<Int, Any?>> {
61+
val indices = indexedRequests.map { it.index }
62+
val batch = indexedRequests.map { it.value }
63+
val results = resolveBatch(typeName, batch)
64+
return if (results.size != indices.size) {
65+
indices.map {
66+
it to FederatedRequestFailure("Federation batch request for $typeName generated different number of results than requested, representations=${indices.size}, results=${results.size}")
67+
}
68+
} else {
69+
indices.zip(results)
70+
}
71+
}
72+
73+
@Suppress("TooGenericExceptionCaught")
74+
private suspend fun resolveBatch(typeName: String, batch: List<Map<String, Any>>): List<Any?> {
75+
val resolver = federatedTypeRegistry.getFederatedResolver(typeName)
76+
return if (resolver != null) {
77+
try {
78+
resolver.resolve(batch)
79+
} catch (e: Exception) {
80+
batch.map {
81+
FederatedRequestFailure("Exception was thrown while trying to resolve federated type, representation=$it", e)
82+
}
83+
}
84+
} else {
85+
batch.map {
86+
InvalidFederatedRequest("Unable to resolve federated type, representation=$it")
87+
}
88+
}
89+
}
90+
}

graphql-kotlin-federation/src/main/kotlin/com/expedia/graphql/federation/FederatedTypeRegistry.kt renamed to graphql-kotlin-federation/src/main/kotlin/com/expedia/graphql/federation/execution/FederatedTypeRegistry.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.expedia.graphql.federation
1+
package com.expedia.graphql.federation.execution
22

33
/**
44
* Simple registry that holds mapping of all registered federated GraphQL types and their corresponding resolvers.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.expedia.graphql.federation.execution
2+
3+
/**
4+
* Resolver used to retrieve target federated types.
5+
*/
6+
interface FederatedTypeResolver<out T> {
7+
8+
/**
9+
* Resolves underlying federated types based on the passed in _entities query representations. Entities
10+
* need to be resolved in the same order they were specified by the list of representations. Each passed
11+
* in representation should either be resolved to a target entity OR NULL if entity cannot be resolved.
12+
*
13+
* @param representations _entity query representations that are required to instantiate the target type
14+
* @return list of the target federated type instances
15+
*/
16+
suspend fun resolve(representations: List<Map<String, Any>>): List<T?>
17+
}

graphql-kotlin-federation/src/test/kotlin/com/expedia/graphql/federation/FederatedSchemaGeneratorTest.kt

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ package com.expedia.graphql.federation
22

33
import com.expedia.graphql.TopLevelObject
44
import com.expedia.graphql.extensions.print
5+
import com.expedia.graphql.federation.execution.FederatedTypeRegistry
56
import graphql.schema.GraphQLUnionType
67
import org.junit.jupiter.api.Assertions.assertEquals
78
import org.junit.jupiter.api.Test
8-
import test.data.queries.federated.Book
9-
import test.data.queries.federated.User
109
import test.data.queries.simple.SimpleQuery
1110
import kotlin.test.assertNotNull
1211
import kotlin.test.assertTrue
@@ -84,26 +83,9 @@ class FederatedSchemaGeneratorTest {
8483

8584
@Test
8685
fun `verify can generate federated schema`() {
87-
val bookResolver = object : FederatedTypeResolver<Book> {
88-
override fun resolve(keys: Map<String, Any>): Book {
89-
val book = Book(keys["id"].toString())
90-
keys["weight"]?.toString()?.toDoubleOrNull()?.let {
91-
book.weight = it
92-
}
93-
return book
94-
}
95-
}
96-
val userResolver = object : FederatedTypeResolver<User> {
97-
override fun resolve(keys: Map<String, Any>): User {
98-
val id = keys["userId"].toString().toInt()
99-
val name = keys["name"].toString()
100-
return User(id, name)
101-
}
102-
}
103-
10486
val config = FederatedSchemaGeneratorConfig(
10587
supportedPackages = listOf("test.data.queries.federated"),
106-
hooks = FederatedSchemaGeneratorHooks(FederatedTypeRegistry(mapOf("Book" to bookResolver, "User" to userResolver)))
88+
hooks = FederatedSchemaGeneratorHooks(FederatedTypeRegistry(emptyMap()))
10789
)
10890

10991
val schema = toFederatedSchema(config)

0 commit comments

Comments
 (0)