Skip to content
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

Add API to make it easier to write method interceptors and support Kotlin suspend functions #3921

Merged
merged 1 commit into from Sep 18, 2020

Conversation

dstepanov
Copy link
Contributor

Added util classes to work with Kotlin's suspend functions in MethodInterceptors and implemented to use it with @Retryable.

It should be possible to implement other interceptors like micronaut-projects/micronaut-cache#183.

Personally I'm not a Kotlin user just found it interesting.

Copy link
Contributor

@graemerocher graemerocher left a comment

Choose a reason for hiding this comment

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

Nice work! Definitely a 2.1 feature this

@dstepanov
Copy link
Contributor Author

I think there should be a better way to do interception instead of the current code that is almost the same everywhere: "check if publisher", "check if future" and now this will require adding "check if kotlin suspend".

@graemerocher
Copy link
Contributor

@dstepanov yes we need some generalized abstraction for handling all these cases I agree

@graemerocher graemerocher added this to the 2.1.0 milestone Aug 20, 2020
@graemerocher graemerocher added the type: enhancement New feature or request label Aug 20, 2020
@dstepanov
Copy link
Contributor Author

Extracted a helper class, looks much better now.

@graemerocher
Copy link
Contributor

Seem to be some checkstyle violations

@graemerocher
Copy link
Contributor

The only doubt I have about this is I am unsure if it is going into micronaut-aop that we should couple it to RxJava 2

In general I think we need to move rxjava 2 to a module like the other frameworks and add a dependency on the modules that use it to RxJava 2 to make sure we have a road to move to rxjava 3 in the future

@JasonTypesCodes JasonTypesCodes added this to To Do in Micronaut 2.1 Aug 20, 2020
@dstepanov dstepanov force-pushed the kot-sf branch 3 times, most recently from 4454fb3 to 8057a1d Compare August 21, 2020 05:05
@dstepanov
Copy link
Contributor Author

I have removed RxJava 2 dependency, only one use case with publishers/futures that is not changed is Http client.

@dstepanov
Copy link
Contributor Author

@graemerocher I have made some refactoring to introduce reactive introduce call API, the core should have all interceptors refactored to use new API. Let me know what do you think.

@jameskleeh jameskleeh changed the base branch from master to 2.1.x August 25, 2020 19:10
@graemerocher
Copy link
Contributor

@dstepanov this is really awesome work and will definitely help avoid duplication! There are a few things I am unsure about:

@dstepanov
Copy link
Contributor Author

@graemerocher To retry the method call the interceptor needs to be supplied context.proceed(interceptor), not sure how to represent it in the API of the reactive interceptor. Maybe to have just one Supplier which would call context.proceed() for the first invocation and context.proceed(interceptor) next. WDYT?

@graemerocher
Copy link
Contributor

ah ok that confused me, maybe it should be called restart or reset instead as you are restarting from the beginning of the interceptor chain?

@dstepanov
Copy link
Contributor Author

Naming is not the best, for the users of the API it might not be very clear what is the interceptor chain etc. that's why retry is most clear but it does hide some impl. details.

@dstepanov
Copy link
Contributor Author

@graemerocher How would you name InterceptCompletionStageSupplierWithRetry?

@graemerocher
Copy link
Contributor

@dstepanov maybe InterceptCompletionStageRestartSupplier?

@graemerocher graemerocher changed the title Support Kotlin suspend functions in MethodInterceptors Add API to make it easier to write method interceptors and support Kotlin suspend functions Aug 27, 2020
@dstepanov dstepanov force-pushed the kot-sf branch 2 times, most recently from 8875dc9 to d4c2700 Compare August 27, 2020 16:30
@dstepanov
Copy link
Contributor Author

Refactored ValidatingInterceptor to use the builder

@dstepanov dstepanov force-pushed the kot-sf branch 2 times, most recently from 8d976ff to 6dd71af Compare August 27, 2020 17:04
@graemerocher
Copy link
Contributor

I think we should probably leave it like it is for now

@graemerocher
Copy link
Contributor

@dstepanov one final change. I think we should not include this as a method on MethodInvocationContext and instead have a static factory method on the interface. I can image wanting to create subclasses of this to support other reactive frameworks and maybe the Flow API in Java 9

@dstepanov
Copy link
Contributor Author

@graemerocher something like static InterceptBuilder<T, R> of(MethodInvocationContext<T, R> context) on InterceptBuilder?

@graemerocher
Copy link
Contributor

yes something like that

@dstepanov
Copy link
Contributor Author

@graemerocher Renamed

@dstepanov
Copy link
Contributor Author

I was thinking maybe it would be better to replace null checks with instanceof, impl. would need to pass an instance that implements 2-3 interfaces.

     return InterceptHelper.intercept(new ReactiveIntercept() {
            @Override
            public CompletionStage<Object> intercept(Argument<Object> valueType, Supplier<CompletionStage<Object>> resultSupplier, Supplier<CompletionStage<Object>> retryResultSupplier) {
                ...
            }

            @Override
            public Publisher<Object> intercept(Argument<Object> valueType, Supplier<Publisher<Object>> publisherSupplier) {
                ...
            }

            @Override
            public Object intercept() {
                ...
            }
        }, this);
    }
    
    private abstract class ReactiveRetryIntercept implements InterceptPublisherSupplier, InterceptCompletionStageRestartSupplier, InterceptSynchronous {
    }

The API is a bit strange because Java doesn't allow it to implement multiple interfaces for anonymous classes, but the performance should be better.

Also I would add MethodInvocationContext<Object, Object> context to every new intercept interface to look it more like MethodInterceptor<Object, Object>.

WDYT?

@dstepanov
Copy link
Contributor Author

@graemerocher Should I try to refactor it?

@graemerocher
Copy link
Contributor

@dstepanov I wonder if it is worth providing a worse API in the name of performance.

@dstepanov
Copy link
Contributor Author

Probably not

@jameskleeh
Copy link
Contributor

@dstepanov I'd like to get this merged in pretty soon so I can look into coroutine support for the client. Overall it looks to be a big improvement over what we have currently. I am also concerned about the performance implication and I'd like to propose an alternative.

What if instead of having a builder style, an object was returned InterceptedMethod.of(context) which would then have methods on it to return the type (an enum), among others. The calling code could then have a switch block to handle the different types with a fallback default of interceptedMethod.unsupported() or something of the sort. This wouldn't require the multiple lambdas to be created for every method call.

I haven't dug into this logic very deeply so perhaps this wouldn't work for some reason, but it seems like it wouldn't complicate the usage of the API very much.

@dstepanov
Copy link
Contributor Author

@jameskleeh That's one of the ways to do it with the disadvantage of moving the switch outside to every interceptor and removing the strick check.

It would be possible to completely remove the builder:

switch(InterceptMethodUtils.invocationType(context)) { 
    case COMPLETION_STAGE:
        return InterceptMethodUtils.interceptCompletionStage(context, (..) -> {});
    case PUBLISHER:
         return ..
    case SYNCHRONOUS:
        return ..
}

This PR also adds coroutine support for the http client:

import io.micronaut.http.HttpResponse
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
@Client("/suspend")
interface SuspendClient {
@Get("/simple", consumes = [MediaType.TEXT_PLAIN])
suspend fun simple(): String
@Get("/simple", consumes = [MediaType.TEXT_PLAIN])
suspend fun simpleIgnoreResult()
@Get("/simple", consumes = [MediaType.TEXT_PLAIN])
suspend fun simpleResponse(): HttpResponse<String>
@Get("/simple", consumes = [MediaType.TEXT_PLAIN])
suspend fun simpleResponseIgnoreResult(): HttpResponse<Any>
@Get("/delayed", consumes = [MediaType.TEXT_PLAIN])
suspend fun delayed(): String
@Get("/illegal", consumes = [MediaType.ALL])
suspend fun errorCall(): String
@Get("/illegal", consumes = [MediaType.ALL])
suspend fun errorCallResponse(): HttpResponse<String>
}

@jameskleeh
Copy link
Contributor

@dstepanov I see now that coroutines are handled as part of the completion stage handlers. Awesome!

@graemerocher Can you give your thoughts? Perhaps we should have an idea of the performance implications before moving to a less friendly API.

@graemerocher
Copy link
Contributor

@jameskleeh only way to know the performance implications is benchmarking, however in general it is safe to assume a switch style would be much faster and more efficient in terms of memory consumption since there is less object creation to perform method dispatch.

@dstepanov
Copy link
Contributor Author

Should I make some changes?

@jameskleeh
Copy link
Contributor

@dstepanov You can, however I think the code you showed above wouldn't result in increased performance:

switch(InterceptMethodUtils.invocationType(context)) { 
    case COMPLETION_STAGE:
        return InterceptMethodUtils.interceptCompletionStage(context, (..) -> {}); //still using a lambda here

I think it would need to be more like

InterceptedMethod interceptedMethod = InterceptedMethod.of(context);
switch (interceptedMethod.returnType()) {
    case COMPLETION_STAGE:
        CompletableFuture f = toCompletableFuture(invokeFn(body, functionName, functionDefinition, valueType))
        return interceptedMethod.handleResult(f);
        break;
    default: //strict mode, up to the caller if the call this
        interceptedMethod.unsupported(); //throws exception

If that seems doable and you can get to it within the next week or so, that would be fantastic. If not, I will make sure it gets taken care of before 2.1

@dstepanov
Copy link
Contributor Author

@jameskleeh @graemerocher Please review

@graemerocher
Copy link
Contributor

That looks great to me. @jameskleeh ?

@jameskleeh
Copy link
Contributor

@graemerocher Looks good to me. Really great contribution @dstepanov!

@jameskleeh jameskleeh merged commit d035f9f into micronaut-projects:2.1.x Sep 18, 2020
@dstepanov
Copy link
Contributor Author

@jameskleeh Thanks! You had a good suggestion for the API

@dstepanov dstepanov deleted the kot-sf branch September 18, 2020 14:05
@ilopmar ilopmar moved this from To Do to Done in Micronaut 2.1 Sep 22, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement New feature or request
Projects
No open projects
Development

Successfully merging this pull request may close these issues.

None yet

3 participants