Skip to content

feature: WebSocket ping/pong heartbeat — JVM client (PR 4)#587

Merged
xerial merged 2 commits into
mainfrom
feature/websocket-heartbeat-jvm-client
Jun 20, 2026
Merged

feature: WebSocket ping/pong heartbeat — JVM client (PR 4)#587
xerial merged 2 commits into
mainfrom
feature/websocket-heartbeat-jvm-client

Conversation

@xerial

@xerial xerial commented Jun 20, 2026

Copy link
Copy Markdown
Member

Why

PR 4 (final) of the WebSocket heartbeat series (after #584 shared core + Native, #585 Node server, #586 Netty server). Wires the heartbeat into the JVM java.net.http client — the last backend that can do it.

What

  • JavaWebSocketListener drives the shared WebSocketHeartbeat from a shared daemon ScheduledExecutorService (one for all client connections): sendPing on an idle tick, close(1011, "ping timeout") on an unanswered ping. Resets on any inbound frame (onText/onBinary/onPong); cancels the scheduled future in notifyClose.
  • JavaWebSocketContext.sendPing enqueues WebSocket.sendPing through the existing serialized send chain (the JDK forbids overlapping sends).
  • This consumes the pingIntervalMillis param added to WebSocketClient.connect in feature: WebSocket ping/pong heartbeat — shared core + Native server & client #584 (previously accept-and-ignored on the JVM client).

The JS client stays the only backend without a heartbeat — the browser/Node global WebSocket can't send protocol pings from JS — documented on the connect API.

Testing

A Netty WebSocketClientTest case: the client pings every 150 ms, the server auto-pongs, and the live-but-idle connection is not reaped. uniJVM/test: 1640, netty/test: 38 pass.

Heartbeat series complete

Backend PR
Shared WebSocketHeartbeat + Native server & client #584
Node server #585
Netty server #586
JVM client this PR
JS client N/A — can't send protocol pings

🤖 Generated with Claude Code

The last backend in the heartbeat series. Wires the pingIntervalMillis param
(added to WebSocketClient.connect in #584) into the JVM java.net.http client.

- JavaWebSocketListener drives the shared WebSocketHeartbeat from a shared daemon
  ScheduledExecutorService (one for all client connections): sendPing on an idle
  tick, close(1011) on an unanswered ping. Resets on any inbound frame
  (onText/onBinary/onPong). Cancels the scheduled future in notifyClose.
- JavaWebSocketContext.sendPing enqueues WebSocket.sendPing through the existing
  serialized send chain.

The JS client remains the only backend without a heartbeat (the browser/Node
global WebSocket can't send protocol pings) — documented on the connect API.

Test: a Netty WebSocketClientTest case — the client pings every 150ms, the
server auto-pongs, the live-but-idle connection is not reaped. uniJVM/test 1640,
netty/test 38 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@xerial

xerial commented Jun 20, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@github-actions github-actions Bot added the feature New feature label Jun 20, 2026

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request implements a client-side heartbeat mechanism for Java WebSocket connections to keep idle connections alive using ping/pong frames. It introduces a daemon scheduler to manage heartbeat ticks, records activity on incoming messages, and adds a test to verify the behavior. The review feedback highlights two important reliability issues: a potential race condition between scheduling and cancelling the heartbeat task that could lead to resource leaks, and the risk of silent cancellation of the heartbeat task if an exception is thrown during execution.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread uni/.jvm/src/main/scala/wvlet/uni/http/JavaWebSocketClient.scala Outdated
Comment thread uni/.jvm/src/main/scala/wvlet/uni/http/JavaWebSocketClient.scala

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request implements a client heartbeat mechanism for JavaWebSocketClient to keep idle connections alive using ping/pong frames, driven by a daemon scheduler. The feedback highlights critical concurrency and robustness improvements: synchronizing the scheduling and cancellation of the heartbeat task to prevent a potential race condition and task leak, wrapping the heartbeat tick execution in a try-catch block to avoid silent suppression of the scheduler on exceptions, and overriding onPing to ensure inbound ping frames also reset the idle timer.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread uni/.jvm/src/main/scala/wvlet/uni/http/JavaWebSocketClient.scala Outdated
Comment thread uni/.jvm/src/main/scala/wvlet/uni/http/JavaWebSocketClient.scala
Comment thread uni/.jvm/src/main/scala/wvlet/uni/http/JavaWebSocketClient.scala
Comment thread uni/.jvm/src/main/scala/wvlet/uni/http/JavaWebSocketClient.scala
- Close the onOpen/notifyClose schedule-vs-cancel race: after assigning
  heartbeatFuture, re-check `closed` and cancel if notifyClose ran during
  scheduling (it would have seen a null future and couldn't cancel).
- Wrap the scheduled tick in try/catch: scheduleAtFixedRate suppresses all
  future runs once a task throws, which would silently kill the heartbeat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@xerial xerial merged commit 042badf into main Jun 20, 2026
14 checks passed
@xerial xerial deleted the feature/websocket-heartbeat-jvm-client branch June 20, 2026 01:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant