├── project ├── build.properties └── plugins.sbt ├── README.md ├── .travis.yml ├── .gitignore ├── src └── main │ └── scala │ └── org │ └── eiennohito │ └── grpc │ └── stream │ ├── GrpcStreaming.scala │ ├── impl │ ├── GrpcMessages.scala │ ├── client │ │ ├── BidiCallImpl.scala │ │ ├── OneInStreamOutImpl.scala │ │ ├── StreamInOneOutCallImpl.scala │ │ ├── UnaryCallImpl.scala │ │ └── GrpcClientHandler.scala │ ├── ScalaMetadata.scala │ └── ServerAkkaStreamHandler.scala │ ├── adapters │ ├── UnaryFutureWrapper.scala │ ├── StreamObserverSinkOnce.scala │ ├── GrpcToSourceAdapter.scala │ └── GrpcToSinkAdapter.scala │ ├── GrpcNames.scala │ ├── ServerCallBuilder.scala │ ├── server │ └── ServiceBuilder.scala │ └── client │ └── AStreamClient.scala ├── tests └── src │ ├── test │ ├── scala │ │ └── org │ │ │ └── eiennohito │ │ │ └── grpc │ │ │ ├── GrpcAkkaSpec.scala │ │ │ ├── MarshallerSpec.scala │ │ │ ├── ExceptionsArePropagatedToClient.scala │ │ │ ├── SanityCheck.scala │ │ │ ├── GrpcClientAkkaStreamSpec.scala │ │ │ ├── GrpcSpec.scala │ │ │ ├── GrpcClientStreamingSpec.scala │ │ │ ├── IdPropagationSpec.scala │ │ │ ├── GrpcServerAkkaStreamSpec.scala │ │ │ └── GrpcBothSidesBackpressure.scala │ └── resources │ │ └── logback-test.xml │ └── main │ ├── protobuf │ └── hello.proto │ └── scala │ └── org │ └── eiennohito │ ├── util │ └── Jul2Logback.scala │ └── grpc │ └── GreeterImpl.scala └── LICENSE /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.1.2 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gRPC bindings for Akka-Stream 2 | 3 | This is a very small library that enables to consume gRPC services as akka-stream Flows 4 | or implement services using akka-stream flows. 5 | 6 | # Example 7 | 8 | TODO -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 3 | - 2.11.8 4 | - 2.12.1 5 | jdk: 6 | - oraclejdk8 7 | script: 8 | - sbt ++$TRAVIS_SCALA_VERSION clean grpc-tests/test 9 | cache: 10 | directories: 11 | - $HOME/.ivy2/cache 12 | - $HOME/.sbt/boot 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | dist/* 6 | target/ 7 | lib_managed/ 8 | src_managed/ 9 | project/boot/ 10 | project/plugins/project/ 11 | 12 | # Scala-IDE specific 13 | .scala_dependencies 14 | 15 | #IDEA files 16 | .idea/ 17 | .idea_modules/ 18 | *.ipr 19 | *.iml 20 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | val scalaPbVersion = "0.7.0" 2 | 3 | addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.15") 4 | 5 | libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % scalaPbVersion 6 | 7 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.8") 8 | 9 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.1") 10 | 11 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.0") 12 | -------------------------------------------------------------------------------- /src/main/scala/org/eiennohito/grpc/stream/GrpcStreaming.scala: -------------------------------------------------------------------------------- 1 | package org.eiennohito.grpc.stream 2 | 3 | import akka.stream.scaladsl.Flow 4 | import org.eiennohito.grpc.stream.server.CallMetadata 5 | 6 | import scala.concurrent.Future 7 | 8 | /** 9 | * @author eiennohito 10 | * @since 2016/10/27 11 | */ 12 | object GrpcStreaming { 13 | type ServerHandler[Req, Resp, Mat] = CallMetadata => Future[Flow[Req, Resp, Mat]] 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/org/eiennohito/grpc/stream/impl/GrpcMessages.scala: -------------------------------------------------------------------------------- 1 | package org.eiennohito.grpc.stream.impl 2 | 3 | import io.grpc.{Metadata, Status} 4 | 5 | /** 6 | * @author eiennohito 7 | * @since 2016/10/27 8 | */ 9 | object GrpcMessages { 10 | case object StopRequests 11 | case object Complete 12 | case object Ready 13 | case class Close(status: Status, trailers: Metadata) 14 | case class Headers(headers: Metadata) 15 | } 16 | -------------------------------------------------------------------------------- /tests/src/test/scala/org/eiennohito/grpc/GrpcAkkaSpec.scala: -------------------------------------------------------------------------------- 1 | package org.eiennohito.grpc 2 | 3 | import akka.actor.ActorSystem 4 | import akka.stream.ActorMaterializer 5 | import akka.testkit.TestKit 6 | import org.scalatest.LoneElement 7 | 8 | import scala.concurrent.ExecutionContext 9 | 10 | /** 11 | * @author eiennohito 12 | * @since 2016/10/31 13 | */ 14 | abstract class GrpcAkkaSpec extends TestKit(ActorSystem()) with GrpcServerClientSpec with LoneElement { 15 | implicit def ec: ExecutionContext = system.dispatcher 16 | implicit lazy val mat = ActorMaterializer.create(system) 17 | 18 | override protected def afterAll() = { 19 | super.afterAll() 20 | system.terminate() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/scala/org/eiennohito/grpc/stream/impl/client/BidiCallImpl.scala: -------------------------------------------------------------------------------- 1 | package org.eiennohito.grpc.stream.impl.client 2 | 3 | import akka.stream.scaladsl.Flow 4 | import io.grpc.{CallOptions, Channel, MethodDescriptor} 5 | import org.eiennohito.grpc.stream.client.{BidiStreamCall, GrpcCallStatus} 6 | 7 | class BidiCallImpl[Request, Reply]( 8 | chan: Channel, 9 | md: MethodDescriptor[Request, Reply], 10 | callOpts: CallOptions) 11 | extends BidiStreamCall[Request, Reply] { 12 | override val flow: Flow[Request, Reply, GrpcCallStatus] = { 13 | Flow.fromGraph(new GrpcClientHandler[Request, Reply](chan, md, callOpts)) 14 | } 15 | 16 | override def withOpts(copts: CallOptions): BidiStreamCall[Request, Reply] = 17 | new BidiCallImpl(chan, md, copts) 18 | } 19 | -------------------------------------------------------------------------------- /src/main/scala/org/eiennohito/grpc/stream/impl/client/OneInStreamOutImpl.scala: -------------------------------------------------------------------------------- 1 | package org.eiennohito.grpc.stream.impl.client 2 | 3 | import akka.stream.scaladsl.{Flow, Keep, Source} 4 | import io.grpc.{CallOptions, Channel, MethodDescriptor} 5 | import org.eiennohito.grpc.stream.client.{GrpcCallStatus, OneInStreamOutCall} 6 | 7 | /** 8 | * @author eiennohito 9 | * @since 2016/10/27 10 | */ 11 | class OneInStreamOutImpl[Req, Resp]( 12 | chan: Channel, 13 | md: MethodDescriptor[Req, Resp], 14 | opts: CallOptions) 15 | extends OneInStreamOutCall[Req, Resp] { 16 | 17 | override def withOpts(cops: CallOptions): OneInStreamOutCall[Req, Resp] = { 18 | new OneInStreamOutImpl[Req, Resp](chan, md, cops) 19 | } 20 | 21 | override def apply(o: Req): Source[Resp, GrpcCallStatus] = { 22 | Source.single(o).viaMat(flow)(Keep.right) 23 | } 24 | 25 | override val flow = Flow.fromGraph(new GrpcClientHandler[Req, Resp](chan, md, opts)) 26 | } 27 | -------------------------------------------------------------------------------- /src/main/scala/org/eiennohito/grpc/stream/impl/client/StreamInOneOutCallImpl.scala: -------------------------------------------------------------------------------- 1 | package org.eiennohito.grpc.stream.impl.client 2 | 3 | import scala.concurrent.Future 4 | 5 | import akka.stream.scaladsl.{Flow, Keep, Sink} 6 | import io.grpc.{CallOptions, Channel, MethodDescriptor} 7 | import org.eiennohito.grpc.stream.client.{GrpcCallStatus, StreamInOneOutCall} 8 | 9 | class StreamInOneOutCallImpl[Req, Resp]( 10 | chan: Channel, 11 | md: MethodDescriptor[Req, Resp], 12 | opts: CallOptions) 13 | extends StreamInOneOutCall[Req, Resp] { 14 | 15 | override def withOpts(cops: CallOptions): StreamInOneOutCall[Req, Resp] = { 16 | new StreamInOneOutCallImpl[Req, Resp](chan, md, cops) 17 | } 18 | 19 | override def apply(): Sink[Req, (GrpcCallStatus, Future[Resp])] = 20 | flow.toMat(Sink.head)(Keep.both) 21 | 22 | override def flow: Flow[Req, Resp, GrpcCallStatus] = 23 | Flow.fromGraph(new GrpcClientHandler[Req, Resp](chan, md, opts)) 24 | } 25 | -------------------------------------------------------------------------------- /tests/src/main/protobuf/hello.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package org.eiennohito.grpc; 4 | 5 | // The greeting service definition. 6 | service Greeter { 7 | // Sends a greeting 8 | rpc SayHello (HelloRequest) returns (HelloReply) {} 9 | rpc SayHelloSvrStream (HelloRequestStream) returns (stream HelloReply) {} 10 | rpc SayHelloClientStream (stream HelloRequest) returns (HelloStreamReply) {} 11 | } 12 | 13 | // The request message containing the user's name. 14 | message HelloRequest { 15 | string name = 1; 16 | } 17 | 18 | message HelloRequestStream { 19 | int32 number = 1; 20 | string name = 2; 21 | } 22 | 23 | message HelloStreamReply { 24 | int32 number = 1; 25 | string message = 2; 26 | } 27 | 28 | // The response message containing the greetings 29 | message HelloReply { 30 | string message = 1; 31 | } 32 | 33 | message HaveOneOf { 34 | oneof request { 35 | HelloRequest simple = 1; 36 | HelloRequestStream streaming = 2; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/src/test/scala/org/eiennohito/grpc/MarshallerSpec.scala: -------------------------------------------------------------------------------- 1 | package org.eiennohito.grpc 2 | 3 | import java.util.UUID 4 | 5 | import org.eiennohito.grpc.stream.impl.ScalaMetadata.{ThrowableMarshaller, UUIDMarshaller} 6 | import org.scalatest.{FreeSpec, Matchers} 7 | 8 | /** 9 | * @author eiennohito 10 | * @since 2016/10/27 11 | */ 12 | class MarshallerSpec extends FreeSpec with Matchers { 13 | "UUID marshaller" - { 14 | "works" in { 15 | for (_ <- 0 until 1000) { 16 | val u = UUID.randomUUID() 17 | val toBytes = UUIDMarshaller.toBytes(u) 18 | val ru = UUIDMarshaller.parseBytes(toBytes) 19 | ru shouldBe u 20 | } 21 | } 22 | } 23 | 24 | "Throwable marshaller" - { 25 | "works" in { 26 | val ex = new Exception("the message") 27 | val bytes = ThrowableMarshaller.toBytes(ex) 28 | val obj = ThrowableMarshaller.parseBytes(bytes) 29 | ex.getMessage shouldBe obj.getMessage 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/src/test/scala/org/eiennohito/grpc/ExceptionsArePropagatedToClient.scala: -------------------------------------------------------------------------------- 1 | package org.eiennohito.grpc 2 | 3 | import akka.stream.scaladsl.Flow 4 | import org.eiennohito.grpc.stream.impl.client.UnaryCallImpl 5 | import org.eiennohito.grpc.stream.server.ServiceBuilder 6 | 7 | import scala.concurrent.Await 8 | 9 | /** 10 | * @author eiennohito 11 | * @since 2016/10/28 12 | */ 13 | class ExceptionsArePropagatedToClient extends GrpcAkkaSpec { 14 | import scala.concurrent.duration._ 15 | 16 | override def init = { _.addService(failSvc) } 17 | 18 | def failSvc = { 19 | val bldr = ServiceBuilder(GreeterGrpc.SERVICE) 20 | bldr.method(GreeterGrpc.METHOD_SAY_HELLO).handleWith(Flow[HelloRequest].map(_ => throw new Exception("fail!"))) 21 | bldr.result() 22 | } 23 | 24 | "ExceptionsArePropagated" - { 25 | "works" in { 26 | val call = new UnaryCallImpl(client, GreeterGrpc.METHOD_SAY_HELLO, defaultOpts) 27 | val f = call(HelloRequest("hello!")) 28 | Await.ready(f, 10.seconds) 29 | val thr = f.value.get.failed.get 30 | val ex = thr.getSuppressed()(0) 31 | ex.getMessage shouldBe "fail!" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/scala/org/eiennohito/grpc/stream/impl/client/UnaryCallImpl.scala: -------------------------------------------------------------------------------- 1 | package org.eiennohito.grpc.stream.impl.client 2 | 3 | import akka.stream.Materializer 4 | import akka.stream.scaladsl.{Flow, Keep, Sink, Source} 5 | import com.typesafe.scalalogging.StrictLogging 6 | import io.grpc.{CallOptions, Channel, MethodDescriptor} 7 | import org.eiennohito.grpc.stream.client.{GrpcCallStatus, UnaryCall} 8 | 9 | /** 10 | * @author eiennohito 11 | * @since 2016/10/27 12 | */ 13 | class UnaryCallImpl[Request, Reply]( 14 | chan: Channel, 15 | md: MethodDescriptor[Request, Reply], 16 | opts: CallOptions) 17 | extends UnaryCall[Request, Reply] 18 | with StrictLogging { 19 | override def withOpts(copts: CallOptions) = { 20 | val wrapped = new UnaryCallImpl(chan, md, copts) 21 | wrapped 22 | } 23 | 24 | override val flow: Flow[Request, Reply, GrpcCallStatus] = { 25 | val flow = Flow[Request] 26 | .take(1) 27 | .viaMat(Flow.fromGraph(new GrpcClientHandler[Request, Reply](chan, md, opts)))(Keep.right) 28 | .take(1) 29 | Flow.fromGraph(flow) 30 | } 31 | 32 | override def apply(v1: Request)(implicit mat: Materializer) = { 33 | Source.single(v1).via(flow).runWith(Sink.head) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/src/main/scala/org/eiennohito/util/Jul2Logback.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 eiennohito (Tolmachev Arseny) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.eiennohito.util 18 | 19 | import java.util.logging.{Level, LogManager, Logger} 20 | 21 | import com.typesafe.scalalogging.StrictLogging 22 | import org.slf4j.bridge.SLF4JBridgeHandler 23 | 24 | /** 25 | * @author eiennohito 26 | * @since 2016/04/29 27 | */ 28 | object Jul2Logback extends StrictLogging { 29 | 30 | LogManager.getLogManager.reset() 31 | SLF4JBridgeHandler.removeHandlersForRootLogger() 32 | SLF4JBridgeHandler.install() 33 | Logger.getGlobal.setLevel(Level.FINEST) 34 | 35 | logger.debug("installed loggers for grpc") 36 | 37 | def init(): Unit = {} 38 | } 39 | -------------------------------------------------------------------------------- /src/main/scala/org/eiennohito/grpc/stream/adapters/UnaryFutureWrapper.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 eiennohito (Tolmachev Arseny) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.eiennohito.grpc.stream.adapters 18 | 19 | import io.grpc.stub.ServerCalls.UnaryMethod 20 | import io.grpc.stub.StreamObserver 21 | 22 | import scala.concurrent.{ExecutionContext, Future} 23 | 24 | /** 25 | * @author eiennohito 26 | * @since 2016/04/28 27 | */ 28 | class UnaryFutureWrapper[R, T](fn: R => Future[T])(implicit ec: ExecutionContext) 29 | extends UnaryMethod[R, T] { 30 | override def invoke(request: R, obs: StreamObserver[T]): Unit = { 31 | fn(request).onComplete { 32 | case scala.util.Success(s) => 33 | obs.onNext(s) 34 | obs.onCompleted() 35 | case scala.util.Failure(f) => 36 | obs.onError(f) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/scala/org/eiennohito/grpc/stream/GrpcNames.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 eiennohito (Tolmachev Arseny) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.eiennohito.grpc.stream 18 | 19 | import scalapb.grpc.{AbstractService, ServiceCompanion} 20 | import io.grpc.{MethodDescriptor, ServiceDescriptor} 21 | 22 | /** 23 | * @author eiennohito 24 | * @since 2016/05/04 25 | */ 26 | object GrpcNames { 27 | def svcName(md: MethodDescriptor[_, _]): String = { 28 | MethodDescriptor.extractFullServiceName(md.getFullMethodName) 29 | } 30 | 31 | def svcName(svc: ServiceCompanion[_]): String = { 32 | svc.javaDescriptor.getFullName 33 | } 34 | 35 | def svcName(service: ServiceDescriptor): String = { 36 | service.getName 37 | } 38 | 39 | def svcName[T <: AbstractService](svc: T)(implicit sc: ServiceCompanion[T]): String = svcName(sc) 40 | } 41 | -------------------------------------------------------------------------------- /tests/src/test/scala/org/eiennohito/grpc/SanityCheck.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 eiennohito (Tolmachev Arseny) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.eiennohito.grpc 18 | 19 | import scala.concurrent.ExecutionContext 20 | 21 | /** 22 | * @author eiennohito 23 | * @since 2016/04/28 24 | */ 25 | class SanityCheck extends GrpcServerClientSpec { 26 | "GrpcSimple" - { 27 | "works" in { 28 | val syncStub = blocking(new GreeterGrpc.GreeterBlockingStub(_, _)) 29 | val reply = syncStub.sayHello(HelloRequest("me")) 30 | reply.message shouldBe "Hi, me" 31 | } 32 | 33 | "server stream works" in { 34 | val syncStub = blocking(new GreeterGrpc.GreeterBlockingStub(_, _)) 35 | val reply = syncStub.sayHelloSvrStream(HelloRequestStream(5, "me")).toList 36 | reply should have length 5 37 | } 38 | } 39 | 40 | override def init = _.addService(GreeterGrpc.bindService(new GreeterImpl, ExecutionContext.global)) 41 | } 42 | -------------------------------------------------------------------------------- /src/main/scala/org/eiennohito/grpc/stream/ServerCallBuilder.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 eiennohito (Tolmachev Arseny) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.eiennohito.grpc.stream 18 | 19 | import akka.stream.Materializer 20 | import akka.stream.scaladsl.Flow 21 | import com.typesafe.scalalogging.StrictLogging 22 | import io.grpc._ 23 | import org.eiennohito.grpc.stream.impl.ServerAkkaStreamHandler 24 | import org.eiennohito.grpc.stream.server.CallMetadata 25 | 26 | import scala.concurrent.{ExecutionContext, Future} 27 | import scala.reflect.ClassTag 28 | 29 | /** 30 | * @author eiennohito 31 | * @since 2016/04/28 32 | */ 33 | class ServerCallBuilder[Request: ClassTag, Reply](md: MethodDescriptor[Request, Reply]) 34 | extends StrictLogging { 35 | def handleWith[T](flow: CallMetadata => Future[Flow[Request, Reply, T]])( 36 | implicit mat: Materializer, 37 | ec: ExecutionContext) = { 38 | val handler: ServerCallHandler[Request, Reply] = 39 | new ServerAkkaStreamHandler[Request, Reply](flow) 40 | handler 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/src/main/scala/org/eiennohito/grpc/GreeterImpl.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 eiennohito (Tolmachev Arseny) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.eiennohito.grpc 18 | 19 | import scala.collection.mutable 20 | 21 | import io.grpc.stub.StreamObserver 22 | import org.eiennohito.grpc.GreeterGrpc.Greeter 23 | import scala.concurrent.Future 24 | 25 | /** 26 | * @author eiennohito 27 | * @since 2016/04/28 28 | */ 29 | class GreeterImpl extends Greeter { 30 | override def sayHello(request: HelloRequest) = { 31 | Future.successful(HelloReply("Hi, " + request.name)) 32 | } 33 | 34 | override def sayHelloSvrStream( 35 | request: HelloRequestStream, 36 | responseObserver: StreamObserver[HelloReply]) = { 37 | for (i <- 0.until(request.number)) { 38 | responseObserver.onNext(HelloReply(s"Hi, ${request.name}, #$i")) 39 | } 40 | responseObserver.onCompleted() 41 | } 42 | 43 | def sayHelloClientStream( 44 | responseObserver: StreamObserver[HelloStreamReply]): StreamObserver[HelloRequest] = 45 | new StreamObserver[HelloRequest] { 46 | val arr = mutable.ArrayBuffer.empty[HelloRequest] 47 | def onError(t: Throwable): Unit = responseObserver.onError(t) 48 | def onNext(value: HelloRequest): Unit = arr += value 49 | def onCompleted(): Unit = { 50 | responseObserver.onNext(HelloStreamReply(arr.size, "Hi, " + arr.mkString(", "))) 51 | responseObserver.onCompleted() 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/src/test/scala/org/eiennohito/grpc/GrpcClientAkkaStreamSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 eiennohito (Tolmachev Arseny) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.eiennohito.grpc 18 | 19 | import akka.stream.scaladsl.{Keep, Sink, Source} 20 | import org.eiennohito.grpc.stream.impl.client.{OneInStreamOutImpl, StreamInOneOutCallImpl} 21 | import scala.concurrent.Await 22 | import scala.concurrent.duration._ 23 | /** 24 | * @author eiennohito 25 | * @since 2016/04/29 26 | */ 27 | class GrpcClientAkkaStreamSpec extends GrpcAkkaSpec { 28 | override def init = _.addService(GreeterGrpc.bindService(new GreeterImpl, system.dispatcher)) 29 | 30 | "Stream client" - { 31 | "server->client stream" in { 32 | val call = new OneInStreamOutImpl(client, GreeterGrpc.METHOD_SAY_HELLO_SVR_STREAM, defaultOpts) 33 | val source = call(HelloRequestStream(10, "me")) 34 | val ex = source.toMat(Sink.seq)(Keep.right) 35 | val result = Await.result(ex.run(), 30.seconds) 36 | result should have length 10 37 | } 38 | 39 | "client->server stream" in { 40 | val call = new StreamInOneOutCallImpl(client, GreeterGrpc.METHOD_SAY_HELLO_CLIENT_STREAM, defaultOpts) 41 | val sink = call.apply() 42 | val source = Source(1 to 10).map(i => HelloRequest("a" + i)) 43 | val ex = source.toMat(sink)(Keep.right) 44 | val result = Await.result(ex.run()._2, 30.seconds) 45 | result.number shouldBe 10 46 | } 47 | } 48 | } 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/main/scala/org/eiennohito/grpc/stream/adapters/StreamObserverSinkOnce.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 eiennohito (Tolmachev Arseny) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.eiennohito.grpc.stream.adapters 18 | 19 | import akka.stream.stage.{GraphStage, GraphStageLogic, InHandler} 20 | import akka.stream.{Attributes, Inlet, SinkShape} 21 | import com.typesafe.scalalogging.StrictLogging 22 | import io.grpc.Status 23 | import io.grpc.stub.StreamObserver 24 | 25 | /** 26 | * @author eiennohito 27 | * @since 2016/04/28 28 | */ 29 | class StreamObserverSinkOnce[T](so: StreamObserver[T]) 30 | extends GraphStage[SinkShape[T]] 31 | with StrictLogging { 32 | val in: Inlet[T] = Inlet("StreamObserverOnce") 33 | override val shape = SinkShape(in) 34 | 35 | override def createLogic(inheritedAttributes: Attributes) = { 36 | new GraphStageLogic(shape) { 37 | setHandler( 38 | in, 39 | new InHandler { 40 | override def onPush() = { 41 | val elem = grab(in) 42 | so.onNext(elem) 43 | completeStage() 44 | so.onCompleted() 45 | } 46 | 47 | override def onUpstreamFinish() = { 48 | so.onCompleted() 49 | } 50 | 51 | override def onUpstreamFailure(ex: Throwable) = { 52 | logger.error("error in stream observer once", ex) 53 | so.onError(Status.INTERNAL.withCause(ex).asException()) 54 | } 55 | } 56 | ) 57 | 58 | override def preStart() = { 59 | super.preStart() 60 | pull(in) 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | %date{ISO8601} %-5level [%thread] %logger{20} %marker%X{akkaSource} - %msg%n 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/main/scala/org/eiennohito/grpc/stream/impl/ScalaMetadata.scala: -------------------------------------------------------------------------------- 1 | package org.eiennohito.grpc.stream.impl 2 | 3 | import java.io.{ByteArrayInputStream, ByteArrayOutputStream, ObjectInputStream, ObjectOutputStream} 4 | import java.nio.ByteBuffer 5 | import java.util.UUID 6 | 7 | import akka.stream.Attributes.Attribute 8 | import io.grpc.Metadata.Key 9 | import io.grpc.{Metadata, Status, StatusException, StatusRuntimeException} 10 | 11 | /** 12 | * @author eiennohito 13 | * @since 2016/10/28 14 | */ 15 | object ScalaMetadata { 16 | 17 | def get[T](meta: Metadata, key: Key[T]): Option[T] = { 18 | if (meta.containsKey(key)) { 19 | Some(meta.get(key)) 20 | } else None 21 | } 22 | 23 | def forException(t: Throwable): Metadata = { 24 | val base = t match { 25 | case _: StatusException | _: StatusRuntimeException => 26 | Status.trailersFromThrowable(t) 27 | case _ => 28 | new Metadata() 29 | } 30 | base.put(ScalaException, t) 31 | base 32 | } 33 | 34 | def make(id: UUID): Metadata = { 35 | val obj = new Metadata() 36 | obj.put(ReqId, id) 37 | obj 38 | } 39 | 40 | case class InitialRequestId(id: UUID) extends Attribute 41 | 42 | val ReqId: Key[UUID] = Metadata.Key.of("reqid-bin", UUIDMarshaller) 43 | val ScalaException = Metadata.Key.of("jvm-exception-bin", ThrowableMarshaller) 44 | 45 | object UUIDMarshaller extends Metadata.BinaryMarshaller[UUID] { 46 | override def toBytes(value: UUID) = { 47 | val buf = ByteBuffer.allocate(16) 48 | buf.putLong(value.getMostSignificantBits) 49 | buf.putLong(value.getLeastSignificantBits) 50 | buf.array() 51 | } 52 | 53 | override def parseBytes(serialized: Array[Byte]) = { 54 | val buf = ByteBuffer.wrap(serialized) 55 | val mostSig = buf.getLong 56 | val leastSig = buf.getLong 57 | new UUID(mostSig, leastSig) 58 | } 59 | } 60 | 61 | object ThrowableMarshaller extends Metadata.BinaryMarshaller[Throwable] { 62 | override def toBytes(value: Throwable) = { 63 | val baos = new ByteArrayOutputStream(4096) 64 | val oos = new ObjectOutputStream(baos) 65 | oos.writeObject(value) 66 | oos.flush() 67 | baos.toByteArray 68 | } 69 | 70 | override def parseBytes(serialized: Array[Byte]) = { 71 | val bais = new ByteArrayInputStream(serialized) 72 | val ois = new ObjectInputStream(bais) 73 | ois.readObject().asInstanceOf[Throwable] 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/src/test/scala/org/eiennohito/grpc/GrpcSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 eiennohito (Tolmachev Arseny) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.eiennohito.grpc 18 | 19 | import java.util.concurrent.TimeUnit 20 | 21 | import io.grpc.stub.AbstractStub 22 | import io.grpc._ 23 | import org.eiennohito.util.Jul2Logback 24 | import org.scalatest.{BeforeAndAfterAll, FreeSpecLike, Matchers} 25 | 26 | import scala.concurrent.ExecutionContext 27 | import scala.util.Random 28 | 29 | trait GrpcServerClientSpec extends FreeSpecLike with Matchers with BeforeAndAfterAll { 30 | 31 | def init: ServerBuilder[_] => Unit 32 | 33 | Jul2Logback.init() 34 | 35 | val (server, port) = { 36 | GrpcServer.makeServer(init) 37 | } 38 | 39 | val client = { 40 | val bldr = ManagedChannelBuilder.forAddress("localhost", port) 41 | bldr.usePlaintext(true) 42 | bldr.executor(ExecutionContext.global) 43 | bldr.build() 44 | } 45 | 46 | def blocking[T <: AbstractStub[T]](f: (Channel, CallOptions) => T): T = { 47 | f(client, defaultOpts) 48 | } 49 | 50 | def defaultOpts: CallOptions = { 51 | CallOptions.DEFAULT.withDeadlineAfter(10, TimeUnit.SECONDS) 52 | } 53 | 54 | override protected def afterAll() = { 55 | super.afterAll() 56 | client.shutdown() 57 | client.awaitTermination(1, TimeUnit.MINUTES) 58 | server.shutdown() 59 | } 60 | } 61 | 62 | object GrpcServer { 63 | private def makeServer0(f: ServerBuilder[_] => Unit, trial: Int, maxTrial: Int): (Server, Int) = { 64 | try { 65 | val port = Random.nextInt(10000) + 50000 66 | val sb = ServerBuilder.forPort(port) 67 | f(sb) 68 | val srv = sb.build() 69 | srv.start() 70 | (srv, port) 71 | } catch { 72 | case e: Exception => 73 | if (trial >= maxTrial) { 74 | throw e 75 | } else { 76 | makeServer0(f, trial + 1, maxTrial) 77 | } 78 | } 79 | } 80 | 81 | def makeServer(f: ServerBuilder[_] => Unit): (Server, Int) = { 82 | makeServer0(f, 0, 5) 83 | } 84 | } 85 | 86 | 87 | -------------------------------------------------------------------------------- /tests/src/test/scala/org/eiennohito/grpc/GrpcClientStreamingSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 eiennohito (Tolmachev Arseny) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.eiennohito.grpc 18 | 19 | import scala.concurrent.duration._ 20 | import scala.concurrent.{Await, ExecutionContext} 21 | 22 | import akka.actor.ActorSystem 23 | import akka.stream.scaladsl.{Flow, Keep, Source} 24 | import akka.stream.ActorMaterializer 25 | import akka.testkit.TestKit 26 | import io.grpc.ServerServiceDefinition 27 | import org.eiennohito.grpc.stream.impl.client.StreamInOneOutCallImpl 28 | import org.eiennohito.grpc.stream.server.ServiceBuilder 29 | 30 | class GrpcClientStreamingSpec extends TestKit(ActorSystem()) with GrpcServerClientSpec { 31 | 32 | implicit lazy val mat = ActorMaterializer.create(system) 33 | implicit def ec: ExecutionContext = system.dispatcher 34 | 35 | override def init = { b => 36 | val names = GreeterGrpc.METHOD_SAY_HELLO.getFullMethodName.split('/') 37 | val bldr = ServerServiceDefinition.builder(names(0)) 38 | val bld2 = ServiceBuilder(bldr) 39 | 40 | bld2.method(GreeterGrpc.METHOD_SAY_HELLO_CLIENT_STREAM).handleWith(Service.svc) 41 | b.addService(bld2.result()) 42 | } 43 | 44 | object Service { 45 | def svc: Flow[HelloRequest, HelloStreamReply, _] = { 46 | Flow[HelloRequest] 47 | .fold(Seq.empty[String]) { case (acc, req) => acc :+ req.name} 48 | .map { seq => HelloStreamReply(seq.size, s"Hi, ${seq.mkString(", ")}") } 49 | } 50 | } 51 | 52 | "GrpcClientStreamingSpec" - { 53 | "client streaming" in { 54 | val call = new StreamInOneOutCallImpl(client, GreeterGrpc.METHOD_SAY_HELLO_CLIENT_STREAM, defaultOpts) 55 | val sink = call.apply() 56 | 57 | val source = Source(1 to 100).map(i => HelloRequest("a" + i)) 58 | 59 | val (_, response) = source.toMat(sink)(Keep.right).run() 60 | 61 | val result = Await.result(response, 30.seconds) 62 | result.number shouldBe 100 63 | } 64 | } 65 | 66 | override protected def afterAll() = { 67 | system.terminate() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/src/test/scala/org/eiennohito/grpc/IdPropagationSpec.scala: -------------------------------------------------------------------------------- 1 | package org.eiennohito.grpc 2 | 3 | import akka.stream.scaladsl.{Flow, Keep, Sink, Source} 4 | import akka.stream.stage.{GraphStage, GraphStageLogic, OutHandler} 5 | import akka.stream.{Attributes, Outlet, SourceShape} 6 | import org.eiennohito.grpc.stream.client.ClientBuilder 7 | import org.eiennohito.grpc.stream.impl.ScalaMetadata 8 | import org.eiennohito.grpc.stream.server.ServiceBuilder 9 | 10 | import scala.concurrent.Await 11 | 12 | /** 13 | * @author eiennohito 14 | * @since 2016/10/28 15 | */ 16 | class IdPropagationSpec extends GrpcAkkaSpec { 17 | import scala.concurrent.duration._ 18 | 19 | override def init = { _.addService(serverSvc) } 20 | 21 | lazy val clientImpl = { 22 | val bldr = ClientBuilder(client, defaultOpts) 23 | bldr.unary(GreeterGrpc.METHOD_SAY_HELLO) 24 | } 25 | 26 | def serverSvc = { 27 | val bldr = ServiceBuilder(GreeterGrpc.SERVICE) 28 | 29 | val stage = Source.fromGraph(new IdGraphStage) 30 | bldr.method(GreeterGrpc.METHOD_SAY_HELLO) 31 | .handleWith( 32 | Flow[HelloRequest] 33 | .flatMapMerge(1, _ => stage) 34 | .map(s => HelloReply(s)) 35 | ) 36 | 37 | bldr.method(GreeterGrpc.METHOD_SAY_HELLO_SVR_STREAM).handleWith( 38 | Flow[HelloRequestStream] 39 | .mapConcat(rs => List.fill(rs.number)(HelloRequest(rs.name))) 40 | .flatMapMerge(4, r => Source.single(r).via(clientImpl.flow))) 41 | bldr.result() 42 | } 43 | 44 | "IdPropagation" - { 45 | "works" ignore { //TODO: akka do not propagate attributes to dynamically created streams https://github.com/akka/akka/issues/21743 46 | val call = ClientBuilder(client, defaultOpts).serverStream(GreeterGrpc.METHOD_SAY_HELLO_SVR_STREAM) 47 | val (status, objsF) = call(HelloRequestStream(100, "me")).toMat(Sink.seq)(Keep.both).run() 48 | val objs = Await.result(objsF, 100.seconds) 49 | objs should have length (100) 50 | objs.distinct.loneElement shouldBe status.id 51 | } 52 | } 53 | 54 | override protected def afterAll() = { 55 | super.afterAll() 56 | system.terminate() 57 | } 58 | } 59 | 60 | class IdGraphStage extends GraphStage[SourceShape[String]] { 61 | val out = Outlet[String]("out") 62 | val shape = SourceShape(out) 63 | 64 | override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = { 65 | val attr = inheritedAttributes.get[ScalaMetadata.InitialRequestId] 66 | new GraphStageLogic(shape) with OutHandler { 67 | setHandler(out, this) 68 | 69 | private val res = attr.get.id.toString 70 | 71 | override def onPull(): Unit = { 72 | push(out, res) 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/scala/org/eiennohito/grpc/stream/server/ServiceBuilder.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 eiennohito (Tolmachev Arseny) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.eiennohito.grpc.stream.server 18 | 19 | import akka.stream.Materializer 20 | import akka.stream.scaladsl.Flow 21 | 22 | import scalapb.grpc.ServiceCompanion 23 | import io.grpc.ServerServiceDefinition.Builder 24 | import io.grpc._ 25 | import org.eiennohito.grpc.stream.{GrpcNames, ServerCallBuilder} 26 | 27 | import scala.concurrent.{ExecutionContext, Future} 28 | import scala.reflect.ClassTag 29 | 30 | /** 31 | * @author eiennohito 32 | * @since 2016/05/04 33 | */ 34 | class ServiceBuilder(private val bldr: ServerServiceDefinition.Builder) { 35 | 36 | def this(name: String) = this(ServerServiceDefinition.builder(name)) 37 | 38 | def method[T: ClassTag, R](md: MethodDescriptor[T, R]) = { 39 | val scb = new ServerCallBuilder[T, R](md) 40 | new ServiceDefBuilder(bldr, md, scb) 41 | } 42 | 43 | def result(): ServerServiceDefinition = bldr.build() 44 | } 45 | 46 | object ServiceBuilder { 47 | def apply(service: ServiceDescriptor): ServiceBuilder = new ServiceBuilder(service.getName) 48 | 49 | @deprecated("use _Grpc.SERVICE instead", "20180308") 50 | def apply(grpc: ServiceCompanion[_]): ServiceBuilder = new ServiceBuilder(GrpcNames.svcName(grpc)) 51 | def apply(name: String): ServiceBuilder = new ServiceBuilder(name) 52 | def apply(bldr: Builder): ServiceBuilder = new ServiceBuilder(bldr) 53 | } 54 | 55 | case class CallMetadata(ctx: Context, metadata: Metadata) 56 | 57 | class ServiceDefBuilder[T, R]( 58 | bldr: ServerServiceDefinition.Builder, 59 | mdesc: MethodDescriptor[T, R], 60 | scb: ServerCallBuilder[T, R]) { 61 | def handleSingle(fn: T => Future[R])(implicit mat: Materializer, ec: ExecutionContext): Unit = { 62 | val flow = Flow[T].mapAsync(1)(fn) 63 | handleWith(flow) 64 | } 65 | 66 | def handleWith[Mat]( 67 | flow: Flow[T, R, Mat])(implicit mat: Materializer, ec: ExecutionContext): Unit = { 68 | handleWith(_ => Future.successful(flow)) 69 | } 70 | 71 | def handleWith[Mat](flowFactory: CallMetadata => Future[Flow[T, R, Mat]])( 72 | implicit mat: Materializer, 73 | ec: ExecutionContext): Unit = { 74 | val sch = scb.handleWith(flowFactory) 75 | bldr.addMethod(mdesc, sch) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/src/test/scala/org/eiennohito/grpc/GrpcServerAkkaStreamSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 eiennohito (Tolmachev Arseny) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.eiennohito.grpc 18 | 19 | import akka.NotUsed 20 | import akka.actor.ActorSystem 21 | import akka.stream.{ActorMaterializer, Materializer} 22 | import akka.stream.scaladsl.{Flow, Source} 23 | import akka.testkit.TestKit 24 | import io.grpc.{MethodDescriptor, ServerServiceDefinition} 25 | import org.eiennohito.grpc.stream.ServerCallBuilder 26 | import org.eiennohito.grpc.stream.server.ServiceBuilder 27 | 28 | import scala.concurrent.ExecutionContext 29 | 30 | /** 31 | * @author eiennohito 32 | * @since 2016/04/29 33 | */ 34 | class GrpcServerAkkaStreamSpec extends GrpcAkkaSpec { 35 | 36 | override def init = { b => b.addService(AkkaServer.make(ActorMaterializer.create(system), system.dispatcher)) } 37 | 38 | "works" in { 39 | val syncStub = blocking(new GreeterGrpc.GreeterBlockingStub(_, _)) 40 | val reply = syncStub.sayHello(HelloRequest("me")) 41 | reply.message shouldBe "Hi, me" 42 | } 43 | 44 | "server stream works" in { 45 | val syncStub = blocking(new GreeterGrpc.GreeterBlockingStub(_, _)) 46 | val reply = syncStub.sayHelloSvrStream(HelloRequestStream(5, "me")).toList 47 | reply should have length 5 48 | } 49 | 50 | "server stream works with large number of msgs" in { 51 | val syncStub = blocking(new GreeterGrpc.GreeterBlockingStub(_, _)) 52 | val reply = syncStub.sayHelloSvrStream(HelloRequestStream(5000, "me")).toList 53 | reply should have length 5000 54 | } 55 | } 56 | 57 | object AkkaServer { 58 | 59 | val simple: Flow[HelloRequest, HelloReply, NotUsed] = { 60 | Flow.fromFunction(x => HelloReply(s"Hi, ${x.name}")) 61 | } 62 | 63 | val serverStream: Flow[HelloRequestStream, HelloReply, NotUsed] = { 64 | Flow[HelloRequestStream].flatMapConcat(r => Source((0 until r.number).map { i => HelloReply(s"Hi, ${r.name} #$i")})) 65 | } 66 | 67 | def make(implicit mat: Materializer, ec: ExecutionContext): ServerServiceDefinition = { 68 | val names = GreeterGrpc.METHOD_SAY_HELLO.getFullMethodName.split('/') 69 | val bldr = ServerServiceDefinition.builder(names(0)) 70 | val bld2 = ServiceBuilder(bldr) 71 | bld2.method(GreeterGrpc.METHOD_SAY_HELLO).handleWith(simple) 72 | bld2.method(GreeterGrpc.METHOD_SAY_HELLO_SVR_STREAM).handleWith(serverStream) 73 | bldr.build() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/scala/org/eiennohito/grpc/stream/adapters/GrpcToSourceAdapter.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 eiennohito (Tolmachev Arseny) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.eiennohito.grpc.stream.adapters 18 | 19 | import akka.stream.{Attributes, Outlet, SourceShape} 20 | import akka.stream.actor.RequestStrategy 21 | import akka.stream.stage.{GraphStageLogic, GraphStageWithMaterializedValue, OutHandler} 22 | import io.grpc.stub.StreamObserver 23 | import org.reactivestreams.Subscription 24 | 25 | trait Requester extends Subscription { 26 | def request(number: Int): Unit = this.request(number.toLong) 27 | def cancel(): Unit 28 | } 29 | 30 | class GrpcToSourceAdapter[T](req: Subscription, rs: RequestStrategy, private var inFlight: Int) 31 | extends GraphStageWithMaterializedValue[SourceShape[T], StreamObserver[T]] { 32 | 33 | private val out = Outlet[T]("Grpc.In") 34 | override val shape = SourceShape(out) 35 | 36 | override def createLogicAndMaterializedValue(inheritedAttributes: Attributes) = { 37 | var observer: StreamObserver[T] = null 38 | 39 | val logic = new GraphStageLogic(shape) { logic => 40 | val queue = new scala.collection.mutable.Queue[T]() 41 | var isCompleted = false 42 | 43 | observer = new StreamObserver[T] { 44 | private val nextAction = getAsyncCallback { x: T => 45 | supply(x) 46 | } 47 | private val errorAction = getAsyncCallback { x: Throwable => 48 | logic.failStage(x) 49 | } 50 | private val completeAction = getAsyncCallback { _: Unit => 51 | complete() 52 | } 53 | 54 | override def onError(t: Throwable) = errorAction.invoke(t) 55 | override def onCompleted() = completeAction.invoke(()) 56 | override def onNext(value: T) = nextAction.invoke(value) 57 | } 58 | 59 | private def supply(x: T) = { 60 | if (isAvailable(out)) { 61 | push(out, x) 62 | } else { 63 | queue += x 64 | } 65 | inFlight -= 1 66 | requestDemand() 67 | } 68 | 69 | private def requestDemand() = { 70 | if (!isCompleted) { 71 | val demand = rs.requestDemand(inFlight) 72 | if (demand > 0) { 73 | req.request(demand) 74 | } 75 | } 76 | } 77 | 78 | private def complete(): Unit = { 79 | if (queue.isEmpty) { 80 | logic.completeStage() 81 | } else { 82 | isCompleted = true 83 | } 84 | } 85 | 86 | setHandler( 87 | out, 88 | new OutHandler { 89 | override def onPull() = { 90 | if (queue.nonEmpty) { 91 | push(out, queue.dequeue()) 92 | requestDemand() 93 | } else if (isCompleted) { 94 | logic.completeStage() 95 | } 96 | } 97 | 98 | override def onDownstreamFinish() = { 99 | req.cancel() 100 | super.onDownstreamFinish() 101 | } 102 | } 103 | ) 104 | } 105 | 106 | (logic, observer) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/scala/org/eiennohito/grpc/stream/adapters/GrpcToSinkAdapter.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 eiennohito (Tolmachev Arseny) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.eiennohito.grpc.stream.adapters 18 | 19 | import akka.stream.stage.{GraphStageLogic, GraphStageWithMaterializedValue, InHandler} 20 | import akka.stream.{Attributes, Inlet, SinkShape} 21 | import com.typesafe.scalalogging.StrictLogging 22 | import io.grpc.{Status, StatusRuntimeException} 23 | import io.grpc.stub.StreamObserver 24 | 25 | import scala.concurrent.{Future, Promise} 26 | 27 | /** 28 | * @author eiennohito 29 | * @since 2016/04/28 30 | */ 31 | trait ReadyHandler { 32 | def onReady(): Unit 33 | def onCancel(): Unit 34 | } 35 | 36 | trait ReadyInput { 37 | def isReady: Boolean 38 | } 39 | 40 | class GrpcToSinkAdapter[T](data: StreamObserver[T], rdy: ReadyInput) 41 | extends GraphStageWithMaterializedValue[SinkShape[T], Future[ReadyHandler]] 42 | with StrictLogging { 43 | 44 | private val in = Inlet[T]("Grpc.Out") 45 | override val shape = SinkShape(in) 46 | 47 | override def createLogicAndMaterializedValue(inheritedAttributes: Attributes) = { 48 | 49 | val hndler = Promise[ReadyHandler] 50 | 51 | val logic = new GraphStageLogic(shape) { logic => 52 | 53 | @volatile private var keepGoing = true 54 | 55 | override def preStart() = { 56 | super.preStart() 57 | 58 | hndler.success(new ReadyHandler { 59 | private val readyCall = getAsyncCallback { _: Unit => 60 | signalReady() 61 | } 62 | private val cancelCall = getAsyncCallback { _: Unit => 63 | signalCancel() 64 | } 65 | override def onReady() = readyCall.invoke(()) 66 | override def onCancel() = { 67 | keepGoing = false 68 | cancelCall.invoke(()) 69 | } 70 | }) 71 | 72 | if (rdy.isReady) { 73 | pull(in) 74 | } 75 | } 76 | 77 | private def signalReady(): Unit = { 78 | if (!hasBeenPulled(in)) { 79 | pull(in) 80 | } 81 | } 82 | 83 | private def signalCancel(): Unit = { 84 | cancel(in) 85 | logic.completeStage() 86 | } 87 | 88 | setHandler( 89 | in, 90 | new InHandler { 91 | override def onPush(): Unit = { 92 | if (!keepGoing) { 93 | return 94 | } 95 | 96 | data.onNext(grab(in)) 97 | if (rdy.isReady) { 98 | pull(in) 99 | } 100 | } 101 | 102 | override def onUpstreamFinish() = 103 | try { 104 | data.onCompleted() 105 | } catch { 106 | case e: StatusRuntimeException => 107 | logger.debug("grpc status exception", e) 108 | } 109 | 110 | override def onUpstreamFailure(ex: Throwable) = { 111 | logger.error("signalling error to grpc", ex) 112 | data.onError(Status.INTERNAL.withCause(ex).asException()) 113 | } 114 | } 115 | ) 116 | } 117 | 118 | (logic, hndler.future) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /tests/src/test/scala/org/eiennohito/grpc/GrpcBothSidesBackpressure.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 eiennohito (Tolmachev Arseny) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.eiennohito.grpc 18 | 19 | import java.util.concurrent.atomic.AtomicLong 20 | 21 | import akka.actor.ActorSystem 22 | import akka.stream.{ActorMaterializer, ThrottleMode} 23 | import akka.stream.scaladsl.{Flow, Keep, Sink, Source} 24 | import akka.testkit.TestKit 25 | import io.grpc.ServerServiceDefinition 26 | import org.eiennohito.grpc.stream.impl.client.OneInStreamOutImpl 27 | import org.eiennohito.grpc.stream.server.ServiceBuilder 28 | 29 | import scala.concurrent.{Await, ExecutionContext} 30 | import scala.concurrent.duration._ 31 | 32 | /** 33 | * @author eiennohito 34 | * @since 2016/04/29 35 | */ 36 | class GrpcBothSidesBackpressure extends TestKit(ActorSystem()) with GrpcServerClientSpec { 37 | 38 | implicit lazy val mat = ActorMaterializer.create(system) 39 | implicit def ec: ExecutionContext = system.dispatcher 40 | 41 | override def init = { b => 42 | val names = GreeterGrpc.METHOD_SAY_HELLO.getFullMethodName.split('/') 43 | val bldr = ServerServiceDefinition.builder(names(0)) 44 | val bld2 = ServiceBuilder(bldr) 45 | 46 | bld2.method(GreeterGrpc.METHOD_SAY_HELLO_SVR_STREAM).handleWith(Service.svc) 47 | b.addService(bld2.result()) 48 | } 49 | 50 | object Service { 51 | val counter = new AtomicLong(0L) 52 | 53 | def svc: Flow[HelloRequestStream, HelloReply, _] = { 54 | val message = (0 until 4096).map(_.toChar).toString() 55 | 56 | val src = Flow[HelloRequestStream] 57 | src.flatMapConcat(i => Source((0 until i.number).toStream.map(x => HelloReply(s"Hi, ${i.name}, #$x, $message")))) 58 | .map(o => {counter.incrementAndGet(); o }) 59 | } 60 | } 61 | 62 | "BothSidesBackpressure" - { 63 | "do not eat all stream" in { 64 | val call = new OneInStreamOutImpl(client, GreeterGrpc.METHOD_SAY_HELLO_SVR_STREAM, defaultOpts) 65 | val stream = call(HelloRequestStream(500000, "me")) 66 | 67 | val data = stream.throttle(10, 30.milli, 2, ThrottleMode.Shaping).take(100).toMat(Sink.seq)(Keep.right) 68 | val results = Await.result(data.run(), 30.seconds) 69 | results.length shouldBe 100 70 | Service.counter.get should be <= 200L 71 | 72 | val call2 = call(HelloRequestStream(10, "me2")) 73 | val graph = call2.toMat(Sink.seq)(Keep.right) 74 | val results2 = Await.result(graph.run(), 30.seconds) 75 | results2 should have length 10 76 | } 77 | 78 | "produce the same results with or without consumer backpressure" in { 79 | val call = new OneInStreamOutImpl(client, GreeterGrpc.METHOD_SAY_HELLO_SVR_STREAM, defaultOpts) 80 | 81 | val stream1 = call(HelloRequestStream(100, "me")) 82 | val data1 = stream1.throttle(10, 30.milli, 1, ThrottleMode.Shaping).toMat(Sink.seq)(Keep.right) 83 | val results1 = Await.result(data1.run(), 30.seconds) 84 | 85 | val stream2 = call(HelloRequestStream(100, "me")) 86 | val data2 = stream2.toMat(Sink.seq)(Keep.right) 87 | val results2 = Await.result(data2.run(), 30.seconds) 88 | 89 | results1.length shouldBe results2.length 90 | } 91 | } 92 | 93 | override protected def afterAll() = { 94 | system.terminate() 95 | } 96 | } 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/main/scala/org/eiennohito/grpc/stream/client/AStreamClient.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 eiennohito (Tolmachev Arseny) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.eiennohito.grpc.stream.client 18 | 19 | import java.util.UUID 20 | 21 | import akka.stream.Materializer 22 | import akka.{Done, NotUsed} 23 | import akka.stream.scaladsl.{Flow, Sink, Source} 24 | import io.grpc.MethodDescriptor.MethodType 25 | import io.grpc.{CallOptions, Channel, Metadata, MethodDescriptor} 26 | import org.eiennohito.grpc.stream.impl.client.{ 27 | BidiCallImpl, 28 | OneInStreamOutImpl, 29 | StreamInOneOutCallImpl, 30 | UnaryCallImpl 31 | } 32 | 33 | import scala.concurrent.Future 34 | 35 | class AStreamChannel(val chan: Channel) 36 | 37 | trait AStreamClient {} 38 | 39 | trait AClientFactory { 40 | type Service <: AStreamClient 41 | def name: String 42 | def build(channel: Channel, callOptions: CallOptions): Service 43 | } 44 | 45 | trait AClientCompanion[T <: AStreamClient] extends AClientFactory { 46 | type Service = T 47 | } 48 | 49 | trait AStreamCall[T, R] { 50 | def flow: Flow[T, R, GrpcCallStatus] 51 | } 52 | 53 | trait UnaryCall[T, R] extends AStreamCall[T, R] { 54 | def withOpts(copts: CallOptions): UnaryCall[T, R] 55 | def apply(v: T)(implicit mat: Materializer): Future[R] 56 | def func(implicit mat: Materializer): T => Future[R] = apply 57 | } 58 | 59 | trait OneInStreamOutCall[T, R] extends (T => Source[R, GrpcCallStatus]) with AStreamCall[T, R] { 60 | def apply(o: T): Source[R, GrpcCallStatus] 61 | def withOpts(cops: CallOptions): OneInStreamOutCall[T, R] 62 | } 63 | 64 | trait StreamInOneOutCall[T, R] 65 | extends (() => Sink[T, (GrpcCallStatus, Future[R])]) 66 | with AStreamCall[T, R] { 67 | def apply(): Sink[T, (GrpcCallStatus, Future[R])] 68 | def withOpts(cops: CallOptions): StreamInOneOutCall[T, R] 69 | } 70 | 71 | trait BidiStreamCall[T, R] extends AStreamCall[T, R] { 72 | def apply(): Flow[T, R, GrpcCallStatus] = flow 73 | def withOpts(copts: CallOptions): BidiStreamCall[T, R] 74 | override def flow: Flow[T, R, GrpcCallStatus] 75 | } 76 | 77 | case class GrpcCallStatus( 78 | id: UUID, 79 | headers: Future[Metadata], 80 | trailers: Future[Metadata], 81 | completion: Future[Done]) 82 | 83 | class ClientBuilder(chan: Channel, callOptions: CallOptions) { 84 | def serverStream[T, R](md: MethodDescriptor[T, R]): OneInStreamOutCall[T, R] = { 85 | assert(md.getType == MethodType.SERVER_STREAMING) 86 | new OneInStreamOutImpl[T, R](chan, md, callOptions) 87 | } 88 | 89 | def clientStream[T, R](md: MethodDescriptor[T, R]): StreamInOneOutCall[T, R] = { 90 | assert(md.getType == MethodType.CLIENT_STREAMING) 91 | new StreamInOneOutCallImpl[T, R](chan, md, callOptions) 92 | } 93 | 94 | def unary[T, R](md: MethodDescriptor[T, R]): UnaryCall[T, R] = { 95 | assert(md.getType == MethodType.UNARY) 96 | new UnaryCallImpl[T, R](chan, md, callOptions) 97 | } 98 | 99 | def bidiStream[T, R](md: MethodDescriptor[T, R]): BidiStreamCall[T, R] = { 100 | assert(md.getType == MethodType.BIDI_STREAMING) 101 | new BidiCallImpl[T, R](chan, md, callOptions) 102 | } 103 | } 104 | 105 | object ClientBuilder { 106 | def apply(chan: Channel, callOptions: CallOptions): ClientBuilder = 107 | new ClientBuilder(chan, callOptions) 108 | } 109 | -------------------------------------------------------------------------------- /src/main/scala/org/eiennohito/grpc/stream/impl/client/GrpcClientHandler.scala: -------------------------------------------------------------------------------- 1 | package org.eiennohito.grpc.stream.impl.client 2 | 3 | import java.util.UUID 4 | 5 | import akka.Done 6 | import akka.stream.stage.{GraphStageLogic, GraphStageWithMaterializedValue, InHandler, OutHandler} 7 | import akka.stream.{Attributes, FlowShape, Inlet, Outlet} 8 | import io.grpc._ 9 | import org.eiennohito.grpc.stream.client.GrpcCallStatus 10 | import org.eiennohito.grpc.stream.impl.{GrpcMessages, ScalaMetadata} 11 | 12 | import scala.collection.mutable 13 | import scala.concurrent.Promise 14 | 15 | /** 16 | * @author eiennohito 17 | * @since 2016/10/27 18 | */ 19 | class GrpcClientHandler[Req, Resp]( 20 | chan: Channel, 21 | md: MethodDescriptor[Req, Resp], 22 | opts: CallOptions 23 | ) extends GraphStageWithMaterializedValue[FlowShape[Req, Resp], GrpcCallStatus] { 24 | val in = Inlet[Req]("GrpcClient.request") 25 | val out = Outlet[Resp]("GrpcClient.response") 26 | val shape = FlowShape(in, out) 27 | 28 | override def createLogicAndMaterializedValue( 29 | inheritedAttributes: Attributes): (GraphStageLogic, GrpcCallStatus) = { 30 | 31 | val hdrs = Promise[Metadata]() 32 | val trls = Promise[Metadata]() 33 | val cmpl = Promise[Done]() 34 | 35 | val myid = 36 | inheritedAttributes.get[ScalaMetadata.InitialRequestId].map(_.id).getOrElse(UUID.randomUUID()) 37 | 38 | val logic = new GraphStageLogic(shape) with InHandler with OutHandler { 39 | setHandlers(in, out, this) 40 | private val call = chan.newCall(md, opts) 41 | private val params = inheritedAttributes.get(Attributes.InputBuffer(8, 16)) 42 | private val buffer = new mutable.Queue[Resp]() 43 | private var inFlight = 0 44 | private var isCompleted = false 45 | 46 | private def request(): Unit = { 47 | if (inFlight < params.initial && buffer.lengthCompare(params.initial) < 0) { 48 | val toRequest = params.max - inFlight 49 | call.request(toRequest) 50 | inFlight += toRequest 51 | } 52 | } 53 | 54 | override def onPush(): Unit = { 55 | val msg = grab(in) 56 | call.sendMessage(msg) 57 | if (!hasBeenPulled(in) && call.isReady) { 58 | pull(in) 59 | } 60 | } 61 | 62 | override def onPull(): Unit = { 63 | if (buffer.nonEmpty) { 64 | push(out, buffer.dequeue()) 65 | } else if (isCompleted) { 66 | cmpl.success(Done) 67 | completeStage() 68 | } 69 | request() 70 | } 71 | 72 | override def onUpstreamFinish() = { 73 | call.halfClose() 74 | } 75 | 76 | override def onUpstreamFailure(ex: Throwable) = { 77 | call.cancel("cancelled because of exception", ex) 78 | super.onUpstreamFailure(ex) 79 | hdrs.tryFailure(ex) 80 | trls.tryFailure(ex) 81 | cmpl.failure(ex) 82 | } 83 | 84 | override def onDownstreamFinish() = { 85 | call.cancel("downstream finished", null) 86 | cmpl.success(Done) 87 | super.onDownstreamFinish() 88 | } 89 | 90 | private def handleResp(resp: Resp) = { 91 | inFlight -= 1 92 | if (isAvailable(out) && buffer.isEmpty) { 93 | push(out, resp) 94 | } else buffer += resp 95 | request() 96 | } 97 | 98 | private def handleClose(t: Status, m: Metadata) = { 99 | trls.success(m) 100 | if (t.isOk) { 101 | if (buffer.isEmpty) { 102 | cmpl.success(Done) 103 | completeStage() 104 | } else { 105 | isCompleted = true 106 | cancel(in) 107 | } 108 | } else { 109 | val ex = ScalaMetadata.get(m, ScalaMetadata.ScalaException) match { 110 | case None => t.asException() 111 | case Some(t2) => 112 | val e = t.asException() 113 | e.addSuppressed(t2) 114 | e 115 | } 116 | cmpl.failure(ex) 117 | failStage(ex) 118 | } 119 | } 120 | 121 | override def preStart() = { 122 | val actor = getStageActor { 123 | case (_, GrpcMessages.Ready) => if (!hasBeenPulled(in) && !isClosed(in)) { pull(in) } 124 | case (_, GrpcMessages.Close(t, m)) => handleClose(t, m) 125 | case (_, GrpcMessages.Headers(h)) => hdrs.success(h) 126 | case (_, x) => handleResp(x.asInstanceOf[Resp]) 127 | } 128 | 129 | call.start( 130 | new ClientCall.Listener[Resp] { 131 | override def onMessage(message: Resp) = actor.ref ! message 132 | override def onClose(status: Status, trailers: Metadata) = 133 | actor.ref ! GrpcMessages.Close(status, trailers) 134 | override def onHeaders(headers: Metadata) = actor.ref ! GrpcMessages.Headers(headers) 135 | override def onReady() = actor.ref ! GrpcMessages.Ready 136 | }, 137 | ScalaMetadata.make(myid) 138 | ) 139 | request() 140 | pull(in) 141 | } 142 | } 143 | 144 | (logic, GrpcCallStatus(myid, hdrs.future, trls.future, cmpl.future)) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main/scala/org/eiennohito/grpc/stream/impl/ServerAkkaStreamHandler.scala: -------------------------------------------------------------------------------- 1 | package org.eiennohito.grpc.stream.impl 2 | 3 | import akka.actor.ActorRef 4 | import akka.stream.scaladsl.{Sink, Source} 5 | import akka.stream.stage.{ 6 | AsyncCallback, 7 | GraphStageLogic, 8 | GraphStageWithMaterializedValue, 9 | InHandler, 10 | OutHandler 11 | } 12 | import akka.stream.{Attributes, Inlet, Materializer, Outlet, SinkShape, SourceShape} 13 | import com.typesafe.scalalogging.StrictLogging 14 | import io.grpc.{Context, Metadata, ServerCall, ServerCallHandler, Status} 15 | import org.eiennohito.grpc.stream.GrpcStreaming 16 | import org.eiennohito.grpc.stream.server.CallMetadata 17 | 18 | import scala.collection.mutable 19 | import scala.concurrent.{Await, ExecutionContext, Future, Promise} 20 | import scala.reflect.ClassTag 21 | import scala.util.{Failure, Success} 22 | 23 | /** 24 | * @author eiennohito 25 | * @since 2016/10/27 26 | */ 27 | class ServerAkkaStreamHandler[Req: ClassTag, Resp]( 28 | handler: GrpcStreaming.ServerHandler[Req, Resp, _])( 29 | implicit ec: ExecutionContext, 30 | mat: Materializer) 31 | extends ServerCallHandler[Req, Resp] { 32 | override def startCall(call: ServerCall[Req, Resp], headers: Metadata) = { 33 | new ServerAkkaHandler(call, headers, handler) 34 | } 35 | } 36 | 37 | class GrpcServerSrc[Req: ClassTag](call: ServerCall[Req, _]) 38 | extends GraphStageWithMaterializedValue[SourceShape[Req], Future[AsyncCallback[Any]]] 39 | with StrictLogging { 40 | val out = Outlet[Req]("GrpcServer.request") 41 | 42 | override val shape: SourceShape[Req] = SourceShape(out) 43 | 44 | override def createLogicAndMaterializedValue(inheritedAttributes: Attributes) = { 45 | val promise = Promise[AsyncCallback[Any]] 46 | 47 | val logic = new GraphStageLogic(shape) with OutHandler { 48 | setHandler(out, this) 49 | 50 | private val buffer = new mutable.Queue[Req]() 51 | private val bufSize = inheritedAttributes.get(new Attributes.InputBuffer(8, 16)) 52 | private var inFlight = 0 53 | 54 | private def request() = { 55 | if (inFlight < bufSize.initial && buffer.size < bufSize.initial) { 56 | val toRequest = bufSize.max - inFlight 57 | call.request(toRequest) 58 | inFlight += toRequest 59 | //logger.debug(s"request $toRequest -> $inFlight") 60 | } 61 | if (buffer.isEmpty && inFlight > bufSize.max) { 62 | completeStage() 63 | } 64 | } 65 | 66 | override def onPull(): Unit = { 67 | if (buffer.nonEmpty) { 68 | push(out, buffer.dequeue()) 69 | //logger.debug("pushed") 70 | } 71 | request() 72 | } 73 | 74 | private def handleInput(o: Req): Unit = { 75 | //logger.debug("input") 76 | inFlight -= 1 77 | if (isAvailable(out) && buffer.isEmpty) { 78 | push(out, o) 79 | } else buffer += o 80 | request() 81 | } 82 | 83 | override def preStart() = { 84 | super.preStart() 85 | val reqTag = implicitly[ClassTag[Req]] 86 | val cb = getAsyncCallback[Any] { 87 | case GrpcMessages.Complete => completeStage() 88 | case GrpcMessages.StopRequests => 89 | inFlight = Int.MaxValue 90 | //logger.debug("stopreqs") 91 | request() 92 | case reqTag(msg) => handleInput(msg) 93 | } 94 | promise.success(cb) 95 | } 96 | request() 97 | } 98 | (logic, promise.future) 99 | } 100 | } 101 | 102 | class GrpcServerSink[Resp](call: ServerCall[_, Resp]) 103 | extends GraphStageWithMaterializedValue[SinkShape[Resp], Future[ActorRef]] 104 | with StrictLogging { 105 | val in = Inlet[Resp]("GrpcServer.response") 106 | 107 | override val shape = SinkShape(in) 108 | 109 | override def createLogicAndMaterializedValue( 110 | inheritedAttributes: Attributes): (GraphStageLogic, Future[ActorRef]) = { 111 | val promise = Promise[ActorRef]() 112 | val logic = new GraphStageLogic(shape) with InHandler { 113 | setHandler(in, this) 114 | 115 | override def onPush(): Unit = { 116 | //logger.debug("-> pushed") 117 | call.sendMessage(grab(in)) 118 | if (call.isReady) { 119 | pull(in) 120 | } 121 | } 122 | 123 | override def preStart() = { 124 | val actor = getStageActor { 125 | case (_, GrpcMessages.Complete) => completeStage() 126 | case (_, GrpcMessages.Ready) => if (!hasBeenPulled(in)) pull(in) 127 | } 128 | promise.success(actor.ref) 129 | pull(in) 130 | //logger.debug("started") 131 | } 132 | 133 | override def onUpstreamFinish() = { 134 | //logger.debug("finished") 135 | call.close(Status.OK, new Metadata()) 136 | super.onUpstreamFinish() 137 | } 138 | 139 | override def onUpstreamFailure(ex: Throwable) = { 140 | call.close(Status.fromThrowable(ex), ScalaMetadata.forException(ex)) 141 | super.onUpstreamFinish() 142 | } 143 | } 144 | (logic, promise.future) 145 | } 146 | } 147 | 148 | class ServerAkkaHandler[Req: ClassTag, Resp]( 149 | call: ServerCall[Req, Resp], 150 | headers: Metadata, 151 | handler: GrpcStreaming.ServerHandler[Req, Resp, _])( 152 | implicit ec: ExecutionContext, 153 | mat: Materializer 154 | ) extends ServerCall.Listener[Req] 155 | with StrictLogging { 156 | 157 | private[this] val inputPromise = Promise[AsyncCallback[Any]]() 158 | private[this] val outputPromise = Promise[ActorRef]() 159 | 160 | { 161 | val ctx = Context.current() 162 | val meta = CallMetadata(ctx, headers) 163 | val logic = handler.apply(meta) 164 | 165 | logic.onComplete { 166 | case Success(f) => 167 | val src = Source.fromGraph(new GrpcServerSrc[Req](call)) 168 | val sink = Sink.fromGraph(new GrpcServerSink[Resp](call)) 169 | 170 | val baseId = ScalaMetadata.get(headers, ScalaMetadata.ReqId) 171 | 172 | val flow = baseId match { 173 | case None => f 174 | case Some(id) => f.addAttributes(Attributes(ScalaMetadata.InitialRequestId(id))) 175 | } 176 | 177 | call.sendHeaders(new Metadata()) 178 | val (f1, f2) = flow.runWith(src, sink) 179 | inputPromise.completeWith(f1) 180 | outputPromise.completeWith(f2) 181 | case Failure(e) => 182 | logger.error("could not create logic", e) 183 | call.close(Status.UNKNOWN.withCause(e), ScalaMetadata.forException(e)) 184 | inputPromise.failure(e) 185 | outputPromise.failure(e) 186 | } 187 | } 188 | 189 | //TODO fix after https://github.com/akka/akka/issues/21741 or https://github.com/akka/akka/issues/20503 190 | import scala.concurrent.duration._ 191 | private[this] lazy val inputRef: AsyncCallback[Any] = Await.result(inputPromise.future, 1.second) 192 | private[this] lazy val outputRef = Await.result(outputPromise.future, 1.second) 193 | 194 | override def onMessage(message: Req) = { 195 | inputRef.invoke(message) 196 | //logger.debug("msg") 197 | } 198 | 199 | override def onCancel() = { 200 | inputRef.invoke(GrpcMessages.Complete) 201 | outputRef ! GrpcMessages.Complete 202 | //logger.debug("cancel") 203 | } 204 | 205 | override def onComplete() = { 206 | inputRef.invoke(GrpcMessages.Complete) 207 | //logger.debug("cmpl") 208 | } 209 | 210 | override def onReady() = { 211 | outputRef ! GrpcMessages.Ready 212 | //logger.debug("rdy") 213 | } 214 | 215 | override def onHalfClose() = { 216 | inputRef.invoke(GrpcMessages.StopRequests) 217 | //logger.debug("half") 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. --------------------------------------------------------------------------------