This is an automated email from the ASF dual-hosted git repository.
sk0x50 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new 0015ec4f1a1 IGNITE-26624 Improve error message when thin client
connects to not initialized cluster (#6755)
0015ec4f1a1 is described below
commit 0015ec4f1a1b066a12560a7739a296be94656f30
Author: Egor <[email protected]>
AuthorDate: Tue Oct 14 20:35:50 2025 +0400
IGNITE-26624 Improve error message when thin client connects to not
initialized cluster (#6755)
Co-authored-by: Egor Kuts <[email protected]>
---
.../ignite/internal/client/TcpClientChannel.java | 16 ++-
.../ItThinClientUninitializedClusterTest.java | 124 +++++++++++++++++++++
2 files changed, 137 insertions(+), 3 deletions(-)
diff --git
a/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
b/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
index 50a00ea8283..bda28cdd2b9 100644
---
a/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
+++
b/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
@@ -156,6 +156,9 @@ class TcpClientChannel implements ClientChannel,
ClientMessageHandler, ClientCon
/** Last receive operation timestamp. */
private volatile long lastReceiveMillis;
+ /** Whether tcp connection was established. */
+ private volatile boolean tcpConnectionEstablished;
+
/**
* Constructor.
*
@@ -194,6 +197,8 @@ class TcpClientChannel implements ClientChannel,
ClientMessageHandler, ClientCon
log.debug("Connection established [remoteAddress=" +
s.remoteAddress() + ']');
}
+ tcpConnectionEstablished = true;
+
ClientTimeoutWorker.INSTANCE.registerClientChannel(this);
sock = s;
@@ -264,8 +269,14 @@ class TcpClientChannel implements ClientChannel,
ClientMessageHandler, ClientCon
}
for (TimeoutObjectImpl pendingReq : pendingReqs.values()) {
- pendingReq.future().completeExceptionally(
- new IgniteClientConnectionException(CONNECTION_ERR,
"Channel is closed", endpoint(), cause));
+ if (tcpConnectionEstablished && lastReceiveMillis == 0) {
+ pendingReq.future().completeExceptionally(
+ new IgniteClientConnectionException(CONNECTION_ERR,
+ "Channel is closed, cluster might not have
been initialised", endpoint(), cause));
+ } else {
+ pendingReq.future().completeExceptionally(
+ new IgniteClientConnectionException(CONNECTION_ERR,
"Channel is closed", endpoint(), cause));
+ }
}
for (CompletableFuture<PayloadInputChannel> handler :
notificationHandlers.values()) {
@@ -665,7 +676,6 @@ class TcpClientChannel implements ClientChannel,
ClientMessageHandler, ClientCon
metrics.handshakesFailedTimeoutIncrement();
throw new
IgniteClientConnectionException(CONNECTION_ERR, "Handshake timeout",
endpoint(), err);
}
-
metrics.handshakesFailedIncrement();
throw new IgniteClientConnectionException(CONNECTION_ERR,
"Handshake error", endpoint(), err);
});
diff --git
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientUninitializedClusterTest.java
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientUninitializedClusterTest.java
new file mode 100644
index 00000000000..2cf623583a5
--- /dev/null
+++
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientUninitializedClusterTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.ignite.internal.runner.app.client;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import org.apache.ignite.client.IgniteClient;
+import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
+import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.apache.ignite.lang.IgniteException;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+
+/**
+ * Tests thin client connection behavior when the cluster is not initialized.
+ * This test simulates a scenario similar to a docker container proxy that
accepts connections
+ * and immediately drops them since the node's process starts listening on the
port only after initialization.
+ */
+@ExtendWith(WorkDirectoryExtension.class)
+public class ItThinClientUninitializedClusterTest extends
BaseIgniteAbstractTest {
+ private static final int CLIENT_PORT = 10809;
+ private EventLoopGroup bossGroup;
+ private EventLoopGroup workerGroup;
+ private Channel serverChannel;
+
+ @BeforeEach
+ void startDummyNettyServer() throws InterruptedException {
+ // Set up a mock tcp socket listener at CLIENT_PORT.
+ bossGroup = new NioEventLoopGroup(1);
+
+ workerGroup = new NioEventLoopGroup();
+
+ ServerBootstrap bootstrap = new ServerBootstrap()
+ .group(bossGroup, workerGroup)
+ .channel(NioServerSocketChannel.class)
+ .childHandler(new ChannelInitializer<SocketChannel>() {
+ @Override
+ protected void initChannel(SocketChannel ch) {
+ // Immediately close every new connection.
+ ch.pipeline().addLast(new
ChannelInboundHandlerAdapter() {
+ @Override
+ public void channelActive(ChannelHandlerContext
ctx) {
+ ctx.close();
+ }
+ });
+ }
+ })
+ .childOption(ChannelOption.SO_KEEPALIVE, false);
+
+ ChannelFuture bindFuture = bootstrap.bind("127.0.0.1",
CLIENT_PORT).sync();
+ serverChannel = bindFuture.channel();
+ }
+
+ @Test
+ void testThinClientConnectionToNotInitializedClusterThrowsException() {
+ IgniteException ex = assertThrows(
+ IgniteException.class,
+ () -> {
+ try (IgniteClient client = IgniteClient.builder()
+ .addresses("127.0.0.1:" + CLIENT_PORT)
+ .build()) {
+ // Client construction should fail after tcp handshake.
+ }
+ }
+ );
+
+ // Check that the cause chain contains the helpful error message.
+ boolean foundMessage = false;
+ Throwable cause = ex;
+ while (cause != null) {
+ if (cause.getMessage() != null &&
cause.getMessage().contains("cluster might not have been initialised")) {
+ foundMessage = true;
+ break;
+ }
+ cause = cause.getCause();
+ }
+
+ assertTrue(foundMessage, "Expected to find 'cluster might not have
been initialised' in exception cause chain");
+ }
+
+ @AfterEach
+ void stopDummyNettyServer() throws InterruptedException {
+ // Stop the listener.
+ if (serverChannel != null) {
+ serverChannel.close().sync();
+ }
+ if (workerGroup != null) {
+ workerGroup.shutdownGracefully().sync();
+ }
+ if (bossGroup != null) {
+ bossGroup.shutdownGracefully().sync();
+ }
+ }
+}