Fix ClientFactory leak in KubernetesEndpointGroup watches - #6909
Conversation
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
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe 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. ChangesKubernetes client lifecycle
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
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
Tick the box to add this pull request to the merge queue (same as
|
| // 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). |
There was a problem hiding this comment.
| // 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
| /** | ||
| * Returns the {@link ClientFactory} used by the underlying {@link WebSocketClient}, or | ||
| * {@code null} if the WebSocket client has not been created yet. | ||
| */ |
There was a problem hiding this comment.
| /** | |
| * 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
| * {@code null} if the WebSocket client has not been created yet. | ||
| */ | ||
| @Nullable | ||
| ClientFactory clientFactoryOrNull() { |
| if (webSocketClient == null) { | ||
| return null; | ||
| } | ||
| return webSocketClient.options().factory(); |
There was a problem hiding this comment.
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.
|
@ikhoon Thanks — replaced Tip: |
Motivation:
KubernetesEndpointGroupwatches open WebSockets through fabric8's derivedHttpClients (e.g. tagged withRequestConfig). Armeria's derivedArmeriaHttpClientdid not share the close flag or WebSocket transport withthe root client (unlike fabric8's JDK/Jetty adapters). Each derived client
built its own WebSocket
ClientFactory, which was never closed when the rootKubernetesClientwas closed, producing:Stack traces point at
ArmeriaWebSocketClient.webSocketClient()viaKubernetesEndpointGroup.doWatchService(#6805).Modifications:
AtomicBoolean closed,WebClient, andArmeriaWebSocketClientwhenbuilding a derived
ArmeriaHttpClientfromArmeriaHttpClientBuilder.ClientFactorylifecycle.shared WebSocket
ClientFactoryis closed.Result:
created for watches on derived clients.