Conversation
| if (checkOrigin(origin)) { | ||
| NotebookSocket notebookSocket = sessionIdNotebookSocketMap | ||
| .computeIfAbsent(session.getId(), unused -> new NotebookSocket(session, headers)); | ||
| // Per-session pong handler so the heartbeat can tell live peers from dead ones. |
There was a problem hiding this comment.
We could remove unnecessary comment.
| // Per-session pong handler so the heartbeat can tell live peers from dead ones. |
| /** Number of pings sent since the last pong was received. */ | ||
| public int getUnansweredPings() { |
There was a problem hiding this comment.
We could let the method name carry this and drop the comment. (sendHeartbeat() and the tests would need the same rename.)
| /** Number of pings sent since the last pong was received. */ | |
| public int getUnansweredPings() { | |
| public int getPingsSinceLastPong() { |
| // Number of consecutive heartbeat pings left unanswered (no pong) before the server closes | ||
| // the session as dead. Needed because the heartbeat itself keeps resetting the Jetty idle | ||
| // timer, so the idle timeout can no longer detect dead clients (ZEPPELIN-6694). With the | ||
| // default 60s interval, a dead client is reaped after ~3-4 minutes. <= 0 disables reaping. |
There was a problem hiding this comment.
The rationale (the heartbeat keeps resetting the Jetty idle timer) is also in the sendHeartbeat() Javadoc. We could keep it in one place and leave only what the setting means here.
| // Number of consecutive heartbeat pings left unanswered (no pong) before the server closes | |
| // the session as dead. Needed because the heartbeat itself keeps resetting the Jetty idle | |
| // timer, so the idle timeout can no longer detect dead clients (ZEPPELIN-6694). With the | |
| // default 60s interval, a dead client is reaped after ~3-4 minutes. <= 0 disables reaping. | |
| // Consecutive unanswered pings before a session is closed as dead. <= 0 disables reaping. |
| int maxMissedPongs = zConf.getWebsocketHeartbeatMaxMissedPongs(); | ||
| for (NotebookSocket conn : connectionManager.connectedSockets) { | ||
| try { | ||
| if (maxMissedPongs > 0 && conn.getUnansweredPings() >= maxMissedPongs) { |
There was a problem hiding this comment.
On Jetty 11.0.24, the next frame on a connection is read only after onMessage returns (StringMessageSink.accept() calls demand(1) after invoking the handler). Since onMessage runs synchronously on the Jetty thread, pongs the client sends on time are not processed while a long-running op such as RELOAD_NOTES_FROM_REPO, CHECKPOINT_NOTE or IMPORT_NOTE is being handled. As a result, a live connection can be reaped.
I reproduced this locally with a 1s interval, max.missed.pongs set to 3, and reloadAllNotes() taking 6s. The client kept answering pings, but the connection was closed with 1001 (No pong received for 3 consecutive pings) in the middle of the op, and the op's response never reached the client. With the op taking 2s instead, the counter went up to 2 and dropped back to 0 right after the op finished, even though no new ping had been sent. It looks like the queued pongs were processed at that point.
With the defaults, a single op would have to take 3-4 minutes or more, so this is rare, but the threshold goes down as the interval is lowered. How about skipping reaping for a session that is currently handling a message? Roughly:
// NotebookSocket
private volatile boolean handlingMessage;
// NotebookServer#onMessage(Session, String)
conn.setHandlingMessage(true);
try {
onMessage(conn, msg);
} finally {
conn.setHandlingMessage(false);
}
// NotebookServer#sendHeartbeat
if (maxMissedPongs > 0 && !conn.isHandlingMessage()
&& conn.getUnansweredPings() >= maxMissedPongs) {| String sessionId = conn.getSessionId(); | ||
| if (sessionId != null) { | ||
| sessionIdNotebookSocketMap.remove(sessionId); | ||
| } |
There was a problem hiding this comment.
Since onOpen uses session.getId() as a ConcurrentHashMap key, a registered connection can never have a null session id. This null check seems to be needed only because the mock in the test returns null from getSessionId(). We could stub it in the test, e.g. when(dead.getSessionId()).thenReturn("dead-session");, and drop the check.
| String sessionId = conn.getSessionId(); | |
| if (sessionId != null) { | |
| sessionIdNotebookSocketMap.remove(sessionId); | |
| } | |
| sessionIdNotebookSocketMap.remove(conn.getSessionId()); |
| NotebookSocket notebookSocket = sessionIdNotebookSocketMap | ||
| .computeIfAbsent(session.getId(), unused -> new NotebookSocket(session, headers)); | ||
| // Per-session pong handler so the heartbeat can tell live peers from dead ones. | ||
| session.addMessageHandler(PongMessage.class, pong -> notebookSocket.onPong()); |
There was a problem hiding this comment.
All existing tests still pass with this line removed. The socket tests call the onOpen(NotebookSocket) overload directly, so none of them go through onOpen(Session, EndpointConfig), where this handler is registered. Without this line, every healthy client gets disconnected after a few heartbeats (I checked this locally), so it would be nice to have a test covering this path.
436a717 to
21dcedf
Compare
|
Thanks for the thorough review, @tbonelee! I've addressed all comments and amended them into the original commit. Reaping during long-running ops Test for the pong handler path Cleanups
I also added tests for skipping busy sessions and for the |
Since ZEPPELIN-6092, the heartbeat pings keep resetting Jetty's idle timer, so the idle timeout no longer detects dead clients and a dead session stays open until TCP retransmission gives up. Track liveness explicitly: register a per-session PongMessage handler, count pings sent since the last pong, and close a session once it reaches zeppelin.websocket.heartbeat.max.missed.pongs (default 3, <= 0 disables). Sessions that are handling a message are not reaped, because Jetty reads their pongs only after onMessage returns and a long-running op would otherwise look like a dead peer.
21dcedf to
b978ce9
Compare
What is this PR for?
Since ZEPPELIN-6092, heartbeat pings keep resetting Jetty's idle timer, so the idle timeout no longer detects dead clients. A dead session stays open until TCP retransmission gives up.
This PR tracks pongs to detect dead sessions explicitly:
What type of PR is it?
Improvement
Todos
zeppelin-site.xml.templateWhat is the Jira issue?
ZEPPELIN-6694
How should this be tested?
./mvnw -pl zeppelin-server test -Dtest='NotebookSocketTest,NotebookServerHeartbeatTest,ZeppelinConfigurationTest'Questions:
docs/setup/operation/configuration.md