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

Support Kotlin-gRPC client CoroutineStub #2669

Merged
merged 10 commits into from
May 6, 2020
Merged

Conversation

ikhoon
Copy link
Contributor

@ikhoon ikhoon commented Apr 16, 2020

Motivation:

gRPC for Kotlin is actively developed and
0.1.1 has been released last week.

Modifications:

  • Allow GrpcClientFactory creating client from CoroutineStub
  • Migrate Kotlin example to gRPC-Kotlin

Result:

@ikhoon ikhoon added this to the 0.99.4 milestone Apr 16, 2020
@ikhoon
Copy link
Contributor Author

ikhoon commented Apr 16, 2020

/cc @gary-lo

@trustin
Copy link
Member

trustin commented Apr 16, 2020

/cc @gary-lo

@gary-lo
Copy link
Contributor

gary-lo commented Apr 16, 2020

Wow thanks for doing this. It's surprising and awesome that so much of our code looks the same 😅 even though we worked independently. I'll close my PR.

@codecov
Copy link

codecov bot commented Apr 16, 2020

Codecov Report

Merging #2669 into master will decrease coverage by 0.46%.
The diff coverage is 47.02%.

Impacted file tree graph

@@             Coverage Diff              @@
##             master    #2669      +/-   ##
============================================
- Coverage     73.40%   72.94%   -0.47%     
- Complexity    11274    11293      +19     
============================================
  Files           993      997       +4     
  Lines         43329    44057     +728     
  Branches       5392     5463      +71     
============================================
+ Hits          31807    32136     +329     
- Misses         8760     9127     +367     
- Partials       2762     2794      +32     
Impacted Files Coverage Δ Complexity Δ
...rmeria/internal/client/grpc/GrpcClientFactory.java 54.54% <25.00%> (-8.15%) 12.00 <0.00> (ø)
...c/main/java/example/armeria/grpc/kotlin/Hello.java 34.72% <34.72%> (ø) 2.00 <2.00> (?)
.../example/armeria/grpc/kotlin/HelloServiceGrpc.java 48.37% <48.37%> (ø) 13.00 <13.00> (?)
...linGrpc/example/armeria/grpc/kotlin/HelloGrpcKt.kt 89.02% <89.02%> (ø) 0.00 <0.00> (?)
...in/example/armeria/grpc/kotlin/HelloServiceImpl.kt 97.14% <97.05%> (+12.52%) 7.00 <7.00> (ø)
...n/java/com/linecorp/armeria/server/HttpServer.java 40.00% <0.00%> (-20.00%) 2.00% <0.00%> (-1.00%)
...om/linecorp/armeria/client/HttpSessionHandler.java 64.13% <0.00%> (-8.28%) 40.00% <0.00%> (-3.00%)
...p/armeria/common/stream/AbstractStreamMessage.java 82.05% <0.00%> (-1.71%) 19.00% <0.00%> (ø%)
...com/linecorp/armeria/server/HttpServerHandler.java 81.00% <0.00%> (-1.34%) 79.00% <0.00%> (-2.00%)
...inecorp/armeria/server/HttpResponseSubscriber.java 77.16% <0.00%> (-1.19%) 53.00% <0.00%> (-2.00%)
... and 18 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update ad3ac18...c0a578c. Read the comment docs.

@ikhoon
Copy link
Contributor Author

ikhoon commented Apr 16, 2020

Wow thanks for doing this. It's surprising and awesome that so much of our code looks the same 😅 even though we worked independently. I'll close my PR.

To make a test code under gRPC-Kotlin coroutine stub, I refer to your PR airtasker#3

Let me add you as a co-author. 🙇‍♂️

@gary-lo gary-lo mentioned this pull request Apr 16, 2020
ikhoon added a commit to ikhoon/grpc-kotlin that referenced this pull request Apr 17, 2020
This gRPC-Kotlin is really awesome. ❤️

Motivation:

Armeria is going to early adopt gRPC-Kotlin by our users' requests.
line/armeria#2669

Armeria gRPC-Java client access `ServiceDescriptor` via reflection.
I think we can access to the orignal gRPC-Java stub via `StubFor` annotation in coroutine stub.

Modifications:
- Change AnnotationRetention from `BINARY` to `RUNTIME`

Result:
Better interop between Armeria and gRPC-Kotlin 😀
Copy link
Collaborator

@anuraaga anuraaga left a comment

Choose a reason for hiding this comment

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

Hmm - propagating armeria context into grpc context and kotlin context is pretty wild and it'd be nice if we can keep on playing with the Dispatcher, it should work somehow in theory. It's generally a code smell that we have to add to armeria-grpc only to support kotlin

I tried playing with this branch to look at the generated code, but probably since I'm on Windows, even if I run with JAVA_HOME set to a JDK 11, the compiler just shows a GUI prompt saying it requires a JDK 11 and doesn't generate any protos... Maybe we should wait a little more for the project to mature?

@anuraaga
Copy link
Collaborator

Found the kotlin compiler is this shell script

https://github.com/grpc/grpc-kotlin/blob/master/compiler/stub.sh

Indeed seems to have way too many implicit dependencies on the run environment 😓

@ikhoon
Copy link
Contributor Author

ikhoon commented Apr 17, 2020

I tried playing with this branch to look at the generated code

I updated the generated gRPC-Kotlin files to a different branch.😉
ikhoon@af3b29f

@anuraaga
Copy link
Collaborator

anuraaga commented Apr 17, 2020

I came up with this very hacky approach that seems to work. It relies on the implementation detail that the grpc-kotlin stub calls context + grpccontext.

It's not great to rely on the implementation detail, and only really works for gRPC singe other usages aren't guaranteed to call plus, but seems to provide good UX (not just context mounted but event loop is used too). Can you file an issue with grpc-kotlin that instead of taking a context as a constructor parameter for the service they should instead take a context provider or an abstract method? Contexts conceptually are per-request so the API as is doesn't make sense as it requires a singleton context as the service implementation is also a singleton. I don't know if I'd proceed with this PR until that's improved upstream.

package example.armeria.grpc.kotlin

import com.linecorp.armeria.common.RequestContext
import kotlinx.coroutines.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.ContinuationInterceptor
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

internal object ArmeriaContext: CoroutineContext {
    override fun <R> fold(initial: R, operation: (R, CoroutineContext.Element) -> R): R =
            EmptyCoroutineContext.fold(initial, operation)

    override fun <E : CoroutineContext.Element> get(key: CoroutineContext.Key<E>): E? =
            EmptyCoroutineContext.get(key)

    override fun minusKey(key: CoroutineContext.Key<*>): CoroutineContext =
            EmptyCoroutineContext.minusKey(key)

    override fun plus(context: CoroutineContext): CoroutineContext {
        val requestCtx: RequestContext = RequestContext.current()
        return requestCtx.contextAwareExecutor().asCoroutineDispatcher() + context
    }
}

val Dispatchers.Armeria: CoroutineContext
    get() = ArmeriaContext

@ikhoon
Copy link
Contributor Author

ikhoon commented Apr 17, 2020

Can you file an issue with grpc-kotlin that instead of taking a context as a constructor parameter for the service they should instead take a context provider or an abstract method?

Yes, I think we need to file two issues

1. To call RequestContext.current(), the *CoroutineStub should be called by an Armeria event loop directly. If not, we can't acquire a request context from the beginning. I think the upstream could invoke a coroutine stub in current call frame if the current thread is an instance of event loop such as io.netty.channel.EventLoop.

2. As you said it would be nice if the upstream offers a context provider or an abstract method for customizing context for requests.

@anuraaga
Copy link
Collaborator

Do we need anything special for point 1? My hack already works ok. What matters is that the context provider method of point 2) is called in the caller frame, and I guess it always would be since the context is needed to start the coroutine.

@ikhoon
Copy link
Contributor Author

ikhoon commented Apr 17, 2020

Do we need anything special for point 1? My hack already works ok

Ah... now I understood how your hack works exactly. Your hack looks awesome. 👍

@ikhoon
Copy link
Contributor Author

ikhoon commented Apr 18, 2020

Filed the issue. grpc/grpc-kotlin#66

@anuraaga
Copy link
Collaborator

Thanks! As long as the hack is kept to the example and not a published artifact, I guess it's ok. For this, can we also go ahead and check in the generated code? The proto compiler plugin is very much not ready for wide use right now and should leave it out of our build until it's more mature.

Motivation:

gRPC for Kotlin is actively developed and
[0.1.1](https://github.com/grpc/grpc-kotlin/releases/tag/v0.1.1) has been released last week.

Modifications:

- Allow GrpcClientFactory creating client from CoroutineStub
- Migrate Kotlin example to gRPC-Kotlin

Result:

- (Partially) Fixes line#2662
- You can now run Armeria gRPC client with `gproto` protocol and gRPC-Kotline CoroutinStub

Co-authored-by: Gary Lo <gary.lo@airtasker.com>
ikhoon added a commit to ikhoon/gradle-scripts that referenced this pull request Apr 27, 2020
Generate kotlin stub if a project has `kotlin-grpc` flag and `io.grpc:grpc-kotlin-stub` dependency
See line/armeria#2669
examples/grpc-kotlin/build.gradle.kts Outdated Show resolved Hide resolved
examples/grpc-kotlin/build.gradle.kts Outdated Show resolved Hide resolved
Copy link
Member

@trustin trustin left a comment

Choose a reason for hiding this comment

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

Thanks a lot, @ikhoon !

@trustin
Copy link
Member

trustin commented Apr 29, 2020

@anuraaga @gary-lo @minwoox Any other comments?

Copy link
Contributor

@gary-lo gary-lo left a comment

Choose a reason for hiding this comment

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

LGTM thank you

@ikhoon
Copy link
Contributor Author

ikhoon commented Apr 29, 2020

@gary-lo We built this PR together! 😀

}

/**
* Sends 5 [HelloReply] responses when receiving a request.
*
* @see lazyHello(HelloRequest, StreamObserver)
*/
override fun lotsOfReplies(request: HelloRequest, responseObserver: StreamObserver<HelloReply>) {
override fun lotsOfReplies(request: HelloRequest): Flow<HelloReply> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this need to be flow? I wouldn't expect a for loop in a coroutine to cause any differences compared to our normal withArmeriaContext methods

Copy link
Collaborator

Choose a reason for hiding this comment

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

To clarify, it doesn't matter so much whether it's a flow or not but was hoping withArmeriaContext applies to all the methods without having yet another pattern of flowOn

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I’ve tried but it was impossible to apply because withContext only could be applied to suspend function. This function is just return flow and not suspend :-(

Copy link
Contributor Author

@ikhoon ikhoon Apr 29, 2020

Choose a reason for hiding this comment

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

Or we can wrap withArmeriaContext with the inside of flow block.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I see - does that mean we can have

flow {
  withArmeriaContext {
    for (i...)

?

Definitely not great but I guess a bit better than a new concept flowOn. And probably good evidence to add to our issue about getting better support for customizing the context globally :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see - does that mean we can have

Yes, flow takes suspend function.
https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/common/src/flow/Builders.kt#L49

And probably good evidence to add to our issue about getting better support for customizing the context globally :)

That sounds good. 👍

Copy link
Contributor Author

@ikhoon ikhoon Apr 29, 2020

Choose a reason for hiding this comment

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

The test failed, the nested block does not work...

flow {
  withArmeriaContext {
    for (i...)

emit() should be called from dispatchers of flow block unless it throws ISE.
https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/common/src/flow/Builders.kt#L37-L45

Copy link
Collaborator

Choose a reason for hiding this comment

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

Too bad :( Let's stick with flowOn then since best we can do

Copy link
Member

@minwoox minwoox left a comment

Choose a reason for hiding this comment

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

👍

@trustin
Copy link
Member

trustin commented May 6, 2020

@anuraaga Shall we merge this and revisit when grpc-kotlin is updated, then?

@trustin trustin merged commit b1decb5 into line:master May 6, 2020
@trustin
Copy link
Member

trustin commented May 6, 2020

Thanks, @ikhoon, @gary-lo and other reviewers! Added @gary-lo as a co-author.

@ikhoon ikhoon deleted the grpc-kotlin branch May 13, 2020 08:02
fmguerreiro pushed a commit to fmguerreiro/armeria that referenced this pull request Sep 19, 2020
Motivation:

gRPC for Kotlin is actively developed and
[0.1.1](https://github.com/grpc/grpc-kotlin/releases/tag/v0.1.1) has been released last week.

Modifications:

- Allow GrpcClientFactory creating client from CoroutineStub
- Migrate Kotlin example to gRPC-Kotlin

Result:

- Partially fixes line#2662
- You can now run Armeria gRPC client with `gproto*` and gRPC-Kotlin CoroutinStub

Co-authored-by: Gary Lo <gary.lo@airtasker.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support CoroutineStubs from grpc-kotlin
5 participants