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-http.git


The following commit(s) were added to refs/heads/main by this push:
     new 9c63776a9 Follow-up to max-connection-age: infinite grace test, docs, 
and FOREVER round-trip in Java setters (#1323)
9c63776a9 is described below

commit 9c63776a983bce89ea93a666b94f8e5d0cb42612
Author: PJ Fanning <[email protected]>
AuthorDate: Sat Oct 3 11:32:26 2026 +0100

    Follow-up to max-connection-age: infinite grace test, docs, and FOREVER 
round-trip in Java setters (#1323)
    
    * Follow-up to max-connection-age review: document and test infinite grace 
with a later terminate
    
    Motivation:
    Review of #1316 noted that reference.conf only documents one direction of
    the interaction between max-connection-age and a server binding termination,
    that the combination of max-connection-age-grace = infinite with a later
    terminate was not tested, and that some new tests had tight timing margins.
    
    Modification:
    - reference.conf: state that a later terminate with an earlier deadline
      shortens a drain started by the age, also with an infinite grace period,
      mirroring http2.md.
    - Http2ServerSpec: add a test for max-connection-age-grace = infinite
      followed by terminate(10.millis).
    - Http2ServerSpec: widen timing margins (age 1s vs expectNoBytes(700ms),
      terminate(2s) vs expectNoBytes(700ms)).
    
    Result:
    The documented and tested behaviour covers both directions, and the timing
    sensitive tests have more headroom on loaded CI machines.
    
    Tests:
    - sbt "http2-tests/testOnly ...Http2ServerSpec -- -z max-connection-age": 7 
tests pass
    - scalafmt on the changed spec
    
    References:
    Refs #1316
    
    * Map ChronoUnit.FOREVER back to Duration.Inf in the other Java duration 
setters
    
    Motivation:
    Getters such as ConnectionPoolSettings.getKeepAliveTimeout return
    ChronoUnit.FOREVER.getDuration for an infinite value, but the matching Java
    setters converted with toScala, which throws IllegalArgumentException for
    that value. Passing a getter's result back to its setter failed on the
    default settings of keep-alive-timeout, max-connection-lifetime and
    periodic-keep-alive-max-idle.
    
    Modification:
    Use JavaDurationConverter.toScala, the inverse of the toJava used by the
    getters, in the Java setters of ClientConnectionSettings.idleTimeout,
    ConnectionPoolSettings idleTimeout, keepAliveTimeout, maxConnectionLifetime
    and responseEntitySubscriptionTimeout, and
    WebSocketSettings.periodicKeepAliveMaxIdle.
    
    Result:
    Infinite durations round-trip through the Java API.
    
    Tests:
    - sbt "http-core/testOnly org.apache.pekko.http.scaladsl.settings.*": 27 
pass;
      the 3 new round-trip tests fail without the fix
    - sbt "http-core/mimaReportBinaryIssues": clean
    
    References:
    Refs #1316
---
 http-core/src/main/resources/reference.conf        |  3 +-
 .../settings/ClientConnectionSettingsImpl.scala    |  2 +-
 .../impl/settings/ConnectionPoolSettingsImpl.scala |  8 ++---
 .../http/javadsl/settings/WebSocketSettings.scala  |  4 +--
 .../settings/ClientConnectionSettingsSpec.scala    | 22 +++++++++++++-
 .../settings/ConnectionPoolSettingsSpec.scala      | 35 ++++++++++++++++++++++
 .../http/impl/engine/http2/Http2ServerSpec.scala   | 31 ++++++++++++++++---
 7 files changed, 92 insertions(+), 13 deletions(-)

diff --git a/http-core/src/main/resources/reference.conf 
b/http-core/src/main/resources/reference.conf
index bea6d5582..e591cead0 100644
--- a/http-core/src/main/resources/reference.conf
+++ b/http-core/src/main/resources/reference.conf
@@ -366,7 +366,8 @@ pekko.http {
       #
       # If the connection is already being terminated when its age expires, 
for example because the server
       # binding is being terminated, the expiry has no effect and the 
termination in progress keeps its own
-      # deadline.
+      # deadline. Conversely, terminating the server binding with an earlier 
deadline shortens a drain that the
+      # age started, also when `max-connection-age-grace` is `infinite`.
       #
       # The value `infinite` disables this mechanism and is the default.
       max-connection-age = infinite
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/impl/settings/ClientConnectionSettingsImpl.scala
 
b/http-core/src/main/scala/org/apache/pekko/http/impl/settings/ClientConnectionSettingsImpl.scala
index c5b13e8d8..a8e02f9b8 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/impl/settings/ClientConnectionSettingsImpl.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/impl/settings/ClientConnectionSettingsImpl.scala
@@ -60,7 +60,7 @@ private[pekko] final case class ClientConnectionSettingsImpl(
     withConnectingTimeout(newValue.toScala)
 
   override def withIdleTimeout(newValue: java.time.Duration): 
pekko.http.scaladsl.settings.ClientConnectionSettings =
-    withIdleTimeout(newValue.toScala)
+    withIdleTimeout(JavaDurationConverter.toScala(newValue))
 
   override def withStreamCancellationDelay(
       newValue: java.time.Duration): 
pekko.http.scaladsl.settings.ClientConnectionSettings =
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/impl/settings/ConnectionPoolSettingsImpl.scala
 
b/http-core/src/main/scala/org/apache/pekko/http/impl/settings/ConnectionPoolSettingsImpl.scala
index f0dbf40d1..7aa72ba69 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/impl/settings/ConnectionPoolSettingsImpl.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/impl/settings/ConnectionPoolSettingsImpl.scala
@@ -66,13 +66,13 @@ private[pekko] final case class ConnectionPoolSettingsImpl(
     withMaxConnectionBackoff(newValue.toScala)
 
   override def withIdleTimeout(newValue: java.time.Duration): 
ConnectionPoolSettings =
-    withIdleTimeout(newValue.toScala)
+    withIdleTimeout(JavaDurationConverter.toScala(newValue))
 
   override def withKeepAliveTimeout(newValue: java.time.Duration): 
ConnectionPoolSettings =
-    withKeepAliveTimeout(newValue.toScala)
+    withKeepAliveTimeout(JavaDurationConverter.toScala(newValue))
 
   override def withMaxConnectionLifetime(newValue: java.time.Duration): 
ConnectionPoolSettings =
-    withMaxConnectionLifetime(newValue.toScala)
+    withMaxConnectionLifetime(JavaDurationConverter.toScala(newValue))
 
   def withUpdatedConnectionSettings(
       f: ClientConnectionSettings => ClientConnectionSettings): 
ConnectionPoolSettingsImpl =
@@ -80,7 +80,7 @@ private[pekko] final case class ConnectionPoolSettingsImpl(
       hostOverrides = hostOverrides.map { case (k, v) => k -> 
v.withUpdatedConnectionSettings(f) })
 
   override def withResponseEntitySubscriptionTimeout(newValue: 
java.time.Duration): ConnectionPoolSettings =
-    withResponseEntitySubscriptionTimeout(newValue.toScala)
+    
withResponseEntitySubscriptionTimeout(JavaDurationConverter.toScala(newValue))
 
   /** INTERNAL API */
   private[http] def copyDeep(
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/WebSocketSettings.scala
 
b/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/WebSocketSettings.scala
index a8d90ef30..eabe96f42 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/WebSocketSettings.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/WebSocketSettings.scala
@@ -21,11 +21,11 @@ import org.apache.pekko
 import pekko.actor.ActorSystem
 import pekko.annotation.DoNotInherit
 import pekko.http.impl.settings.WebSocketSettingsImpl
+import pekko.http.impl.util.JavaDurationConverter
 import pekko.util.ByteString
 import com.typesafe.config.Config
 
 import scala.concurrent.duration.Duration
-import scala.jdk.DurationConverters._
 
 /**
  * Public API but not intended for subclassing
@@ -60,7 +60,7 @@ trait WebSocketSettings { self: WebSocketSettingsImpl =>
    * @since 1.3.0
    */
   def withPeriodicKeepAliveMaxIdle(newValue: JDuration): WebSocketSettings =
-    copy(periodicKeepAliveMaxIdle = newValue.toScala)
+    copy(periodicKeepAliveMaxIdle = JavaDurationConverter.toScala(newValue))
   def withPeriodicKeepAliveData(newValue: Supplier[ByteString]): 
WebSocketSettings =
     copy(periodicKeepAliveData = () => newValue.get())
 
diff --git 
a/http-core/src/test/scala/org/apache/pekko/http/scaladsl/settings/ClientConnectionSettingsSpec.scala
 
b/http-core/src/test/scala/org/apache/pekko/http/scaladsl/settings/ClientConnectionSettingsSpec.scala
index a6eda2193..147d04705 100644
--- 
a/http-core/src/test/scala/org/apache/pekko/http/scaladsl/settings/ClientConnectionSettingsSpec.scala
+++ 
b/http-core/src/test/scala/org/apache/pekko/http/scaladsl/settings/ClientConnectionSettingsSpec.scala
@@ -13,6 +13,26 @@
 
 package org.apache.pekko.http.scaladsl.settings
 
+import java.time.temporal.ChronoUnit
+
+import scala.concurrent.duration._
+
 import org.apache.pekko.testkit.PekkoSpec
 
-class ClientConnectionSettingsSpec extends PekkoSpec {}
+class ClientConnectionSettingsSpec extends PekkoSpec {
+  "ClientConnectionSettings" should {
+    "round-trip an infinite idle-timeout through the Java API" in {
+      val settings = 
ClientConnectionSettings(system).withIdleTimeout(Duration.Inf)
+      settings.getIdleTimeout should ===(ChronoUnit.FOREVER.getDuration)
+      val roundTripped = settings.withIdleTimeout(settings.getIdleTimeout)
+      roundTripped.getIdleTimeout should ===(ChronoUnit.FOREVER.getDuration)
+    }
+
+    "round-trip an infinite websocket periodic-keep-alive-max-idle through the 
Java API" in {
+      val settings = 
ClientConnectionSettings(system).websocketSettings.withPeriodicKeepAliveMaxIdle(Duration.Inf)
+      settings.getPeriodicKeepAliveMaxIdle should 
===(ChronoUnit.FOREVER.getDuration)
+      val roundTripped = 
settings.withPeriodicKeepAliveMaxIdle(settings.getPeriodicKeepAliveMaxIdle)
+      roundTripped.getPeriodicKeepAliveMaxIdle should 
===(ChronoUnit.FOREVER.getDuration)
+    }
+  }
+}
diff --git 
a/http-core/src/test/scala/org/apache/pekko/http/scaladsl/settings/ConnectionPoolSettingsSpec.scala
 
b/http-core/src/test/scala/org/apache/pekko/http/scaladsl/settings/ConnectionPoolSettingsSpec.scala
index f82a0cb75..07b1e067e 100644
--- 
a/http-core/src/test/scala/org/apache/pekko/http/scaladsl/settings/ConnectionPoolSettingsSpec.scala
+++ 
b/http-core/src/test/scala/org/apache/pekko/http/scaladsl/settings/ConnectionPoolSettingsSpec.scala
@@ -13,6 +13,10 @@
 
 package org.apache.pekko.http.scaladsl.settings
 
+import java.time.temporal.ChronoUnit
+
+import scala.concurrent.duration._
+
 import org.apache.pekko
 import pekko.testkit.PekkoSpec
 import pekko.http.scaladsl.model.headers.`User-Agent`
@@ -160,6 +164,37 @@ class ConnectionPoolSettingsSpec extends PekkoSpec {
     }
   }
 
+  "ConnectionPoolSettings Java API" should {
+    "round-trip infinite durations" in {
+      val settings = ConnectionPoolSettings(system)
+        .withIdleTimeout(Duration.Inf)
+        .withKeepAliveTimeout(Duration.Inf)
+        .withMaxConnectionLifetime(Duration.Inf)
+        .withResponseEntitySubscriptionTimeout(Duration.Inf)
+      val forever = ChronoUnit.FOREVER.getDuration
+      settings.getIdleTimeout should ===(forever)
+      settings.getKeepAliveTimeout should ===(forever)
+      settings.getMaxConnectionLifetime should ===(forever)
+      settings.getResponseEntitySubscriptionTimeout should ===(forever)
+
+      val roundTripped = settings
+        .withIdleTimeout(settings.getIdleTimeout)
+        .withKeepAliveTimeout(settings.getKeepAliveTimeout)
+        .withMaxConnectionLifetime(settings.getMaxConnectionLifetime)
+        
.withResponseEntitySubscriptionTimeout(settings.getResponseEntitySubscriptionTimeout)
+      roundTripped.getIdleTimeout should ===(forever)
+      roundTripped.getKeepAliveTimeout should ===(forever)
+      roundTripped.getMaxConnectionLifetime should ===(forever)
+      roundTripped.getResponseEntitySubscriptionTimeout should ===(forever)
+    }
+
+    "round-trip finite durations" in {
+      val settings = ConnectionPoolSettings(system).withIdleTimeout(42.seconds)
+      settings.getIdleTimeout should ===(java.time.Duration.ofSeconds(42))
+      settings.withIdleTimeout(settings.getIdleTimeout).getIdleTimeout should 
===(java.time.Duration.ofSeconds(42))
+    }
+  }
+
   def config(configString: String): ConnectionPoolSettings =
     ConnectionPoolSettings(configString)
 }
diff --git 
a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala
 
b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala
index 2b6d19ab4..f7aea6ccf 100644
--- 
a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala
+++ 
b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala
@@ -2119,12 +2119,12 @@ class Http2ServerSpec extends 
Http2SpecWithMaterializer("""
           override def settings: ServerSettings = {
             val default = super.settings
             default.withHttp2Settings(
-              
default.http2Settings.withMaxConnectionAge(500.millis).withMaxConnectionAgeJitter(0))
+              
default.http2Settings.withMaxConnectionAge(1.second).withMaxConnectionAgeJitter(0))
           }
 
-          // with jitter disabled no GOAWAY is sent before the configured age: 
checked for 400 of the 500 ms,
+          // with jitter disabled no GOAWAY is sent before the configured age: 
checked for 700 of the 1000 ms,
           // the rest is left as a margin for timer scheduling
-          network.expectNoBytes(400.millis)
+          network.expectNoBytes(700.millis)
           val (_, errorCode) = network.expectGOAWAY()
           errorCode should ===(ErrorCode.NO_ERROR)
           network.expectComplete()
@@ -2190,6 +2190,29 @@ class Http2ServerSpec extends 
Http2SpecWithMaterializer("""
           network.expectComplete()
           terminated.futureValue
         })
+      "close within the deadline of a later server binding termination while 
draining with an infinite grace period".inAssertAllStagesStopped(
+        new TestSetup with RequestResponseProbes {
+          override def settings: ServerSettings = {
+            val default = super.settings
+            default.withHttp2Settings(
+              
default.http2Settings.withMaxConnectionAge(500.millis).withMaxConnectionAgeJitter(0)
+                .withMaxConnectionAgeGrace(Duration.Inf))
+          }
+
+          network.sendRequest(1, HttpRequest())
+          user.expectRequest()
+
+          val (_, errorCode) = network.expectGOAWAY(1)
+          errorCode should ===(ErrorCode.NO_ERROR)
+
+          // with an infinite grace period no forced close is scheduled, the 
connection waits for the request in flight
+          network.expectNoBytes(300.millis)
+
+          // so the deadline of a later termination always wins
+          val terminated = serverTerminator.terminate(10.millis)
+          network.expectComplete()
+          terminated.futureValue
+        })
       "keep the remaining grace period when a later server binding termination 
has a later deadline".inAssertAllStagesStopped(
         new TestSetup with RequestResponseProbes {
           override def settings: ServerSettings = {
@@ -2222,7 +2245,7 @@ class Http2ServerSpec extends 
Http2SpecWithMaterializer("""
           network.sendRequest(1, HttpRequest())
           user.expectRequest()
 
-          val terminated = serverTerminator.terminate(1.second)
+          val terminated = serverTerminator.terminate(2.seconds)
           val (_, errorCode) = network.expectGOAWAY(1)
           errorCode should ===(ErrorCode.NO_ERROR)
 


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

Reply via email to