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

timoninmaxim 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 2017e43f96a IGNITE-25534 Ignite accepts joining node with rolling 
upgrade version (#12301)
2017e43f96a is described below

commit 2017e43f96a972a310b518de9f844abca36c284b
Author: Aleksandr Chesnokov <[email protected]>
AuthorDate: Wed Nov 5 10:20:27 2025 +0300

    IGNITE-25534 Ignite accepts joining node with rolling upgrade version 
(#12301)
    
    Co-authored-by: Maksim Timonin <[email protected]>
    Co-authored-by: Maksim Davydov <[email protected]>
---
 .../apache/ignite/internal/GridKernalContext.java  |   8 +
 .../ignite/internal/GridKernalContextImpl.java     |  12 +
 .../org/apache/ignite/internal/IgniteKernal.java   |   4 +-
 .../wal/reader/StandaloneGridKernalContext.java    |   6 +
 .../OsDiscoveryNodeValidationProcessor.java        |  73 ----
 .../rollingupgrade/RollingUpgradeProcessor.java    | 309 ++++++++++++++
 .../ignite/spi/discovery/tcp/TcpDiscoverySpi.java  |  11 +
 .../tcp/internal/TcpDiscoveryNodesRing.java        |  30 +-
 .../ignite/internal/GridReleaseTypeSelfTest.java   | 447 +++++++++++++++++++--
 9 files changed, 779 insertions(+), 121 deletions(-)

diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/GridKernalContext.java 
b/modules/core/src/main/java/org/apache/ignite/internal/GridKernalContext.java
index 75531b9c523..742105bf4da 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/GridKernalContext.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/GridKernalContext.java
@@ -67,6 +67,7 @@ import 
org.apache.ignite.internal.processors.port.GridPortProcessor;
 import org.apache.ignite.internal.processors.query.GridQueryProcessor;
 import org.apache.ignite.internal.processors.resource.GridResourceProcessor;
 import org.apache.ignite.internal.processors.rest.IgniteRestProcessor;
+import 
org.apache.ignite.internal.processors.rollingupgrade.RollingUpgradeProcessor;
 import 
org.apache.ignite.internal.processors.schedule.IgniteScheduleProcessorAdapter;
 import org.apache.ignite.internal.processors.security.IgniteSecurity;
 import 
org.apache.ignite.internal.processors.segmentation.GridSegmentationProcessor;
@@ -640,6 +641,13 @@ public interface GridKernalContext extends 
Iterable<GridComponent> {
      */
     public PerformanceStatisticsProcessor performanceStatistics();
 
+    /**
+     * Gets Rolling upgrade processor.
+     *
+     * @return Rolling upgrade processor.
+     */
+    public RollingUpgradeProcessor rollingUpgrade();
+
     /**
      * Executor that is in charge of processing user async continuations.
      *
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/GridKernalContextImpl.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/GridKernalContextImpl.java
index 5d8ae5d4af9..2a7c7f66afa 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/GridKernalContextImpl.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/GridKernalContextImpl.java
@@ -91,6 +91,7 @@ import 
org.apache.ignite.internal.processors.query.GridQueryProcessor;
 import org.apache.ignite.internal.processors.query.QueryEngine;
 import org.apache.ignite.internal.processors.resource.GridResourceProcessor;
 import org.apache.ignite.internal.processors.rest.IgniteRestProcessor;
+import 
org.apache.ignite.internal.processors.rollingupgrade.RollingUpgradeProcessor;
 import 
org.apache.ignite.internal.processors.schedule.IgniteScheduleProcessorAdapter;
 import org.apache.ignite.internal.processors.security.IgniteSecurity;
 import 
org.apache.ignite.internal.processors.segmentation.GridSegmentationProcessor;
@@ -362,6 +363,10 @@ public class GridKernalContextImpl implements 
GridKernalContext, Externalizable
     @GridToStringExclude
     private PerformanceStatisticsProcessor perfStatProc;
 
+    /** Rolling upgrade processor. */
+    @GridToStringExclude
+    private RollingUpgradeProcessor rollUpProc;
+
     /** */
     private Thread.UncaughtExceptionHandler hnd;
 
@@ -596,6 +601,8 @@ public class GridKernalContextImpl implements 
GridKernalContext, Externalizable
             transProc = (CacheObjectTransformerProcessor)comp;
         else if (comp instanceof PerformanceStatisticsProcessor)
             perfStatProc = (PerformanceStatisticsProcessor)comp;
+        else if (comp instanceof RollingUpgradeProcessor)
+            rollUpProc = (RollingUpgradeProcessor)comp;
         else if (comp instanceof IndexProcessor)
             indexProc = (IndexProcessor)comp;
         else if (!(comp instanceof DiscoveryNodeValidationProcessor
@@ -1105,6 +1112,11 @@ public class GridKernalContextImpl implements 
GridKernalContext, Externalizable
         return perfStatProc;
     }
 
+    /** {@inheritDoc} */
+    @Override public RollingUpgradeProcessor rollingUpgrade() {
+        return rollUpProc;
+    }
+
     /** {@inheritDoc} */
     @Override public Executor getAsyncContinuationExecutor() {
         return config().getAsyncContinuationExecutor() == null
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java 
b/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java
index 1878ef042e8..56191e68d9e 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java
@@ -146,7 +146,6 @@ import 
org.apache.ignite.internal.processors.marshaller.GridMarshallerMappingPro
 import 
org.apache.ignite.internal.processors.metastorage.persistence.DistributedMetaStorageImpl;
 import org.apache.ignite.internal.processors.metric.GridMetricManager;
 import 
org.apache.ignite.internal.processors.nodevalidation.DiscoveryNodeValidationProcessor;
-import 
org.apache.ignite.internal.processors.nodevalidation.OsDiscoveryNodeValidationProcessor;
 import org.apache.ignite.internal.processors.odbc.ClientListenerProcessor;
 import 
org.apache.ignite.internal.processors.performancestatistics.PerformanceStatisticsProcessor;
 import org.apache.ignite.internal.processors.platform.PlatformNoopProcessor;
@@ -160,6 +159,7 @@ import 
org.apache.ignite.internal.processors.resource.GridResourceProcessor;
 import 
org.apache.ignite.internal.processors.resource.GridSpringResourceContext;
 import org.apache.ignite.internal.processors.rest.GridRestProcessor;
 import org.apache.ignite.internal.processors.rest.IgniteRestProcessor;
+import 
org.apache.ignite.internal.processors.rollingupgrade.RollingUpgradeProcessor;
 import org.apache.ignite.internal.processors.security.GridSecurityProcessor;
 import org.apache.ignite.internal.processors.security.IgniteSecurity;
 import org.apache.ignite.internal.processors.security.IgniteSecurityProcessor;
@@ -3225,7 +3225,7 @@ public class IgniteKernal implements IgniteEx, 
Externalizable {
             return (T)new CacheObjectBinaryProcessorImpl(ctx);
 
         if (cls.equals(DiscoveryNodeValidationProcessor.class))
-            return (T)new OsDiscoveryNodeValidationProcessor(ctx);
+            return (T)new RollingUpgradeProcessor(ctx);
 
         if (cls.equals(IGridClusterStateProcessor.class))
             return (T)new GridClusterStateProcessor(ctx);
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/reader/StandaloneGridKernalContext.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/reader/StandaloneGridKernalContext.java
index 2e729960808..84af46b6e31 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/reader/StandaloneGridKernalContext.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/reader/StandaloneGridKernalContext.java
@@ -90,6 +90,7 @@ import 
org.apache.ignite.internal.processors.port.GridPortProcessor;
 import org.apache.ignite.internal.processors.query.GridQueryProcessor;
 import org.apache.ignite.internal.processors.resource.GridResourceProcessor;
 import org.apache.ignite.internal.processors.rest.IgniteRestProcessor;
+import 
org.apache.ignite.internal.processors.rollingupgrade.RollingUpgradeProcessor;
 import 
org.apache.ignite.internal.processors.schedule.IgniteScheduleProcessorAdapter;
 import org.apache.ignite.internal.processors.security.IgniteSecurity;
 import 
org.apache.ignite.internal.processors.security.NoOpIgniteSecurityProcessor;
@@ -741,6 +742,11 @@ public class StandaloneGridKernalContext implements 
GridKernalContext {
         return null;
     }
 
+    /** {@inheritDoc} */
+    @Override public RollingUpgradeProcessor rollingUpgrade() {
+        return null;
+    }
+
     /** {@inheritDoc} */
     @Override public Executor getAsyncContinuationExecutor() {
         return null;
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/nodevalidation/OsDiscoveryNodeValidationProcessor.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/nodevalidation/OsDiscoveryNodeValidationProcessor.java
deleted file mode 100644
index 21a9fde19ee..00000000000
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/nodevalidation/OsDiscoveryNodeValidationProcessor.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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.processors.nodevalidation;
-
-import java.util.Objects;
-import org.apache.ignite.cluster.ClusterNode;
-import org.apache.ignite.internal.GridKernalContext;
-import org.apache.ignite.internal.processors.GridProcessorAdapter;
-import org.apache.ignite.internal.util.typedef.internal.LT;
-import org.apache.ignite.internal.util.typedef.internal.U;
-import org.apache.ignite.spi.IgniteNodeValidationResult;
-import org.jetbrains.annotations.Nullable;
-
-import static org.apache.ignite.internal.IgniteNodeAttributes.ATTR_BUILD_VER;
-
-/**
- * Node validation.
- */
-public class OsDiscoveryNodeValidationProcessor extends GridProcessorAdapter 
implements DiscoveryNodeValidationProcessor {
-    /**
-     * @param ctx Kernal context.
-     */
-    public OsDiscoveryNodeValidationProcessor(GridKernalContext ctx) {
-        super(ctx);
-    }
-
-    /** {@inheritDoc} */
-    @Nullable @Override public IgniteNodeValidationResult 
validateNode(ClusterNode node) {
-        ClusterNode locNode = ctx.discovery().localNode();
-
-        // Check version.
-        String locBuildVer = locNode.attribute(ATTR_BUILD_VER);
-        String rmtBuildVer = node.attribute(ATTR_BUILD_VER);
-
-        if (!Objects.equals(rmtBuildVer, locBuildVer)) {
-            // OS nodes don't support rolling updates.
-            if (!locBuildVer.equals(rmtBuildVer)) {
-                String errMsg = "Local node and remote node have different 
version numbers " +
-                    "(node will not join, Ignite does not support rolling 
updates, " +
-                    "so versions must be exactly the same) " +
-                    "[locBuildVer=" + locBuildVer + ", rmtBuildVer=" + 
rmtBuildVer +
-                    ", locNodeAddrs=" + U.addressesAsString(locNode) +
-                    ", rmtNodeAddrs=" + U.addressesAsString(node) +
-                    ", locNodeId=" + locNode.id() + ", rmtNodeId=" + node.id() 
+ ']';
-
-                LT.warn(log, errMsg);
-
-                // Always output in debug.
-                if (log.isDebugEnabled())
-                    log.debug(errMsg);
-
-                return new IgniteNodeValidationResult(node.id(), errMsg);
-            }
-        }
-
-        return null;
-    }
-}
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/rollingupgrade/RollingUpgradeProcessor.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/rollingupgrade/RollingUpgradeProcessor.java
new file mode 100644
index 00000000000..14af99193ab
--- /dev/null
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/rollingupgrade/RollingUpgradeProcessor.java
@@ -0,0 +1,309 @@
+/*
+ * 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.processors.rollingupgrade;
+
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.internal.GridKernalContext;
+import org.apache.ignite.internal.processors.GridProcessorAdapter;
+import 
org.apache.ignite.internal.processors.metastorage.DistributedMetaStorage;
+import 
org.apache.ignite.internal.processors.metastorage.DistributedMetastorageLifecycleListener;
+import 
org.apache.ignite.internal.processors.metastorage.ReadableDistributedMetaStorage;
+import 
org.apache.ignite.internal.processors.nodevalidation.DiscoveryNodeValidationProcessor;
+import org.apache.ignite.internal.util.lang.IgnitePair;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.internal.LT;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.lang.IgniteProductVersion;
+import org.apache.ignite.spi.IgniteNodeValidationResult;
+import org.apache.ignite.spi.discovery.DiscoverySpi;
+import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
+import org.apache.ignite.spi.discovery.tcp.internal.TcpDiscoveryNodesRing;
+import org.jetbrains.annotations.Nullable;
+
+import static org.apache.ignite.internal.IgniteNodeAttributes.ATTR_BUILD_VER;
+import static 
org.apache.ignite.internal.processors.metastorage.DistributedMetaStorage.IGNITE_INTERNAL_KEY_PREFIX;
+
+/** Rolling upgrade processor. Manages current and target versions of cluster. 
*/
+public class RollingUpgradeProcessor extends GridProcessorAdapter implements 
DiscoveryNodeValidationProcessor {
+    /** Key for the distributed property that holds current and target 
versions. */
+    private static final String ROLLING_UPGRADE_VERSIONS_KEY = 
IGNITE_INTERNAL_KEY_PREFIX + "rolling.upgrade.versions";
+
+    /** Metastorage with the write access. */
+    @Nullable private volatile DistributedMetaStorage metastorage;
+
+    /** TCP discovery nodes ring. */
+    private TcpDiscoveryNodesRing ring;
+
+    /** Last joining node. */
+    private ClusterNode lastJoiningNode;
+
+    /** Last joining node timestamp. */
+    private long lastJoiningNodeTimestamp;
+
+    /** Lock for synchronization between tcp-disco-msg-worker thread and 
management operations. */
+    private final Object lock = new Object();
+
+    /** */
+    private final CountDownLatch startLatch = new CountDownLatch(1);
+
+    /** Pair with current and target versions. {@code null} when rolling 
upgrade is disabled. */
+    @Nullable private volatile IgnitePair<IgniteProductVersion> rollUpVers;
+
+    /**
+     * @param ctx Context.
+     */
+    public RollingUpgradeProcessor(GridKernalContext ctx) {
+        super(ctx);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void onKernalStart(boolean active) throws 
IgniteCheckedException {
+        DiscoverySpi spi = ctx.config().getDiscoverySpi();
+
+        if (spi instanceof TcpDiscoverySpi)
+            ring = ((TcpDiscoverySpi)spi).discoveryRing();
+
+        startLatch.countDown();
+    }
+
+    /** {@inheritDoc} */
+    @Override public void start() throws IgniteCheckedException {
+        
ctx.internalSubscriptionProcessor().registerDistributedMetastorageListener(new 
DistributedMetastorageLifecycleListener() {
+            @Override public void onReadyForWrite(DistributedMetaStorage 
metastorage) {
+                RollingUpgradeProcessor.this.metastorage = metastorage;
+            }
+
+            @Override public void 
onReadyForRead(ReadableDistributedMetaStorage metastorage) {
+                try {
+                    rollUpVers = 
metastorage.read(ROLLING_UPGRADE_VERSIONS_KEY);
+                }
+                catch (IgniteCheckedException e) {
+                    throw new IgniteException(e);
+                }
+
+                // Keep the current and target version pair in sync with 
metastorage updates, e.g., to handle coordinator changes.
+                metastorage.listen(ROLLING_UPGRADE_VERSIONS_KEY::equals, (key, 
oldVal, newVal) -> {
+                    rollUpVers = (IgnitePair<IgniteProductVersion>)newVal;
+                });
+            }
+        });
+    }
+
+    /** {@inheritDoc} The joining node is stored to verify later whether it 
successfully connected to the ring or failed to join. */
+    @Override public @Nullable IgniteNodeValidationResult 
validateNode(ClusterNode node) {
+        synchronized (lock) {
+            lastJoiningNode = node;
+
+            lastJoiningNodeTimestamp = U.currentTimeMillis();
+        }
+
+        ClusterNode locNode = ctx.discovery().localNode();
+
+        String locBuildVer = locNode.attribute(ATTR_BUILD_VER);
+        String rmtBuildVer = node.attribute(ATTR_BUILD_VER);
+
+        IgniteProductVersion rmtVer = 
IgniteProductVersion.fromString(rmtBuildVer);
+
+        IgnitePair<IgniteProductVersion> pair = rollUpVers;
+
+        IgniteProductVersion curVer = pair == null ? 
IgniteProductVersion.fromString(locBuildVer) : pair.get1();
+        IgniteProductVersion targetVer = pair == null ? null : pair.get2();
+
+        if (Objects.equals(rmtVer, curVer) || Objects.equals(rmtVer, 
targetVer))
+            return null;
+
+        String errMsg = "Remote node rejected due to incompatible version for 
cluster join.\n"
+            + "Remote node info:\n"
+            + "  - Version     : " + rmtBuildVer + "\n"
+            + "  - Addresses   : " + U.addressesAsString(node) + "\n"
+            + "  - Node ID     : " + node.id() + "\n"
+            + "Local node info:\n"
+            + "  - Version     : " + locBuildVer + "\n"
+            + "  - Addresses   : " + U.addressesAsString(locNode) + "\n"
+            + "  - Node ID     : " + locNode.id() + "\n"
+            + "Allowed versions for joining: " + curVer + (targetVer == null ? 
"" : ", " + targetVer);
+
+        LT.warn(log, errMsg);
+
+        if (log.isDebugEnabled())
+            log.debug(errMsg);
+
+        return new IgniteNodeValidationResult(node.id(), errMsg);
+    }
+
+    /**
+     * Enables rolling upgrade with specified target version.
+     * This method can only be called on coordinator node with {@link 
TcpDiscoverySpi}.
+     *
+     * @param target Target version.
+     * @throws IgniteCheckedException If:
+     *     <ul>
+     *         <li>The current and target versions are incompatible;</li>
+     *         <li>The local node is not a coordinator;</li>
+     *         <li>The discovery SPI is not {@link TcpDiscoverySpi};</li>
+     *         <li>The distributed metastorage is not ready;</li>
+     *     </ul>
+     */
+    public void enable(IgniteProductVersion target) throws 
IgniteCheckedException {
+        if (startLatch.getCount() > 0)
+            throw new IgniteCheckedException("Cannot enable rolling upgrade: 
processor has not been started yet");
+
+        if (!U.isLocalNodeCoordinator(ctx.discovery()))
+            throw new IgniteCheckedException("Rolling upgrade can be enabled 
only on coordinator node");
+
+        if (metastorage == null)
+            throw new IgniteCheckedException("Metastorage is not ready yet. 
Try again later");
+
+        if (!(ctx.config().getDiscoverySpi() instanceof TcpDiscoverySpi))
+            throw new IgniteCheckedException("Rolling upgrade is supported 
only with TCP discovery SPI");
+
+        String curBuildVer = 
ctx.discovery().localNode().attribute(ATTR_BUILD_VER);
+        IgniteProductVersion curVer = 
IgniteProductVersion.fromString(curBuildVer);
+
+        if (!checkVersionsForEnabling(curVer, target))
+            return;
+
+        IgnitePair<IgniteProductVersion> newPair = F.pair(curVer, target);
+
+        if (!metastorage.compareAndSet(ROLLING_UPGRADE_VERSIONS_KEY, null, 
newPair)) {
+            IgnitePair<IgniteProductVersion> oldVerPair = 
metastorage.read(ROLLING_UPGRADE_VERSIONS_KEY);
+
+            if (newPair.equals(oldVerPair))
+                return;
+
+            if (oldVerPair == null)
+                throw new IgniteCheckedException("Could not enable rolling 
upgrade. Try again");
+
+            throw new IgniteCheckedException("Rolling upgrade is already 
enabled with a different current and target version: " +
+                oldVerPair.get1() + " , " + oldVerPair.get2());
+        }
+
+        rollUpVers = newPair;
+
+        if (log.isInfoEnabled())
+            log.info("Rolling upgrade enabled [current=" + curVer + ", 
target=" + target + ']');
+    }
+
+    /**
+     * Disables rolling upgrade.
+     * This method can only be called on coordinator node.
+     *
+     * @throws IgniteCheckedException If cluster has two or more nodes with 
different versions or if node is not coordinator
+     * or metastorage is not ready.
+     */
+    public void disable() throws IgniteCheckedException {
+        if (!U.isLocalNodeCoordinator(ctx.discovery()))
+            throw new IgniteCheckedException("Rolling upgrade can be disabled 
only on coordinator node");
+
+        if (metastorage == null)
+            throw new IgniteCheckedException("Meta storage is not ready. Try 
again");
+
+        if (rollUpVers == null)
+            return;
+
+        IgnitePair<IgniteProductVersion> minMaxVerPair = 
ring.minMaxNodeVersions();
+
+        if (!minMaxVerPair.get1().equals(minMaxVerPair.get2()))
+            throw new IgniteCheckedException("Can't disable rolling upgrade 
with different versions in cluster: "
+                + minMaxVerPair.get1() + ", " + minMaxVerPair.get2());
+
+        synchronized (lock) {
+            if (lastJoiningNode != null) {
+                long timeout = 
((TcpDiscoverySpi)ctx.config().getDiscoverySpi()).getJoinTimeout() * 3;
+
+                if (ring.node(lastJoiningNode.id()) != null || (timeout > 0 && 
U.currentTimeMillis() - lastJoiningNodeTimestamp > timeout))
+                    lastJoiningNode = null;
+            }
+
+            if (lastJoiningNode != null) {
+                IgniteProductVersion lastJoiningNodeVer = 
IgniteProductVersion.fromString(lastJoiningNode.attribute(ATTR_BUILD_VER));
+
+                if (!minMaxVerPair.get1().equals(lastJoiningNodeVer))
+                    throw new IgniteCheckedException("Can't disable rolling 
upgrade with different versions in cluster: "
+                        + minMaxVerPair.get1() + ", " + lastJoiningNodeVer);
+            }
+
+            rollUpVers = null;
+        }
+
+        metastorage.remove(ROLLING_UPGRADE_VERSIONS_KEY);
+
+        if (log.isInfoEnabled())
+            log.info("Rolling upgrade disabled. Current version of nodes in 
cluster: " + minMaxVerPair.get1());
+    }
+
+    /**
+     * Returns a pair containing the current and target versions of the 
cluster.
+     * <p>
+     * This method returns {@code null} if rolling upgrade has not been 
enabled yet
+     * or if version information has not been read from the distributed 
metastorage.
+     *
+     * @return A pair where:
+     *     <ul>
+     *         <li><b>First element</b> — current version of the cluster.</li>
+     *         <li><b>Second element</b> — target version to which the cluster 
is being upgraded.</li>
+     *     </ul>
+     *     or {@code null} if rolling upgrade is not active.
+     */
+    @Nullable public IgnitePair<IgniteProductVersion> versions() {
+        return rollUpVers;
+    }
+
+    /** Checks whether the cluster is in the rolling upgrade mode. */
+    public boolean enabled() {
+        return versions() != null;
+    }
+
+    /**
+     * Checks cur and target versions.
+     *
+     * @param cur Current cluster version.
+     * @param target Target cluster version.
+     * @return {@code false} if there is no need to update versions {@code 
true} otherwise.
+     * @throws IgniteCheckedException If versions are incorrect.
+     */
+    private boolean checkVersionsForEnabling(IgniteProductVersion cur, 
IgniteProductVersion target) throws IgniteCheckedException {
+        IgnitePair<IgniteProductVersion> oldVerPair = rollUpVers;
+        if (oldVerPair != null) {
+            if (oldVerPair.get1().equals(cur) && 
oldVerPair.get2().equals(target))
+                return false;
+
+            throw new IgniteCheckedException("Rolling upgrade is already 
enabled with a different current and target version: " +
+                oldVerPair.get1() + " , " + oldVerPair.get2());
+        }
+
+        if (cur.major() != target.major())
+            throw new IgniteCheckedException("Major versions are different");
+
+        if (cur.minor() != target.minor()) {
+            if (target.minor() == cur.minor() + 1 && target.maintenance() == 0)
+                return true;
+
+            throw new IgniteCheckedException("Minor version can only be 
incremented by 1");
+        }
+
+        if (cur.maintenance() + 1 != target.maintenance())
+            throw new IgniteCheckedException("Patch version can only be 
incremented by 1");
+
+        return true;
+    }
+}
diff --git 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java
 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java
index 6df10d88a0f..b82f3fca33a 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java
@@ -97,6 +97,7 @@ import 
org.apache.ignite.spi.discovery.DiscoverySpiNodeAuthenticator;
 import org.apache.ignite.spi.discovery.DiscoverySpiOrderSupport;
 import org.apache.ignite.spi.discovery.tcp.internal.DiscoveryDataPacket;
 import org.apache.ignite.spi.discovery.tcp.internal.TcpDiscoveryNode;
+import org.apache.ignite.spi.discovery.tcp.internal.TcpDiscoveryNodesRing;
 import org.apache.ignite.spi.discovery.tcp.internal.TcpDiscoveryStatistics;
 import org.apache.ignite.spi.discovery.tcp.ipfinder.TcpDiscoveryIpFinder;
 import 
org.apache.ignite.spi.discovery.tcp.ipfinder.jdbc.TcpDiscoveryJdbcIpFinder;
@@ -504,6 +505,16 @@ public class TcpDiscoverySpi extends IgniteSpiAdapter 
implements IgniteDiscovery
         return getNode(id);
     }
 
+    /**
+     * @return TCP discovery nodes ring.
+     */
+    @Nullable public TcpDiscoveryNodesRing discoveryRing() {
+        if (impl instanceof ServerImpl)
+            return ((ServerImpl)impl).ring();
+
+        return null;
+    }
+
     /** {@inheritDoc} */
     @Override public boolean pingNode(UUID nodeId) {
         return impl.pingNode(nodeId);
diff --git 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/internal/TcpDiscoveryNodesRing.java
 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/internal/TcpDiscoveryNodesRing.java
index 981745d8e82..6a316a9a599 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/internal/TcpDiscoveryNodesRing.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/internal/TcpDiscoveryNodesRing.java
@@ -30,6 +30,7 @@ import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import org.apache.ignite.cluster.ClusterNode;
 import org.apache.ignite.internal.util.lang.ClusterNodeFunc;
+import org.apache.ignite.internal.util.lang.IgnitePair;
 import org.apache.ignite.internal.util.tostring.GridToStringExclude;
 import org.apache.ignite.internal.util.tostring.GridToStringInclude;
 import org.apache.ignite.internal.util.typedef.F;
@@ -91,17 +92,18 @@ public class TcpDiscoveryNodesRing {
     @GridToStringExclude
     private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
 
-    /** */
+    /** Minimum node version in the cluster. */
     private IgniteProductVersion minNodeVer;
 
-    /**
-     * @return Minimum node version.
-     */
-    public IgniteProductVersion minimumNodeVersion() {
+    /** Maximum node version in the cluster. */
+    private IgniteProductVersion maxNodeVer;
+
+    /** Returns min and max node versions. */
+    public IgnitePair<IgniteProductVersion> minMaxNodeVersions() {
         rwLock.readLock().lock();
 
         try {
-            return minNodeVer;
+            return F.pair(minNodeVer, maxNodeVer);
         }
         finally {
             rwLock.readLock().unlock();
@@ -257,7 +259,7 @@ public class TcpDiscoveryNodesRing {
 
             maxInternalOrder = node.internalOrder();
 
-            initializeMinimumVersion();
+            initializeMinMaxVersions();
         }
         finally {
             rwLock.writeLock().unlock();
@@ -329,7 +331,7 @@ public class TcpDiscoveryNodesRing {
 
             nodeOrder = topVer;
 
-            initializeMinimumVersion();
+            initializeMinMaxVersions();
         }
         finally {
             rwLock.writeLock().unlock();
@@ -376,7 +378,7 @@ public class TcpDiscoveryNodesRing {
                 nodes.remove(rmv);
             }
 
-            initializeMinimumVersion();
+            initializeMinMaxVersions();
 
             return rmv;
         }
@@ -410,8 +412,10 @@ public class TcpDiscoveryNodesRing {
 
             topVer = 0;
 
-            if (locNode != null)
+            if (locNode != null) {
                 minNodeVer = locNode.version();
+                maxNodeVer = locNode.version();
+            }
         }
         finally {
             rwLock.writeLock().unlock();
@@ -695,12 +699,16 @@ public class TcpDiscoveryNodesRing {
     /**
      *
      */
-    private void initializeMinimumVersion() {
+    private void initializeMinMaxVersions() {
         minNodeVer = null;
+        maxNodeVer = null;
 
         for (TcpDiscoveryNode node : nodes) {
             if (minNodeVer == null || node.version().compareTo(minNodeVer) < 0)
                 minNodeVer = node.version();
+
+            if (maxNodeVer == null || node.version().compareTo(maxNodeVer) > 0)
+                maxNodeVer = node.version();
         }
     }
 
diff --git 
a/modules/core/src/test/java/org/apache/ignite/internal/GridReleaseTypeSelfTest.java
 
b/modules/core/src/test/java/org/apache/ignite/internal/GridReleaseTypeSelfTest.java
index d4485b20e98..36035e32b81 100644
--- 
a/modules/core/src/test/java/org/apache/ignite/internal/GridReleaseTypeSelfTest.java
+++ 
b/modules/core/src/test/java/org/apache/ignite/internal/GridReleaseTypeSelfTest.java
@@ -17,23 +17,59 @@
 
 package org.apache.ignite.internal;
 
-import java.io.PrintWriter;
-import java.io.StringWriter;
+import java.util.Collection;
+import java.util.List;
 import java.util.Map;
+import java.util.function.UnaryOperator;
 import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.cluster.ClusterState;
+import org.apache.ignite.configuration.DataStorageConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.util.function.ThrowableSupplier;
+import org.apache.ignite.internal.util.lang.IgnitePair;
+import org.apache.ignite.internal.util.lang.RunnableX;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.X;
 import org.apache.ignite.lang.IgniteProductVersion;
+import org.apache.ignite.spi.IgniteSpiException;
 import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
+import org.apache.ignite.testframework.GridTestUtils;
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import static org.apache.ignite.testframework.GridTestUtils.assertThrows;
+import static org.apache.ignite.testframework.GridTestUtils.waitForCondition;
+import static org.junit.Assume.assumeTrue;
 
 /**
- * Test grids starting with non compatible release types.
+ * Test Rolling Upgrade release types.
  */
+@RunWith(Parameterized.class)
 public class GridReleaseTypeSelfTest extends GridCommonAbstractTest {
     /** */
     private String nodeVer;
 
+    /**
+     * Indicates whether the tested node is started as a client.
+     * This flag is used to run all test cases for both client and server node 
configurations.
+     */
+    @Parameterized.Parameter
+    public boolean client;
+
+    /** Persistence. */
+    @Parameterized.Parameter(1)
+    public boolean persistence;
+
+    /** @return Test parameters. */
+    @Parameterized.Parameters(name = "client={0}, persistence={1}")
+    public static Collection<?> parameters() {
+        return GridTestUtils.cartesianProduct(List.of(false, true), 
List.of(false, true));
+    }
+
     /** {@inheritDoc} */
     @Override protected IgniteConfiguration getConfiguration(String 
igniteInstanceName) throws Exception {
         IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);
@@ -51,67 +87,408 @@ public class GridReleaseTypeSelfTest extends 
GridCommonAbstractTest {
 
         cfg.setDiscoverySpi(discoSpi);
 
+        DataStorageConfiguration storageCfg = new DataStorageConfiguration();
+
+        
storageCfg.getDefaultDataRegionConfiguration().setPersistenceEnabled(persistence);
+
+        cfg.setDataStorageConfiguration(storageCfg);
+
         return cfg;
     }
 
     /** {@inheritDoc} */
     @Override protected void afterTest() throws Exception {
         stopAllGrids();
+        cleanPersistenceDir();
     }
 
-    /**
-     * @throws Exception If failed.
-     */
+    /** */
+    @Test
+    public void testTwoConflictVersions() {
+        testConflictVersions("2.18.0", "2.16.0", client);
+        testConflictVersions("2.21.0", "2.23.1", client);
+        testConflictVersions("2.20.1", "2.20.2", client);
+    }
+
+    /** */
     @Test
-    public void testOsEditionDoesNotSupportRollingUpdates() throws Exception {
-        nodeVer = "1.0.0";
+    public void testThreeConflictVersions() throws Exception {
+        testConflictVersionsWithRollingUpgrade("2.18.0", "2.18.1", "2.18.2", 
client, "2.18.1");
+
+        testConflictVersionsWithRollingUpgrade("2.18.0", "2.18.1", "2.17.2", 
client, "2.18.1");
 
-        startGrid(0);
+        testConflictVersionsWithRollingUpgrade("2.18.1", "2.19.0", "2.19.1", 
client, "2.19.0");
 
-        try {
-            nodeVer = "1.0.1";
+        testConflictVersionsWithRollingUpgrade("2.18.1", "2.18.2", "2.18.0", 
client, "2.18.2");
+    }
 
-            startGrid(1);
+    /** */
+    @Test
+    public void testTwoCompatibleVersions() throws Exception {
+        testCompatibleVersions("2.18.0", "2.18.0", client, null);
+        testCompatibleVersions("2.19.2", "2.19.2", client, null);
 
-            fail("Exception has not been thrown.");
+        testCompatibleVersions("2.18.0", "2.18.1", client, "2.18.1");
+        testCompatibleVersions("2.18.2", "2.19.0", client, "2.19.0");
+    }
+
+    /** */
+    @Test
+    public void testThreeCompatibleVersions() throws Exception {
+        testCompatibleVersions("2.18.0", "2.18.0", "2.18.0", client, null);
+        testCompatibleVersions("2.18.2", "2.18.2", "2.18.2", client, null);
+
+        testCompatibleVersions("2.18.0", "2.18.1", "2.18.1", client, "2.18.1");
+        testCompatibleVersions("2.18.1", "2.19.0", "2.18.1", client, "2.19.0");
+    }
+
+    /** */
+    @Test
+    public void testForwardRollingUpgrade() throws Exception {
+        cleanPersistenceDir();
+        IgniteEx ign0 = startGrid(0, "2.18.0", false);
+        IgniteEx ign1 = startGrid(1, "2.18.0", client);
+        IgniteEx ign2 = startGrid(2, "2.18.0", client);
+
+        assertClusterSize(3);
+
+        assertRemoteRejected(() -> startGrid(3, "2.18.1", client));
+
+        configureRollingUpgradeVersion(ign0, "2.18.1");
+
+        for (int i = 0; i < 3; i++) {
+            int finalI = i;
+            assertTrue(waitForCondition(() -> 
grid(finalI).context().rollingUpgrade().enabled(), getTestTimeout()));
         }
-        catch (IgniteCheckedException e) {
-            StringWriter errors = new StringWriter();
 
-            e.printStackTrace(new PrintWriter(errors));
+        ign2.close();
+
+        assertClusterSize(2);
+
+        startGrid(2, "2.18.1", client);
+
+        assertClusterSize(3);
+
+        ign1.close();
+
+        assertClusterSize(2);
+
+        startGrid(1, "2.18.1", client);
+
+        assertClusterSize(3);
 
-            String stackTrace = errors.toString();
+        ign0.close();
 
-            if (!stackTrace.contains("Local node and remote node have 
different version numbers"))
-                throw e;
+        assertClusterSize(2);
+
+        startGrid(0, "2.18.1", false);
+
+        assertClusterSize(3);
+
+        if (client)
+            grid(0).context().rollingUpgrade().disable();
+        else
+            grid(2).context().rollingUpgrade().disable();
+
+        for (int i = 0; i < 3; i++) {
+            if (!grid(i).localNode().isClient())
+                assertFalse(grid(i).context().rollingUpgrade().enabled());
         }
+
+        assertRemoteRejected(() -> startGrid(3, "2.18.0", client));
     }
 
-    /**
-     * @throws Exception If failed.
-     */
+    /** */
+    @Test
+    public void testJoiningNodeFailed() throws Exception {
+        int joinTimeout = 5_000;
+
+        IgniteEx ign0 = startGrid(0, "2.18.0", false,
+            cfg -> {
+                
((TcpDiscoverySpi)cfg.getDiscoverySpi()).setJoinTimeout(joinTimeout);
+                return cfg;
+            });
+
+        configureRollingUpgradeVersion(ign0, "2.18.1");
+
+        RunnableX runnableX = () -> startGrid(1, "2.18.1", false,
+            cfg -> {
+                TcpDiscoverySpi oldSpi = 
(TcpDiscoverySpi)cfg.getDiscoverySpi();
+
+                TcpDiscoverySpi newSpi = new TcpDiscoverySpi() {
+                    @Override public void setNodeAttributes(Map<String, 
Object> attrs, IgniteProductVersion ver) {
+                        super.setNodeAttributes(attrs, ver);
+
+                        attrs.put(IgniteNodeAttributes.ATTR_BUILD_VER, 
nodeVer);
+                        attrs.put(IgniteNodeAttributes.ATTR_MARSHALLER, 
"null");
+                    }
+                };
+
+                newSpi.setIpFinder(oldSpi.getIpFinder());
+
+                return cfg.setDiscoverySpi(newSpi);
+            });
+
+        Throwable e = assertThrows(log, runnableX, IgniteException.class, 
null);
+
+        assertTrue(X.hasCause(e, "Local node's marshaller differs from remote 
node's marshaller", IgniteSpiException.class));
+
+        assertDisablingFails(ign0, "Can't disable rolling upgrade with 
different versions in cluster");
+
+        doSleep(joinTimeout * 3);
+
+        ign0.context().rollingUpgrade().disable();
+
+        assertFalse(ign0.context().rollingUpgrade().enabled());
+    }
+
+    /** */
+    @Test
+    public void testCoordinatorChange() throws Exception {
+        IgniteEx ign0 = startGrid(0, "2.18.0", false);
+        IgniteEx ign1 = startGrid(1, "2.18.0", false);
+
+        configureRollingUpgradeVersion(ign0, "2.19.0");
+
+        startGrid(2, "2.19.0", false);
+
+        assertClusterSize(3);
+
+        ign0.close();
+        ign1.close();
+
+        assertClusterSize(1);
+
+        startGrid(0, "2.18.0", client);
+        startGrid(1, "2.19.0", client);
+
+        assertClusterSize(3);
+
+        assertRemoteRejected(() -> startGrid(4, "2.20.0", client));
+
+        assertClusterSize(3);
+    }
+
+    /** */
     @Test
-    public void testOsEditionDoesNotSupportRollingUpdatesClientMode() throws 
Exception {
-        nodeVer = "1.0.0";
+    public void testNodeRestart() throws Exception {
+        assumeTrue("Distributed metastorage is only preserved across restarts 
when persistence is enabled", persistence);
+
+        for (int i = 0; i < 3; i++)
+            startGrid(i, "2.18.0", false);
+
+        assertClusterSize(3);
 
-        startGrid(0);
+        configureRollingUpgradeVersion(grid(0), "2.18.1");
 
-        try {
-            nodeVer = "1.0.1";
+        for (int i = 0; i < 3; i++)
+            grid(i).close();
 
-            startClientGrid(1);
+        assertClusterSize(0);
 
-            fail("Exception has not been thrown.");
+        for (int i = 0; i < 3; i++)
+            startGrid(i, "2.18.0", false);
+
+        assertClusterSize(3);
+
+        for (int i = 0; i < 3; i++) {
+            assertTrue(grid(i).context().rollingUpgrade().enabled());
+
+            IgnitePair<IgniteProductVersion> stored = 
grid(i).context().rollingUpgrade().versions();
+
+            assertEquals(F.pair(IgniteProductVersion.fromString("2.18.0"), 
IgniteProductVersion.fromString("2.18.1")), stored);
+        }
+    }
+
+    /** */
+    @Test
+    public void testRollingUpgradeProcessorVersionCheck() throws Exception {
+        IgniteEx grid0 = startGrid(0, "2.18.0", false);
+        startGrid(1, "2.18.0", client);
+
+        assertClusterSize(2);
+
+        assertEnablingFails(grid0, "3.0.0", "Major versions are different");
+        assertEnablingFails(grid0, "2.19.2", "Minor version can only be 
incremented by 1");
+        assertEnablingFails(grid0, "2.18.2", "Patch version can only be 
incremented by 1");
+
+        IgnitePair<IgniteProductVersion> newPair = 
F.pair(IgniteProductVersion.fromString("2.18.0"),
+            IgniteProductVersion.fromString("2.19.0"));
+
+        grid0.context().rollingUpgrade().enable(newPair.get2());
+
+        assertEnablingFails(grid0, "2.18.1", "Rolling upgrade is already 
enabled with a different current and target version");
+
+        for (int i = 0; i < 2; i++) {
+            
assertTrue(waitForCondition(grid(i).context().rollingUpgrade()::enabled, 
getTestTimeout()));
+
+            assertEquals(newPair, 
grid(i).context().rollingUpgrade().versions());
         }
-        catch (IgniteCheckedException e) {
-            StringWriter errors = new StringWriter();
+    }
+
+    /**
+     * Checks that enabling rolling upgrade fails with expected error message.
+     *
+     * @param ex Ex.
+     * @param ver New version.
+     * @param errMsg Expected error message.
+     */
+    private void assertEnablingFails(IgniteEx ex, String ver, String errMsg) {
+        Throwable e = assertThrows(log,
+            () -> 
ex.context().rollingUpgrade().enable(IgniteProductVersion.fromString(ver)),
+            IgniteException.class,
+            null);
+
+        assertTrue(e.getMessage().contains(errMsg));
+    }
+
+    /**
+     * Checks that disabling rolling upgrade fails with expected error message.
+     *
+     * @param ex Ex.
+     * @param errMsg Expected error message.
+     */
+    private void assertDisablingFails(IgniteEx ex, String errMsg) {
+        Throwable e = assertThrows(log,
+            () -> ex.context().rollingUpgrade().disable(),
+            IgniteException.class,
+            null);
+
+        assertTrue(e.getMessage().contains(errMsg));
+    }
+
+    /** Tests that starting a node with rejected version fails with remote 
rejection. */
+    private void testConflictVersions(String acceptedVer, String rejVer, 
boolean client) {
+        ThrowableSupplier<IgniteEx, Exception> sup = () -> {
+            IgniteEx ign = startGrid(0, acceptedVer, false);
+
+            startGrid(1, rejVer, client, cfg -> {
+                TcpDiscoverySpi spi = (TcpDiscoverySpi)cfg.getDiscoverySpi();
+
+                // Decrease network timeout to reduce waiting time for node 
failure
+                // after it has been rejected by the coordinator due to 
version conflict.
+                spi.setNetworkTimeout(1_000);
+
+                return cfg;
+            });
+
+            return ign;
+        };
+
+        assertRemoteRejected(sup);
+
+        stopAllGrids();
+    }
+
+    /** Checks that the third grid is not compatible when rolling upgrade 
version is set. */
+    private void testConflictVersionsWithRollingUpgrade(String acceptedVer1, 
String acceptedVer2, String rejVer,
+        boolean client, String rollUpVer) throws Exception {
+        ThrowableSupplier<IgniteEx, Exception> sup = () -> {
+            IgniteEx ign = startGrid(0, acceptedVer1, false);
+
+            configureRollingUpgradeVersion(ign, rollUpVer);
+
+            startGrid(1, acceptedVer2, client);
 
-            e.printStackTrace(new PrintWriter(errors));
+            startGrid(2, rejVer, client);
 
-            String stackTrace = errors.toString();
+            return ign;
+        };
+
+        assertRemoteRejected(sup);
+
+        stopAllGrids();
+
+        cleanPersistenceDir();
+    }
+
+    /** Checks that remote node rejected due to incompatible version. */
+    private void assertRemoteRejected(ThrowableSupplier<IgniteEx, Exception> 
gridStart) {
+        Throwable e = assertThrows(log, gridStart::get, 
IgniteCheckedException.class, null);
+
+        assertTrue(X.hasCause(e, "Remote node rejected due to incompatible 
version for cluster join", IgniteSpiException.class));
+    }
+
+    /** Tests two compatible grids. */
+    private void testCompatibleVersions(String acceptedVer1,
+        String acceptedVer2,
+        boolean client,
+        String rollUpVerCheck) throws Exception {
+        IgniteEx grid = startGrid(0, acceptedVer1, false);
+
+        if (rollUpVerCheck != null)
+            configureRollingUpgradeVersion(grid, rollUpVerCheck);
+
+        startGrid(1, acceptedVer2, client);
+
+        assertClusterSize(2);
+
+        stopAllGrids();
+
+        if (persistence)
+            cleanPersistenceDir();
+    }
+
+    /** Tests three compatible grids. */
+    private void testCompatibleVersions(
+        String acceptedVer1,
+        String acceptedVer2,
+        String acceptedVer3,
+        boolean client,
+        String rollUpVerCheck
+    ) throws Exception {
+        IgniteEx grid = startGrid(0, acceptedVer1, false);
+
+        if (rollUpVerCheck != null)
+            configureRollingUpgradeVersion(grid, rollUpVerCheck);
+
+        startGrid(1, acceptedVer2, client);
+        startGrid(2, acceptedVer3, client);
 
-            if (!stackTrace.contains("Local node and remote node have 
different version numbers"))
-                throw e;
+        assertClusterSize(3);
+
+        stopAllGrids();
+
+        if (persistence)
+            cleanPersistenceDir();
+    }
+
+    /** Starts grid with required version. */
+    private IgniteEx startGrid(int idx, String ver, boolean isClient) throws 
Exception {
+        return startGrid(idx, ver, isClient, null);
+    }
+
+    /** Starts grid with required version and custom configuration. */
+    private IgniteEx startGrid(int idx, String ver, boolean isClient, 
UnaryOperator<IgniteConfiguration> cfgOp) throws Exception {
+        nodeVer = ver;
+
+        IgniteEx ign = isClient ? startClientGrid(idx, cfgOp) : startGrid(idx, 
cfgOp);
+
+        if (persistence)
+            ign.cluster().state(ClusterState.ACTIVE);
+
+        return ign;
+    }
+
+    /**
+     * @param ver Version for rolling upgrade support.
+     */
+    private void configureRollingUpgradeVersion(IgniteEx grid, String ver) 
throws IgniteCheckedException {
+        if (ver == null) {
+            grid.context().rollingUpgrade().disable();
+            return;
         }
+
+        IgniteProductVersion target = IgniteProductVersion.fromString(ver);
+
+        grid.context().rollingUpgrade().enable(target);
+    }
+
+    /**
+     * @param size Expected cluster size.
+     */
+    private void assertClusterSize(int size) throws 
IgniteInterruptedCheckedException {
+        assertTrue("Expected cluster size: " + size + ", but was: " + 
Ignition.allGrids().size(),
+            waitForCondition(() -> Ignition.allGrids().size() == size, 
getTestTimeout()));
     }
 }

Reply via email to