-
Notifications
You must be signed in to change notification settings - Fork 348
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
1965: make FunctionDataFetcher
handle blocking scopes resulting in a thrown Error
as expected
#1966
Conversation
…leFuture` This makes the result where `runBlockingFunction` ends in a thrown `Error` (or descendant of `Error`) be handled as expected. Also for consistency makes `runBlockingFunction` wrap the result in a `CompletableFuture` even for the successful case. Lastly improves grammar a bit adding some commas where needed.
… return `CompletableFuture`
@@ -49,7 +49,7 @@ open class FunctionDataFetcher( | |||
/** | |||
* Invoke a suspend function or blocking function, passing in the [target] if not null or default to using the source from the environment. | |||
*/ | |||
override fun get(environment: DataFetchingEnvironment): Any? { | |||
override fun get(environment: DataFetchingEnvironment): CompletableFuture<Any?> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i believe that this change is going to cause incompatiblity with the update to graphql-java 22
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you help me understand more specifically what you mean so that maybe I can try to adjust my solution accordingly?
Otherwise, I'd say I'm happy to remove the change to this get
function signature as it's not required for my solution to work. Would that be an okay compromise?
*I mainly made the change as it made me have to make less changes to the Test class; where without this change I'll have to cast to CompletableFuturw
repeatedly. This felt like an efficiency and type-safety boon.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TLDR; graphql-java 22 introduces polymorphic resolution, in previous versions, even if your data fetcher was returning a materialized object (not a CF), it was anyways creating a CF causing a terrible memory bottleneck where GC pause times were paying the price.
having said that, i wonder, why would you want to catch Error
s ?
screenshot taken from Error
docs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TLDR; graphql-java 22 introduces polymorphic resolution, in previous versions, even if your data fetcher was returning a materialized object (not a CF), it was anyways creating a CF causing a terrible memory bottleneck where GC pause times were paying the price.
😅 I need to read up on this, as tbh that is pretty far out of my depth of current expertise lol. So I'm inclined to defer to your judgement and undo this part of my change regardless of where the larger change lands as accepted or not.
having said that, i wonder, why would you want to catch Errors ?
screenshot taken from Error docs
In regards to this, please see my thoughts/discussion here #1966 (comment) if you would.
} catch (exception: InvocationTargetException) { | ||
throw exception.cause ?: exception | ||
} | ||
protected open fun runBlockingFunction(parameterValues: Map<KParameter, Any?>): CompletableFuture<Any?> = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will cause an allocation of an object of type CompletableFuture
for fields that are already materialized (in-memory).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made the CompletableFuture
wrap the success-case for consistency, but could also peel that back as it's really only the throw ...
case that is the problem and inconsistent with the equivalent behavior for suspend
resolvers.
Shall I do that?
what if instead you handle your exceptions in your data fetchers ?, that way, you will have a more resilient way to recover from an error state. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.
throw exception.cause ?: exception | ||
} | ||
protected open fun runBlockingFunction(parameterValues: Map<KParameter, Any?>): CompletableFuture<Any?> = | ||
CompletableFuture<Any?>().apply { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as above, this change is going to cause incompatibility with the update to graphql-java 22
If by "... exceptions ..." you mean thrown More broadly though, I'm trying to point out an inconsistency between the way For and for non- So while yes my original Issue/PR are written as trying to make non- TLDR do you want me to adjust this work so that *you'll have to pardon my naivety regarding |
yes, that sounds good, we would appreciate that contribution |
Instead of changing the data fetcher I think we should probably just catch the error here -> https://github.com/ExpediaGroup/graphql-kotlin/blob/master/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/execution/GraphQLRequestHandler.kt#L99-L137? |
Sweet @samuelAndalon! happy to help. Will try to get to that sometime in the next few days...except now I'm a bit confused by @dariuszkuc's suggestion:
Are you saying to make the change I originally proposed, to make non- This would be the former: private suspend fun execute(
graphQLRequest: GraphQLRequest,
batchGraphQLContext: GraphQLContext,
dataLoaderRegistry: KotlinDataLoaderRegistry?,
): GraphQLResponse<*> =
try {
graphQL.executeAsync(
graphQLRequest.toExecutionInput(batchGraphQLContext, dataLoaderRegistry),
).await().toGraphQLResponse()
} catch (throwable: Throwable) {
// Catch any Throwable, Error or Exception, and convert it to a GraphQLError
val error = throwable.toGraphQLError()
GraphQLResponse<Any?>(errors = listOf(error.toGraphQLKotlinType()))
} and this is the latter: private suspend fun execute(
graphQLRequest: GraphQLRequest,
batchGraphQLContext: GraphQLContext,
dataLoaderRegistry: KotlinDataLoaderRegistry?,
): GraphQLResponse<*> =
try {
val result = graphQL.executeAsync(
graphQLRequest.toExecutionInput(batchGraphQLContext, dataLoaderRegistry),
).await()
// Look for Error (or any descendant) and re-throw so it's not swallowed
result.errors?.forEach {
if (it is ExceptionWhileDataFetching && it.exception is Error) {
throw it.exception
}
}
result.toGraphQLResponse()
} catch (exception: Exception) {
val error = exception.toGraphQLError()
GraphQLResponse<Any?>(errors = listOf(error.toGraphQLKotlinType()))
} Either way, I think I see why you're steering me away from |
i understand both approaches and i see reasons to follow one or the other, however, IMHO, we should not attempt to catch |
Since |
@samuelAndalon and @dariuszkuc I appreciate the thoughtful discussion from both of you, but must admit I am not clear on the desired direction. For now I am going to defer to @dariuszkuc's suggestion, as he seems to be the primary repository maintainer, and do what I originally intended "modify handling for non- private suspend fun execute(
graphQLRequest: GraphQLRequest,
batchGraphQLContext: GraphQLContext,
dataLoaderRegistry: KotlinDataLoaderRegistry?,
): GraphQLResponse<*> =
try {
graphQL.executeAsync(
graphQLRequest.toExecutionInput(batchGraphQLContext, dataLoaderRegistry),
).await().toGraphQLResponse()
} catch (throwable: Throwable) {
// Catch any Throwable, Error or Exception, and convert it to a GraphQLError
val error = throwable.toGraphQLError()
GraphQLResponse<Any?>(errors = listOf(error.toGraphQLKotlinType()))
} If that's not right, or I'm somehow missing the mark don't hesitate to let me know. *In the meantime I remain open to discussion, and will certainly not be rushing this through! |
As Sam mentioned, when So the TLDR is -> we probably shouldn't change anything unless there is a good way to propagate errors out of the completable futures/errors. |
Understood! I will look into this "unless there is a good way to propagate errors out of the completable futures/errors." bit sometime this week. From the digging I was doing earlier I don't think it is possible, as I'm sure you may already know. However it sounds like a fun challenge, so I'll spend some time going after it. Thanks again for your time and energy talking this through with me! *I'll cleanup and close the PR/Issue with non-resolveable outcome/commentary later this week to keep the repo clean too |
Closing this for now as non-resolvable |
📝 Description
Changes
FunctionDataFetcher
to always return aCompletableFuture
(no matter if scope issuspend
or not) in order to cleanly handle cases whererunBlockingFunction
results in a thrownError
(or descendant ofError
). See #1965 for more detail.🔗 Related Issues
Resolves #1965
*this is my first contribution to this repo; so if I need to do anything differently or in addition please let me know