This is an automated email from the ASF dual-hosted git repository.
tbonelee pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/master by this push:
new 933da6862c [ZEPPELIN-6531] Fail fast when ZeppelinWebSocketClient
cannot connect
933da6862c is described below
commit 933da6862c18240002c8e930b9148a8ab96402bb
Author: HwangRock <[email protected]>
AuthorDate: Tue Jul 14 22:43:41 2026 +0900
[ZEPPELIN-6531] Fail fast when ZeppelinWebSocketClient cannot connect
### What is this PR for?
`ZeppelinWebSocketClient.connect()` blocks forever when the websocket
handshake fails. It waits on `connectLatch.await()` with no timeout, and the
latch is only counted down in the success callback (`onConnect`). Pre-handshake
failures — connection refused, TLS error, rejected upgrade — are not delivered
through the annotated callbacks at all; Jetty completes the `CompletableFuture`
returned by `WebSocketClient.connect()` exceptionally instead. The original
code discards that future, [...]
This affects any `zeppelin-client` (`ZSession`) user that passes a
`MessageHandler` to `start()`/`reconnect()`: if the target Zeppelin server is
down or unreachable, the calling thread blocks indefinitely.
Fix: wait on the future with a bounded timeout and surface handshake
failures as `IOException`. On failure, stop the Jetty client so its threads are
not leaked. `connectLatch` is unused after this change, so it is removed
together with its unused `getConnectLatch()` getter.
Note on approach: adding `connectLatch.countDown()` to `onError` does not
fix this. Against jetty-websocket 11, pre-handshake failures reach only the
future, never `<at>OnWebSocketError`, so the latch would still never be
released. The future is the only path that carries the failure.
### What type of PR is it?
Bug Fix
### Todos
* [ ] - Follow-up: make the connect timeout configurable via `ClientConfig`
(currently a 30s constant)
### What is the Jira issue?
* https://issues.apache.org/jira/browse/ZEPPELIN-6531
### How should this be tested?
Added `ZeppelinWebSocketClientTest` (the first unit test in
`zeppelin-client`): connecting to a closed port must fail within a bounded time
instead of hanging.
Also verified end-to-end against a docker-exposed port. Ran `nginx` on a
port, which accepts the TCP connection but rejects the websocket upgrade (HTTP
404 on `/ws`), and pointed the client at both that port and a closed port.
Before (original code, same docker port):
```
[INFO] Running org.apache.zeppelin.client.websocket.ManualE2EVerifyTest
```
Hangs here — no result line. The forked surefire JVM stayed alive past 45s
and had to be killed.
After (this PR):
```
[E2E] closed-port(1) failed-fast in 295ms -> IOException: Failed to
establish websocket connection to ws://127.0.0.1:1/ws
[E2E] open-port-non-ws(18080) failed-fast in 45ms -> IOException: Failed to
establish websocket connection to ws://127.0.0.1:18080/ws
```
The docker probe was a throwaway check and is not part of the PR; the
committed test uses a closed port and needs no docker.
### Screenshots (if appropriate)
### Questions:
* Does the license files need to update? No.
* Is there breaking changes for older versions? `getConnectLatch()` is
removed. It is public but has no callers in the repo, and it only exposed
`connect()`'s internal latch, which no longer exists.
* Does this needs documentation? No.
Closes #5300 from HwangRock/ZEPPELIN-6531.
Signed-off-by: ChanHo Lee <[email protected]>
---
.../client/websocket/ZeppelinWebSocketClient.java | 35 ++++++++++++++------
.../websocket/ZeppelinWebSocketClientTest.java | 37 ++++++++++++++++++++++
2 files changed, 62 insertions(+), 10 deletions(-)
diff --git
a/zeppelin-client/src/main/java/org/apache/zeppelin/client/websocket/ZeppelinWebSocketClient.java
b/zeppelin-client/src/main/java/org/apache/zeppelin/client/websocket/ZeppelinWebSocketClient.java
index a3e700398a..c09fb845ad 100644
---
a/zeppelin-client/src/main/java/org/apache/zeppelin/client/websocket/ZeppelinWebSocketClient.java
+++
b/zeppelin-client/src/main/java/org/apache/zeppelin/client/websocket/ZeppelinWebSocketClient.java
@@ -33,8 +33,11 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URI;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
/**
@@ -45,8 +48,8 @@ import java.util.concurrent.TimeUnit;
public class ZeppelinWebSocketClient {
private static final Logger LOGGER =
LoggerFactory.getLogger(ZeppelinWebSocketClient.class);
private static final Gson GSON = new Gson();
+ private static final long DEFAULT_CONNECT_TIMEOUT_MS = 30_000;
- private CountDownLatch connectLatch = new CountDownLatch(1);
private CountDownLatch closeLatch = new CountDownLatch(1);
private Session session;
@@ -63,8 +66,18 @@ public class ZeppelinWebSocketClient {
URI echoUri = new URI(url);
ClientUpgradeRequest request = new ClientUpgradeRequest();
request.setHeader("Origin", "*");
- wsClient.connect(this, echoUri, request);
- connectLatch.await();
+ CompletableFuture<Session> future = wsClient.connect(this, echoUri,
request);
+ try {
+ future.get(DEFAULT_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ } catch (TimeoutException e) {
+ stopQuietly();
+ throw new IOException("Timeout(" + DEFAULT_CONNECT_TIMEOUT_MS
+ + "ms) establishing websocket connection to " + url, e);
+ } catch (ExecutionException e) {
+ stopQuietly();
+ throw new IOException("Failed to establish websocket connection to " +
url,
+ e.getCause());
+ }
LOGGER.info("WebSocket connect established");
}
@@ -93,7 +106,6 @@ public class ZeppelinWebSocketClient {
public void onConnect(Session session) {
LOGGER.info("Got connect: {}", session.getRemote());
this.session = session;
- connectLatch.countDown();
}
@OnWebSocketMessage
@@ -103,22 +115,25 @@ public class ZeppelinWebSocketClient {
@OnWebSocketError
public void onError(Throwable cause) {
- LOGGER.info("WebSocket Error: " + cause.getMessage());
- cause.printStackTrace(System.out);
+ LOGGER.error("WebSocket error", cause);
}
public void send(Message message) throws IOException {
session.getRemote().sendString(GSON.toJson(message));
}
- public CountDownLatch getConnectLatch() {
- return connectLatch;
- }
-
public void stop() throws Exception {
if (this.wsClient != null) {
this.wsClient.stop();
}
}
+ private void stopQuietly() {
+ try {
+ stop();
+ } catch (Exception e) {
+ LOGGER.warn("Failed to stop websocket client after connection failure",
e);
+ }
+ }
+
}
diff --git
a/zeppelin-client/src/test/java/org/apache/zeppelin/client/websocket/ZeppelinWebSocketClientTest.java
b/zeppelin-client/src/test/java/org/apache/zeppelin/client/websocket/ZeppelinWebSocketClientTest.java
new file mode 100644
index 0000000000..7f6cabd1e2
--- /dev/null
+++
b/zeppelin-client/src/test/java/org/apache/zeppelin/client/websocket/ZeppelinWebSocketClientTest.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.zeppelin.client.websocket;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
+
+import org.junit.jupiter.api.Test;
+
+import java.time.Duration;
+
+class ZeppelinWebSocketClientTest {
+
+ @Test
+ void connectFailsFastWhenPortClosed() {
+ ZeppelinWebSocketClient client = new ZeppelinWebSocketClient(msg -> { });
+
+ assertTimeoutPreemptively(Duration.ofSeconds(20), () ->
+ assertThrows(Exception.class, () ->
client.connect("ws://127.0.0.1:1/ws")));
+ }
+
+}