Skip to content

Fix ClientFactory leak in KubernetesEndpointGroup watches - #6909

Open
arimu1 wants to merge 2 commits into
line:mainfrom
arimu1:fix/6805-kubernetes-endpointgroup-clientfactory-leak
Open

Fix ClientFactory leak in KubernetesEndpointGroup watches#6909
arimu1 wants to merge 2 commits into
line:mainfrom
arimu1:fix/6805-kubernetes-endpointgroup-clientfactory-leak

Conversation

@arimu1

@arimu1 arimu1 commented Aug 7, 2026

Copy link
Copy Markdown

Motivation:

KubernetesEndpointGroup watches open WebSockets through fabric8's derived
HttpClients (e.g. tagged with RequestConfig). Armeria's derived
ArmeriaHttpClient did not share the close flag or WebSocket transport with
the root client (unlike fabric8's JDK/Jetty adapters). Each derived client
built its own WebSocket ClientFactory, which was never closed when the root
KubernetesClient was closed, producing:

LEAK: ClientFactory.release() was not called before it's garbage-collected.

Stack traces point at ArmeriaWebSocketClient.webSocketClient() via
KubernetesEndpointGroup.doWatchService (#6805).

Modifications:

  • Share AtomicBoolean closed, WebClient, and ArmeriaWebSocketClient when
    building a derived ArmeriaHttpClient from ArmeriaHttpClientBuilder.
  • Package-private helpers to inspect shared WebSocket ClientFactory lifecycle.
  • Regression test: open WebSocket on a derived client, close root, assert the
    shared WebSocket ClientFactory is closed.

Result:

Derived ArmeriaHttpClient instances (used for watches/tags) each created
a new AtomicBoolean and ArmeriaWebSocketClient, so WebSocket ClientFactory
objects built during KubernetesEndpointGroup watches were never closed
when the root KubernetesClient was closed.

Share closed state, WebClient, and WebSocket transport with the root
client, matching fabric8 JDK/Jetty adapters.

Fixes line#6805
@CLAassistant

CLAassistant commented Aug 7, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dba1b31d-8731-48e1-bd8b-201dc4fa9851

📥 Commits

Reviewing files that changed from the base of the PR and between 25da081 and 91cff15.

📒 Files selected for processing (2)
  • kubernetes/src/main/java/com/linecorp/armeria/client/kubernetes/ArmeriaWebSocketClient.java
  • kubernetes/src/test/java/com/linecorp/armeria/client/kubernetes/ArmeriaHttpClientTest.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • kubernetes/src/test/java/com/linecorp/armeria/client/kubernetes/ArmeriaHttpClientTest.java

📝 Walkthrough

Walkthrough

The Kubernetes client now shares WebSocket resources and close state between root and derived HTTP clients. A lifecycle test verifies shared instances, lazy WebSocket factory creation, and shutdown propagation.

Changes

Kubernetes client lifecycle

Layer / File(s) Summary
Shared resource contract
kubernetes/src/main/java/com/linecorp/armeria/client/kubernetes/ArmeriaHttpClient.java, kubernetes/src/main/java/com/linecorp/armeria/client/kubernetes/ArmeriaWebSocketClient.java
Root clients initialize shared resources. Derived clients can reuse them. WebSocket clients expose their current client state without triggering creation.
Derived client resource wiring
kubernetes/src/main/java/com/linecorp/armeria/client/kubernetes/ArmeriaHttpClientBuilder.java
Existing-client builds now share the WebClient, WebSocket client, and close-state reference.
Shared lifecycle validation
kubernetes/src/test/java/com/linecorp/armeria/client/kubernetes/ArmeriaHttpClientTest.java
The test verifies shared resources, WebSocket factory creation, and closure propagation from the root client.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ArmeriaHttpClientBuilder
  participant DerivedArmeriaHttpClient
  participant ArmeriaWebSocketClient
  participant RootArmeriaHttpClient
  ArmeriaHttpClientBuilder->>DerivedArmeriaHttpClient: share WebClient, WebSocket client, and close state
  DerivedArmeriaHttpClient->>ArmeriaWebSocketClient: open WebSocket
  ArmeriaWebSocketClient-->>DerivedArmeriaHttpClient: create or return WebSocket client
  RootArmeriaHttpClient->>DerivedArmeriaHttpClient: propagate close state
  RootArmeriaHttpClient->>ArmeriaWebSocketClient: close shared WebSocket factory
Loading

Possibly related PRs

  • line/armeria#6914: Related changes to ArmeriaHttpClient and ArmeriaWebSocketClient lifecycle and closure behavior.

Suggested reviewers: jrhee17, minwoox, ikhoon

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the ClientFactory leak fix in KubernetesEndpointGroup watches.
Description check ✅ Passed The description explains the WebSocket ClientFactory leak, the lifecycle-sharing changes, and the regression test.
Linked Issues check ✅ Passed The changes share WebSocket lifecycle state so root-client closure releases factories created by derived clients, addressing issue #6805.
Out of Scope Changes check ✅ Passed All implementation changes, helpers, and tests directly support the ClientFactory leak fix described in issue #6805.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mergify

mergify Bot commented Aug 7, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@jrhee17 jrhee17 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.

👍 👍

Comment on lines +58 to +61
// Share close state, HTTP client, and WebSocket transport with the root client so that
// ClientFactory instances created for watches are released when the root is closed.
// Matches fabric8 JDK/Jetty clients; without this, each derived client builds its own
// WebSocket ClientFactory that is never closed (LEAK / #6805).

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.

Suggested change
// Share close state, HTTP client, and WebSocket transport with the root client so that
// ClientFactory instances created for watches are released when the root is closed.
// Matches fabric8 JDK/Jetty clients; without this, each derived client builds its own
// WebSocket ClientFactory that is never closed (LEAK / #6805).

Optional) I think the intention of this code is already clear from the constructor javadocs already

Comment on lines +139 to +142
/**
* Returns the {@link ClientFactory} used by the underlying {@link WebSocketClient}, or
* {@code null} if the WebSocket client has not been created yet.
*/

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.

Suggested change
/**
* Returns the {@link ClientFactory} used by the underlying {@link WebSocketClient}, or
* {@code null} if the WebSocket client has not been created yet.
*/

Optional) no need for javadocs for obvious methods

@minwoox minwoox 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.

Thanks!

* {@code null} if the WebSocket client has not been created yet.
*/
@Nullable
ClientFactory clientFactoryOrNull() {

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.

nit: @VisibleForTesting

if (webSocketClient == null) {
return null;
}
return webSocketClient.options().factory();

@ikhoon ikhoon Aug 11, 2026

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.

If this is only for testing, the amount of code should be minimized. Should we add a getter for this.webSocketClient instead?

Replace clientFactoryOrNull helper with a minimal package-private
getter for the underlying WebSocketClient field so tests can inspect
factory lifecycle without extra factory-wrapping code.
@arimu1

arimu1 commented Aug 11, 2026

Copy link
Copy Markdown
Author

@ikhoon Thanks — replaced clientFactoryOrNull() with a minimal package-private getter for the underlying WebSocketClient field (webSocketClientOrNull()). The test now reaches options().factory() from that getter.

Tip: 91cff150796dbc06e2fe14f34c3770120e413c68
Focused test derivedClientSharesWebSocketFactoryLifecycle green (Temurin 25).

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.

Leak in KubernetesEndpointGroup

5 participants