Skip to content

feat: add max-connection-age setting for HTTP/2 server connections - #1316

Merged
pjfanning merged 15 commits into
apache:mainfrom
Kreinoee:http2-server-max-connection-age
Oct 2, 2026
Merged

pjfanning merged 15 commits into
apache:mainfrom
Kreinoee:http2-server-max-connection-age

Conversation

@Kreinoee

@Kreinoee Kreinoee commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Long-lived HTTP/2 connections (as used by gRPC) lead to an uneven load distribution across server
instances: clients stay connected to the instances they found at connect time, and instances added
later (after a scale-out or a rolling deploy) receive no share of the existing traffic. The server
is the side that can retire a connection gracefully, via GOAWAY. grpc-java offers this as
maxConnectionAge; pekko-http has no equivalent (akka/akka-grpc#967 is the corresponding request
on the Akka side).

Modification

Add a pekko.http.server.http2.max-connection-age setting, default infinite (disabled). When a
server connection reaches the configured age, the existing graceful termination path is triggered:
GOAWAY(NO_ERROR) is sent, streams that are in flight complete normally, streams opened after the
GOAWAY are refused with RST_STREAM(REFUSED_STREAM), and the connection is closed once no streams
remain.

The age is jittered per connection by a configurable fraction, max-connection-age-jitter (default
0.1 = +/- 10%, the value grpc-java applies; 0 disables jitter), so that connections that were opened
together, for example after a deploy, are not all closed at the same time.

triggerTermination now accepts an infinite deadline, in which case no forced-close timer is
scheduled. Also corrects the termination debug log, which printed the timer key instead of the
deadline.

Result

Operators can cap the lifetime of server-side HTTP/2 connections to rebalance long-lived
connections across server instances. Behavior is unchanged by default.

Verified against a real grpc-java (1.75.0) client: with max-connection-age = 5s on a pekko-http
server, a client issuing a unary call every 200 ms for 16 s observed 0 failures across 3 connection
retirements (the server saw 4 distinct client connections), and a unary call that was in flight when
the GOAWAY was sent completed normally on the connection being drained.

Tests

  • sbt "http2-tests/test": 376 tests pass, including 2 new directional tests for
    max-connection-age in Http2ServerSpec (GOAWAY(NO_ERROR) + connection close on an idle
    connection; in-flight stream completes and late stream is refused)
  • sbt "+http-core/mimaReportBinaryIssues": clean, with 5 new ReversedMissingMethodProblem
    filters for the added methods on Http2ServerSettings (@ApiMayChange/@DoNotInherit) and the
    internal Http2Demux
  • sbt scalafmtCheckAll scalafmtSbtCheck: clean
  • sbt headerCreateAll: no changes
  • sbt docs/paradox: builds (remaining warnings are pre-existing and unrelated)
  • sbt validatePullRequest: passes
  • sbt sortImports: environment failure unrelated to this change (http/scalafixAll fails with
    NoSuchMethodError in scala.meta on a clean checkout of main too); the files changed here are
    sort-clean
  • manual end-to-end check against grpc-java 1.75.0 (see Result)

References

None - no pekko-http issue tracks this; akka/akka-grpc#967 is the equivalent request against
akka-http/akka-grpc.

Motivation:
Long-lived HTTP/2 connections (as used by gRPC) lead to an uneven load
distribution across server instances: clients stay connected to the
instances they found at connect time, and instances added later (after
a scale-out or a rolling deploy) receive no share of the existing
traffic. The server is the side that can retire a connection
gracefully, via GOAWAY. grpc-java offers this as maxConnectionAge;
pekko-http has no equivalent (akka/akka-grpc#967 is the corresponding
request on the Akka side).

Modification:
Add a `pekko.http.server.http2.max-connection-age` setting, default
`infinite` (disabled). When a server connection reaches the configured
age, the existing graceful termination path is triggered:
GOAWAY(NO_ERROR) is sent, streams that are in flight complete normally,
streams opened after the GOAWAY are refused with
RST_STREAM(REFUSED_STREAM), and the connection is closed once no
streams remain. The age is jittered per connection by a configurable
fraction, `max-connection-age-jitter` (default 0.1 = +/- 10%, the value
grpc-java applies; 0 disables jitter), so that connections that were
opened together are not all closed at the same time.
`triggerTermination` now accepts an infinite deadline, in which case no
forced-close timer is scheduled. Also corrects the termination debug
log, which printed the timer key instead of the deadline.

Result:
Operators can cap the lifetime of server-side HTTP/2 connections to
rebalance long-lived connections across server instances. Behavior is
unchanged by default.

Tests:
- sbt "http2-tests/test": 376 tests pass, including 2 new directional
  tests for max-connection-age in Http2ServerSpec
- sbt validatePullRequest: passes
- sbt "+http-core/mimaReportBinaryIssues": clean, with 5 new
  ReversedMissingMethodProblem filters for the added methods
- sbt checkCodeStyle: clean; sbt headerCreateAll: no changes
- sbt docs/paradox: builds, remaining warnings pre-existing
- sbt sortImports: environment failure unrelated to this change
  (http/scalafixAll fails with NoSuchMethodError in scala.meta on a
  clean checkout of main too); the files changed here are sort-clean
- manual end-to-end check against grpc-java 1.75.0: with
  max-connection-age = 5s, a client calling every 200 ms for 16 s saw
  0 failures across 3 connection retirements, and a unary call that
  was in flight when the GOAWAY was sent completed normally

References:
None - no pekko-http issue tracks this; akka/akka-grpc#967 is the
equivalent request against akka-http/akka-grpc.
@pjfanning

Copy link
Copy Markdown
Member

1.4.1 is in RC - so use 1.4.2 here, replace the 2.0.0 values. Once this is merged, we can backport the change to the 1.4.x branch.

@@ -0,0 +1,23 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move to 1.4.x.backwards.excludes

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually hold off on the version changes because I'm wondering if these chnages are too much for 1.x

There should be a 2.0.0 milestone release in the next few weeks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just change the @SInCE annotations, but they can easily be changed back. Just let me know what you decide.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kreinoee apologies but I had changed my mind - can we concentrate on this as a 2.0.0 change for now

1247 is a security hardening fix but this one seems more like a new feature

I would hope that we can get a new 2.0.0 milestone out in the next few weeks.

We can consider this for a future 1.5.0 release but I'd like to concentrate on getting it in 2.0.0 first.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, I will just change them back. But can I ask when 2.0.0 is estimated to get released. We are running 1.x in our production stack and have a need for this, so I would just like to know if I should look into an work arround on our own side until it is released.

And thanks for the super fast review, really appriciated. I am unfortunately out of time for today, and I have an all day meeting tommorrow, but I should have some time in the evening tomorrow, where I will have a look at you comments.

@pjfanning

Copy link
Copy Markdown
Member

Thanks for this, the feature is useful and reusing the existing graceful-termination path is the right approach. The jitter handling and the corrected termination debug log look good.

Scope: we'd like to treat this as a 2.0.0-only change for now. It is a new feature (new public settings and methods on Http2ServerSettings, new config keys, new connection-lifecycle behaviour) rather than a fix, so it doesn't fit a 1.4.x patch release. The max-header-list-size backport (#1247) was an exception because it addressed unbounded memory use from never-finished CONTINUATION sequences (RFC 9113 §10.5). Please close #1317. On your @since question: the tag should name the first release that carries the API, so @since 2.0.0 and the 2.0.x.backwards.excludes filter file are correct here as written.

Issues to address

1. The drain after max-connection-age is unbounded (Http2Demux.scala, MaxConnectionAge timer)

Before this PR, every call to triggerTermination passed a finite deadline (its only caller was terminate(deadline: FiniteDuration)), so a draining connection always had a CompletionTimeout that force-closes it. triggerTermination(Duration.Inf) introduces the first drain with no deadline: a long-running stream (e.g. gRPC server streaming) keeps the connection open indefinitely, which also defeats the rebalancing this feature is for.

Please add a max-connection-age-grace setting with a finite default (grpc-java has the equivalent maxConnectionAgeGrace), pass it from the MaxConnectionAge timer, and keep triggerTermination(deadline: FiniteDuration) unchanged. The widened Duration signature and the deadline match then go away.

2. A later ServerBinding.terminate(hardDeadline) is ignored on a connection that is already draining

triggerTermination is guarded by if (!terminating). Once the max-connection-age drain has started, a binding-level terminate(hardDeadline) (e.g. from CoordinatedShutdown) is a no-op for that connection. For HTTP/2 the demux logic is the registered per-connection ServerTerminator, and GracefulTerminatorStage only covers HTTP/1.1, so nothing else enforces the hard deadline. With the current Duration.Inf the binding's termination can hang for as long as a stream stays open. With a finite grace (issue 1) it still overruns whenever hardDeadline is shorter than the remaining grace.

When already terminating, please (re)schedule CompletionTimeout if the new deadline is earlier than the one already scheduled, and add a directional test: age expires with a stream in flight, terminate(short) is called, and the connection closes within the short deadline.

3. The Java API cannot express or round-trip "infinite" (javadsl/settings/Http2ServerSettings.scala)

getMaxConnectionAge maps Duration.Inf to ChronoUnit.FOREVER.getDuration (via JavaDurationConverter.toJava), but withMaxConnectionAge(java.time.Duration) calls age.toMillis, which throws ArithmeticException for that value. So settings.withMaxConnectionAge(settings.getMaxConnectionAge) fails with the default settings, and Java users have no way to disable an age that was set in config. Please map ChronoUnit.FOREVER.getDuration back to Duration.Inf (or add an explicit way to disable it), and add a test covering the round trip.

Minor (docs / comments)

  • docs/.../http2.md and reference.conf: please state that the setting only applies to HTTP/2 connections, and that HTTP/1.1 connections accepted on the same port are not age-limited.
  • Http2ServerSpec: the comment "with jitter disabled the connection is closed no earlier than the configured age" is stronger than the assertion, which only checks for no bytes during 400ms of a 500ms age. Either reword the comment or tighten the margin.

@pjfanning

pjfanning commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

I can't guarantee any release time lines. ASF requires us to have at least 3 PMC members review releases. We are struggling to get that this month.
I would appreciate if you continue to help us to get this merged but if you urgently need a solution for your production use case, you should consider using a service mesh or an L7 load balancer and many of these would let you implement a max connection age.
Pekko strongly encourages users to have something like a service mesh or load balancer anyway unless your traffic is entirely inside your own network or equivalent.

@Kreinoee

Copy link
Copy Markdown
Contributor Author

No pressure intended — I just wanted to know whether it's worth setting up a workaround on our side. A service mesh or L7 load balancer would be more than we need: our gRPC traffic is entirely in-cluster, and client-side load balancing paired with a max connection age is simple and works well for that case.

The likely workaround is cherry-picking this onto my own fork of the 1.4.x branch and releasing it internally until an official release includes it.

And yes, I'll keep helping to get this merged.

…tionAge

withMaxConnectionAge(getMaxConnectionAge) threw ArithmeticException on the
default settings: toMillis overflows on ChronoUnit.FOREVER.getDuration, the
value getMaxConnectionAge returns for an infinite age. Adds the inverse of
JavaDurationConverter.toJava and a round-trip test.
The drain started by the MaxConnectionAge timer had no deadline, so a
stream that never completes kept the connection open indefinitely. The
new setting bounds it, with a default of 30s. The value infinite keeps
the previous behaviour of waiting for all requests in flight to complete.
triggerTermination ignored every call after the first, so a server binding
termination could not enforce its deadline on a connection that was already
draining after max-connection-age. Now an earlier deadline reschedules the
forced close, a later one is ignored. The max-connection-age timer is
cancelled once any termination starts, so the age and its grace period never
shorten a termination in progress.
…rmination starts

Same behaviour, but the rule is visible where it applies: the timer handler
checks whether a termination is already in progress and logs that there is
nothing to do, rather than relying on the cancelled timer's message being
dropped by the stage.
…letion-timeout setting

The message is shared by both sides. On the client the deadline is the
completion-timeout setting, on the server it is the deadline passed to
terminate or the max-connection-age-grace setting.
@Kreinoee

Copy link
Copy Markdown
Contributor Author

I looked into your comments now. Some I implemented as you suggested, and on some I pushed back a little:

Scope: understood, 2.0.0 it is. The @since tags are back to 2.0.0.

1. Unbounded drain. Added max-connection-age-grace, passed from the MaxConnectionAge timer, with a finite default of 30s. The value is a plain judgment call (longer than the 20s request-timeout that bounds ordinary requests, short enough that one stream cannot hold up rebalancing for long), so if you have a better number in mind, I will change it.

One deliberate deviation from your suggestion: infinite is still an accepted value, parsed like max-connection-age, so the widened triggerTermination(deadline: Duration) stays. "Never cut a live stream just to rebalance" is a policy some deployments want, mine included. So if infinite is not allowed, we would set it to an arbitrarily high value, which in practice means the same thing, and I think it is better to be able to say so explicitly. Also, you pointed at grpc-java yourself, but there the default actually is infinite (NettyServerBuilder). With the fix for 2 below, an infinite grace is still bounded by the binding's own terminate deadline at shutdown. If you would rather have the grace strictly finite, say so and we will just have to set it to a very high value in our setup.

2. terminate on a draining connection. I made MaxConnectionAge guarded against a termination that is already in progress, and made triggerTermination reschedule the forced close if the deadline it received is earlier than an already scheduled forced close. In practice that makes it work like this:

  • MaxConnectionAge has no effect if a termination is already in progress
  • calling terminate after MaxConnectionAge has fired only reschedules the forced close timer if its deadline is earlier than the one already scheduled by the MaxConnectionAge logic, or if none was scheduled because the grace is infinite.

I figured that this behaviour is easy to document, and therefore easy for users to reason about: the documentation of max-connection-age just states that it has no effect if a termination is already in progress. The alternative I could think of is the "the connection closes no later than the earliest deadline anyone asked for" semantic, but I believe that would be harder for users to reason about. If you prefer it, or a different solution, then let me know.

3. Java API round trip. Added JavaDurationConverter.toScala, the inverse of the existing toJava, mapping ChronoUnit.FOREVER.getDuration back to Duration.Inf. withMaxConnectionAge and the new withMaxConnectionAgeGrace go through it, and a new Http2ServerSettingsSpec has the round-trip tests. Note that the existing Java setters for the other possibly-infinite settings (ConnectionPoolSettings.withIdleTimeout / withKeepAliveTimeout / withMaxConnectionLifetime, ClientConnectionSettings.withIdleTimeout, WebSocketSettings.withPeriodicKeepAliveMaxIdle) have the same problem, since toScala throws on that value too. I left them out of this PR; I can open a follow-up switching them to the same helper once this is merged.

Minor: both done. The HTTP/2-only note is in reference.conf and the docs, and the test comment now says what the assertion checks rather than tightening the margin.

One extra: while running the grace period against a real grpc-java client I noticed that the server-side forced-close log message pointed at completion-timeout, which is a client-only setting. I made the message side-aware, so on the server it names the terminate deadline and max-connection-age-grace instead. Shout if you would rather keep that out of this PR.

Verification after the changes: http2-tests 380/380 (4 new tests), +http-core/mimaReportBinaryIssues clean on 2.13 and 3 (7 filters now), checkCodeStyle, validatePullRequest and the docs build all pass. The grpc-java 1.75.0 probe from the description was rerun with max-connection-age = 5s: with max-connection-age-grace = 2s, 77 calls, 0 failures, and a call that never completes server-side failed 3.7 s after it started with UNAVAILABLE: Connection closed after GOAWAY; with max-connection-age-grace = infinite, again 0 failures, and the same call was kept open by the server until the client's 10 s deadline ended it.

@pjfanning

Copy link
Copy Markdown
Member

Thanks @Kreinoee for the thorough follow-up. All the points from the earlier review are addressed, and the explanations were really helpful. Keeping infinite as an allowed grace value is fine by me, since with the fix for the binding-level terminate it can't hold up shutdown. The side-aware forced-close log message is a nice extra, and a follow-up PR for the other Java setters with the same FOREVER round-trip problem would be welcome.

A few small points, none of them blocking:

  • reference.conf: the max-connection-age comment says an age expiry has no effect on a termination already in progress, but not the converse (a later terminate with an earlier deadline shortens a drain the age started). http2.md covers both, so it would be good to mirror that in reference.conf.
  • Test coverage: there's no test for max-connection-age-grace = infinite followed by a later terminate(short). The code handles it (no forced close is scheduled, so the new deadline always wins), but since that combination is what justifies allowing infinite, a test pinning it down would be worthwhile.
  • Some of the new tests have fairly tight timing margins (e.g. expectNoBytes(400.millis) against a 500ms age, expectNoBytes(700.millis) against a 1s terminate). They're probably fine; if any of them turn out flaky on CI, widening the margins would be the first thing to try.

@pjfanning

Copy link
Copy Markdown
Member

@Kreinoee I'm hoping to produce an RC for 2.0.0-M2 soon. The nits above, would you time to look at them. If not, I might just merge this and add a follow up PR later.

@pjfanning pjfanning left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm - I will follow up with a new PR to handle some nits

@pjfanning
pjfanning merged commit 478c58b into apache:main Oct 2, 2026
6 checks passed
@pjfanning pjfanning added this to the 2.0.0-M2 milestone Oct 2, 2026
pjfanning added a commit to pjfanning/incubator-pekko-http that referenced this pull request Oct 2, 2026
…n 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 apache#1316
pjfanning added a commit to pjfanning/incubator-pekko-http that referenced this pull request Oct 2, 2026
Motivation:
Twenty commits landed on main after the model was pinned to `40b07a2`.
Two touch a claim it makes. apache#1264 closes the one P1 gap the model
recorded as open: the HTTP/2 frame parser now rejects a frame over
`max-frame-size` (512kB) on its frame header, before buffering the
payload. apache#1296 validates the two application-supplied parts of the
request line at construction, which changes what the §11
`Raw-Request-URI` misuse can reach.

The rest do not move a claim: apache#1316 adds `max-connection-age`, default
`infinite`; apache#1301 encodes HPACK literals as ISO-8859-1, which still
substitutes '?' above 0xFF, and the P2 guard checks the rendered bytes;
apache#1305, apache#1298, apache#1318 and the dependency updates are not security-relevant.

Modification:
Re-pin to `478c58b`. Add `max-frame-size` to §5a, §6 and the §15 back-map,
add apache#1264 to the P1 verification paragraph, and drop the "one P1 gap is
open" paragraph. Restate the §11 `Raw-Request-URI` misuse: injection is
now rejected at construction, what remains is client bytes choosing an
unnormalized target, and a value that passes construction yet corrupts
the request line is `VALID` under §5b.4. Add the matching §15 row.

Result:
The model is verified against `478c58b` and records no open P1 gap.

Tests:
Not run - docs only

References:
Refs apache#1264, apache#1296
@Kreinoee

Kreinoee commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Super, thanks. And sorry I did not follow up on you nits, I just had a busy week. I hope I can maybe do it in a follow up next week, where I can also make one for the other fix than I promised.

@pjfanning

Copy link
Copy Markdown
Member

Super, thanks. And sorry I did not follow up on you nits, I just had a busy week. I hope I can maybe do it in a follow up next week, where I can also make one for the other fix than I promised.

#1323 is open for the nits but if you see anything else that is needed, feel free to make suggestions

pjfanning added a commit that referenced this pull request Oct 3, 2026
…ER 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
pjfanning added a commit to Rayan-and-beyond/pekko-http that referenced this pull request Oct 3, 2026
…che#1316

Motivation:
apache#1316 merged with `infinite` as the disabled value for
`max-connection-age`, a `Duration` setting type and
`JavaDurationConverter` for the Java API. The client setting used `0s`
and `FiniteDuration`.

Modification:
- `persistent-connection-max-age` defaults to `infinite`, is a
  `Duration`, and must be > 0 or `infinite`, as on the server
- Java accessors use `JavaDurationConverter`, so
  `ChronoUnit.FOREVER.getDuration` round-trips to `Duration.Inf`
- reference.conf, scaladoc and docs follow the server wording
- the jitter scheduling matches `Http2Demux`
- settings tests move to a new `Http2ClientSettingsSpec`, mirroring
  `Http2ServerSettingsSpec`
- MiMa excludes are reduced to the abstract members that need them
- pekko-style imports in the changed files

Result:
The client and server max connection age settings share naming
conventions, defaults, validation and Java conversion behaviour.

Tests:
- sbt "http-core/testOnly ...Http2ClientSettingsSpec ...Http2CommonSettingsSpec ...Http2ServerSettingsSpec": 16 passed
- sbt "http2-tests/testOnly ...Http2PersistentClient*": 26 passed
- sbt "http-core/mimaReportBinaryIssues": clean
- sbt scalafmtAll and headerCreateAll run on changed modules

References:
Refs apache#1319, apache#1316
pjfanning added a commit that referenced this pull request Oct 3, 2026
* http2: add persistent client connection max age #1319

Motivation:
Long-lived managed HTTP/2 clients can stay pinned to existing server instances.

Modification:
Add a configurable maximum age that retires managed persistent HTTP/2 connections after in-flight requests drain.

Result:
Requests arriving after retirement establish a fresh connection while in-flight requests complete normally.

Tests:
- sbt validatePullRequest
- sbt http2-tests/test
- sbt +http-core/mimaReportBinaryIssues
- sbt scalafmtCheckAll scalafmtSbtCheck
- sbt +headerCheckAll
- sbt docs/paradox
- sbt checkCodeStyle
- git diff --check

References:
Refs #1319

* http2: address max-age review feedback

Expose client max-age and jitter as public settings, add per-connection jitter, and preserve a buffered request when the request source completes during retirement. Document break-before-make behavior and extend retirement/reconnect coverage.

* http2: preserve Java max-age precision

* Align client max connection age with the server-side setting from #1316

Motivation:
#1316 merged with `infinite` as the disabled value for
`max-connection-age`, a `Duration` setting type and
`JavaDurationConverter` for the Java API. The client setting used `0s`
and `FiniteDuration`.

Modification:
- `persistent-connection-max-age` defaults to `infinite`, is a
  `Duration`, and must be > 0 or `infinite`, as on the server
- Java accessors use `JavaDurationConverter`, so
  `ChronoUnit.FOREVER.getDuration` round-trips to `Duration.Inf`
- reference.conf, scaladoc and docs follow the server wording
- the jitter scheduling matches `Http2Demux`
- settings tests move to a new `Http2ClientSettingsSpec`, mirroring
  `Http2ServerSettingsSpec`
- MiMa excludes are reduced to the abstract members that need them
- pekko-style imports in the changed files

Result:
The client and server max connection age settings share naming
conventions, defaults, validation and Java conversion behaviour.

Tests:
- sbt "http-core/testOnly ...Http2ClientSettingsSpec ...Http2CommonSettingsSpec ...Http2ServerSettingsSpec": 16 passed
- sbt "http2-tests/testOnly ...Http2PersistentClient*": 26 passed
- sbt "http-core/mimaReportBinaryIssues": clean
- sbt scalafmtAll and headerCreateAll run on changed modules

References:
Refs #1319, #1316

---------

Co-authored-by: PJ Fanning <pjfanning@users.noreply.github.com>
@Kreinoee

Kreinoee commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Cool, that looks good to me, so nothing left for me to do. Thanks for the review and help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants