Skip to content

[ZEPPELIN-6092] Send server-initiated websocket ping frames to keep connections alive - #5432

Open
HwangRock wants to merge 1 commit into
apache:masterfrom
HwangRock:ZEPPELIN-6092
Open

[ZEPPELIN-6092] Send server-initiated websocket ping frames to keep connections alive#5432
HwangRock wants to merge 1 commit into
apache:masterfrom
HwangRock:ZEPPELIN-6092

Conversation

@HwangRock

Copy link
Copy Markdown
Contributor

What is this PR for?

Zeppelin's websocket keep-alive is client-driven only. Both UIs send an application-level {"op":"PING"} every 10 seconds (websocket-event.factory.js, message.ts) and the server answers nothing — case PING: in NotebookServer.onMessage() is a bare break. The server never writes first, so a connection survives only as long as the client's timer keeps firing.

That timer is not reliable. Chrome's intensive throttling drops background-tab timers to once per minute once the tab has been hidden for a few seconds, the chain count reaches 5, and no WebRTC is in use — all of which a 10-second setInterval satisfies within a minute. An open websocket is not an exemption; only WebRTC is. A discarded tab or a sleeping laptop stops the timer outright. When it stops, nothing resets the idle timer and the connection dies.

This PR has the server send a WebSocket protocol ping frame on a schedule. Per RFC 6455 section 5.5.2 the peer answers with a pong automatically, so no client-side change is required, and writing to the session resets Jetty's idle timeout — SocketChannelEndPoint.flush() calls IdleTimeout.notIdle() — along with any intermediate proxy's idle timer. This is what the nginx websocket proxying guide recommends as well: "the proxied server can be configured to periodically send WebSocket ping frames to reset the timeout and check if the connection is still alive."

The PR also makes the idle timeout itself configurable. setupNotebookServer() in ZeppelinServer sets the text message buffer size but never calls setDefaultMaxSessionIdleTimeout(), so the effective value has always been whatever Jetty defaults to. That default is not stable across versions: WebSocketPolicy used 300000ms under Jetty 9, while WebSocketConstants.DEFAULT_IDLE_TIMEOUT is 30 seconds under Jetty 11. The new key restores an explicit value and gives operators a knob.

Two new keys, documented in zeppelin-site.xml.template, zeppelin-env.sh.template, and docs/setup/operation/configuration.md:

Key Default Meaning
zeppelin.websocket.heartbeat.interval 60000 Interval in ms between server-initiated ping frames. 0 or negative disables the heartbeat.
zeppelin.websocket.idle.timeout 300000 Idle timeout in ms applied to websocket sessions.

NotebookSocket.sendPing() swallows and logs its exceptions so one dead session cannot break the loop over the others. The scheduler runs on a single daemon thread and starts lazily on the first connection.

What type of PR is it?

Improvement

What is the Jira issue?

ZEPPELIN-6092

How should this be tested?

Unit tests cover the ping send path, the disabled-when-non-positive case, isolation of a failing session, and the two configuration keys.

End to end, with a client that sends nothing after the handshake, against Jetty 11's 30-second default idle timeout:

Before

[+0000.0s] connected (HTTP/1.1 101 Switching Protocols)
[+0030.0s] closed code=1001 reason='Connection Idle Timeout'
[+0030.0s] RESULT [A-direct]: closed code=1001 reason='Connection Idle Timeout'
ERROR NotebookServer.java[onError] - Error in WebSocket Session to /[0:0:0:0:0:0:0:1]:59302
org.eclipse.jetty.websocket.core.exception.WebSocketTimeoutException: Connection Idle Timeout
	at org.eclipse.jetty.websocket.core.internal.WebSocketConnection.onIdleExpired(WebSocketConnection.java:242)
	at org.eclipse.jetty.io.IdleTimeout.checkIdleTimeout(IdleTimeout.java:170)

After — same client, same 30-second idle timeout, heartbeat pinned to 10 seconds

[+0000.0s] connected (HTTP/1.1 101 Switching Protocols)
[+0120.0s] RESULT [A-direct]: still connected after 120s (no disconnect)
INFO NotebookServer.java[startHeartbeatScheduler] - Started websocket heartbeat scheduler with interval 10000 ms

Zero WebSocketTimeoutException in the server log across the 120-second window. The idle timeout is identical between the two runs, so the difference comes from the heartbeat writes alone.

To reproduce: set zeppelin.websocket.idle.timeout to 30000 and zeppelin.websocket.heartbeat.interval to 10000, open a websocket to /ws, send nothing after the handshake, and watch for a close past the 30-second mark.

Questions:

  • Does the license files need updating? No
  • Are there breaking changes for older versions? No. The heartbeat uses protocol ping frames, which every websocket client answers automatically. No message op was added or changed, and the existing client-driven PING path is untouched.
  • Does this need documentation? Yes — both keys are documented in the config template, the env template, and the configuration docs.

…onnections alive

Keep-alive is client-driven only: both UIs send an application-level
{"op":"PING"} every 10 seconds and the server never writes first. When the
client timer stops -- a backgrounded tab under Chrome's intensive throttling,
a discarded tab, a sleeping laptop -- nothing resets the idle timer and the
connection dies.

Send a WebSocket protocol ping frame from the server on a schedule. The peer
answers automatically per RFC 6455 section 5.5.2, so no client change is
needed, and writing to the session resets Jetty's idle timeout along with any
intermediate proxy's idle timer.

Also make the idle timeout configurable. setupNotebookServer() never called
setDefaultMaxSessionIdleTimeout(), so the effective value was whatever Jetty
defaulted to -- 300000ms under Jetty 9, 30 seconds under Jetty 11.

Measured with a client that sends nothing after the handshake, against
Jetty 11's 30s default:

  before:  closed at 30.0s, code=1001 'Connection Idle Timeout'
  after:   still connected at 120.0s
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.

1 participant