Skip to content

Commit 2ad2767

Browse files
samuelAndalonSamuel Vazquez
and
Samuel Vazquez
authored
feat: SingletonPropertyDataFetcher, avoid allocating a PropertyDataFetcher per property per graphql operation (#2079)
### 📝 Description Inspired by graphql-java/graphql-java#3754. Currently, graphql-kotlin, through the [KotlinDataFetcherFactoryProvider](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/KotlinDataFetcherFactoryProvider.kt#L62) creates a [PropertyDataFetcher](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/PropertyDataFetcher.kt) per source's property. This instance is created [every single time ](https://github.com/graphql-java/graphql-java/blob/master/src/main/java/graphql/schema/GraphQLCodeRegistry.java#L100)the graphql-java [DataFetcherFactory](https://github.com/graphql-java/graphql-java/blob/master/src/main/java/graphql/schema/DataFetcherFactory.java) is invoked, which happens to be on runtime per property per graphql-operation. This PR will introduce a new object class `SingletonPropertyDataFetcher` which will host its own singleton factory that will always return `SingletonPropertyDataFetcher` which will store all `KProperty.Getter<*>`s in a ConcurrentHashMap. Instead of just replacing the SimpleKotlinDataFetcherFactoryProvider, I am creating a new one, to avoid breaking changes, and to allow users to decide what they want, this switch might come with a cost, we are avoiding object allocations, in favor of a singleton that will possibly hold thousands of `KProperty.Getter<*>`s. --------- Co-authored-by: Samuel Vazquez <samvazquez@expediagroup.com>
1 parent db7f386 commit 2ad2767

File tree

8 files changed

+204
-80
lines changed

8 files changed

+204
-80
lines changed

generator/graphql-kotlin-schema-generator/build.gradle.kts

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ tasks {
1919
limit {
2020
counter = "INSTRUCTION"
2121
value = "COVEREDRATIO"
22-
minimum = "0.96".toBigDecimal()
22+
minimum = "0.95".toBigDecimal()
2323
}
2424
limit {
2525
counter = "BRANCH"
2626
value = "COVEREDRATIO"
27-
minimum = "0.92".toBigDecimal()
27+
minimum = "0.91".toBigDecimal()
2828
}
2929
}
3030
}

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/KotlinDataFetcherFactoryProvider.kt

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023 Expedia, Inc
2+
* Copyright 2025 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.
@@ -64,3 +64,12 @@ open class SimpleKotlinDataFetcherFactoryProvider : KotlinDataFetcherFactoryProv
6464
PropertyDataFetcher(kProperty.getter)
6565
}
6666
}
67+
68+
/**
69+
* [SimpleSingletonKotlinDataFetcherFactoryProvider] is a specialization of [SimpleKotlinDataFetcherFactoryProvider] that will provide a
70+
* a [SingletonPropertyDataFetcher] that should be used to target property resolutions without allocating a DataFetcher per property
71+
*/
72+
open class SimpleSingletonKotlinDataFetcherFactoryProvider : SimpleKotlinDataFetcherFactoryProvider() {
73+
override fun propertyDataFetcherFactory(kClass: KClass<*>, kProperty: KProperty<*>): DataFetcherFactory<Any?> =
74+
SingletonPropertyDataFetcher.getFactoryAndRegister(kClass, kProperty)
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.expediagroup.graphql.generator.execution
2+
3+
import graphql.schema.DataFetcher
4+
import graphql.schema.DataFetcherFactory
5+
import graphql.schema.DataFetchingEnvironment
6+
import graphql.schema.GraphQLFieldDefinition
7+
import graphql.schema.LightDataFetcher
8+
import java.util.concurrent.ConcurrentHashMap
9+
import java.util.function.Supplier
10+
import kotlin.reflect.KClass
11+
import kotlin.reflect.KProperty
12+
13+
/**
14+
* Singleton Property [DataFetcher] that stores references to underlying properties getters.
15+
*/
16+
internal object SingletonPropertyDataFetcher : LightDataFetcher<Any?> {
17+
18+
private val factory: DataFetcherFactory<Any?> = DataFetcherFactory<Any?> { SingletonPropertyDataFetcher }
19+
20+
private val getters: ConcurrentHashMap<String, KProperty.Getter<*>> = ConcurrentHashMap()
21+
22+
fun getFactoryAndRegister(kClass: KClass<*>, kProperty: KProperty<*>): DataFetcherFactory<Any?> {
23+
getters.computeIfAbsent("${kClass.java.name}.${kProperty.name}") {
24+
kProperty.getter
25+
}
26+
return factory
27+
}
28+
29+
override fun get(
30+
fieldDefinition: GraphQLFieldDefinition,
31+
sourceObject: Any?,
32+
environmentSupplier: Supplier<DataFetchingEnvironment>
33+
): Any? =
34+
sourceObject?.let {
35+
getters["${sourceObject.javaClass.name}.${fieldDefinition.name}"]?.call(sourceObject)
36+
}
37+
38+
override fun get(environment: DataFetchingEnvironment): Any? =
39+
environment.getSource<Any?>()?.let { sourceObject ->
40+
getters["${sourceObject.javaClass.name}.${environment.fieldDefinition.name}"]?.call(sourceObject)
41+
}
42+
}

generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/ToSchemaTest.kt

+111-73
Large diffs are not rendered by default.

generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/testSchemaConfig.kt

+8-1
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,19 @@ package com.expediagroup.graphql.generator
1818

1919
import com.expediagroup.graphql.generator.directives.KotlinDirectiveWiringFactory
2020
import com.expediagroup.graphql.generator.directives.KotlinSchemaDirectiveWiring
21+
import com.expediagroup.graphql.generator.execution.KotlinDataFetcherFactoryProvider
22+
import com.expediagroup.graphql.generator.execution.SimpleKotlinDataFetcherFactoryProvider
2123
import com.expediagroup.graphql.generator.hooks.SchemaGeneratorHooks
2224
import io.mockk.every
2325
import io.mockk.spyk
2426

2527
val defaultSupportedPackages = listOf("com.expediagroup.graphql.generator")
26-
fun testSchemaConfig() = SchemaGeneratorConfig(defaultSupportedPackages)
28+
fun testSchemaConfig(
29+
dataFetcherFactoryProvider: KotlinDataFetcherFactoryProvider = SimpleKotlinDataFetcherFactoryProvider()
30+
) = SchemaGeneratorConfig(
31+
defaultSupportedPackages,
32+
dataFetcherFactoryProvider = dataFetcherFactoryProvider
33+
)
2734

2835
fun getTestSchemaConfigWithHooks(hooks: SchemaGeneratorHooks) = SchemaGeneratorConfig(defaultSupportedPackages, hooks = hooks)
2936

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ tasks {
2727
limit {
2828
counter = "INSTRUCTION"
2929
value = "COVEREDRATIO"
30-
minimum = "0.86".toBigDecimal()
30+
minimum = "0.85".toBigDecimal()
3131
}
3232
limit {
3333
counter = "BRANCH"

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

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023 Expedia, Inc
2+
* Copyright 2025 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,18 +17,30 @@
1717
package com.expediagroup.graphql.server.spring.execution
1818

1919
import com.expediagroup.graphql.generator.execution.SimpleKotlinDataFetcherFactoryProvider
20+
import com.expediagroup.graphql.generator.execution.SimpleSingletonKotlinDataFetcherFactoryProvider
2021
import graphql.schema.DataFetcherFactory
2122
import org.springframework.context.ApplicationContext
2223
import kotlin.reflect.KClass
2324
import kotlin.reflect.KFunction
2425

2526
/**
2627
* This provides a wrapper around the [SimpleKotlinDataFetcherFactoryProvider] to call the [SpringDataFetcher] on functions.
27-
* This allows you to use Spring beans as function arugments and they will be populated by the data fetcher.
28+
* This allows you to use Spring beans as function arguments, and they will be populated by the data fetcher.
2829
*/
2930
open class SpringKotlinDataFetcherFactoryProvider(
3031
private val applicationContext: ApplicationContext
3132
) : SimpleKotlinDataFetcherFactoryProvider() {
3233
override fun functionDataFetcherFactory(target: Any?, kClass: KClass<*>, kFunction: KFunction<*>): DataFetcherFactory<Any?> =
3334
DataFetcherFactory { SpringDataFetcher(target, kFunction, applicationContext) }
3435
}
36+
37+
/**
38+
* This provides a wrapper around the [SimpleSingletonKotlinDataFetcherFactoryProvider] to call the [SpringDataFetcher] on functions.
39+
* This allows you to use Spring beans as function arguments, and they will be populated by the data fetcher.
40+
*/
41+
open class SpringSingletonKotlinDataFetcherFactoryProvider(
42+
private val applicationContext: ApplicationContext
43+
) : SimpleSingletonKotlinDataFetcherFactoryProvider() {
44+
override fun functionDataFetcherFactory(target: Any?, kClass: KClass<*>, kFunction: KFunction<*>): DataFetcherFactory<Any?> =
45+
DataFetcherFactory { SpringDataFetcher(target, kFunction, applicationContext) }
46+
}

website/docs/schema-generator/execution/fetching-data.md

+16
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,19 @@ You can provide your own custom data fetchers to resolve functions and propertie
5555
to your [SchemaGeneratorConfig](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/SchemaGeneratorConfig.kt).
5656

5757
See our [spring example app](https://github.com/ExpediaGroup/graphql-kotlin/tree/master/examples/server/spring-server) for an example of `CustomDataFetcherFactoryProvider`.
58+
59+
:::info
60+
61+
Currently, graphql-kotlin, through the [KotlinDataFetcherFactoryProvider](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/KotlinDataFetcherFactoryProvider.kt#L62)
62+
creates a [PropertyDataFetcher](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/PropertyDataFetcher.kt)
63+
per source's property. This instance is created [every single time](https://github.com/graphql-java/graphql-java/blob/master/src/main/java/graphql/schema/GraphQLCodeRegistry.java#L100)
64+
the graphql-java [DataFetcherFactory](https://github.com/graphql-java/graphql-java/blob/master/src/main/java/graphql/schema/DataFetcherFactory.java) is invoked,
65+
which happens to be on runtime per property per GraphQL operation.
66+
67+
If you want to avoid that, use or extend the [SimpleSingletonKotlinDataFetcherFactoryProvider](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/KotlinDataFetcherFactoryProvider.kt#L72) which will provide a
68+
[SingletonPropertyDataFetcher](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/SingletonPropertyDataFetcher.kt) that will host its own singleton factory, and it will store
69+
all `KProperty.Getter<*>`s in a `ConcurrentHashMap`.
70+
71+
This is inspired by this [graphql-java's PR](https://github.com/graphql-java/graphql-java/pull/3754)
72+
73+
:::

0 commit comments

Comments
 (0)