Skip to content

feat(6.x.x): SingletonPropertyDataFetcher, avoid allocating a PropertyDataFetcher per property per graphql operation #2084

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
run: ./gradlew clean build

- name: Archive failure build reports
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: build-reports
Expand All @@ -47,7 +47,7 @@ jobs:
run: ./gradlew clean build

- name: Archive examples failure build reports
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: build-examples-reports
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr-check-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
with:
node-version: 16

- uses: actions/cache@v2
- uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('website/package-lock.json') }}
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:

# Used by maven-plugin integration tests
- name: Set up Maven cache
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
Expand All @@ -41,7 +41,7 @@ jobs:
run: ./gradlew clean build

- name: Archive failure build reports
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: build-reports
Expand All @@ -56,7 +56,7 @@ jobs:
run: ./gradlew clean build

- name: Archive examples failure build reports
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: build-examples-reports
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-latest-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
with:
node-version: 16

- uses: actions/cache@v2
- uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('website/package-lock.json') }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
PLUGIN_PORTAL_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }}

- name: Archive failure build reports
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: build-reports
Expand Down
2 changes: 1 addition & 1 deletion generator/graphql-kotlin-schema-generator/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ tasks {
limit {
counter = "BRANCH"
value = "COVEREDRATIO"
minimum = "0.93".toBigDecimal()
minimum = "0.91".toBigDecimal()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,12 @@ open class SimpleKotlinDataFetcherFactoryProvider : KotlinDataFetcherFactoryProv
PropertyDataFetcher(kProperty.getter)
}
}

/**
* [SimpleSingletonKotlinDataFetcherFactoryProvider] is a specialization of [SimpleKotlinDataFetcherFactoryProvider] that will provide a
* a [SingletonPropertyDataFetcher] that should be used to target property resolutions without allocating a DataFetcher per property
*/
open class SimpleSingletonKotlinDataFetcherFactoryProvider : SimpleKotlinDataFetcherFactoryProvider() {
override fun propertyDataFetcherFactory(kClass: KClass<*>, kProperty: KProperty<*>): DataFetcherFactory<Any?> =
SingletonPropertyDataFetcher.getFactoryAndRegister(kClass, kProperty)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.expediagroup.graphql.generator.execution

import graphql.schema.DataFetcher
import graphql.schema.DataFetcherFactory
import graphql.schema.DataFetchingEnvironment
import graphql.schema.GraphQLFieldDefinition
import graphql.schema.LightDataFetcher
import java.util.concurrent.ConcurrentHashMap
import java.util.function.Supplier
import kotlin.reflect.KClass
import kotlin.reflect.KProperty

/**
* Singleton Property [DataFetcher] that stores references to underlying properties getters.
*/
internal object SingletonPropertyDataFetcher : LightDataFetcher<Any?> {

private val factory: DataFetcherFactory<Any?> = DataFetcherFactory<Any?> { SingletonPropertyDataFetcher }

private val getters: ConcurrentHashMap<String, KProperty.Getter<*>> = ConcurrentHashMap()

fun getFactoryAndRegister(kClass: KClass<*>, kProperty: KProperty<*>): DataFetcherFactory<Any?> {
getters.computeIfAbsent("${kClass.java.name}.${kProperty.name}") {
kProperty.getter
}
return factory
}

override fun get(
fieldDefinition: GraphQLFieldDefinition,
sourceObject: Any?,
environmentSupplier: Supplier<DataFetchingEnvironment>
): Any? =
sourceObject?.let {
getters["${sourceObject.javaClass.name}.${fieldDefinition.name}"]?.call(sourceObject)
}

override fun get(environment: DataFetchingEnvironment): Any? =
environment.getSource<Any?>()?.let { sourceObject ->
getters["${sourceObject.javaClass.name}.${environment.fieldDefinition.name}"]?.call(sourceObject)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class PolymorphicTests {

@Test
fun `Schema generator creates union types from marked up interface`() {
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithUnion())), config = testSchemaConfig)
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithUnion())), config = testSchemaConfig())

val graphqlType = schema.getType("BodyPart") as? GraphQLUnionType
assertNotNull(graphqlType)
Expand All @@ -54,7 +54,7 @@ class PolymorphicTests {

@Test
fun `SchemaGenerator can expose an interface and its implementations`() {
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithInterface())), config = testSchemaConfig)
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithInterface())), config = testSchemaConfig())

val interfaceType = schema.getType("AnInterface") as? GraphQLInterfaceType
assertNotNull(interfaceType)
Expand All @@ -68,20 +68,20 @@ class PolymorphicTests {
@Test
fun `Interfaces cannot be used as input field types`() {
assertThrows(InvalidInputFieldTypeException::class.java) {
toSchema(queries = listOf(TopLevelObject(QueryWithUnAuthorizedInterfaceArgument())), config = testSchemaConfig)
toSchema(queries = listOf(TopLevelObject(QueryWithUnAuthorizedInterfaceArgument())), config = testSchemaConfig())
}
}

@Test
fun `Union cannot be used as input field types`() {
assertThrows(InvalidInputFieldTypeException::class.java) {
toSchema(queries = listOf(TopLevelObject(QueryWithUnAuthorizedUnionArgument())), config = testSchemaConfig)
toSchema(queries = listOf(TopLevelObject(QueryWithUnAuthorizedUnionArgument())), config = testSchemaConfig())
}
}

@Test
fun `Object types implementing union and interfaces are only created once`() {
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithInterfaceAndUnion())), config = testSchemaConfig)
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithInterfaceAndUnion())), config = testSchemaConfig())

val carType = schema.getType("Car") as? GraphQLObjectType
assertNotNull(carType)
Expand All @@ -95,15 +95,15 @@ class PolymorphicTests {

@Test
fun `Interfaces can declare properties of their own type`() {
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithRecursiveType())), config = testSchemaConfig)
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithRecursiveType())), config = testSchemaConfig())

val personType = schema.getType("Person")
assertNotNull(personType)
}

@Test
fun `Abstract classes should be converted to interfaces`() {
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithAbstract())), config = testSchemaConfig)
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithAbstract())), config = testSchemaConfig())

val abstractInterface = schema.getType("MyAbstract") as? GraphQLInterfaceType
assertNotNull(abstractInterface)
Expand All @@ -116,7 +116,7 @@ class PolymorphicTests {

@Test
fun `Interface types can be correctly resolved`() {
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithRenamedAbstracts())), config = testSchemaConfig)
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithRenamedAbstracts())), config = testSchemaConfig())

val cakeInterface = schema.getType("Cake") as? GraphQLInterfaceType
assertNotNull(cakeInterface)
Expand All @@ -132,7 +132,7 @@ class PolymorphicTests {

@Test
fun `Union types can be correctly resolved`() {
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithRenamedAbstracts())), config = testSchemaConfig)
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithRenamedAbstracts())), config = testSchemaConfig())

val dessertUnion = schema.getType("Dessert") as? GraphQLUnionType
assertNotNull(dessertUnion)
Expand All @@ -148,7 +148,7 @@ class PolymorphicTests {

@Test
fun `Interface implementations are not computed when marked with GraphQLIgnore annotation`() {
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithIgnoredInfo())), config = testSchemaConfig)
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithIgnoredInfo())), config = testSchemaConfig())
val service = schema.getType("Service") as? GraphQLInterfaceType
assertNotNull(service)

Expand All @@ -161,7 +161,7 @@ class PolymorphicTests {

@Test
fun `Ignored interface properties should not appear in the subtype`() {
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithIgnoredInfo())), config = testSchemaConfig)
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithIgnoredInfo())), config = testSchemaConfig())
val service = schema.getType("Service") as? GraphQLInterfaceType
assertNotNull(service)
val interfaceIgnoredField = service.getFieldDefinition("shouldNotBeInTheSchema")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class SchemaGeneratorAsyncTests {

@Test
fun `SchemaGenerator strips type argument from CompletableFuture to support async servlet`() {
val schema = toSchema(queries = listOf(TopLevelObject(AsyncQuery())), config = testSchemaConfig)
val schema = toSchema(queries = listOf(TopLevelObject(AsyncQuery())), config = testSchemaConfig())
val returnType =
(schema.getObjectType("Query").getFieldDefinition("asynchronouslyDo").type as? GraphQLNonNull)?.wrappedType
assertNotNull(returnType)
Expand Down
Loading