Skip to content

Commit 2a9b030

Browse files
Addressed PR comments
1 parent 04a1b8e commit 2a9b030

10 files changed

Lines changed: 61 additions & 63 deletions

File tree

lib/src/main/java/graphql/nadel/NextgenEngine.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ internal class NextgenEngine(
206206
try {
207207
val resolvedService = fieldToService.resolveDynamicService(fields, service)
208208
executeTopLevelField(
209-
topLevelField = fields,
209+
topLevelFields = fields,
210210
service = resolvedService,
211211
executionContext = executionContext,
212212
)
@@ -221,7 +221,7 @@ internal class NextgenEngine(
221221
}.awaitAll()
222222

223223
if (executionHints.newResultMergerAndNamespacedTypename()) {
224-
NadelResultMerger.mergeResults(fields, engineSchema, results)
224+
NadelResultMerger.mergeResults(operation.topLevelFields, engineSchema, results)
225225
} else {
226226
graphql.nadel.engine.util.mergeResults(results)
227227
}
@@ -260,7 +260,7 @@ internal class NextgenEngine(
260260
): ServiceExecutionResult {
261261
return try {
262262
executeTopLevelField(
263-
topLevelField = listOf(topLevelField),
263+
topLevelFields = listOf(topLevelField),
264264
service = service,
265265
executionContext = executionContext.copy(
266266
hydrationDetails = hydrationDetails,
@@ -283,7 +283,7 @@ internal class NextgenEngine(
283283
executionContext: NadelExecutionContext,
284284
): ServiceExecutionResult {
285285
return executeTopLevelField(
286-
topLevelField = listOf(topLevelField),
286+
topLevelFields = listOf(topLevelField),
287287
service = service,
288288
executionContext = executionContext.copy(
289289
isPartitionedCall = true,
@@ -292,7 +292,7 @@ internal class NextgenEngine(
292292
}
293293

294294
private suspend fun executeTopLevelField(
295-
topLevelField: List<ExecutableNormalizedField>,
295+
topLevelFields: List<ExecutableNormalizedField>,
296296
service: Service,
297297
executionContext: NadelExecutionContext,
298298
): ServiceExecutionResult {
@@ -305,7 +305,7 @@ internal class NextgenEngine(
305305
serviceExecutionContext = serviceExecutionContext,
306306
services = services,
307307
service = service,
308-
rootField = topLevelField,
308+
rootFields = topLevelFields,
309309
serviceHydrationDetails = executionContext.hydrationDetails,
310310
)
311311
}
@@ -315,7 +315,7 @@ internal class NextgenEngine(
315315
executionContext = executionContext,
316316
serviceExecutionContext = serviceExecutionContext,
317317
executionPlan = executionPlan,
318-
fields = topLevelField
318+
fields = topLevelFields
319319
)
320320
}
321321
val result: ServiceExecutionResult = timer.time(step = RootStep.ServiceExecution.child(service.name)) {
@@ -354,7 +354,7 @@ internal class NextgenEngine(
354354
val transformedResult: ServiceExecutionResult = when {
355355
// Introspection fields are never batched with other fields (see NadelFieldToService),
356356
// so an all-introspection batch needs no result transformation.
357-
topLevelField.all { it.name.startsWith("__") } -> result
357+
topLevelFields.all { it.name.startsWith("__") } -> result
358358
else -> timer.time(step = RootStep.ResultTransforming) {
359359
resultTransformer.transform(
360360
executionContext = executionContext,

lib/src/main/java/graphql/nadel/engine/plan/NadelExecutionPlanFactory.kt

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,24 +53,22 @@ internal class NadelExecutionPlanFactory(
5353

5454
/**
5555
* This derives an execution plan from with the main input parameters being the
56-
* [rootField] and [executionBlueprint].
56+
* [rootFields] and [executionBlueprint].
5757
*/
5858
suspend fun create(
5959
executionContext: NadelExecutionContext,
6060
serviceExecutionContext: NadelServiceExecutionContext,
6161
services: Map<String, Service>,
6262
service: Service,
63-
rootField: List<ExecutableNormalizedField>,
63+
rootFields: List<ExecutableNormalizedField>,
6464
serviceHydrationDetails: ServiceExecutionHydrationDetails?,
6565
): NadelExecutionPlan {
6666
val executionSteps: MutableMap<ExecutableNormalizedField, List<NadelExecutionPlan.Step<Any>>> =
6767
mutableMapOf()
6868
val transformContexts: MutableMap<NadelTransform<Any>, NadelTransformServiceExecutionContext?> =
6969
mutableMapOf()
7070
executionContext.timer.batch { timer ->
71-
// Plan across every root field: batched root fields (see NadelBatchRootFieldsHint) share
72-
// one service call, so all their transform steps belong to this single execution plan.
73-
traverseQuery(rootField) { field ->
71+
traverseQuery(rootFields) { field ->
7472
val steps = transformsWithTimingStepInfo.mapNotNull { transformWithTimingInfo ->
7573
val transform = transformWithTimingInfo.transform
7674
// This is a patch to prevent errors
@@ -89,9 +87,7 @@ internal class NadelExecutionPlanFactory(
8987
executionBlueprint,
9088
services,
9189
service,
92-
// buildContext is memoized once per service call; the first root
93-
// field is passed as the representative root field of the batch.
94-
rootField.first(),
90+
rootFields,
9591
serviceHydrationDetails
9692
)
9793
}

lib/src/main/java/graphql/nadel/engine/transform/NadelTransform.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ interface NadelTransform<State : Any> {
2525
* of the transform on all the fields.
2626
*
2727
* @param executionBlueprint the [NadelOverallExecutionBlueprint] of the Nadel instance being operated on
28-
* @param rootField the root [ExecutableNormalizedField] of the operation this [NadelTransform] runs on
28+
* @param rootFields the root [ExecutableNormalizedField]s of the operation this [NadelTransform] runs on.
29+
* There is more than one when sibling root fields are batched into a single service call.
2930
* @param hydrationDetails the [ServiceExecutionHydrationDetails] when the [NadelTransform] is applied to fields inside
3031
* hydrations, `null` otherwise
3132
*
@@ -39,7 +40,7 @@ interface NadelTransform<State : Any> {
3940
executionBlueprint: NadelOverallExecutionBlueprint,
4041
services: Map<String, Service>,
4142
service: Service,
42-
rootField: ExecutableNormalizedField,
43+
rootFields: List<ExecutableNormalizedField>,
4344
hydrationDetails: ServiceExecutionHydrationDetails?,
4445
): NadelTransformServiceExecutionContext? {
4546
return null

lib/src/main/java/graphql/nadel/engine/transform/NadelTransformJavaCompat.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ interface NadelTransformJavaCompat<State : Any> {
113113
executionBlueprint: NadelOverallExecutionBlueprint,
114114
services: Map<String, Service>,
115115
service: Service,
116-
rootField: ExecutableNormalizedField,
116+
rootFields: List<ExecutableNormalizedField>,
117117
hydrationDetails: ServiceExecutionHydrationDetails?,
118118
): NadelTransformServiceExecutionContext? {
119119
return compat.buildContext(
@@ -122,7 +122,8 @@ interface NadelTransformJavaCompat<State : Any> {
122122
executionBlueprint,
123123
services,
124124
service,
125-
rootField,
125+
// The Java compat API predates root-field batching and takes a single root field.
126+
rootFields.first(),
126127
hydrationDetails
127128
).asDeferred().await()
128129
}

lib/src/main/java/graphql/nadel/engine/transform/query/NadelFieldToService.kt

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,45 +27,39 @@ internal class NadelFieldToService(
2727
query: ExecutableNormalizedOperation,
2828
executionHints: NadelExecutionHints,
2929
): List<NadelFieldAndService> {
30-
val topLevelFields = query.topLevelFields
31-
32-
// Resolve the owning service for each non-namespaced root field, and bucket the ones that
33-
// are eligible for batching by service. LinkedHashMap preserves first-occurrence order.
34-
val fieldToService = HashMap<ExecutableNormalizedField, Service>()
35-
val batchedByService = LinkedHashMap<Service, MutableList<ExecutableNormalizedField>>()
36-
for (topLevelField in topLevelFields) {
37-
if (isNamespacedField(topLevelField)) {
38-
continue
39-
}
40-
val service = getService(topLevelField)
41-
fieldToService[topLevelField] = service
42-
if (canBatchRootField(topLevelField, service, executionHints)) {
43-
batchedByService.getOrPut(service) { mutableListOf() }.add(topLevelField)
30+
// Feature flag: when root-field batching is globally disabled, keep the original behaviour
31+
// of one entry (i.e. one service call) per root field.
32+
if (!executionHints.batchRootFields()) {
33+
return query.topLevelFields.flatMap { topLevelField ->
34+
if (isNamespacedField(topLevelField)) {
35+
getServicePairsForNamespacedFields(topLevelField, executionHints)
36+
} else {
37+
listOf(NadelFieldAndService(field = listOf(topLevelField), service = getService(topLevelField)))
38+
}
4439
}
4540
}
4641

47-
// Emit entries in query order. A batched service's root fields are emitted as a single
48-
// entry (one service call) at the position of the service's first batchable root field;
49-
// everything else keeps its own single-field entry, exactly as before.
42+
// Group batch-eligible root fields per service into a single entry (one service call).
5043
val result = mutableListOf<NadelFieldAndService>()
51-
val emittedBatches = HashSet<Service>()
52-
for (topLevelField in topLevelFields) {
44+
val batchedByService = LinkedHashMap<Service, MutableList<ExecutableNormalizedField>>()
45+
for (topLevelField in query.topLevelFields) {
5346
if (isNamespacedField(topLevelField)) {
5447
result += getServicePairsForNamespacedFields(topLevelField, executionHints)
5548
continue
5649
}
5750

58-
val service = fieldToService.getValue(topLevelField)
59-
val batchedFields = batchedByService[service]
60-
if (batchedFields != null && topLevelField in batchedFields) {
61-
if (emittedBatches.add(service)) {
62-
result += NadelFieldAndService(field = batchedFields, service = service)
63-
}
51+
val service = getService(topLevelField)
52+
if (canBatchRootField(topLevelField, service, executionHints)) {
53+
batchedByService.getOrPut(service) { mutableListOf() }.add(topLevelField)
6454
} else {
6555
result += NadelFieldAndService(field = listOf(topLevelField), service = service)
6656
}
6757
}
6858

59+
batchedByService.forEach { (service, batchedFields) ->
60+
result += NadelFieldAndService(field = batchedFields, service = service)
61+
}
62+
6963
return result
7064
}
7165

lib/src/main/java/graphql/nadel/hints/NadelBatchRootFieldsHint.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@ import graphql.nadel.Service
44

55
fun interface NadelBatchRootFieldsHint {
66
/**
7-
* Determines whether multiple top level (root) fields destined for the same service should be
8-
* combined into a single service call (i.e. one request), rather than one call per root field.
7+
* Global feature flag for root-field batching. When false, Nadel keeps its original behaviour
8+
* of making one service call per root field. Use this to feature flag the whole feature.
9+
*/
10+
operator fun invoke(): Boolean
11+
12+
/**
13+
* Per-service opt-in for root-field batching. Defaults to the global flag [invoke].
914
*
10-
* This only affects sibling root fields with no shared namespace wrapper (e.g. the prefixed
15+
* Only affects sibling root fields with no shared namespace wrapper (e.g. the prefixed
1116
* `jira_foo`, `jira_bar` form). Namespaced fields are already batched per service regardless.
1217
*
1318
* @param service the service the root fields would be sent to
1419
* @return true to batch this service's root fields into a single call
1520
*/
16-
operator fun invoke(service: Service): Boolean
21+
operator fun invoke(service: Service): Boolean = invoke()
1722
}

lib/src/main/java/graphql/nadel/result/NadelResultMerger.kt

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import graphql.ExecutionResultImpl
55
import graphql.GraphQLError
66
import graphql.introspection.Introspection
77
import graphql.nadel.ServiceExecutionResult
8-
import graphql.nadel.engine.transform.query.NadelFieldAndService
98
import graphql.nadel.engine.transform.result.NadelResultKey
109
import graphql.nadel.engine.util.AnyMap
1110
import graphql.nadel.engine.util.JsonMap
@@ -20,7 +19,7 @@ import graphql.schema.GraphQLSchema
2019

2120
internal object NadelResultMerger {
2221
fun mergeResults(
23-
fields: List<NadelFieldAndService>,
22+
topLevelFields: List<ExecutableNormalizedField>,
2423
engineSchema: GraphQLSchema,
2524
results: List<ServiceExecutionResult>,
2625
): ExecutionResult {
@@ -35,7 +34,7 @@ internal object NadelResultMerger {
3534
}
3635

3736
return ExecutionResultImpl.newExecutionResult()
38-
.data(fixData(fields, engineSchema, data))
37+
.data(fixData(topLevelFields, engineSchema, data))
3938
.extensions(extensions.let {
4039
@Suppress("UNCHECKED_CAST") // .extensions should take in a Map<*, *> instead of strictly Map<Any?, Any?>
4140
it as Map<Any?, Any?>
@@ -47,17 +46,15 @@ internal object NadelResultMerger {
4746
}
4847

4948
private fun fixData(
50-
fields: List<NadelFieldAndService>,
49+
topLevelFields: List<ExecutableNormalizedField>,
5150
engineSchema: GraphQLSchema,
5251
data: MutableJsonMap,
5352
): MutableJsonMap? {
54-
val requiredFieldMap = buildRequiredFieldMap(fields, engineSchema)
53+
val requiredFieldMap = buildRequiredFieldMap(topLevelFields, engineSchema)
5554

5655
for ((topLevelResultKey, children) in requiredFieldMap) {
5756
val topLevelFieldDef by lazy {
58-
fields
59-
.asSequence()
60-
.flatMap { it.field }
57+
topLevelFields
6158
.first { it.resultKey == topLevelResultKey.value }
6259
.getFieldDefinitions(engineSchema)
6360
.single() // This is under Query, Mutation etc. so there is only one field
@@ -116,14 +113,14 @@ internal object NadelResultMerger {
116113
}
117114

118115
private fun buildRequiredFieldMap(
119-
fields: List<NadelFieldAndService>,
116+
topLevelFields: List<ExecutableNormalizedField>,
120117
engineSchema: GraphQLSchema,
121118
): MutableMap<NadelResultKey, MutableList<ExecutableNormalizedField>> {
122119
val requiredFields = mutableMapOf<NadelResultKey, MutableList<ExecutableNormalizedField>>()
123120

124121
// NOTE: please ensure all fields are from object types and will NOT have multiple field defs
125122
// Other code in this file relies on this contract
126-
for (field in fields.flatMap { it.field }) {
123+
for (field in topLevelFields) {
127124
val requiredChildFields = requiredFields
128125
.computeIfAbsent(NadelResultKey(field.resultKey)) {
129126
mutableListOf()

lib/src/test/kotlin/graphql/nadel/transform/NadelTransformJavaCompatTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ class NadelTransformJavaCompatTest : DescribeSpec({
8686
executionBlueprint,
8787
services,
8888
service,
89-
overallField,
89+
listOf(overallField),
9090
hydrationDetails = hydrationDetails,
9191
)
9292

test/src/test/kotlin/graphql/nadel/tests/next/fixtures/batching/BatchRootFieldsPerServiceTest.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package graphql.nadel.tests.next.fixtures.batching
22

33
import graphql.nadel.NadelExecutionHints
4+
import graphql.nadel.hints.NadelBatchRootFieldsHint
45
import graphql.nadel.tests.next.NadelIntegrationTest
56

67
/**
@@ -58,6 +59,9 @@ class BatchRootFieldsPerServiceTest : NadelIntegrationTest(
5859
) {
5960
override fun makeExecutionHints(): NadelExecutionHints.Builder {
6061
return super.makeExecutionHints()
61-
.batchRootFields { service -> service.name == "batched" }
62+
.batchRootFields(object : NadelBatchRootFieldsHint {
63+
override fun invoke(): Boolean = true
64+
override fun invoke(service: graphql.nadel.Service): Boolean = service.name == "batched"
65+
})
6266
}
6367
}

test/src/test/kotlin/graphql/nadel/tests/next/fixtures/execution/ServiceExecutionContextTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,11 @@ class ServiceExecutionContextTest : NadelIntegrationTest(
158158
executionBlueprint: NadelOverallExecutionBlueprint,
159159
services: Map<String, graphql.nadel.Service>,
160160
service: graphql.nadel.Service,
161-
rootField: ExecutableNormalizedField,
161+
rootFields: List<ExecutableNormalizedField>,
162162
hydrationDetails: ServiceExecutionHydrationDetails?,
163163
): NadelTransformServiceExecutionContext? {
164164
val testTransformServiceExecutionContext =
165-
TestTransformServiceExecutionContext(rootField.toExecutionString())
165+
TestTransformServiceExecutionContext(rootFields.single().toExecutionString())
166166
transformServiceExecutionContexts.add(testTransformServiceExecutionContext)
167167
return testTransformServiceExecutionContext
168168
}

0 commit comments

Comments
 (0)