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

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions src/main/java/graphql/execution/ExecutionStrategy.java
Original file line number Diff line number Diff line change
Expand Up @@ -489,12 +489,17 @@ private Object fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext exec

CompletableFuture<CompletableFuture<Object>> handleCF = engineRunningState.handle(fetchedValue, (result, exception) -> {
// because we added an artificial CF, we need to unwrap the exception
fetchCtx.onCompleted(result, exception);
exception = engineRunningState.possibleCancellation(exception);

if (exception != null) {
return handleFetchingException(dataFetchingEnvironment.get(), parameters, exception);
Throwable possibleWrappedException = engineRunningState.possibleCancellation(exception);

if (possibleWrappedException != null) {
CompletableFuture<DataFetcherResult<Object>> handledExceptionResult = handleFetchingException(dataFetchingEnvironment.get(), parameters, possibleWrappedException);
return handledExceptionResult.thenApply( handledResult -> {
fetchCtx.onExceptionHandled(handledResult);
fetchCtx.onCompleted(result, exception);
return handledResult;
});
} else {
fetchCtx.onCompleted(result, exception);
// we can simply return the fetched value CF and avoid a allocation
return fetchedValue;
}
Expand Down Expand Up @@ -578,7 +583,7 @@ private void addExtensionsIfPresent(ExecutionContext executionContext, DataFetch
}
}

protected <T> CompletableFuture<T> handleFetchingException(
protected <T> CompletableFuture<DataFetcherResult<T>> handleFetchingException(
DataFetchingEnvironment environment,
ExecutionStrategyParameters parameters,
Throwable e
Expand All @@ -599,10 +604,10 @@ protected <T> CompletableFuture<T> handleFetchingException(
}
}

private <T> CompletableFuture<T> asyncHandleException(DataFetcherExceptionHandler handler, DataFetcherExceptionHandlerParameters handlerParameters) {
private <T> CompletableFuture<DataFetcherResult<T>> asyncHandleException(DataFetcherExceptionHandler handler, DataFetcherExceptionHandlerParameters handlerParameters) {
//noinspection unchecked
return handler.handleException(handlerParameters).thenApply(
handlerResult -> (T) DataFetcherResult.newResult().errors(handlerResult.getErrors()).build()
handlerResult -> (DataFetcherResult<T>) DataFetcherResult.newResult().errors(handlerResult.getErrors()).build()
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import graphql.Internal;
import graphql.PublicSpi;
import graphql.execution.DataFetcherResult;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
Expand All @@ -26,6 +27,15 @@ public interface FieldFetchingInstrumentationContext extends InstrumentationCont
default void onFetchedValue(Object fetchedValue) {
}

/**
* This is called back after any {@link graphql.execution.DataFetcherExceptionHandler}) has run on any exception raised
* during a {@link graphql.schema.DataFetcher} invocation. This allows to see the final {@link DataFetcherResult}
* that will be used when performing the complete step.
* @param dataFetcherResult the final {@link DataFetcherResult} after the exception handler has run
*/
default void onExceptionHandled(DataFetcherResult<Object> dataFetcherResult) {
}

@Internal
FieldFetchingInstrumentationContext NOOP = new FieldFetchingInstrumentationContext() {
@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package graphql.execution.instrumentation

import graphql.ErrorType
import graphql.ExecutionInput
import graphql.ExecutionResult
import graphql.GraphQL
import graphql.GraphqlErrorBuilder
import graphql.GraphqlErrorBuilderTest
import graphql.StarWarsSchema
import graphql.TestUtil
import graphql.execution.AsyncExecutionStrategy
import graphql.execution.DataFetcherExceptionHandlerResult
import graphql.execution.DataFetcherResult
import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters
import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters
Expand All @@ -23,6 +28,8 @@ import spock.lang.Specification
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong

class InstrumentationTest extends Specification {

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

def "field fetch will instrument exceptions correctly - includes exception handling with onExceptionHandled"() {

given:

def query = """
{
hero {
id
}
}
"""

def instrumentation = new LegacyTestingInstrumentation() {
def onHandledCalled = false
def onCompletedCalled = false
def onDispatchedCalled = false

@Override
DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
return new DataFetcher<Object>() {
@Override
Object get(DataFetchingEnvironment environment) {
throw new RuntimeException("DF BANG!")
}
}
}

@Override
FieldFetchingInstrumentationContext beginFieldFetching(InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
return new FieldFetchingInstrumentationContext() {
@Override
void onDispatched() {
onDispatchedCalled = true
}

@Override
void onCompleted(Object result, Throwable t) {
onCompletedCalled = true
}

@Override
void onExceptionHandled(DataFetcherResult<Object> dataFetcherResult) {
onHandledCalled = true
}
}
}
}

def graphQL = GraphQL
.newGraphQL(StarWarsSchema.starWarsSchema)
.defaultDataFetcherExceptionHandler { it ->
// catch all exceptions and transform to graphql error with a prefixed message
return CompletableFuture.completedFuture(
DataFetcherExceptionHandlerResult.newResult(GraphqlErrorBuilder.newError()
.errorType(ErrorType.DataFetchingException)
.message("Handled " + it.exception.message)
.path(it.path)
.build())
.build())
}
.instrumentation(instrumentation)
.build()

when:
def resp = graphQL.execute(query)

then: "exception handler turned the exception into a graphql error and message prefixed with Handled"
resp.errors.size() == 1
resp.errors[0].message == "Handled DF BANG!"

and: "all instrumentation methods were called"
instrumentation.onDispatchedCalled == true
instrumentation.onCompletedCalled == true
instrumentation.onHandledCalled == true
}


def "field fetch verify order and call of all methods"() {

given:

def query = """
{
hero {
id
}
}
"""

def metric = []
def instrumentation = new SimplePerformantInstrumentation() {
def timeElapsed = new AtomicInteger()

@Override
DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
return new DataFetcher<Object>() {
@Override
Object get(DataFetchingEnvironment environment) {
// simulate latency
timeElapsed.addAndGet(50)
throw new RuntimeException("DF BANG!")
}
}
}

@Override
FieldFetchingInstrumentationContext beginFieldFetching(InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
return new FieldFetchingInstrumentationContext() {
def start = 0
def duration = 0
def hasError = false


@Override
void onDispatched() {
start = 1
}

@Override
void onCompleted(Object result, Throwable t) {
duration = timeElapsed.get() - start
metric = [duration, hasError]
}

@Override
void onExceptionHandled(DataFetcherResult<Object> dataFetcherResult) {
hasError = dataFetcherResult.errors != null && !dataFetcherResult.errors.isEmpty()
&& dataFetcherResult.errors.any { it.message.contains("Handled") }
}
}
}
}

def graphQL = GraphQL
.newGraphQL(StarWarsSchema.starWarsSchema)
.defaultDataFetcherExceptionHandler { it ->
// catch all exceptions and transform to graphql error with a prefixed message
return CompletableFuture.completedFuture(
DataFetcherExceptionHandlerResult.newResult(GraphqlErrorBuilder.newError()
.errorType(ErrorType.DataFetchingException)
.message("Handled " + it.exception.message)
.path(it.path)
.build())
.build())
}
.instrumentation(instrumentation)
.build()

when:
def resp = graphQL.execute(query)

then: "exception handler turned the exception into a graphql error and prefixed its message with 'Handled'"
resp.errors.size() == 1
resp.errors[0].message == "Handled DF BANG!"

and: "metric was captured i.e all instrumentation methods were called in the right order"
metric == [49, true]
}

/**
* This uses a stop and go pattern and multiple threads. Each time
* the execution strategy is invoked, the data fetchers are held
Expand Down
Loading