This is an automated email from the ASF dual-hosted git repository.

petrov-mg pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/master by this push:
     new dda84bf0603 IGNITE-28983 Fixed thin client hanging if the remote 
endpoint stops responding during the SSL handshake without closing the socket 
(#13477)
dda84bf0603 is described below

commit dda84bf0603f4aef28b0fc4c4bf4ac52e6acc972
Author: Mikhail Petrov <[email protected]>
AuthorDate: Fri Aug 14 00:10:31 2026 +0300

    IGNITE-28983 Fixed thin client hanging if the remote endpoint stops 
responding during the SSL handshake without closing the socket (#13477)
---
 .../thin/ThinClientHandshakeTimeoutTest.java       | 199 +++++++++++++++++++++
 .../org/apache/ignite/client/ClientTestSuite.java  |   2 +
 .../ignite/internal/client/thin/ClientUtils.java   |  23 +++
 .../internal/client/thin/TcpClientChannel.java     |  22 ++-
 .../GridNioClientConnectionMultiplexer.java        | 101 +++++++----
 5 files changed, 304 insertions(+), 43 deletions(-)

diff --git 
a/modules/core/src/test/java/org/apache/ignite/internal/client/thin/ThinClientHandshakeTimeoutTest.java
 
b/modules/core/src/test/java/org/apache/ignite/internal/client/thin/ThinClientHandshakeTimeoutTest.java
new file mode 100644
index 00000000000..06da50b5fad
--- /dev/null
+++ 
b/modules/core/src/test/java/org/apache/ignite/internal/client/thin/ThinClientHandshakeTimeoutTest.java
@@ -0,0 +1,199 @@
+/*
+ * 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.client.thin;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.BindException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.client.ClientConnectionException;
+import org.apache.ignite.client.IgniteClient;
+import org.apache.ignite.client.SslMode;
+import org.apache.ignite.configuration.ClientConfiguration;
+import org.apache.ignite.configuration.ClientConnectorConfiguration;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static 
org.apache.ignite.testframework.GridTestUtils.assertThrowsAnyCause;
+import static org.apache.ignite.testframework.GridTestUtils.sslTrustedFactory;
+
+/** */
+public class ThinClientHandshakeTimeoutTest extends GridCommonAbstractTest {
+    /** */
+    private static final String HOST = "127.0.0.1";
+
+    /** */
+    private static final int HANDSHAKE_TIMEOUT = 2_000;
+
+    /** */
+    @Test
+    public void testHandshakeTimeout() throws Exception {
+        try (TestServer srv = new TestServer()) {
+            assertClientConnectionFailed(
+                clientConfiguration(srv.port(), false),
+                "Failed to wait for Ignite Client handshake completion");
+        }
+    }
+
+    /** */
+    @Test
+    public void testSslHandshakeTimeout() throws Exception {
+        try (TestServer srv = new TestServer()) {
+            assertClientConnectionFailed(
+                clientConfiguration(srv.port(), true),
+                "Failed to wait for SSL handshake completion");
+        }
+    }
+
+    /** */
+    @Test
+    public void testConnectionIsReleasedOnHandshakeTimeout() throws Exception {
+        startGrid(0);
+
+        try (TestServer srv = new TestServer()) {
+            ClientConfiguration cfg = new ClientConfiguration()
+                .setAddresses(HOST + ':' + srv.port(), HOST + ':' + 
ClientConnectorConfiguration.DFLT_PORT)
+                .setHandshakeTimeout(HANDSHAKE_TIMEOUT);
+
+            try (IgniteClient cli = Ignition.startClient(cfg)) {
+                srv.awaitConnectionAcceptedAndClosedByClient(getTestTimeout());
+
+                assertEquals(1, cli.cluster().nodes().size());
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        stopAllGrids();
+
+        super.afterTest();
+    }
+
+    /** */
+    private ClientConfiguration clientConfiguration(int port, boolean 
isSslEnabled) {
+        ClientConfiguration cfg = new ClientConfiguration()
+            .setAddresses(HOST + ':' + port)
+            .setHandshakeTimeout(HANDSHAKE_TIMEOUT);
+
+        if (isSslEnabled) {
+            cfg.setSslMode(SslMode.REQUIRED);
+            cfg.setSslContextFactory(sslTrustedFactory("thinClient", 
"trusttwo"));
+        }
+
+        return cfg;
+    }
+
+    /** */
+    private static void assertClientConnectionFailed(ClientConfiguration cfg, 
String errMsg) {
+        assertThrowsAnyCause(log, () -> Ignition.startClient(cfg), 
ClientConnectionException.class, errMsg);
+    }
+
+    /** */
+    private static class TestServer implements AutoCloseable {
+        /** */
+        private static final int DFLT_PORT = 1024;
+
+        /** */
+        private final ServerSocket srvSock;
+
+        /** */
+        private final BlockingQueue<Socket> accepted = new 
LinkedBlockingQueue<>();
+
+        /** */
+        private final Thread acceptor;
+
+        /** */
+        TestServer() throws IOException {
+            srvSock = createServerSocket();
+
+            acceptor = new Thread(() -> {
+                try {
+                    while (!Thread.currentThread().isInterrupted())
+                        accepted.add(srvSock.accept());
+                }
+                catch (IOException ignored) {
+                    // No-op.
+                }
+            }, "test-server-acceptor");
+
+            acceptor.setDaemon(true);
+            acceptor.start();
+        }
+
+        /** */
+        private ServerSocket createServerSocket() throws IOException {
+            int port = DFLT_PORT;
+
+            while (true) {
+                try {
+                    return new ServerSocket(port, 50, 
InetAddress.getByName(HOST));
+                }
+                catch (BindException ignore) {
+                    port++;
+
+                    assertTrue(port < ClientConnectorConfiguration.DFLT_PORT);
+                }
+            }
+        }
+
+        /** */
+        int port() {
+            return srvSock.getLocalPort();
+        }
+
+        /** */
+        void awaitConnectionAcceptedAndClosedByClient(long timeout) throws 
Exception {
+            Socket sock = accepted.poll(timeout, TimeUnit.MILLISECONDS);
+
+            assertNotNull(sock);
+
+            sock.setSoTimeout((int)timeout);
+
+            InputStream in = sock.getInputStream();
+
+            while (in.read() >= 0) {
+                // No-op.
+            }
+        }
+
+        /** {@inheritDoc} */
+        @Override public void close() throws Exception {
+            acceptor.interrupt();
+
+            srvSock.close();
+
+            List<Socket> socks = new ArrayList<>();
+
+            accepted.drainTo(socks);
+
+            for (Socket sock : socks)
+                sock.close();
+
+            acceptor.join(5000);
+        }
+    }
+}
diff --git 
a/modules/indexing/src/test/java/org/apache/ignite/client/ClientTestSuite.java 
b/modules/indexing/src/test/java/org/apache/ignite/client/ClientTestSuite.java
index 7e36b9ae6ba..58264b64be7 100644
--- 
a/modules/indexing/src/test/java/org/apache/ignite/client/ClientTestSuite.java
+++ 
b/modules/indexing/src/test/java/org/apache/ignite/client/ClientTestSuite.java
@@ -43,6 +43,7 @@ import 
org.apache.ignite.internal.client.thin.ServiceAwarenessTest;
 import org.apache.ignite.internal.client.thin.ServicesBinaryArraysTests;
 import org.apache.ignite.internal.client.thin.ServicesTest;
 import org.apache.ignite.internal.client.thin.ThinClientEnpointsDiscoveryTest;
+import org.apache.ignite.internal.client.thin.ThinClientHandshakeTimeoutTest;
 import 
org.apache.ignite.internal.client.thin.ThinClientNonTransactionalOperationsInTxTest;
 import 
org.apache.ignite.internal.client.thin.ThinClientPartitionAwarenessBalancingTest;
 import 
org.apache.ignite.internal.client.thin.ThinClientPartitionAwarenessDiscoveryTest;
@@ -75,6 +76,7 @@ import org.junit.runners.Suite;
     SslParametersTest.class,
     ConnectionTest.class,
     ConnectToStartingNodeTest.class,
+    ThinClientHandshakeTimeoutTest.class,
     AsyncChannelTest.class,
     ComputeTaskTest.class,
     ClusterApiTest.class,
diff --git 
a/modules/thin-client/impl/src/main/java/org/apache/ignite/internal/client/thin/ClientUtils.java
 
b/modules/thin-client/impl/src/main/java/org/apache/ignite/internal/client/thin/ClientUtils.java
index ac8a9a4c7d3..0bf8047dd3a 100644
--- 
a/modules/thin-client/impl/src/main/java/org/apache/ignite/internal/client/thin/ClientUtils.java
+++ 
b/modules/thin-client/impl/src/main/java/org/apache/ignite/internal/client/thin/ClientUtils.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.internal.client.thin;
 
 import java.io.IOException;
+import java.net.InetSocketAddress;
 import java.util.AbstractMap.SimpleEntry;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -35,6 +36,7 @@ import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import javax.cache.expiry.ExpiryPolicy;
+import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.binary.BinaryRawWriter;
 import org.apache.ignite.cache.CacheAtomicityMode;
 import org.apache.ignite.cache.CacheKeyConfiguration;
@@ -48,7 +50,12 @@ import org.apache.ignite.cache.QueryIndexType;
 import org.apache.ignite.cache.query.SqlFieldsQuery;
 import org.apache.ignite.client.ClientAffinityConfiguration;
 import org.apache.ignite.client.ClientCacheConfiguration;
+import org.apache.ignite.client.ClientConnectionException;
 import org.apache.ignite.client.ClientFeatureNotSupportedByServerException;
+import org.apache.ignite.internal.IgniteFutureCancelledCheckedException;
+import org.apache.ignite.internal.IgniteFutureTimeoutCheckedException;
+import org.apache.ignite.internal.IgniteInternalFuture;
+import org.apache.ignite.internal.IgniteInterruptedCheckedException;
 import org.apache.ignite.internal.binary.BinaryContext;
 import org.apache.ignite.internal.binary.BinaryFieldMetadata;
 import org.apache.ignite.internal.binary.BinaryMetadata;
@@ -162,6 +169,22 @@ public final class ClientUtils {
         return map;
     }
 
+    /** */
+    public static <T> T awaitFutureResult(IgniteInternalFuture<T> fut, long 
timeout, String desc) throws IgniteCheckedException {
+        try {
+            return timeout > 0 ? fut.get(timeout) : fut.get();
+        }
+        catch (IgniteFutureTimeoutCheckedException | 
IgniteFutureCancelledCheckedException |
+               IgniteInterruptedCheckedException e) {
+            throw new IgniteCheckedException("Failed to wait for " + desc + " 
completion [timeout=" + timeout + ']', e);
+        }
+    }
+
+    /** */
+    public static ClientConnectionException 
createClientConnectionException(Exception e, InetSocketAddress addr) {
+        return new ClientConnectionException(e.getMessage() + " 
[remoteAddress=" + addr + ']', e);
+    }
+
     /** Deserialize binary type metadata from stream. */
     BinaryMetadata binaryMetadata(BinaryInputStream in) throws IOException {
         try (BinaryReaderEx reader = createBinaryReader(in)) {
diff --git 
a/modules/thin-client/impl/src/main/java/org/apache/ignite/internal/client/thin/TcpClientChannel.java
 
b/modules/thin-client/impl/src/main/java/org/apache/ignite/internal/client/thin/TcpClientChannel.java
index c61a80b4ffd..a7109e0e02e 100644
--- 
a/modules/thin-client/impl/src/main/java/org/apache/ignite/internal/client/thin/TcpClientChannel.java
+++ 
b/modules/thin-client/impl/src/main/java/org/apache/ignite/internal/client/thin/TcpClientChannel.java
@@ -75,6 +75,8 @@ import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.logger.NullLogger;
 import org.jetbrains.annotations.Nullable;
 
+import static 
org.apache.ignite.internal.client.thin.ClientUtils.awaitFutureResult;
+import static 
org.apache.ignite.internal.client.thin.ClientUtils.createClientConnectionException;
 import static 
org.apache.ignite.internal.client.thin.ProtocolBitmaskFeature.HEARTBEAT;
 import static 
org.apache.ignite.internal.client.thin.ProtocolBitmaskFeature.USER_ATTRIBUTES;
 import static 
org.apache.ignite.internal.client.thin.ProtocolVersion.LATEST_VER;
@@ -223,13 +225,16 @@ class TcpClientChannel implements ClientChannel, 
ClientMessageHandler, ClientCon
             try {
                 handshake(DEFAULT_VERSION, cfg.getUserName(), 
cfg.getUserPassword(), cfg.getUserAttributes());
             }
-            catch (ClientConnectionException e) {
-                if (!X.hasCause(e, 
ClientConnectionNodeRecoveryException.class))
+            catch (Exception e) {
+                if (!isCausedByNodeInRecoveryMode(e)) {
+                    close(e);
+
                     throw e;
+                }
 
                 log.info("Can't establish connection with " + addr + ". Node 
in recovery mode.");
 
-                connectionEx = CommonUtils.addSuppressed(connectionEx, e);
+                connectionEx = CommonUtils.addSuppressed(connectionEx, 
(ClientConnectionException)e);
 
                 CommonUtils.closeQuiet(sock);
                 sock = null;
@@ -738,7 +743,7 @@ class TcpClientChannel implements ClientChannel, 
ClientMessageHandler, ClientCon
             handshakeReq(ver, user, pwd, userAttrs);
 
             try {
-                ByteBuffer buf = handshakeTimeout > 0 ? 
fut.get(handshakeTimeout) : fut.get();
+                ByteBuffer buf = awaitFutureResult(fut, handshakeTimeout, 
"Ignite Client handshake");
 
                 BinaryInputStream res = BinaryStreams.inputStream(buf);
 
@@ -824,7 +829,7 @@ class TcpClientChannel implements ClientChannel, 
ClientMessageHandler, ClientCon
                 if (e instanceof IOException)
                     err = handleIOError((IOException)e);
                 else
-                    err = new ClientConnectionException(e.getMessage() + " 
[remoteAddress=" + sock.remoteAddress() + ']', e);
+                    err = createClientConnectionException(e, 
sock.remoteAddress());
 
                 eventListener.onHandshakeFail(
                     new ConnectionDescription(sock.localAddress(), 
sock.remoteAddress(), new ProtocolContext(ver).toString(), null),
@@ -894,7 +899,7 @@ class TcpClientChannel implements ClientChannel, 
ClientMessageHandler, ClientCon
             lastSendMillis = System.currentTimeMillis();
         }
         catch (IgniteCheckedException e) {
-            throw new ClientConnectionException(e.getMessage() + " 
[remoteAddress=" + sock.remoteAddress() + ']', e);
+            throw createClientConnectionException(e, sock.remoteAddress());
         }
     }
 
@@ -960,6 +965,11 @@ class TcpClientChannel implements ClientChannel, 
ClientMessageHandler, ClientCon
         return res;
     }
 
+    /** */
+    private boolean isCausedByNodeInRecoveryMode(Exception e) {
+        return e instanceof ClientConnectionException && X.hasCause(e, 
ClientConnectionNodeRecoveryException.class);
+    }
+
     /** {@inheritDoc} */
     @Override public String toString() {
         return "TcpClientChannel [srvNodeId=" + srvNodeId + ", addr=" + 
sock.remoteAddress() + ']';
diff --git 
a/modules/thin-client/impl/src/main/java/org/apache/ignite/internal/client/thin/io/gridnioserver/GridNioClientConnectionMultiplexer.java
 
b/modules/thin-client/impl/src/main/java/org/apache/ignite/internal/client/thin/io/gridnioserver/GridNioClientConnectionMultiplexer.java
index 5d157fe7251..f5091c31d94 100644
--- 
a/modules/thin-client/impl/src/main/java/org/apache/ignite/internal/client/thin/io/gridnioserver/GridNioClientConnectionMultiplexer.java
+++ 
b/modules/thin-client/impl/src/main/java/org/apache/ignite/internal/client/thin/io/gridnioserver/GridNioClientConnectionMultiplexer.java
@@ -37,6 +37,7 @@ import 
org.apache.ignite.internal.client.thin.io.ClientConnection;
 import org.apache.ignite.internal.client.thin.io.ClientConnectionMultiplexer;
 import org.apache.ignite.internal.client.thin.io.ClientConnectionStateHandler;
 import org.apache.ignite.internal.client.thin.io.ClientMessageHandler;
+import org.apache.ignite.internal.util.CommonUtils;
 import org.apache.ignite.internal.util.future.GridFutureAdapter;
 import org.apache.ignite.internal.util.nio.GridNioCodecFilter;
 import org.apache.ignite.internal.util.nio.GridNioFilter;
@@ -45,6 +46,9 @@ import org.apache.ignite.internal.util.nio.GridNioSession;
 import org.apache.ignite.internal.util.nio.ssl.GridNioSslFilter;
 import org.apache.ignite.logger.NullLogger;
 
+import static 
org.apache.ignite.internal.client.thin.ClientUtils.awaitFutureResult;
+import static 
org.apache.ignite.internal.client.thin.ClientUtils.createClientConnectionException;
+
 /**
  * Client connection multiplexer based on {@link 
org.apache.ignite.internal.util.nio.GridNioServer}.
  */
@@ -139,65 +143,88 @@ public class GridNioClientConnectionMultiplexer 
implements ClientConnectionMulti
     }
 
     /** {@inheritDoc} */
-    @Override public ClientConnection open(InetSocketAddress addr,
-                                           ClientMessageHandler msgHnd,
-                                           ClientConnectionStateHandler 
stateHnd)
-            throws ClientConnectionException {
+    @Override public ClientConnection open(
+        InetSocketAddress addr,
+        ClientMessageHandler msgHnd,
+        ClientConnectionStateHandler stateHnd
+    ) throws ClientConnectionException {
         rwLock.readLock().lock();
 
         try {
             SocketChannel ch = null;
+
             try {
                 ch = SocketChannel.open();
+
                 ch.socket().connect(new InetSocketAddress(addr.getHostName(), 
addr.getPort()), connTimeout);
             }
             catch (Exception e) {
-                if (ch != null) {
-                    if (ch.socket() != null) {
-                        try {
-                            ch.socket().close();
-                        }
-                        catch (Exception ignored) {
-                            // ignore close exception
-                        }
-                    }
-
-                    try {
-                        ch.close();
-                    }
-                    catch (Exception ignored) {
-                        // ignore close exception
-                    }
-                }
-                throw new ClientConnectionException(e.getMessage(), e);
+                CommonUtils.closeQuiet(ch);
+
+                throw createClientConnectionException(e, addr);
             }
 
-            Map<Integer, Object> meta = new HashMap<>();
-            IgniteInternalFuture<?> sslHandshakeFut = null;
+            GridNioSession ses = null;
 
-            if (sslCtx != null) {
-                sslHandshakeFut = new GridFutureAdapter<>();
+            try {
+                ses = createNioSession(ch);
+
+                return new GridNioClientConnection(ses, msgHnd, stateHnd);
+            }
+            catch (Exception e) {
+                if (ses != null)
+                    ses.close();
 
-                meta.put(GridNioSslFilter.HANDSHAKE_FUT_META_KEY, 
sslHandshakeFut);
+                throw createClientConnectionException(e, addr);
             }
+        }
+        finally {
+            rwLock.readLock().unlock();
+        }
+    }
+
+    /** */
+    private GridNioSession createNioSession(SocketChannel ch) throws 
IgniteCheckedException {
+        Map<Integer, Object> meta = new HashMap<>();
 
-            IgniteInternalFuture<GridNioSession> sesFut = 
srv.createSession(ch, meta, false, null);
+        GridFutureAdapter<?> sslHandshakeFut = null;
 
-            if (sesFut.error() != null)
-                sesFut.get();
+        if (sslCtx != null) {
+            sslHandshakeFut = new GridFutureAdapter<>();
+
+            meta.put(GridNioSslFilter.HANDSHAKE_FUT_META_KEY, sslHandshakeFut);
+        }
 
-            if (sslHandshakeFut != null)
-                sslHandshakeFut.get();
+        IgniteInternalFuture<GridNioSession> sesFut = srv.createSession(ch, 
meta, false, null);
 
-            GridNioSession ses = sesFut.get();
+        GridNioSession ses;
 
-            return new GridNioClientConnection(ses, msgHnd, stateHnd);
+        try {
+            ses = awaitFutureResult(sesFut, connTimeout, "NIO session 
creation");
         }
         catch (Exception e) {
-            throw new ClientConnectionException(e.getMessage() + " 
[remoteAddress=" + addr + ']', e);
+            sesFut.listen(fut -> {
+                if (fut.error() == null)
+                    fut.result().close();
+            });
+
+            CommonUtils.closeQuiet(ch);
+
+            throw e;
         }
-        finally {
-            rwLock.readLock().unlock();
+
+        if (sslHandshakeFut == null)
+            return ses;
+
+        try {
+            awaitFutureResult(sslHandshakeFut, connTimeout, "SSL handshake");
         }
+        catch (Exception e) {
+            ses.close();
+
+            throw e;
+        }
+
+        return ses;
     }
 }

Reply via email to