Skip to content

Gate HTTP/2 PING failure-based connection close exception rewraps to PING enablement flag.#49403

Merged
jeet1995 merged 6 commits into
Azure:mainfrom
jeet1995:users/jeet1995/http2-ping-perf-gate
Jun 8, 2026
Merged

Gate HTTP/2 PING failure-based connection close exception rewraps to PING enablement flag.#49403
jeet1995 merged 6 commits into
Azure:mainfrom
jeet1995:users/jeet1995/http2-ping-perf-gate

Conversation

@jeet1995

@jeet1995 jeet1995 commented Jun 7, 2026

Copy link
Copy Markdown
Member

Two corrections to how Http2PingCloseRewrapHandler (from #49095) is installed.

1. Install per H2 child stream via .observe(STREAM_CONFIGURED). The handler must sit at the head of each child-stream pipeline to rewrap a parent PING-timeout close into the typed Http2PingTimeoutChannelClosedException as opposed to checking if the handler was installed on a per-request basis.

2. Gate the install on Http2PingHandler.isPingHealthEffectivelyEnabled so disabling PING-health reverts to baseline (no per-stream handler).

jeet1995 and others added 2 commits June 7, 2026 15:17
…equest

The previous .doOnRequest() chained onto the HTTP/2 HttpClient builder
wrapped every outgoing request's Mono in a Reactor operator. That paid
per-request costs even when the rewrap handler was already installed:

* MonoPeekTerminal wrap + per-request subscriber allocation
  (InternalMonoOperator.subscribe +2.60% in alloc profile)
* per-request BiConsumer lambda invocation
  (Http2Pool\$\...run +1.16% in alloc profile)
* per-request channel pipeline String-key walk via pipeline.get(...)

Long-run async-profiler diff vs v4.80.0 on D2 attributed the PR's
-7.6% QPS / +14.5% mean / +166% tail-max regression primarily to a
+10.2% allocation rate -- with the operators above dominating the delta.

Move the install into the existing .doOnConnected() block, which
already runs once per HTTP/2 child stream (the customHeaderCleaner
install right above it relies on the same contract). The rewrap
handler is @sharable, so installing once per stream is correct and
amortizes the install + pipeline-walk away from the hot path.

The ch.parent() != null guard is kept so the install only targets
child streams, never the parent TCP channel (which uses
Http2ParentChannelExceptionHandler instead).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…velyEnabled

Follow-up to the doOnConnected install move (previous commit on this PR).
The rewrap handler translates Http2MultiplexHandler's per-child-stream
channelInactive into a typed Http2PingTimeoutChannelClosedException when
the PING sender closes the parent TCP channel after consecutive PING-ACK
timeouts. If the PING sender is disabled there is no such signal to
translate, so the install adds per-child-stream pipeline-add cost and an
extra pipeline hop on every inbound frame for zero behavioral benefit.

Empirical motivation: long-run perf matrix on a 2 vCPU D2s_v5 VM
(point-read workload, Gateway+H2, ~98% CPU steady state, baseline =
v4.80.0):

    baseline (v4.80.0)                          : 9010 QPS
    ping-prefix (PR before doOnConnected fix)   : 8301 QPS  (-7.9%)
    ping-postfix (doOnConnected install only)   : 8170 QPS  (-9.3%)
    ping-off (postfix + HTTP2_PING_HEALTH=false): 8102 QPS  (-10.1%)

Toggling the documented kill switch did not recover baseline because
the rewrap install was unconditional -- the kill switch only suppressed
the PING sender, not the per-child-stream rewrap install. async-profiler
CPU diffs ping-off-vs-baseline and ping-off-vs-postfix were essentially
identical, confirming the rewrap install dominates the residual cost.

Consolidating both PING-sender install and rewrap install under
Http2PingHandler.isPingHealthEffectivelyEnabled makes
HTTP2_PING_HEALTH_ENABLED=false a true revert-to-baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 7, 2026 19:21
@jeet1995 jeet1995 requested review from a team and kirankumarkolli as code owners June 7, 2026 19:21
@github-actions github-actions Bot added the Cosmos label Jun 7, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR refines the Cosmos Gateway HTTP/2 PING-health feature’s Reactor Netty integration to reduce steady-state overhead and ensure the documented kill switch (COSMOS.HTTP2_PING_HEALTH_ENABLED=false) truly reverts behavior to the pre-feature baseline.

Changes:

  • Move Http2PingCloseRewrapHandler installation from a per-request hook (.doOnRequest()) to a connection hook (.doOnConnected()), amortizing setup costs away from the per-call hot path.
  • Gate Http2PingCloseRewrapHandler installation on Http2PingHandler.isPingHealthEffectivelyEnabled(http2Cfg) so disabling PING-health suppresses both the PING sender and the rewrap translation handler.
  • Document the perf regression fix in the Cosmos CHANGELOG.md.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/http/ReactorNettyClient.java Reworks the handler install site and adds gating so PING-health overhead is avoided when disabled.
sdk/cosmos/azure-cosmos/CHANGELOG.md Adds an entry describing the HTTP/2 PING-handler perf regression fixes.
Comments suppressed due to low confidence (1)

sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/http/ReactorNettyClient.java:231

  • Now that the rewrap handler installation has moved from .doOnRequest() to .doOnConnected(), there are still Javadocs in Http2PingCloseRewrapHandler and Http2PingHandler that describe the old .doOnRequest(...) install site. Updating those references would keep the documentation accurate and reduce future confusion when debugging PING-close behavior.
                    // Install a @Sharable head-of-pipeline rewrap handler on each H2
                    // child-stream pipeline. When Http2PingHandler closes the parent
                    // (TCP) channel after consecutive PING-ACK timeouts or PING-send
                    // failures, the H2 multiplex codec propagates channelInactive to
                    // every child stream; the rewrap handler intercepts that and fires

Comment thread sdk/cosmos/azure-cosmos/CHANGELOG.md Outdated
@jeet1995 jeet1995 changed the title perf(cosmos): amortise + gate HTTP/2 PING rewrap handler install (recovers baseline when PING-health is disabled) [WIP]: perf(cosmos): amortise + gate HTTP/2 PING rewrap handler install (recovers baseline when PING-health is disabled) Jun 7, 2026
@jeet1995

jeet1995 commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

/azp run java - cosmos - tests

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jeet1995 jeet1995 changed the title [WIP]: perf(cosmos): amortise + gate HTTP/2 PING rewrap handler install (recovers baseline when PING-health is disabled) [WIP]: perf(cosmos): Gate HTTP/2 PING rewrap handler install to PING-health disablement. Jun 7, 2026
@jeet1995

jeet1995 commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

@sdkReviewAgent

jeet1995 and others added 2 commits June 7, 2026 17:24
…NFIGURED)

The rewrap handler install lived inside .doOnConnected(...), which reactor-netty fires only on the H2 parent TCP channel (State.CONFIGURED, parent==null). The install was gated on ch.parent()!=null, so it never ran on the parent and never reached a child stream -- the handler was installed 0 times and PING-timeout parent closes surfaced as bare PrematureCloseException instead of the typed Http2PingTimeoutChannelClosedException.

Move the install to .observe((connection, state) -> ...) keyed on HttpClientState.STREAM_CONFIGURED, the per-child-stream lifecycle event. The handler is now added at the head of each H2 child-stream pipeline, where it intercepts channelInactive ahead of HttpClientOperations and rewraps the close into the typed exception so ClientRetryPolicy can suppress region mark-down. Keeps the PING-health gate, ch.parent()!=null defensive check, get(HANDLER_NAME)==null guard, and TOCTOU try/catch. Also fixes stale doc-comments referencing the old install hook.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The handler is a single @sharable instance installed per H2 child stream, not a per-request object. Reword the class-doc summary from 'Per-request' to 'Per-child-stream' for consistency with the rest of the doc.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jeet1995

jeet1995 commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

/azp run java - cosmos - tests

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

Extract the inline .observe() install decision into a package-private static
installHttp2PingCloseRewrapHandlerIfNeeded(...) so the per-child-stream gate is
unit-testable without a live HTTP/2 connection. Add a TestNG unit test (group
"unit") that drives the real method via an EmbeddedChannel and proves the
disablement path (kill-switch off, non-positive PING interval, HTTP/2 disabled,
non-STREAM_CONFIGURED state, parent-less channel) is a true revert-to-baseline
that installs nothing -- plus positive install, head-of-pipeline position,
idempotency, and state-before-predicate short-circuit ordering.

Behavior-preserving refactor; no user-facing change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jeet1995 jeet1995 changed the title [WIP]: perf(cosmos): Gate HTTP/2 PING rewrap handler install to PING-health disablement. [WIP]: Gate HTTP/2 PING failure-based connection close exception rewraps to PING enablement flag. Jun 7, 2026
@Azure Azure deleted a comment from azure-pipelines Bot Jun 7, 2026
@jeet1995

jeet1995 commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

/azp run java - cosmos - tests

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jeet1995

jeet1995 commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

P95 / P99 — HTTP/2 PING perf (PR vs harness-matched baseline)

45-min D2s_v5, GW HTTP/2, conc=80, CPU pinned ~98% on both. Baseline pins the benchmark pom to azure-cosmos:4.80.0 so the only delta is this PR''s SDK code. Same harness, same container, sequential runs.

Metric Baseline (4.80.0 SDK) PR #49403 Δ
lat-mean p50 9.33 ms 9.67 ms +0.34 ms
lat-mean p95 9.77 ms 10.01 ms +0.24 ms
lat-mean p99 10.10 ms 10.31 ms +0.21 ms
lat-max p50 97.19 ms 83.01 ms −14 ms
lat-max p95 334.56 ms 148.70 ms −186 ms
lat-max p99 1203.84 ms 1323.65 ms +120 ms
  • Steady-state latency (lat-mean p95/p99) is within 0.2–0.3 ms — noise floor for a CPU-bound workload at ~98% CPU.
  • Operation Type: ReadThroughput

@xinlian12

Copy link
Copy Markdown
Member

Review complete (25:17)

No new comments — existing review coverage is sufficient.

Steps: ✓ context, correctness, cross-sdk, design, history, past-prs, synthesis, test-coverage

@jeet1995 jeet1995 changed the title [WIP]: Gate HTTP/2 PING failure-based connection close exception rewraps to PING enablement flag. Gate HTTP/2 PING failure-based connection close exception rewraps to PING enablement flag. Jun 8, 2026

@FabianMeiswinkel FabianMeiswinkel 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

@jeet1995

jeet1995 commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

"Collection not available for read" failures in com.azure.cosmos.rx.OrderbyDocumentQueryTest.before_OrderbyDocumentQueryTest - collection creation is async - merging this PR and following up with an approach to wait until collections available for read across regions.

@jeet1995 jeet1995 merged commit c905251 into Azure:main Jun 8, 2026
107 of 109 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants