Skip to content

Commit

Permalink
add coroutine scope stub builder ext (#43)
Browse files Browse the repository at this point in the history
* remove coroutine scope interface from generated stubs

* add coroutine scope stub builder ext and generic stub def interface

* bug fix for additional context builder for abstract stub ext

* add suspending stub builder to generated code
  • Loading branch information
marcoferrer committed Apr 3, 2019
1 parent 202e1f4 commit 4f706e1
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,20 @@ public val <T : AbstractStub<T>> T.coroutineContext: CoroutineContext
* Returns a new stub with the value of [coroutineContext] attached as a [CallOptions].
* Any rpcs invoked on the resulting stub will use this context to participate in cooperative cancellation.
*/
public fun <T : AbstractStub<T>> T.withCoroutineContext(coroutineContext: CoroutineContext): T =
this.withOption(CALL_OPTION_COROUTINE_CONTEXT, coroutineContext)
public fun <T : AbstractStub<T>> T.withCoroutineContext(context: CoroutineContext): T{
val newContext = this.coroutineContext + context
return this.withOption(CALL_OPTION_COROUTINE_CONTEXT, newContext)
}


/**
* Returns a new stub with the 'coroutineContext' from the current suspension attached as a [CallOptions].
* Any rpcs invoked on the resulting stub will use this context to participate in cooperative cancellation.
*/
public suspend fun <T : AbstractStub<T>> T.withCoroutineContext(): T =
this.withOption(CALL_OPTION_COROUTINE_CONTEXT, kotlin.coroutines.coroutineContext)
public suspend fun <T : AbstractStub<T>> T.withCoroutineContext(): T {
val newContext = this.coroutineContext + kotlin.coroutines.coroutineContext
return this.withOption(CALL_OPTION_COROUTINE_CONTEXT, newContext)
}

internal fun CallOptions.withCoroutineContext(coroutineContext: CoroutineContext): CallOptions =
this.withOption(CALL_OPTION_COROUTINE_CONTEXT, coroutineContext)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright 2019 Kroto+ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.github.marcoferrer.krotoplus.coroutines

import io.grpc.Channel
import io.grpc.stub.AbstractStub
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.newCoroutineContext
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

/**
* Represents the metadata related to a specific grpc stub type. This interface is used to provide a generic form of
* instantiating grpc stubs. This interface is used mainly in implementations of companion objects in the generated
* coroutine stub clients.
*
* ```
* // GreeterCoroutineGrpc.GreeterCoroutineStub.Companion implements the [StubDefinition] interface
*
* println(GreeterCoroutineGrpc.GreeterCoroutineStub.serviceName)
*
* val stub = GreeterCoroutineGrpc.GreeterCoroutineStub.newStub(channel)
*
* ```
*/
public interface StubDefinition<T : AbstractStub<T>> {

/**
* The canonical name of the service this stub represents
*/
public val serviceName: String

/**
* Create a new stub of type [T] that is bound to the supplied [channel]
*/
public fun newStub(channel: Channel): T

/**
* Create a new stub of type [T] that is bound to the supplied [channel] and implicit coroutineContext
* as a call option.
*/
public suspend fun newStubWithContext(channel: Channel): T

}

/**
* Creates a new grpc stub, inheriting the context of the receiving [CoroutineScope]. Additional context elements can
* be specified with the [context] argument.
*
* This builder is meant to provide a mechanism for creating a new stub instance while explicitly defining what scope
* the executed rpcs wil run in. This method makes it clear that the resulting stub will use the receiving scope to
* create any child coroutines if necessary.
*
* One case of child jobs being created using this scope as a parent is during manual flow control management in
* streaming variations of rpcs.
*
* ```
*
* launch {
* val stub = newGrpcStub(GreeterCoroutineStub, channel)
* ....
* }
*
* ```
*
* @param context additional to [CoroutineScope.coroutineContext] context of the coroutine.
* @param stubDefinition the definition of the stub to create. Usually implemented in the companion object of the
* generated client stubs.
*
* @param channel the channel that this stub will use to do communications
*
*/
fun <T : AbstractStub<T>> CoroutineScope.newGrpcStub(
stubDefinition: StubDefinition<T>,
channel: Channel,
context: CoroutineContext = EmptyCoroutineContext
): T {
val newContext = newCoroutineContext(context)

return stubDefinition
.newStub(channel)
.withCoroutineContext(newContext)
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2019 Kroto+ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.github.marcoferrer.krotoplus.coroutines

import io.grpc.Channel
import io.grpc.examples.helloworld.GreeterCoroutineGrpc
import io.mockk.mockk
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlin.test.Test
import kotlin.test.assertEquals

class NewGrpcStubTests {

@Test
fun `Stub inherits context from coroutine scope`(){

val nameElement = CoroutineName("test_name")
val channel = mockk<Channel>()
val scope = CoroutineScope(nameElement)
val stub = scope
.newGrpcStub(GreeterCoroutineGrpc.GreeterCoroutineStub, channel)

assertEquals(nameElement.name, stub.coroutineContext[CoroutineName]?.name)
}


@Test
fun `Stub inherits context from coroutine scope and context argument`(){

val scopeJob = Job()
val scopeNameElement = CoroutineName("test_name")
val expectedNameElement = CoroutineName("expected_name")
val channel = mockk<Channel>()
val stub = CoroutineScope(scopeNameElement + scopeJob)
.newGrpcStub(GreeterCoroutineGrpc.GreeterCoroutineStub, channel,expectedNameElement)

assertEquals(expectedNameElement.name, stub.coroutineContext[CoroutineName]?.name)
assertEquals(scopeJob, stub.coroutineContext[Job])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,21 @@ object GrpcCoroutinesGenerator : Generator {
private fun ProtoService.buildOuterObject(): TypeSpec =
TypeSpec.objectBuilder(outerObjectName)
.addAnnotation(protoFile.getGeneratedAnnotationSpec())
.addFunction(buildNewStubMethod())
.addFunction(
FunSpec.builder("newStub")
.returns(stubClassName)
.addParameter("channel",CommonClassNames.grpcChannel)
.addCode("return %T.newStub(channel)",stubClassName)
.build()
)
.addFunction(
FunSpec.builder("newStubWithContext")
.returns(stubClassName)
.addModifiers(KModifier.SUSPEND)
.addParameter("channel", CommonClassNames.grpcChannel)
.addCode("return %T.newStubWithContext(channel)", stubClassName)
.build()
)
.addType(buildClientStubImpl())
.addType(buildServiceBaseImpl())
.addProperty(
Expand Down Expand Up @@ -393,7 +407,6 @@ object GrpcCoroutinesGenerator : Generator {

return TypeSpec.classBuilder(stubName)
.superclass(CommonClassNames.grpcAbstractStub.parameterizedBy(stubClassName))
.addSuperinterface(CommonClassNames.coroutineScope)
.addSuperclassConstructorParameter(paramNameChannel)
.addSuperclassConstructorParameter(paramNameCallOptions)
.primaryConstructor(FunSpec
Expand All @@ -408,20 +421,6 @@ object GrpcCoroutinesGenerator : Generator {
)
.build()
)
.addProperty(
PropertySpec
.builder("coroutineContext", CommonClassNames.coroutineContext)
.addModifiers(KModifier.OVERRIDE)
.getter(
FunSpec.getterBuilder()
.addCode(
"return callOptions.getOption(%T)",
ClassName(CommonPackages.krotoCoroutineLib,"CALL_OPTION_COROUTINE_CONTEXT")
)
.build()
)
.build()
)
.addFunction(FunSpec
.builder("build")
.addModifiers(KModifier.OVERRIDE)
Expand All @@ -440,13 +439,29 @@ object GrpcCoroutinesGenerator : Generator {

private fun ProtoService.buildClientStubCompanion(): TypeSpec =
TypeSpec.companionObjectBuilder()
.addSuperinterface(CommonClassNames.stubDefinition.parameterizedBy(stubClassName))
.addProperty(
PropertySpec.builder("serviceName", String::class.asClassName())
.addModifiers(KModifier.OVERRIDE)
.initializer("%T.SERVICE_NAME", enclosingServiceClassName)
.build()
)
.addFunction(
FunSpec.builder("newStub")
.returns(stubClassName)
.addModifiers(KModifier.OVERRIDE)
.addParameter("channel", CommonClassNames.grpcChannel)
.addCode("return %T(channel)", stubClassName)
.build()
)
.addFunction(
FunSpec.builder("newStubWithContext")
.returns(stubClassName)
.addModifiers(KModifier.OVERRIDE, KModifier.SUSPEND)
.addParameter("channel", CommonClassNames.grpcChannel)
.addCode("return %T(channel).%T()", stubClassName, CommonClassNames.withCoroutineContext)
.build()
)
.build()

private fun ProtoService.buildNewStubMethod(): FunSpec =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ object CommonClassNames{

val experimentalKrotoPlusCoroutinesApi = ClassName(krotoCoroutineLib, "ExperimentalKrotoPlusCoroutinesApi")
val serviceScope = ClassName("$krotoCoroutineLib.server", "ServiceScope")
val stubDefinition = ClassName(krotoCoroutineLib, "StubDefinition")
val withCoroutineContext = ClassName(krotoCoroutineLib, "withCoroutineContext")

val listenableFuture = ClassName("com.google.common.util.concurrent", "ListenableFuture")
val grpcContextElement = ClassName(krotoCoroutineLib,"GrpcContextElement")
Expand Down

0 comments on commit 4f706e1

Please sign in to comment.