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 5d493211 apply deadline on pekko-http backend (#824)
5d493211 is described below

commit 5d4932115c7e5f861c3ef9e02c447ad00ef9c9b3
Author: PJ Fanning <[email protected]>
AuthorDate: Sat Aug 8 08:37:45 2026 +0100

    apply deadline on pekko-http backend (#824)
    
    * apply deadline on pekko-http backend
    
    * Refactor applyDeadline call for clarity
    
    * Update PekkoHttpClientUtils.scala
---
 .../pekko/grpc/internal/PekkoHttpClientUtils.scala | 32 +++++++++++++++++++-
 .../grpc/internal/PekkoHttpClientUtilsSpec.scala   | 35 +++++++++++++++++++++-
 2 files changed, 65 insertions(+), 2 deletions(-)

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 c4548ed4..4cda5ef2 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
@@ -37,6 +37,7 @@ import io.grpc.{ CallOptions, MethodDescriptor, Status, 
StatusRuntimeException }
 import javax.net.ssl.{ KeyManager, SSLContext, TrustManager }
 import scala.collection.immutable
 import scala.concurrent.{ ExecutionContext, Future, Promise }
+import scala.concurrent.duration.DurationLong
 import scala.jdk.FutureConverters._
 import scala.util.{ Failure, Success }
 
@@ -207,7 +208,36 @@ object PekkoHttpClientUtils {
             descriptor.getFullMethodName),
           GrpcEntityHelpers.metadataHeaders(headers.entries),
           source)
-        responseToSource(singleRequest(httpRequest), deserializer)
+        applyDeadline(responseToSource(singleRequest(httpRequest), 
deserializer), options)
+      }
+    }
+  }
+
+  /**
+   * INTERNAL API
+   */
+  @InternalApi
+  private[internal] def applyDeadline[O](source: Source[O, 
Future[GrpcResponseMetadata]], options: CallOptions)
+      : Source[O, Future[GrpcResponseMetadata]] = {
+    val deadline = options.getDeadline
+    if (deadline == null) source
+    else {
+      val remaining = 
deadline.timeRemaining(java.util.concurrent.TimeUnit.MILLISECONDS)
+      if (remaining <= 0) {
+        val ex = Status.DEADLINE_EXCEEDED
+          .withDescription(s"Deadline expired ${-remaining} ms ago")
+          .asRuntimeException()
+        Source.failed(ex).mapMaterializedValue(_ => Future.failed(ex))
+      } else {
+        source.completionTimeout(remaining.millis).recoverWithRetries(
+          1,
+          {
+            case _: pekko.stream.CompletionTimeoutException =>
+              val ex = Status.DEADLINE_EXCEEDED
+                .withDescription(s"Deadline of ${remaining}ms exceeded")
+                .asRuntimeException()
+              Source.failed(ex)
+          })
       }
     }
   }
diff --git 
a/runtime/src/test/scala/org/apache/pekko/grpc/internal/PekkoHttpClientUtilsSpec.scala
 
b/runtime/src/test/scala/org/apache/pekko/grpc/internal/PekkoHttpClientUtilsSpec.scala
index b67a59ed..36db8f6a 100644
--- 
a/runtime/src/test/scala/org/apache/pekko/grpc/internal/PekkoHttpClientUtilsSpec.scala
+++ 
b/runtime/src/test/scala/org/apache/pekko/grpc/internal/PekkoHttpClientUtilsSpec.scala
@@ -18,13 +18,15 @@ import scala.concurrent.duration._
 
 import org.apache.pekko
 import pekko.actor.ActorSystem
+import pekko.grpc.GrpcResponseMetadata
 import pekko.http.scaladsl.model.HttpEntity.Strict
 import pekko.http.scaladsl.model._
 import pekko.http.scaladsl.model.StatusCodes._
 import pekko.http.scaladsl.model.headers.RawHeader
+import pekko.stream.scaladsl.{ Sink, Source }
 import pekko.testkit.TestKit
 import pekko.util.ByteString
-import io.grpc.{ Metadata, Status, StatusRuntimeException }
+import io.grpc.{ CallOptions, Deadline, Metadata, Status, 
StatusRuntimeException }
 import org.scalatest.concurrent.ScalaFutures
 import org.scalatest.matchers.should.Matchers
 import org.scalatest.time.Span
@@ -86,4 +88,35 @@ class PekkoHttpClientUtilsSpec extends 
TestKit(ActorSystem()) with AnyWordSpecLi
     lazy val key = Metadata.Key.of("custom-key", 
Metadata.ASCII_STRING_MARSHALLER)
     lazy val keyBin = Metadata.Key.of("custom-key-bin", 
Metadata.BINARY_BYTE_MARSHALLER)
   }
+
+  "applyDeadline" should {
+    type Resp = Future[GrpcResponseMetadata]
+
+    "pass source through unchanged when no deadline is set" in {
+      val source = Source.single(ByteString(1, 2, 3))
+      val options = CallOptions.DEFAULT
+      val result = 
PekkoHttpClientUtils.applyDeadline(source.asInstanceOf[Source[Any, Resp]], 
options)
+      val value = result.runWith(Sink.head).futureValue
+      value shouldBe ByteString(1, 2, 3)
+    }
+
+    "fail immediately when deadline is already expired" in {
+      val source = Source.single(ByteString(1, 2, 3))
+      val options = CallOptions.DEFAULT.withDeadline(Deadline.after(0, 
java.util.concurrent.TimeUnit.MILLISECONDS))
+      val result = 
PekkoHttpClientUtils.applyDeadline(source.asInstanceOf[Source[Any, Resp]], 
options)
+      val failure = result.runWith(Sink.head).failed.futureValue
+      failure shouldBe a[StatusRuntimeException]
+      failure.asInstanceOf[StatusRuntimeException].getStatus.getCode shouldBe 
Status.Code.DEADLINE_EXCEEDED
+    }
+
+    "fail with DEADLINE_EXCEEDED when source does not complete in time" in {
+      // Source that never completes
+      val source = Source.maybe[ByteString]
+      val options = CallOptions.DEFAULT.withDeadline(Deadline.after(100, 
java.util.concurrent.TimeUnit.MILLISECONDS))
+      val result = 
PekkoHttpClientUtils.applyDeadline(source.asInstanceOf[Source[Any, Resp]], 
options)
+      val failure = result.runWith(Sink.head).failed.futureValue
+      failure shouldBe a[StatusRuntimeException]
+      failure.asInstanceOf[StatusRuntimeException].getStatus.getCode shouldBe 
Status.Code.DEADLINE_EXCEEDED
+    }
+  }
 }


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

Reply via email to