This is an automated email from the ASF dual-hosted git repository.

pjfanning pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko-grpc.git


The following commit(s) were added to refs/heads/main by this push:
     new 2629e0af Use FastFuture for already completed marshalling futures 
(#863)
2629e0af is described below

commit 2629e0af13344bd82bee60114d4c0b0de8d0b5ce
Author: PJ Fanning <[email protected]>
AuthorDate: Tue Sep 1 11:20:59 2026 +0100

    Use FastFuture for already completed marshalling futures (#863)
    
    * Use FastFuture for completed futures.
    
    Use FastFuture.apply and FastFuture.successful/failed for trivially 
completed futures to optimise subsequent transforms.
    This speeds up a lot of service invocations in existing generated code that 
depend on these, especially GrpcMarshalling.unmarshal on strict request 
entities.
    
    * Benchmark and test FastFuture completion of marshalling futures
    
    Motivation:
    The FastFuture change is only observable as a threading/scheduling
    property: the returned futures are already completed, so transforms
    chained onto them run directly instead of being scheduled on an
    ExecutionContext. Nothing asserted or measured that.
    
    Modification:
    Add scaladsl GrpcMarshallingSpec, which asserts that unmarshal of a
    strict entity (both success and failure), unmarshalStream,
    GrpcExceptionHandler.from and the ServiceHandler notFound /
    unsupportedMediaType responses are already completed, and that a
    transform chained onto each still completes when given an
    ExecutionContext that throws if anything is scheduled on it.
    
    Add GrpcMarshallingBenchmark.unmarshallStrictChained, covering the
    unmarshal-then-transform chain that generated unary handlers use, and
    
ScalaUnaryHandlerBenchmark.oldStyleUnaryStrictRequestProcessingFastImplementation,
    a variant whose service implementation returns an already completed
    FastFuture so the whole handler chain can run directly.
    
    Result:
    The intent of the change is pinned by tests, and the benchmarks show
    where the saved dispatches actually come from.
    
    Tests:
    - sbt runtime/test - 139 succeeded, 0 failed
    - sbt checkCodeStyle - success
    - sbt "benchmarks/Jmh/run -f 2 -wi 5 -i 5 ..." - see PR body
    
    References:
    Refs #862
    
    ---------
    
    Co-authored-by: Tim Whittington <[email protected]>
---
 .../pekko/grpc/GrpcMarshallingBenchmark.scala      |  14 +++
 .../pekko/grpc/ScalaUnaryHandlerBenchmark.scala    |  20 +++-
 .../grpc/internal/HardcodedServiceDiscovery.scala  |   3 +-
 .../pekko/grpc/internal/PekkoHttpClientUtils.scala |  11 +-
 .../pekko/grpc/internal/RequestBuilderImpl.scala   |   3 +-
 .../pekko/grpc/scaladsl/GrpcExceptionHandler.scala |   4 +-
 .../pekko/grpc/scaladsl/GrpcMarshalling.scala      |   6 +-
 .../pekko/grpc/scaladsl/ServiceHandler.scala       |   5 +-
 .../pekko/grpc/scaladsl/GrpcMarshallingSpec.scala  | 113 +++++++++++++++++++++
 9 files changed, 162 insertions(+), 17 deletions(-)

diff --git 
a/benchmarks/src/main/scala/org/apache/pekko/grpc/GrpcMarshallingBenchmark.scala
 
b/benchmarks/src/main/scala/org/apache/pekko/grpc/GrpcMarshallingBenchmark.scala
index 4304cc13..deebe886 100644
--- 
a/benchmarks/src/main/scala/org/apache/pekko/grpc/GrpcMarshallingBenchmark.scala
+++ 
b/benchmarks/src/main/scala/org/apache/pekko/grpc/GrpcMarshallingBenchmark.scala
@@ -14,6 +14,8 @@
 package org.apache.pekko.grpc
 
 import scala.concurrent.Await
+import scala.concurrent.ExecutionContext
+import scala.concurrent.Future
 import scala.concurrent.duration.Duration
 
 import com.google.protobuf.{ Any => JavaAny, ByteString => JavaByteString }
@@ -52,6 +54,7 @@ class GrpcMarshallingBenchmark extends CommonBenchmark {
         isTrailer = false))
 
   val mat = SystemMaterializer(system).materializer
+  implicit val ec: ExecutionContext = mat.executionContext
 
   @Benchmark
   def marshall(): HttpResponse = {
@@ -68,6 +71,17 @@ class GrpcMarshallingBenchmark extends CommonBenchmark {
     Await.result(GrpcMarshalling.unmarshal(entity), Duration.Inf)
   }
 
+  // Unmarshalling a strict entity and then chaining the transforms a 
generated handler applies to it.
+  // This is the shape of the call chain in generated (and hand written) unary 
handlers.
+  @Benchmark
+  def unmarshallStrictChained(): ServerReflectionRequest = {
+    val result = GrpcMarshalling
+      .unmarshal(entity)
+      .flatMap(req => Future.successful(req))
+      .map(identity)
+    Await.result(result, Duration.Inf)
+  }
+
   @Benchmark
   def unmarshallJavaStrict(): JavaAny = {
     pekko.grpc.javadsl.GrpcMarshalling.unmarshal(javaEntity, javaSerializer, 
mat, reader).toCompletableFuture.get()
diff --git 
a/benchmarks/src/main/scala/org/apache/pekko/grpc/ScalaUnaryHandlerBenchmark.scala
 
b/benchmarks/src/main/scala/org/apache/pekko/grpc/ScalaUnaryHandlerBenchmark.scala
index 1384ecce..a4215c4d 100644
--- 
a/benchmarks/src/main/scala/org/apache/pekko/grpc/ScalaUnaryHandlerBenchmark.scala
+++ 
b/benchmarks/src/main/scala/org/apache/pekko/grpc/ScalaUnaryHandlerBenchmark.scala
@@ -48,6 +48,7 @@ import pekko.http.scaladsl.model.HttpResponse
 import pekko.http.scaladsl.model.StatusCodes
 import pekko.http.scaladsl.model.TransferEncodings
 import pekko.http.scaladsl.model.Uri
+import pekko.http.scaladsl.util.FastFuture
 import pekko.stream.Materializer
 import pekko.stream.SystemMaterializer
 import pekko.stream.scaladsl.Sink
@@ -66,7 +67,11 @@ class ScalaUnaryHandlerBenchmark extends CommonBenchmark {
   private val writer = GrpcProtocolNative.newWriter(Identity)
   private val requestMessage = HelloRequest("Alice")
   private val responseMessage = HelloReply("Hello, Alice")
-  private val implementation = new BenchmarkGreeterService(responseMessage)
+  // A typical service implementation, returning an ordinary completed Future.
+  private val implementation = new 
BenchmarkGreeterService(Future.successful(responseMessage))
+  // A service implementation that returns an already completed FastFuture, so 
that every transform
+  // the handler chains onto it can also run directly instead of being 
scheduled.
+  private val fastImplementation = new 
BenchmarkGreeterService(FastFuture.successful(responseMessage))
 
   private val request: HttpRequest = {
     val data =
@@ -88,7 +93,7 @@ class ScalaUnaryHandlerBenchmark extends CommonBenchmark {
   private val generatedHandler: HttpRequest => Future[HttpResponse] =
     GreeterServiceHandler(implementation)
 
-  private val oldStyleHandler: HttpRequest => Future[HttpResponse] = {
+  private def oldStyleHandlerFor(implementation: GreeterService): HttpRequest 
=> Future[HttpResponse] = {
     val notFound = Future.successful(HttpResponse(StatusCodes.NotFound))
     val unsupportedMediaType = 
Future.successful(HttpResponse(StatusCodes.UnsupportedMediaType))
     val spi = TelemetryExtension(system).spi
@@ -117,6 +122,9 @@ class ScalaUnaryHandlerBenchmark extends CommonBenchmark {
       }
   }
 
+  private val oldStyleHandler: HttpRequest => Future[HttpResponse] = 
oldStyleHandlerFor(implementation)
+  private val fastOldStyleHandler: HttpRequest => Future[HttpResponse] = 
oldStyleHandlerFor(fastImplementation)
+
   @Benchmark
   def generatedUnaryStrictRequestProcessing(blackhole: Blackhole): Unit =
     consumeResponse(Await.result(generatedHandler(request), Duration.Inf), 
blackhole)
@@ -125,6 +133,10 @@ class ScalaUnaryHandlerBenchmark extends CommonBenchmark {
   def oldStyleUnaryStrictRequestProcessing(blackhole: Blackhole): Unit =
     consumeResponse(Await.result(oldStyleHandler(request), Duration.Inf), 
blackhole)
 
+  @Benchmark
+  def oldStyleUnaryStrictRequestProcessingFastImplementation(blackhole: 
Blackhole): Unit =
+    consumeResponse(Await.result(fastOldStyleHandler(request), Duration.Inf), 
blackhole)
+
   private def consumeResponse(response: HttpResponse, blackhole: Blackhole): 
Unit = {
     blackhole.consume(response.status)
     response.entity match {
@@ -139,9 +151,9 @@ class ScalaUnaryHandlerBenchmark extends CommonBenchmark {
   def tearDown(): Unit =
     system.terminate()
 
-  private final class BenchmarkGreeterService(response: HelloReply) extends 
GreeterService {
+  private final class BenchmarkGreeterService(response: Future[HelloReply]) 
extends GreeterService {
     override def sayHello(in: HelloRequest): Future[HelloReply] =
-      Future.successful(response)
+      response
 
     override def itKeepsTalking(in: Source[HelloRequest, NotUsed]): 
Future[HelloReply] =
       throw new UnsupportedOperationException("itKeepsTalking")
diff --git 
a/runtime/src/main/scala/org/apache/pekko/grpc/internal/HardcodedServiceDiscovery.scala
 
b/runtime/src/main/scala/org/apache/pekko/grpc/internal/HardcodedServiceDiscovery.scala
index 4054faa6..7ceda5e3 100644
--- 
a/runtime/src/main/scala/org/apache/pekko/grpc/internal/HardcodedServiceDiscovery.scala
+++ 
b/runtime/src/main/scala/org/apache/pekko/grpc/internal/HardcodedServiceDiscovery.scala
@@ -16,11 +16,12 @@ package org.apache.pekko.grpc.internal
 import org.apache.pekko
 import pekko.discovery.{ Lookup, ServiceDiscovery }
 import pekko.discovery.ServiceDiscovery.Resolved
+import pekko.http.scaladsl.util.FastFuture
 
 import scala.concurrent.Future
 import scala.concurrent.duration.FiniteDuration
 
 class HardcodedServiceDiscovery(resolved: Resolved) extends ServiceDiscovery {
   override def lookup(lookup: Lookup, resolveTimeout: FiniteDuration): 
Future[Resolved] =
-    Future.successful(resolved)
+    FastFuture.successful(resolved)
 }
diff --git 
a/runtime/src/main/scala/org/apache/pekko/grpc/internal/PekkoHttpClientUtils.scala
 
b/runtime/src/main/scala/org/apache/pekko/grpc/internal/PekkoHttpClientUtils.scala
index 43fe647f..f513f72d 100644
--- 
a/runtime/src/main/scala/org/apache/pekko/grpc/internal/PekkoHttpClientUtils.scala
+++ 
b/runtime/src/main/scala/org/apache/pekko/grpc/internal/PekkoHttpClientUtils.scala
@@ -30,6 +30,7 @@ import pekko.http.scaladsl.{ ClientTransport, 
ConnectionContext, Http, HttpsConn
 import pekko.http.scaladsl.model._
 import pekko.http.scaladsl.model.headers.RawHeader
 import pekko.http.scaladsl.settings.ClientConnectionSettings
+import pekko.http.scaladsl.util.FastFuture
 import pekko.stream.{ Materializer, QueueOfferResult }
 import pekko.stream.scaladsl.{ Keep, Sink, Source }
 import pekko.util.ByteString
@@ -317,7 +318,7 @@ object PekkoHttpClientUtils {
           if (response.status != StatusCodes.OK) {
             response.entity.discardBytes()
             val failure = mapToStatusException(response, immutable.Seq.empty)
-            Source.failed(failure).mapMaterializedValue(_ => 
Future.failed(failure))
+            Source.failed(failure).mapMaterializedValue(_ => 
FastFuture.failed(failure))
           } else {
             Codecs.detect(response) match {
               case Success(codec) =>
@@ -362,7 +363,7 @@ object PekkoHttpClientUtils {
                   .via(reader.dataFrameDecoder)
                   .map(deserializer.deserialize)
                   .mapMaterializedValue(_ =>
-                    Future.successful(new GrpcResponseMetadata() {
+                    FastFuture.successful(new GrpcResponseMetadata() {
                       override def headers: pekko.grpc.scaladsl.Metadata =
                         new HeaderMetadataImpl(response.headers)
 
@@ -379,7 +380,7 @@ object PekkoHttpClientUtils {
                           .asJava
                     }))
               case Failure(e) =>
-                Source.failed[O](e).mapMaterializedValue(_ => Future.failed(e))
+                Source.failed[O](e).mapMaterializedValue(_ => 
FastFuture.failed(e))
             }
           }
         }
@@ -391,9 +392,9 @@ object PekkoHttpClientUtils {
     val allHeaders = response.headers ++ trailers
     allHeaders.find(_.name == "grpc-status").map(_.value) match {
       case Some("0") =>
-        Future.successful(())
+        FastFuture.successful(())
       case _ =>
-        Future.failed(mapToStatusException(response, trailers))
+        FastFuture.failed(mapToStatusException(response, trailers))
     }
   }
 
diff --git 
a/runtime/src/main/scala/org/apache/pekko/grpc/internal/RequestBuilderImpl.scala
 
b/runtime/src/main/scala/org/apache/pekko/grpc/internal/RequestBuilderImpl.scala
index 648c2021..ea1f980a 100644
--- 
a/runtime/src/main/scala/org/apache/pekko/grpc/internal/RequestBuilderImpl.scala
+++ 
b/runtime/src/main/scala/org/apache/pekko/grpc/internal/RequestBuilderImpl.scala
@@ -19,6 +19,7 @@ import org.apache.pekko
 import pekko.NotUsed
 import pekko.annotation.{ InternalApi, InternalStableApi }
 import pekko.grpc.{ GrpcClientSettings, GrpcResponseMetadata, 
GrpcServiceException, GrpcSingleResponse }
+import pekko.http.scaladsl.util.FastFuture
 import pekko.stream.{ Graph, Materializer, SourceShape }
 import pekko.stream.javadsl.{ Source => JavaSource }
 import pekko.stream.scaladsl.{ Keep, Sink, Source }
@@ -349,7 +350,7 @@ object RequestBuilderImpl {
   }
 
   def richError[U]: PartialFunction[Throwable, Future[U]] = {
-    case item => Future.failed(RequestBuilderImpl.lift(item))
+    case item => FastFuture.failed(RequestBuilderImpl.lift(item))
   }
 
   def lift(item: Throwable): scala.Throwable = item match {
diff --git 
a/runtime/src/main/scala/org/apache/pekko/grpc/scaladsl/GrpcExceptionHandler.scala
 
b/runtime/src/main/scala/org/apache/pekko/grpc/scaladsl/GrpcExceptionHandler.scala
index ab60d298..a0c5d47c 100644
--- 
a/runtime/src/main/scala/org/apache/pekko/grpc/scaladsl/GrpcExceptionHandler.scala
+++ 
b/runtime/src/main/scala/org/apache/pekko/grpc/scaladsl/GrpcExceptionHandler.scala
@@ -21,6 +21,7 @@ import pekko.grpc.{ GrpcServiceException, Trailers }
 import pekko.grpc.GrpcProtocol.GrpcProtocolWriter
 import pekko.grpc.internal.{ GrpcMetadataImpl, GrpcResponseHelpers, 
MissingParameterException }
 import pekko.http.scaladsl.model.HttpResponse
+import pekko.http.scaladsl.util.FastFuture
 import io.grpc.{ Status, StatusRuntimeException }
 import org.apache.pekko.http.scaladsl.model.http2.PeerClosedStreamException
 
@@ -71,6 +72,7 @@ object GrpcExceptionHandler {
   def from(mapper: PartialFunction[Throwable, Trailers])(
       implicit system: ClassicActorSystemProvider,
       writer: GrpcProtocolWriter): PartialFunction[Throwable, 
Future[HttpResponse]] =
-    mapper.orElse(defaultMapper(system.classicSystem)).andThen(s => 
Future.successful(GrpcResponseHelpers.status(s)))
+    mapper.orElse(defaultMapper(system.classicSystem)).andThen(s =>
+      FastFuture.successful(GrpcResponseHelpers.status(s)))
 
 }
diff --git 
a/runtime/src/main/scala/org/apache/pekko/grpc/scaladsl/GrpcMarshalling.scala 
b/runtime/src/main/scala/org/apache/pekko/grpc/scaladsl/GrpcMarshalling.scala
index dd316325..b781fb85 100644
--- 
a/runtime/src/main/scala/org/apache/pekko/grpc/scaladsl/GrpcMarshalling.scala
+++ 
b/runtime/src/main/scala/org/apache/pekko/grpc/scaladsl/GrpcMarshalling.scala
@@ -57,7 +57,7 @@ object GrpcMarshalling {
   def negotiated[T](req: HttpRequest, f: (GrpcProtocolReader, 
GrpcProtocolWriter) => Future[T]): Option[Future[T]] =
     GrpcProtocol.negotiate(req).map {
       case (Success(reader), writer) => f(reader, writer)
-      case (Failure(ex), _)          => Future.failed(ex)
+      case (Failure(ex), _)          => FastFuture.failed(ex)
     }
 
   /**
@@ -87,7 +87,7 @@ object GrpcMarshalling {
   def unmarshal[T](
       entity: HttpEntity)(implicit u: ProtobufSerializer[T], mat: 
Materializer, reader: GrpcProtocolReader): Future[T] =
     entity match {
-      case HttpEntity.Strict(_, data) => 
Future.fromTry(Try(u.deserialize(reader.decodeSingleFrame(data))))
+      case HttpEntity.Strict(_, data) => 
FastFuture(Try(u.deserialize(reader.decodeSingleFrame(data))))
       case _                          => unmarshal(entity.dataBytes)
     }
 
@@ -95,7 +95,7 @@ object GrpcMarshalling {
       implicit u: ProtobufSerializer[T],
       @nowarn("msg=is never used") mat: Materializer,
       reader: GrpcProtocolReader): Future[Source[T, NotUsed]] = {
-    Future.successful(
+    FastFuture.successful(
       data
         .mapMaterializedValue(_ => NotUsed)
         .via(reader.dataFrameDecoder)
diff --git 
a/runtime/src/main/scala/org/apache/pekko/grpc/scaladsl/ServiceHandler.scala 
b/runtime/src/main/scala/org/apache/pekko/grpc/scaladsl/ServiceHandler.scala
index b020474a..970b2da7 100644
--- a/runtime/src/main/scala/org/apache/pekko/grpc/scaladsl/ServiceHandler.scala
+++ b/runtime/src/main/scala/org/apache/pekko/grpc/scaladsl/ServiceHandler.scala
@@ -19,16 +19,17 @@ import pekko.grpc.GrpcProtocol
 import pekko.grpc.internal.{ GrpcProtocolWeb, GrpcProtocolWebText }
 import pekko.http.javadsl.{ model => jmodel }
 import pekko.http.scaladsl.model.{ HttpRequest, HttpResponse, StatusCodes }
+import pekko.http.scaladsl.util.FastFuture
 
 import scala.concurrent.Future
 
 @ApiMayChange
 object ServiceHandler {
 
-  private[scaladsl] val notFound: Future[HttpResponse] = 
Future.successful(HttpResponse(StatusCodes.NotFound))
+  private[scaladsl] val notFound: Future[HttpResponse] = 
FastFuture.successful(HttpResponse(StatusCodes.NotFound))
 
   private[scaladsl] val unsupportedMediaType: Future[HttpResponse] =
-    Future.successful(HttpResponse(StatusCodes.UnsupportedMediaType))
+    FastFuture.successful(HttpResponse(StatusCodes.UnsupportedMediaType))
 
   private def matchesVariant(variants: Set[GrpcProtocol])(request: 
jmodel.HttpRequest) =
     
variants.exists(_.mediaTypes.contains(request.entity.getContentType.mediaType))
diff --git 
a/runtime/src/test/scala/org/apache/pekko/grpc/scaladsl/GrpcMarshallingSpec.scala
 
b/runtime/src/test/scala/org/apache/pekko/grpc/scaladsl/GrpcMarshallingSpec.scala
new file mode 100644
index 00000000..550a343e
--- /dev/null
+++ 
b/runtime/src/test/scala/org/apache/pekko/grpc/scaladsl/GrpcMarshallingSpec.scala
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.pekko.grpc.scaladsl
+
+import scala.concurrent.{ Await, ExecutionContext }
+import scala.concurrent.duration._
+import scala.util.Success
+
+import com.google.protobuf.ByteString
+import com.google.protobuf.any.{ Any => ScalapbAny }
+import org.scalatest.BeforeAndAfterAll
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+import org.apache.pekko
+import pekko.actor.ActorSystem
+import pekko.grpc.GrpcProtocol.{ GrpcProtocolReader, GrpcProtocolWriter }
+import pekko.grpc.internal.{ AbstractGrpcProtocol, GrpcProtocolNative, 
Identity }
+import pekko.http.scaladsl.model.HttpEntity
+import pekko.stream.{ Materializer, SystemMaterializer }
+import pekko.util.{ ByteString => PekkoByteString }
+
+class GrpcMarshallingSpec extends AnyWordSpec with Matchers with 
BeforeAndAfterAll {
+
+  private implicit val system: ActorSystem = ActorSystem("GrpcMarshallingSpec")
+  private implicit val mat: Materializer = 
SystemMaterializer(system).materializer
+  private implicit val serializer: ScalapbProtobufSerializer[ScalapbAny] =
+    new ScalapbProtobufSerializer(ScalapbAny)
+  private implicit val reader: GrpcProtocolReader = 
GrpcProtocolNative.newReader(Identity)
+  private implicit val writer: GrpcProtocolWriter = 
GrpcProtocolNative.newWriter(Identity)
+
+  /**
+   * An ExecutionContext that refuses to run anything, so that a transform 
which needs to be
+   * scheduled fails loudly instead of silently completing on another thread.
+   */
+  private object NoDispatch extends ExecutionContext {
+    override def execute(runnable: Runnable): Unit =
+      throw new AssertionError("transform was scheduled on an ExecutionContext 
instead of completing directly")
+    override def reportFailure(cause: Throwable): Unit = throw cause
+  }
+
+  private val message = ScalapbAny("type.googleapis.com/test", 
ByteString.copyFromUtf8("payload"))
+
+  private def strictEntity(data: PekkoByteString): HttpEntity.Strict =
+    HttpEntity.Strict(GrpcProtocolNative.contentType, data)
+
+  private val validEntity =
+    
strictEntity(AbstractGrpcProtocol.encodeFrameData(serializer.serialize(message),
 isCompressed = false,
+      isTrailer = false))
+
+  // A single zero byte is an invalid protobuf tag, so deserialization of this 
frame fails.
+  private val corruptEntity =
+    strictEntity(
+      AbstractGrpcProtocol.encodeFrameData(PekkoByteString(0), isCompressed = 
false, isTrailer = false))
+
+  override protected def afterAll(): Unit = Await.result(system.terminate(), 
10.seconds)
+
+  "The scaladsl GrpcMarshalling" should {
+
+    "complete unmarshal of a strict entity directly, without scheduling" in {
+      val unmarshalled = GrpcMarshalling.unmarshal[ScalapbAny](validEntity)
+
+      unmarshalled.value should be(Some(Success(message)))
+      // A generated handler chains further transforms onto this future; they 
must not need a dispatch either.
+      unmarshalled.flatMap(a => 
GrpcMarshalling.unmarshal[ScalapbAny](validEntity).map(_ => a)(NoDispatch))(
+        NoDispatch).value should be(Some(Success(message)))
+    }
+
+    "complete a failed unmarshal of a strict entity directly, without 
scheduling" in {
+      val unmarshalled = GrpcMarshalling.unmarshal[ScalapbAny](corruptEntity)
+
+      unmarshalled.value.map(_.isFailure) should be(Some(true))
+      unmarshalled.recover { case _ => message }(NoDispatch).value should 
be(Some(Success(message)))
+    }
+
+    "complete unmarshalStream directly, without scheduling" in {
+      val unmarshalled = 
GrpcMarshalling.unmarshalStream[ScalapbAny](validEntity)
+
+      unmarshalled.value.map(_.isSuccess) should be(Some(true))
+      unmarshalled.map(_ => message)(NoDispatch).value should 
be(Some(Success(message)))
+    }
+
+    "complete the exception handler response directly, without scheduling" in {
+      val handled =
+        
GrpcExceptionHandler.from(GrpcExceptionHandler.defaultMapper(system))(system, 
writer)(
+          new RuntimeException("boom"))
+
+      handled.value.map(_.isSuccess) should be(Some(true))
+      handled.map(_.status.intValue)(NoDispatch).value should 
be(Some(Success(200)))
+      handled.value.get.get.getHeader("grpc-status").get.value should be("13")
+    }
+
+    "complete the not-found and unsupported-media-type responses directly, 
without scheduling" in {
+      ServiceHandler.notFound.map(_.status.intValue)(NoDispatch).value should 
be(Some(Success(404)))
+      
ServiceHandler.unsupportedMediaType.map(_.status.intValue)(NoDispatch).value 
should be(Some(Success(415)))
+    }
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to