Skip to content

Conversation

@rwinch
Copy link

@rwinch rwinch commented May 19, 2021

No description provided.

rwinch added 2 commits May 19, 2021 14:58
Create DelegatingDataFetcherExceptionHandler which accepts a mapping of
Predicate to DataFetcherExceptionHandler. The corresponding
DataFetcherHandler of the first Predicate to match is used. If no match is
found, then the default handler is used.
Allows easily creating a DelegatingDataFetcherExceptionHandler mapping from
the type of Exception that was thrown.
*/
public void setDefaultHandler(DataFetcherExceptionHandler defaultHandler) {
this.defaultHandler = assertNotNull(defaultHandler, () -> "defaultHandler can't be null");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dont make this mutable - use a builder pattern or have constructors for it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've pushed an update to use a Builder to ensure that DelegatingDataFetcherExceptionHandler is immutable.

*
* @return the {@link DelegatingDataFetcherExceptionHandler} to use
*/
public static DelegatingDataFetcherExceptionHandler fromThrowableTypeMapping(LinkedHashMap<Class<? extends Throwable>, DataFetcherExceptionHandler> types) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A specific implementation in parameters is strange

LinkedHashMap - It should be just Map

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get that it wants to be in order - but I think the caller should decide not

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Order matters and LinkedHashMap is the only Map implementation that preserves order.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've pushed an update to use a Builder which encapsulates how the mapping and order are preserved. Internally a List<PredicateDelegateMapping> is now used such that PredicateDelegateMapping is a private class with the members Predicate<Throwable> and DataFetcherExceptionHandler.

return new DelegatingDataFetcherExceptionHandler(handlers);
}

private static <M extends Map<K, V>, K, V> M assertNotEmpty(M map, String message) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should go in the graphql java Assert class AND use a Supplier for the message - see that code

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the code with a commit that moves this to the Assert class as described. However, with the introduction of the Builder which encapsulates the mapping in List<PredicateDelegateMapping> there is no need for it, so I removed it in an additional commit.


then: 'fail assert on validation'
thrown AssertException
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find these types of test low value - asserts are important BUT not the key "regression" point in code base.

Copy link
Author

@rwinch rwinch May 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want me to remove all the tests or just this one or...?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed all tests in DelegatingDataFetcherExceptionHandlerTest that are asserting AssertException was thrown

def actualResult = handler.onException(handlerParameters)

then: 'only the matching delegate is invoked'
0 * _
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure this is testing anything??

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think then needs updated to be none of the delegates are invoked. This is verify that if none of the delegates match, then none of them are used (the default should be used).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated then description to be accurate.

rwinch added 7 commits May 20, 2021 09:17
Previously then was incorrectly stated "only the matching delegate is
invoked". However, the assertion correctly validated that "none of the
delegates are invoked". The then description is now corrected.
Per the feedback in the code review these tests don't test
`key "regression" point in code base`.
Per the review feedback it is preferred to keep this in Assert class
rather than in a private method to DelegatingDataFetcherExceptionHandler.
Previously assertNotEmpty does not have a return type that preserves the
type passed in. This commit returns the same type as what was asserted.
This allows assertion of a List and ensuring the assigning type can
still be declared as a List.
This addresses two concerns:

The first is the use of LinkedHashMap for the delegates. LinkedHashMap was
chosen intentionally because the order matters. However, it is strange to
rely on an implementation. The Builder now encapsulates how the mapping
is done (using a List and private class named PredicateDelegateMapping).

The second concern it addresses is to ensure
DelegatingDataFetcherExceptionHandler is immutable allowing defaultHandler
to only be set in the Builder.
This is no longer required by any code in spring-graphql.
@rwinch
Copy link
Author

rwinch commented May 20, 2021

I believe I have addressed all of your comments. I did so in a commit per comment to make it easier to follow along with what I've done. I'm happy to squash and rebase before the merge or preserve the commits.

@rstoyanchev
Copy link

I think having this packaged as a DataFetcherExceptionHandler implementation doesn't seem an effective way to enable sharing across multiple, independent exception handlers, which I think is the main goal, unless all exception handlers agree to adopt this particular mechanism. The only way I see to do enable sharing effectively is to have it built into the contract. Otherwise DataFetcherExceptionHandler remains nothing more than a helper, utility class for use in cases where all exception handlers are controlled by the application.

In addition, depending on how #2356 is implemented, it may change the way this here is done.

@rwinch
Copy link
Author

rwinch commented May 21, 2021

@rstoyanchev I do agree that it will depend on the result of gh-2356, but I don't understand your other comments:

doesn't seem an effective way to enable sharing across multiple, independent exception handlers, which I think is the main goal, unless all exception handlers agree to adopt this particular mechanism

DelegatingDataFetcherExceptionHandler allows a user to assemble arbitrary DataFetcherExceptionHandler together without the delegate DataFetcherExceptionHandler needing to be aware of DelegatingDataFetcherExceptionHandler. So I'm confused why you feel that all exception handlers need to agree to adopt this particular mechanism.

The only way I see to do enable sharing effectively is to have it built into the contract.

Which contract are you referring to?

Otherwise DataFetcherExceptionHandler remains nothing more than a helper, utility class for use in cases where all exception handlers are controlled by the application.

Why is DataFetcherExceptionHandler a helper/utility class or how does DelegatingDataFetcherExceptionHandler (this PR) change DataFetcherExceptionHandler into a helper/utility class where all exceptions handlers are controlled by the application?

@rstoyanchev
Copy link

DelegatingDataFetcherExceptionHandler could be used to assemble arbitrary DataFetcherExceptionHandler implementations indeed but you need to write predicates to decide which handlers handle which exceptions, and that's knowledge best encapsulated within each DataFetcherExceptionHandler.

Take a scenario with two frameworks having independent DataFetcherExceptionHandler implementations. Ideally each would encapsulate the knowledge of what exceptions it handles and what it doesn't, but as the contract is currently, you're expected to handle all exceptions. An application could use predicates to decide which exceptions go to which handler, but then such predicates would be needed in every application to make the same decisions as far as I can see.

This is why I think the ability to have multiple exception handlers should be built into the very contracts of GraphQL Java or otherwise left as it is.

@rwinch
Copy link
Author

rwinch commented May 24, 2021

I don't think that the logic of determining when to invoke a DataFetcherExceptionHandler is always best embedded within the DataFetcherExceptionHandler. For example, there might be a ForbiddenDataFetcherExceptionHandler that would be invoked differently based upon the context it is being used. In a Serlvet application ForbiddenDataFetcherExceptionHandler might be invoked when an java.security.AccessControlException is thrown. In a Spring Security application, this would happen when org.springframework.security.access.AccessDeniedException is thrown and there is no currently authenticated user.

For cases where the logic for both the decision and the handling logic should be in the same object the object can implement both DelegatingDataFetcherExceptionHandler and Predicate<Throwable>.

I think it's worth pointing out that this is only introducing a contract for a single implementation that should work for a large majority of use cases. It does not prevent additional implementations from having their own semantics.

@rstoyanchev
Copy link

Right, so exception handlers have to be built with DelegatingDataFetcherExceptionHandler in mind (by handling specific exceptions only or by implementing Predicate) or else they wouldn't work.

@bbakerman
Copy link
Member

We intend to merge a "async" exception handler in #2371 . This will affect this code.

Please have a look at tell us of any concerns

@bbakerman bbakerman closed this Feb 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants