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

sergey-chugunov-1985 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 5c899284ac5 IGNITE-28855 Client node permanently loses discovery 
events delivered right after the reconnect response (#13327)
5c899284ac5 is described below

commit 5c899284ac5cc9e3739cfd96bc8a8686a319ff04
Author: Anton Vinogradov <[email protected]>
AuthorDate: Fri Jul 31 16:08:54 2026 +0300

    IGNITE-28855 Client node permanently loses discovery events delivered right 
after the reconnect response (#13327)
---
 .../ignite/spi/discovery/tcp/ClientImpl.java       |  25 +++-
 .../tcp/TcpDiscoveryClientTopologyGapTest.java     | 135 +++++++++++++++++++++
 .../IgniteSpiDiscoverySelfTestSuite2.java          |   2 +
 3 files changed, 157 insertions(+), 5 deletions(-)

diff --git 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/ClientImpl.java
 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/ClientImpl.java
index 918a29bab42..43d4f1b4867 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/ClientImpl.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/ClientImpl.java
@@ -776,7 +776,7 @@ class ClientImpl extends TcpDiscoveryImpl {
                     log.debug("Message has been sent to address [msg=" + msg + 
", addr=" + addr +
                         ", rmtNodeId=" + rmtNodeId + ']');
 
-                return new T2<>(new SocketStream(sock), spi.readReceipt(sock, 
timeoutHelper.nextTimeoutChunk(ackTimeout0)));
+                return new T2<>(new SocketStream(sock, ses), 
spi.readReceipt(sock, timeoutHelper.nextTimeoutChunk(ackTimeout0)));
             }
             catch (IOException | IgniteCheckedException e) {
                 U.closeQuiet(sock);
@@ -1162,7 +1162,7 @@ class ClientImpl extends TcpDiscoveryImpl {
                     + ":" + sockStream.sock.getPort());
 
                 try {
-                    TcpDiscoveryIoSession ses = createSession(sock);
+                    TcpDiscoveryIoSession ses = sockStream.session();
 
                     assert sock.getKeepAlive() && sock.getTcpNoDelay() : 
"Socket wasn't configured properly:" +
                         " KeepAlive " + sock.getKeepAlive() +
@@ -1573,7 +1573,7 @@ class ClientImpl extends TcpDiscoveryImpl {
                     sockStream = joinRes;
 
                     Socket sock = sockStream.socket();
-                    TcpDiscoveryIoSession ses = createSession(sock);
+                    TcpDiscoveryIoSession ses = sockStream.session();
 
                     if (isInterrupted())
                         throw new InterruptedException();
@@ -2709,14 +2709,23 @@ class ClientImpl extends TcpDiscoveryImpl {
         /** */
         private final Socket sock;
 
+        /**
+         * The only session ever used to read messages from the socket. Shared 
by all socket users
+         * ({@link Reconnector}, {@link SocketReader}), otherwise messages 
buffered by one session's
+         * read-ahead would be lost when another session takes the socket over.
+         */
+        private final TcpDiscoveryIoSession ses;
+
         /**
          * @param sock Socket.
-         * @throws IOException If failed to create stream.
+         * @param ses Session bound to the socket.
          */
-        public SocketStream(Socket sock) throws IOException {
+        SocketStream(Socket sock, TcpDiscoveryIoSession ses) {
             assert sock != null;
+            assert ses != null;
 
             this.sock = sock;
+            this.ses = ses;
         }
 
         /**
@@ -2724,7 +2733,13 @@ class ClientImpl extends TcpDiscoveryImpl {
          */
         Socket socket() {
             return sock;
+        }
 
+        /**
+         * @return Session bound to the socket.
+         */
+        TcpDiscoveryIoSession session() {
+            return ses;
         }
 
         /** {@inheritDoc} */
diff --git 
a/modules/core/src/test/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoveryClientTopologyGapTest.java
 
b/modules/core/src/test/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoveryClientTopologyGapTest.java
new file mode 100644
index 00000000000..ba86e441f2f
--- /dev/null
+++ 
b/modules/core/src/test/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoveryClientTopologyGapTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.spi.discovery.tcp;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.failure.AbstractFailureHandler;
+import org.apache.ignite.failure.FailureContext;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.testframework.GridTestUtils;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+/**
+ * Reproducer for a client node losing a discovery event (topology version 
gap) while its router and
+ * the coordinator are sequentially stopped. The gap manifests as {@code 
AssertionError: lastVer=N, newVer=N+2}
+ * in {@code ClientImpl.updateTopologyHistory}.
+ */
+public class TcpDiscoveryClientTopologyGapTest extends GridCommonAbstractTest {
+    /** */
+    private static final int SRVS = 5;
+
+    /** */
+    private static final int CLIENTS = 4;
+
+    /** */
+    private static final int ITERS = 300;
+
+    /** Critical failures captured on nodes, by instance name. */
+    private static final Map<String, Throwable> failures = new 
ConcurrentHashMap<>();
+
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String 
igniteInstanceName) throws Exception {
+        IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);
+
+        ((TcpDiscoverySpi)cfg.getDiscoverySpi()).setLocalPort(48700);
+
+        cfg.setFailureHandler(new AbstractFailureHandler() {
+            @Override protected boolean handle(Ignite ignite, FailureContext 
failureCtx) {
+                failures.put(igniteInstanceName, failureCtx.error());
+
+                return false;
+            }
+        });
+
+        return cfg;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        stopAllGrids();
+
+        super.afterTest();
+    }
+
+    /**
+     * Mirrors the topology dance of {@code 
IgniteCachePartitionLossPolicySelfTest} with 5 nodes and
+     * {@code stopNodes={1, 0, 2}}: grid(0) is both the clients' router and 
the coordinator, grid(2)
+     * is the next coordinator. Clients must observe every topology version.
+     */
+    @Test
+    public void testClientSeesAllTopologyVersionsOnSequentialServerStop() 
throws Exception {
+        for (int iter = 0; iter < ITERS; iter++) {
+            log.info(">>> Iteration: " + iter);
+
+            failures.clear();
+
+            startGrids(SRVS);
+
+            IgniteEx[] clients = new IgniteEx[CLIENTS];
+
+            for (int i = 0; i < CLIENTS; i++)
+                clients[i] = startClientGrid(SRVS + i);
+
+            stopGrid(1, true);
+            stopGrid(0, true);
+            stopGrid(2, true);
+
+            long expTopVer = SRVS + CLIENTS + 3;
+
+            // Failure detection may push the topology past the expected 
version, so it is only a lower bound here,
+            // while the invariant the test is about is checked below.
+            for (IgniteEx client : clients) {
+                if (!GridTestUtils.waitForCondition(() -> 
client.cluster().topologyVersion() >= expTopVer, 15_000)) {
+                    fail("Client stuck [name=" + client.name() +
+                        ", topVer=" + client.cluster().topologyVersion() +
+                        ", expTopVer=" + expTopVer +
+                        ", failures=" + failures + ']');
+                }
+            }
+
+            for (IgniteEx client : clients)
+                assertNoTopologyGap(client, expTopVer);
+
+            assertTrue("Critical failures detected: " + failures, 
failures.isEmpty());
+
+            stopAllGrids();
+        }
+    }
+
+    /**
+     * Checks that the node was notified of every topology version up to 
{@code lastVer}. Versions preceding the
+     * node's own join are missing from its history and are skipped.
+     *
+     * @param node Node to check.
+     * @param lastVer Last topology version to check.
+     */
+    private void assertNoTopologyGap(IgniteEx node, long lastVer) {
+        boolean joined = false;
+
+        for (long ver = 1; ver <= lastVer; ver++) {
+            if (node.context().discovery().topology(ver) != null)
+                joined = true;
+            else if (joined)
+                fail("Client lost topology version [name=" + node.name() + ", 
ver=" + ver + ']');
+        }
+    }
+}
diff --git 
a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteSpiDiscoverySelfTestSuite2.java
 
b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteSpiDiscoverySelfTestSuite2.java
index c35d838d055..7d827a4d3ac 100644
--- 
a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteSpiDiscoverySelfTestSuite2.java
+++ 
b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteSpiDiscoverySelfTestSuite2.java
@@ -33,6 +33,7 @@ import 
org.apache.ignite.spi.discovery.tcp.MultiDataCenterSplitTest;
 import 
org.apache.ignite.spi.discovery.tcp.TcpClientDiscoverySpiCoordinatorChangeTest;
 import org.apache.ignite.spi.discovery.tcp.TcpClientDiscoverySpiSelfTest;
 import 
org.apache.ignite.spi.discovery.tcp.TcpClientDiscoveryUnresolvedHostTest;
+import org.apache.ignite.spi.discovery.tcp.TcpDiscoveryClientTopologyGapTest;
 import org.apache.ignite.spi.discovery.tcp.TcpDiscoveryConcurrentStartTest;
 import 
org.apache.ignite.spi.discovery.tcp.TcpDiscoveryDeadNodeAddressResolvingTest;
 import org.apache.ignite.spi.discovery.tcp.TcpDiscoveryIpFinderFailureTest;
@@ -74,6 +75,7 @@ import org.junit.runners.Suite;
     LongClientConnectToClusterTest.class,
     TcpClientDiscoverySpiCoordinatorChangeTest.class,
     TcpClientDiscoveryUnresolvedHostTest.class,
+    TcpDiscoveryClientTopologyGapTest.class,
     TcpDiscoveryNodeConsistentIdSelfTest.class,
     TcpDiscoveryNodeConfigConsistentIdSelfTest.class,
     TcpDiscoveryRestartTest.class,

Reply via email to