Skip to content

Commit 5ebef48

Browse files
authored
fix(10.x.x): keep SynchronizedDataLoader when the registry has an instrumentation, and synchronize loadMany. (#2216)
### 📝 Description `SynchronizedDataLoader` (#2202) has two gaps that let concurrent loads on a cached, non-batching loader call the downstream more than once: - `DataLoaderRegistry` rebuilds each loader with `transform(...)` to add its name and instrumentation. `DelegatingDataLoader.transform` returns the plain delegate, so the wrapper is lost whenever a `DataLoaderInstrumentation` is passed to `generate(...)`, which is always the case with `SYNC_EXHAUSTION`. `transform` now re-wraps the result. - `loadMany` bypasses the wrapper and calls the delegate's `loadImpl` directly. That includes `getValuesFromDataLoader`. The three `loadMany` overloads are now synchronized like `load`. ### 🔗 Related Issues Follow-up to #2202, which addressed the loss of `load` synchronization
1 parent db0bea0 commit 5ebef48

2 files changed

Lines changed: 105 additions & 0 deletions

File tree

‎executions/graphql-kotlin-dataloader/src/main/kotlin/com/expediagroup/graphql/dataloader/SynchronizedDataLoader.kt‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717
package com.expediagroup.graphql.dataloader
1818

1919
import org.dataloader.DataLoader
20+
import org.dataloader.DataLoaderFactory
21+
import org.dataloader.DataLoaderRegistry
2022
import org.dataloader.DelegatingDataLoader
23+
import org.dataloader.instrumentation.DataLoaderInstrumentation
2124
import java.util.concurrent.CompletableFuture
25+
import java.util.function.Consumer
2226

2327
/**
2428
*
@@ -37,4 +41,26 @@ internal class SynchronizedDataLoader<K : Any, V : Any>(
3741
synchronized(this) {
3842
super.load(key, keyContext)
3943
}
44+
45+
override fun loadMany(keys: List<K>): CompletableFuture<List<V>> =
46+
synchronized(this) {
47+
super.loadMany(keys)
48+
}
49+
50+
override fun loadMany(keys: List<K>, keyContexts: List<Any>): CompletableFuture<List<V>> =
51+
synchronized(this) {
52+
super.loadMany(keys, keyContexts)
53+
}
54+
55+
override fun loadMany(keysAndContexts: Map<K, *>): CompletableFuture<Map<K, V>> =
56+
synchronized(this) {
57+
super.loadMany(keysAndContexts)
58+
}
59+
60+
/**
61+
* [DataLoaderRegistry] transforms every registered [DataLoader] to add its name and [DataLoaderInstrumentation],
62+
* the transformed [DataLoader] needs to be synchronized as well
63+
*/
64+
override fun transform(builderConsumer: Consumer<DataLoaderFactory.Builder<K, V>>): DataLoader<K, V> =
65+
SynchronizedDataLoader(super.transform(builderConsumer))
4066
}

‎executions/graphql-kotlin-dataloader/src/test/kotlin/com/expediagroup/graphql/dataloader/KotlinDataLoaderRegistryFactoryTest.kt‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ import org.dataloader.DataLoader
2727
import org.dataloader.DataLoaderFactory
2828
import org.dataloader.DataLoaderOptions
2929
import org.dataloader.instrumentation.DataLoaderInstrumentation
30+
import org.dataloader.instrumentation.DataLoaderInstrumentationContext
3031
import org.junit.jupiter.api.Test
3132
import reactor.kotlin.core.publisher.toFlux
3233
import java.util.concurrent.CompletableFuture
34+
import java.util.concurrent.ConcurrentHashMap
3335
import java.util.concurrent.atomic.AtomicInteger
3436
import kotlin.test.assertEquals
3537
import kotlin.test.assertTrue
@@ -94,4 +96,81 @@ class KotlinDataLoaderRegistryFactoryTest {
9496
assertEquals(1, invocations.get())
9597
}
9698
}
99+
100+
@Test
101+
fun `cached and non-batched data loader is invoked once for concurrent loads of the same key when registry has instrumentation`() {
102+
runBlocking {
103+
val invocations = AtomicInteger()
104+
val instrumentedLoads = AtomicInteger()
105+
val kotlinDataLoader = object : KotlinDataLoader<String, String> {
106+
override val dataLoaderName = "NonBatchingDataLoader"
107+
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, String> =
108+
DataLoaderFactory.newMappedDataLoader(
109+
{ keys ->
110+
invocations.incrementAndGet()
111+
Thread.sleep(100)
112+
CompletableFuture.completedFuture(keys.associateWith { it })
113+
},
114+
DataLoaderOptions.newOptions()
115+
.setBatchingEnabled(false)
116+
.build()
117+
)
118+
}
119+
val countingInstrumentation = object : DataLoaderInstrumentation {
120+
override fun beginLoad(dataLoader: DataLoader<*, *>, key: Any, loadContext: Any?): DataLoaderInstrumentationContext<Any?>? {
121+
instrumentedLoads.incrementAndGet()
122+
return null
123+
}
124+
}
125+
val registry = KotlinDataLoaderRegistryFactory(kotlinDataLoader).generate(mockk(relaxed = true), countingInstrumentation)
126+
127+
val dataLoader = requireNotNull(registry.getDataLoader<String, String>(kotlinDataLoader.dataLoaderName))
128+
129+
List(8) {
130+
async(Dispatchers.Default) {
131+
dataLoader.load("same-key").await()
132+
}
133+
}.awaitAll()
134+
135+
assertEquals(1, invocations.get())
136+
assertEquals(8, instrumentedLoads.get())
137+
}
138+
}
139+
140+
@Test
141+
fun `cached and non-batched data loader is invoked once per key for concurrent loadMany of the same keys`() {
142+
runBlocking {
143+
val invocations = ConcurrentHashMap<String, AtomicInteger>()
144+
val kotlinDataLoader = object : KotlinDataLoader<String, String> {
145+
override val dataLoaderName = "NonBatchingDataLoader"
146+
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, String> =
147+
DataLoaderFactory.newMappedDataLoader(
148+
{ keys ->
149+
keys.forEach { key -> invocations.computeIfAbsent(key) { AtomicInteger() }.incrementAndGet() }
150+
Thread.sleep(100)
151+
CompletableFuture.completedFuture(keys.associateWith { it })
152+
},
153+
DataLoaderOptions.newOptions()
154+
.setBatchingEnabled(false)
155+
.build()
156+
)
157+
}
158+
val registry = KotlinDataLoaderRegistryFactory(kotlinDataLoader).generate(mockk(relaxed = true), object : DataLoaderInstrumentation {})
159+
160+
val dataLoader = requireNotNull(registry.getDataLoader<String, String>(kotlinDataLoader.dataLoaderName))
161+
val keys = listOf("a", "b")
162+
163+
List(9) { index ->
164+
async(Dispatchers.Default) {
165+
when (index % 3) {
166+
0 -> dataLoader.loadMany(keys).await()
167+
1 -> dataLoader.loadMany(keys, keys.map { key -> "context-$key" }).await()
168+
else -> dataLoader.loadMany(keys.associateWith { key -> "context-$key" }).await().values.toList()
169+
}
170+
}
171+
}.awaitAll()
172+
173+
assertEquals(mapOf("a" to 1, "b" to 1), invocations.mapValues { (_, count) -> count.get() })
174+
}
175+
}
97176
}

0 commit comments

Comments
 (0)