Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c465601

Browse files
authored
Merge pull request #4206 from tinnou/field-fetching-instrumentation
Proposal for an additional instrumentation hook point
2 parents 5a4e1dd + 01ace23 commit c465601

File tree

3 files changed

+189
-8
lines changed

3 files changed

+189
-8
lines changed

src/main/java/graphql/execution/ExecutionStrategy.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -489,12 +489,17 @@ private Object fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext exec
489489

490490
CompletableFuture<CompletableFuture<Object>> handleCF = engineRunningState.handle(fetchedValue, (result, exception) -> {
491491
// because we added an artificial CF, we need to unwrap the exception
492-
fetchCtx.onCompleted(result, exception);
493-
exception = engineRunningState.possibleCancellation(exception);
494-
495-
if (exception != null) {
496-
return handleFetchingException(dataFetchingEnvironment.get(), parameters, exception);
492+
Throwable possibleWrappedException = engineRunningState.possibleCancellation(exception);
493+
494+
if (possibleWrappedException != null) {
495+
CompletableFuture<DataFetcherResult<Object>> handledExceptionResult = handleFetchingException(dataFetchingEnvironment.get(), parameters, possibleWrappedException);
496+
return handledExceptionResult.thenApply( handledResult -> {
497+
fetchCtx.onExceptionHandled(handledResult);
498+
fetchCtx.onCompleted(result, exception);
499+
return handledResult;
500+
});
497501
} else {
502+
fetchCtx.onCompleted(result, exception);
498503
// we can simply return the fetched value CF and avoid a allocation
499504
return fetchedValue;
500505
}
@@ -578,7 +583,7 @@ private void addExtensionsIfPresent(ExecutionContext executionContext, DataFetch
578583
}
579584
}
580585

581-
protected <T> CompletableFuture<T> handleFetchingException(
586+
protected <T> CompletableFuture<DataFetcherResult<T>> handleFetchingException(
582587
DataFetchingEnvironment environment,
583588
ExecutionStrategyParameters parameters,
584589
Throwable e
@@ -599,10 +604,10 @@ protected <T> CompletableFuture<T> handleFetchingException(
599604
}
600605
}
601606

602-
private <T> CompletableFuture<T> asyncHandleException(DataFetcherExceptionHandler handler, DataFetcherExceptionHandlerParameters handlerParameters) {
607+
private <T> CompletableFuture<DataFetcherResult<T>> asyncHandleException(DataFetcherExceptionHandler handler, DataFetcherExceptionHandlerParameters handlerParameters) {
603608
//noinspection unchecked
604609
return handler.handleException(handlerParameters).thenApply(
605-
handlerResult -> (T) DataFetcherResult.newResult().errors(handlerResult.getErrors()).build()
610+
handlerResult -> (DataFetcherResult<T>) DataFetcherResult.newResult().errors(handlerResult.getErrors()).build()
606611
);
607612
}
608613

src/main/java/graphql/execution/instrumentation/FieldFetchingInstrumentationContext.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import graphql.Internal;
44
import graphql.PublicSpi;
5+
import graphql.execution.DataFetcherResult;
56
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
67
import org.jspecify.annotations.NonNull;
78
import org.jspecify.annotations.Nullable;
@@ -26,6 +27,15 @@ public interface FieldFetchingInstrumentationContext extends InstrumentationCont
2627
default void onFetchedValue(Object fetchedValue) {
2728
}
2829

30+
/**
31+
* This is called back after any {@link graphql.execution.DataFetcherExceptionHandler}) has run on any exception raised
32+
* during a {@link graphql.schema.DataFetcher} invocation. This allows to see the final {@link DataFetcherResult}
33+
* that will be used when performing the complete step.
34+
* @param dataFetcherResult the final {@link DataFetcherResult} after the exception handler has run
35+
*/
36+
default void onExceptionHandled(DataFetcherResult<Object> dataFetcherResult) {
37+
}
38+
2939
@Internal
3040
FieldFetchingInstrumentationContext NOOP = new FieldFetchingInstrumentationContext() {
3141
@Override

src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package graphql.execution.instrumentation
22

3+
import graphql.ErrorType
34
import graphql.ExecutionInput
45
import graphql.ExecutionResult
56
import graphql.GraphQL
7+
import graphql.GraphqlErrorBuilder
8+
import graphql.GraphqlErrorBuilderTest
69
import graphql.StarWarsSchema
710
import graphql.TestUtil
811
import graphql.execution.AsyncExecutionStrategy
12+
import graphql.execution.DataFetcherExceptionHandlerResult
13+
import graphql.execution.DataFetcherResult
914
import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters
1015
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters
1116
import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters
@@ -23,6 +28,8 @@ import spock.lang.Specification
2328
import java.util.concurrent.CompletableFuture
2429
import java.util.concurrent.TimeUnit
2530
import java.util.concurrent.atomic.AtomicBoolean
31+
import java.util.concurrent.atomic.AtomicInteger
32+
import java.util.concurrent.atomic.AtomicLong
2633

2734
class InstrumentationTest extends Specification {
2835

@@ -152,6 +159,165 @@ class InstrumentationTest extends Specification {
152159
instrumentation.throwableList[0].getMessage() == "DF BANG!"
153160
}
154161

162+
def "field fetch will instrument exceptions correctly - includes exception handling with onExceptionHandled"() {
163+
164+
given:
165+
166+
def query = """
167+
{
168+
hero {
169+
id
170+
}
171+
}
172+
"""
173+
174+
def instrumentation = new LegacyTestingInstrumentation() {
175+
def onHandledCalled = false
176+
def onCompletedCalled = false
177+
def onDispatchedCalled = false
178+
179+
@Override
180+
DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
181+
return new DataFetcher<Object>() {
182+
@Override
183+
Object get(DataFetchingEnvironment environment) {
184+
throw new RuntimeException("DF BANG!")
185+
}
186+
}
187+
}
188+
189+
@Override
190+
FieldFetchingInstrumentationContext beginFieldFetching(InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
191+
return new FieldFetchingInstrumentationContext() {
192+
@Override
193+
void onDispatched() {
194+
onDispatchedCalled = true
195+
}
196+
197+
@Override
198+
void onCompleted(Object result, Throwable t) {
199+
onCompletedCalled = true
200+
}
201+
202+
@Override
203+
void onExceptionHandled(DataFetcherResult<Object> dataFetcherResult) {
204+
onHandledCalled = true
205+
}
206+
}
207+
}
208+
}
209+
210+
def graphQL = GraphQL
211+
.newGraphQL(StarWarsSchema.starWarsSchema)
212+
.defaultDataFetcherExceptionHandler { it ->
213+
// catch all exceptions and transform to graphql error with a prefixed message
214+
return CompletableFuture.completedFuture(
215+
DataFetcherExceptionHandlerResult.newResult(GraphqlErrorBuilder.newError()
216+
.errorType(ErrorType.DataFetchingException)
217+
.message("Handled " + it.exception.message)
218+
.path(it.path)
219+
.build())
220+
.build())
221+
}
222+
.instrumentation(instrumentation)
223+
.build()
224+
225+
when:
226+
def resp = graphQL.execute(query)
227+
228+
then: "exception handler turned the exception into a graphql error and message prefixed with Handled"
229+
resp.errors.size() == 1
230+
resp.errors[0].message == "Handled DF BANG!"
231+
232+
and: "all instrumentation methods were called"
233+
instrumentation.onDispatchedCalled == true
234+
instrumentation.onCompletedCalled == true
235+
instrumentation.onHandledCalled == true
236+
}
237+
238+
239+
def "field fetch verify order and call of all methods"() {
240+
241+
given:
242+
243+
def query = """
244+
{
245+
hero {
246+
id
247+
}
248+
}
249+
"""
250+
251+
def metric = []
252+
def instrumentation = new SimplePerformantInstrumentation() {
253+
def timeElapsed = new AtomicInteger()
254+
255+
@Override
256+
DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
257+
return new DataFetcher<Object>() {
258+
@Override
259+
Object get(DataFetchingEnvironment environment) {
260+
// simulate latency
261+
timeElapsed.addAndGet(50)
262+
throw new RuntimeException("DF BANG!")
263+
}
264+
}
265+
}
266+
267+
@Override
268+
FieldFetchingInstrumentationContext beginFieldFetching(InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
269+
return new FieldFetchingInstrumentationContext() {
270+
def start = 0
271+
def duration = 0
272+
def hasError = false
273+
274+
275+
@Override
276+
void onDispatched() {
277+
start = 1
278+
}
279+
280+
@Override
281+
void onCompleted(Object result, Throwable t) {
282+
duration = timeElapsed.get() - start
283+
metric = [duration, hasError]
284+
}
285+
286+
@Override
287+
void onExceptionHandled(DataFetcherResult<Object> dataFetcherResult) {
288+
hasError = dataFetcherResult.errors != null && !dataFetcherResult.errors.isEmpty()
289+
&& dataFetcherResult.errors.any { it.message.contains("Handled") }
290+
}
291+
}
292+
}
293+
}
294+
295+
def graphQL = GraphQL
296+
.newGraphQL(StarWarsSchema.starWarsSchema)
297+
.defaultDataFetcherExceptionHandler { it ->
298+
// catch all exceptions and transform to graphql error with a prefixed message
299+
return CompletableFuture.completedFuture(
300+
DataFetcherExceptionHandlerResult.newResult(GraphqlErrorBuilder.newError()
301+
.errorType(ErrorType.DataFetchingException)
302+
.message("Handled " + it.exception.message)
303+
.path(it.path)
304+
.build())
305+
.build())
306+
}
307+
.instrumentation(instrumentation)
308+
.build()
309+
310+
when:
311+
def resp = graphQL.execute(query)
312+
313+
then: "exception handler turned the exception into a graphql error and prefixed its message with 'Handled'"
314+
resp.errors.size() == 1
315+
resp.errors[0].message == "Handled DF BANG!"
316+
317+
and: "metric was captured i.e all instrumentation methods were called in the right order"
318+
metric == [49, true]
319+
}
320+
155321
/**
156322
* This uses a stop and go pattern and multiple threads. Each time
157323
* the execution strategy is invoked, the data fetchers are held

0 commit comments

Comments
 (0)