WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit d9085c0

Browse files
skevyviaductbot
authored andcommitted
Fix nested object fetching and exception handling in FieldResolver (AIRBNB)
Updates `FieldResolver` to return `Value<Unit>` from `fetchObject`, `fetchObjectSerially`, and `resolveField` instead of returning `Unit` or `Value<FieldResolutionResult>`. This ensures that the execution chain properly waits for all nested object and list fetching to complete before considering the parent operation finished. Refines exception handling in `FieldResolver` to ensure proper error bubbling: - `fetchObject` and `fetchObjectSerially` now allow fatal errors (those not swallowed by `executeField`) to bubble up by propagating exceptions caught in `recover`. - `executeField` continues to encapsulate `FieldFetchingException` and `InternalEngineException` (returning success for partial results). - `maybeFetchNestedObject` now waits for all list items to be fetched. Also updates `IViaductInstrumentation` and related classes to change the `beginFetchObject` return type to `InstrumentationContext<Unit>` to match the new `fetchObject` signature. Github-Change-Id: 967307 GitOrigin-RevId: acc8aca219f0052faf88908de65d2f66e3e09d60
1 parent 50924f3 commit d9085c0

File tree

15 files changed

+467
-65
lines changed

15 files changed

+467
-65
lines changed

engine/api/src/main/kotlin/viaduct/engine/api/instrumentation/ChainedModernGJInstrumentation.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ open class ChainedModernGJInstrumentation(
1111
override fun beginFetchObject(
1212
parameters: InstrumentationExecutionStrategyParameters,
1313
state: InstrumentationState?
14-
): InstrumentationContext<Map<String, Any?>>? =
14+
): InstrumentationContext<Unit> =
1515
ChainedInstrumentationContext(
1616
gjInstrumentations.map { instr ->
1717
instr.beginFetchObject(parameters, getState(instr, state))
@@ -21,7 +21,7 @@ open class ChainedModernGJInstrumentation(
2121
override fun beginCompleteObject(
2222
parameters: InstrumentationExecutionStrategyParameters,
2323
state: InstrumentationState?
24-
): InstrumentationContext<Any>? =
24+
): InstrumentationContext<Any> =
2525
ChainedInstrumentationContext(
2626
gjInstrumentations.map { instr ->
2727
instr.beginCompleteObject(parameters, getState(instr, state))

engine/api/src/main/kotlin/viaduct/engine/api/instrumentation/IViaductInstrumentation.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,14 +169,14 @@ interface IViaductInstrumentation {
169169
fun beginFetchObject(
170170
parameters: InstrumentationExecutionStrategyParameters,
171171
state: InstrumentationState?
172-
): InstrumentationContext<Map<String, Any?>>?
172+
): InstrumentationContext<Unit>
173173
}
174174

175175
interface WithBeginCompleteObject : IViaductInstrumentation {
176176
fun beginCompleteObject(
177177
parameters: InstrumentationExecutionStrategyParameters,
178178
state: InstrumentationState?
179-
): InstrumentationContext<Any>?
179+
): InstrumentationContext<Any>
180180
}
181181

182182
interface WithInstrumentAccessCheck : IViaductInstrumentation {

engine/api/src/main/kotlin/viaduct/engine/api/instrumentation/ViaductInstrumentationAdapter.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ open class ViaductInstrumentationAdapter(
155155
override fun beginFetchObject(
156156
parameters: InstrumentationExecutionStrategyParameters,
157157
state: InstrumentationState?
158-
): InstrumentationContext<Map<String, Any?>>? =
158+
): InstrumentationContext<Unit> =
159159
if (viaductInstrumentation is IViaductInstrumentation.WithBeginFetchObject) {
160160
viaductInstrumentation.beginFetchObject(parameters, state)
161161
} else {
@@ -165,7 +165,7 @@ open class ViaductInstrumentationAdapter(
165165
override fun beginCompleteObject(
166166
parameters: InstrumentationExecutionStrategyParameters,
167167
state: InstrumentationState?
168-
): InstrumentationContext<Any>? =
168+
): InstrumentationContext<Any> =
169169
if (viaductInstrumentation is IViaductInstrumentation.WithBeginCompleteObject) {
170170
viaductInstrumentation.beginCompleteObject(parameters, state)
171171
} else {

engine/api/src/main/kotlin/viaduct/engine/api/instrumentation/ViaductModernInstrumentation.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ interface ViaductModernInstrumentation {
8181
override fun beginFetchObject(
8282
parameters: InstrumentationExecutionStrategyParameters,
8383
state: InstrumentationState?
84-
): InstrumentationContext<Map<String, Any?>>? {
84+
): InstrumentationContext<Unit> {
8585
if (viaductInstrumentation is WithBeginFetchObject) {
8686
return viaductInstrumentation.beginFetchObject(parameters, state)
8787
}
@@ -111,7 +111,7 @@ interface ViaductModernInstrumentation {
111111
override fun beginCompleteObject(
112112
parameters: InstrumentationExecutionStrategyParameters,
113113
state: InstrumentationState?
114-
): InstrumentationContext<Any>? {
114+
): InstrumentationContext<Any> {
115115
if (viaductInstrumentation is WithBeginCompleteObject) {
116116
return viaductInstrumentation.beginCompleteObject(parameters, state)
117117
}
@@ -283,14 +283,14 @@ interface ViaductModernInstrumentation {
283283
fun beginFetchObject(
284284
parameters: InstrumentationExecutionStrategyParameters,
285285
state: InstrumentationState?
286-
): InstrumentationContext<Map<String, Any?>>?
286+
): InstrumentationContext<Unit>
287287
}
288288

289289
interface WithBeginFieldExecution : ViaductModernInstrumentation {
290290
fun beginFieldExecution(
291291
parameters: InstrumentationFieldParameters,
292292
state: InstrumentationState?
293-
): InstrumentationContext<Any>?
293+
): InstrumentationContext<Any>
294294
}
295295

296296
interface WithBeginFieldFetching : ViaductModernInstrumentation {
@@ -304,7 +304,7 @@ interface ViaductModernInstrumentation {
304304
fun beginCompleteObject(
305305
parameters: InstrumentationExecutionStrategyParameters,
306306
state: InstrumentationState?
307-
): InstrumentationContext<Any>?
307+
): InstrumentationContext<Any>
308308
}
309309

310310
interface WithBeginFieldCompletion : ViaductModernInstrumentation {
@@ -396,14 +396,14 @@ interface ViaductModernGJInstrumentation : Instrumentation {
396396
override fun beginFetchObject(
397397
parameters: InstrumentationExecutionStrategyParameters,
398398
state: InstrumentationState?
399-
): InstrumentationContext<Map<String, Any?>>? {
399+
): InstrumentationContext<Unit> {
400400
return noOp()
401401
}
402402

403403
override fun beginCompleteObject(
404404
parameters: InstrumentationExecutionStrategyParameters,
405405
state: InstrumentationState?
406-
): InstrumentationContext<Any>? {
406+
): InstrumentationContext<Any> {
407407
return noOp()
408408
}
409409

@@ -535,14 +535,14 @@ interface ViaductModernGJInstrumentation : Instrumentation {
535535
fun beginFetchObject(
536536
parameters: InstrumentationExecutionStrategyParameters,
537537
state: InstrumentationState?
538-
): InstrumentationContext<Map<String, Any?>>? {
538+
): InstrumentationContext<Unit> {
539539
return noOp()
540540
}
541541

542542
fun beginCompleteObject(
543543
parameters: InstrumentationExecutionStrategyParameters,
544544
state: InstrumentationState?
545-
): InstrumentationContext<Any>? {
545+
): InstrumentationContext<Any> {
546546
return noOp()
547547
}
548548

engine/api/src/test/kotlin/viaduct/engine/api/instrumentation/ChainedModernGJInstrumentationTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ class ChainedModernGJInstrumentationTest {
2929
val parameters = mockk<InstrumentationExecutionStrategyParameters>()
3030
val state1 = object : InstrumentationState {}
3131
val state2 = object : InstrumentationState {}
32-
val context1 = mockk<InstrumentationContext<Map<String, Any?>>>()
33-
val context2 = mockk<InstrumentationContext<Map<String, Any?>>>()
32+
val context1 = mockk<InstrumentationContext<Unit>>()
33+
val context2 = mockk<InstrumentationContext<Unit>>()
3434

3535
val instr1 = mockk<ViaductModernGJInstrumentation> {
3636
every { createStateAsync(any()) } returns CompletableFuture.completedFuture(state1)

engine/api/src/test/kotlin/viaduct/engine/api/instrumentation/ViaductInstrumentationAdapterTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,15 @@ class ViaductInstrumentationAdapterTest {
4040
override fun beginFetchObject(
4141
parameters: InstrumentationExecutionStrategyParameters,
4242
state: InstrumentationState?
43-
): InstrumentationContext<Map<String, Any?>>? {
43+
): InstrumentationContext<Unit> {
4444
beginFetchObjectCalled = true
4545
return noOp()
4646
}
4747

4848
override fun beginCompleteObject(
4949
parameters: InstrumentationExecutionStrategyParameters,
5050
state: InstrumentationState?
51-
): InstrumentationContext<Any>? {
51+
): InstrumentationContext<Any> {
5252
beginCompleteObjectCalled = true
5353
return noOp()
5454
}

engine/api/src/testFixtures/kotlin/viaduct/engine/api/mocks/FeatureTest.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package viaduct.engine.api.mocks
44

55
import graphql.ExecutionResult
6+
import graphql.execution.instrumentation.Instrumentation
67
import kotlinx.coroutines.future.await
78
import kotlinx.coroutines.runBlocking
89
import viaduct.engine.EngineConfiguration
@@ -63,10 +64,11 @@ import viaduct.service.runtime.noderesolvers.ViaductNodeResolverAPIBootstrapper
6364
fun MockTenantModuleBootstrapper.runFeatureTest(
6465
withoutDefaultQueryNodeResolvers: Boolean = false,
6566
schema: ViaductSchema? = null,
67+
additionalInstrumentation: Instrumentation? = null,
6668
block: FeatureTest.() -> Unit
6769
) {
6870
val executableSchema = schema ?: fullSchema
69-
val engine = toEngineFactory(withoutDefaultQueryNodeResolvers).create(executableSchema, fullSchema = fullSchema)
71+
val engine = toEngineFactory(withoutDefaultQueryNodeResolvers, additionalInstrumentation).create(executableSchema, fullSchema = fullSchema)
7072
FeatureTest(engine).block()
7173
}
7274

@@ -80,7 +82,10 @@ fun MockTenantModuleBootstrapper.runFeatureTest(
8082
*
8183
* and an [EngineConfiguration] constructed with MockFlagManager.Enabled.
8284
*/
83-
private fun MockTenantModuleBootstrapper.toEngineFactory(withoutDefaultQueryNodeResolvers: Boolean): EngineFactory {
85+
private fun MockTenantModuleBootstrapper.toEngineFactory(
86+
withoutDefaultQueryNodeResolvers: Boolean,
87+
additionalInstrumentation: Instrumentation? = null
88+
): EngineFactory {
8489
val mods = listOf(this)
8590
val tenantAPIBootstrapper = buildList {
8691
add(MockTenantAPIBootstrapperBuilder(MockTenantAPIBootstrapper(mods)))
@@ -101,6 +106,8 @@ private fun MockTenantModuleBootstrapper.toEngineFactory(withoutDefaultQueryNode
101106
).create(fullSchema)
102107
val config = EngineConfiguration.default.copy(
103108
flagManager = MockFlagManager.Enabled,
109+
chainInstrumentationWithDefaults = true,
110+
additionalInstrumentation = additionalInstrumentation
104111
)
105112
return EngineFactory(config, dispatcherRegistry)
106113
}

engine/runtime/src/main/kotlin/viaduct/engine/runtime/execution/FieldResolver.kt

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,22 @@ class FieldResolver(
9191
* 1. Runs CollectFields on the current uncollected selection set
9292
* 2. Fires off field fetches for each merged object selection, in parallel
9393
*
94+
* Note on return value: This method returns `Value<Unit>` instead of `Value<Map<String, FieldResolutionResult>>`
95+
* because the actual resolved values are stored directly in the `ObjectEngineResult` associated with
96+
* the parent object. The `Value<Unit>` serves as a completion signal for the orchestration layer to
97+
* know when all nested fetching (including any lazy data or nested objects) has finished.
98+
*
99+
* If the Value returned by this method is exceptionally completed, that means that there has been
100+
* a fatal error in resolving this object, and the parent ObjectEngineResult may be incomplete. Thus, callers of this
101+
* method should check for exceptional completion and handle it appropriately.
102+
*
94103
* @param parameters ExecutionParameters containing the execution context and selection set
95104
* @throws Exception Only if there's a fatal error in the supervisorScope itself
96105
*/
97106
fun fetchObject(
98107
objectType: GraphQLObjectType,
99108
parameters: ExecutionParameters
100-
) {
109+
): Value<Unit> {
101110
val instrumentationParameters =
102111
InstrumentationExecutionStrategyParameters(parameters.executionContext, parameters.gjParameters)
103112
val resolveObjectCtx = nonNullCtx(
@@ -110,17 +119,20 @@ class FieldResolver(
110119
try {
111120
val results = collectFields(objectType, parameters)
112121
.selections
113-
.associate { field ->
122+
.map { field ->
114123
field as QueryPlan.CollectedField
115124
val newParams = parameters.forField(objectType, field)
116-
field.responseKey to resolveField(newParams, field)
125+
resolveField(newParams, field)
117126
}
118127

119128
// Wait for all values to be completed.
120-
// Errors will be folded into the result map
121-
Value.waitAll(results.values)
122-
.thenApply { _, t ->
123-
resolveObjectCtx.onCompleted(results, t)
129+
return Value.waitAll(results)
130+
.map {
131+
resolveObjectCtx.onCompleted(Unit, null)
132+
it
133+
}.recover { t ->
134+
resolveObjectCtx.onCompleted(null, t)
135+
Value.fromThrowable(t)
124136
}
125137
} catch (e: Exception) {
126138
resolveObjectCtx.onCompleted(null, e)
@@ -137,13 +149,22 @@ class FieldResolver(
137149
* initiated when the previous selection has completed fetching (either
138150
* successfully or exceptionally)
139151
*
152+
* Note on return value: This method returns `Value<Unit>` instead of `Value<Map<String, FieldResolutionResult>>`
153+
* because the actual resolved values are stored directly in the `ObjectEngineResult` associated with
154+
* the parent object. The `Value<Unit>` serves as a completion signal for the orchestration layer to
155+
* know when all nested fetching (including any lazy data or nested objects) has finished.
156+
*
157+
* If the Value returned by this method is exceptionally completed, that means that there has been
158+
* a fatal error in resolving this object, and the parent ObjectEngineResult may be incomplete. Thus, callers of this
159+
* method should check for exceptional completion and handle it appropriately.
160+
*
140161
* @param parameters ExecutionParameters containing the execution context and selection set
141162
* @throws Exception Only if there's a fatal error in the supervisorScope itself
142163
*/
143164
fun fetchObjectSerially(
144165
objectType: GraphQLObjectType,
145166
parameters: ExecutionParameters
146-
) {
167+
): Value<Unit> {
147168
val instrumentationParameters =
148169
InstrumentationExecutionStrategyParameters(parameters.executionContext, parameters.gjParameters)
149170
val resolveObjectCtx = nonNullCtx(
@@ -156,24 +177,21 @@ class FieldResolver(
156177
try {
157178
val fields = collectFields(objectType, parameters).selections
158179
val initial: Value<Unit> = Value.fromValue(Unit)
159-
val results = mutableMapOf<String, Value<FieldResolutionResult>>()
160180

161181
// iterate over each field to build a chained execution
162182
// Each field will kick off only after the previous one completes
163-
fields.fold(initial) { acc, field ->
183+
return fields.fold(initial) { acc, field ->
164184
field as QueryPlan.CollectedField
165185
acc.flatMap { _ ->
166186
val fieldParameters = parameters.forField(objectType, field)
167-
val value = resolveField(fieldParameters, field)
168-
results.put(field.responseKey, value)
169-
// ignore any errors thrown by the field resolver so that the chained execution can continue
170-
// these field errors will be surfaced during field completion
171-
value.thenApply { _, _ ->
172-
Unit
173-
}
187+
resolveField(fieldParameters, field)
174188
}
175-
}.thenApply { _, t ->
176-
resolveObjectCtx.onCompleted(results, t)
189+
}.map {
190+
resolveObjectCtx.onCompleted(Unit, null)
191+
it
192+
}.recover { t ->
193+
resolveObjectCtx.onCompleted(null, t)
194+
Value.fromThrowable(t)
177195
}
178196
} catch (e: Exception) {
179197
resolveObjectCtx.onCompleted(null, e)
@@ -196,7 +214,7 @@ class FieldResolver(
196214
fun resolveField(
197215
parameters: ExecutionParameters,
198216
field: QueryPlan.CollectedField
199-
): Value<FieldResolutionResult> {
217+
): Value<Unit> {
200218
field.childPlans.forEach { launchQueryPlan(parameters, it) }
201219
return executeField(parameters)
202220
}
@@ -245,7 +263,7 @@ class FieldResolver(
245263
* @param parameters The execution parameters containing field and context information
246264
*/
247265
@Suppress("UNCHECKED_CAST")
248-
private fun executeField(parameters: ExecutionParameters): Value<FieldResolutionResult> {
266+
private fun executeField(parameters: ExecutionParameters): Value<Unit> {
249267
val field = checkNotNull(parameters.field) { "Expected field to be non-null." }
250268

251269
// We're fetching an individual field; the current engine result will always be an ObjectEngineResult
@@ -303,8 +321,12 @@ class FieldResolver(
303321
return fieldResolutionResultValue.thenCompose { v, e ->
304322
fieldInstrumentationCtx.onCompleted(v, e)
305323
if (e != null) {
306-
Value.fromThrowable(e)
324+
// if the field resolution failed, don't attempt to fetch lazy data or nested objects
325+
// and mark this field as completed
326+
Value.fromValue(Unit)
307327
} else {
328+
// otherwise, proceed with lazy data fetching and nested object resolution
329+
308330
// if the result contains lazy data, begin fetching it
309331
maybeFetchLazyData(
310332
v!!,
@@ -321,7 +343,6 @@ class FieldResolver(
321343
executionStepInfo = executionStepInfoForField,
322344
)
323345
)
324-
Value.fromValue(v)
325346
}
326347
}
327348
}
@@ -497,6 +518,7 @@ class FieldResolver(
497518
val result = rawValue.getOrThrow()
498519
result as? FieldResolutionResult ?: throw IllegalStateException("Expected FieldResolutionResult but got ${result!!::class}")
499520
}
521+
500522
else -> throw IllegalStateException(
501523
"Expected the raw value slot to contain a Value.Sync<FieldResolutionResult>, but got ${rawValue::class}"
502524
)
@@ -522,23 +544,25 @@ class FieldResolver(
522544
outputType: GraphQLOutputType,
523545
field: QueryPlan.CollectedField,
524546
parameters: ExecutionParameters,
525-
) {
547+
): Value<Unit> {
526548
// if engineResult is null, then there is no nested object to fetch and we can return early
527-
if (fieldResolutionResult.engineResult == null) return
528-
when (outputType) {
549+
if (fieldResolutionResult.engineResult == null) return Value.fromValue(Unit)
550+
return when (outputType) {
529551
is GraphQLNonNull -> maybeFetchNestedObject(fieldResolutionResult, GraphQLTypeUtil.unwrapOneAs(outputType), field, parameters)
530552
is GraphQLList -> {
531553
val engineResult = checkNotNull(fieldResolutionResult.engineResult as? Iterable<*>) { "Expected iterable engineResult but got ${fieldResolutionResult.engineResult}" }
532-
engineResult.forEachIndexed { i, item ->
554+
val values = engineResult.mapIndexed { i, item ->
533555
check(item is Cell) { "Expected engine result to be a Cell." }
534556
val frr = extractFieldResolutionResult(item)
535557
val newParams = updateListItemParameters(parameters, i)
536558
maybeFetchNestedObject(frr, GraphQLTypeUtil.unwrapOneAs(outputType), field, newParams)
537559
}
560+
Value.waitAll(values)
538561
}
562+
539563
else -> {
540564
// if engineResult is a scalar or simple value, then no nesting is possible and we can return
541-
val oer = fieldResolutionResult.engineResult as? ObjectEngineResultImpl ?: return
565+
val oer = fieldResolutionResult.engineResult as? ObjectEngineResultImpl ?: return Value.fromValue(Unit)
542566
fetchObject(
543567
oer.graphQLObjectType,
544568
parameters.forObjectTraversal(field, oer, fieldResolutionResult.localContext, fieldResolutionResult.originalSource, fieldResolutionResult.resolutionPolicy)

0 commit comments

Comments
 (0)