You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by sk...@apache.org on 2022/02/21 10:28:24 UTC

[ignite] branch master updated: IGNITE-16579 Fixed an issue that caused a failed deactivation of the cluster. Fixes #9834

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

sk0x50 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 45da1f5  IGNITE-16579 Fixed an issue that caused a failed deactivation of the cluster. Fixes #9834
45da1f5 is described below

commit 45da1f56bb2d39e44bbd543882f9d1fe4397a439
Author: Slava Koptilin <sl...@gmail.com>
AuthorDate: Mon Feb 21 13:25:24 2022 +0300

    IGNITE-16579 Fixed an issue that caused a failed deactivation of the cluster. Fixes #9834
---
 .../apache/ignite/util/GridCommandHandlerTest.java |   5 +-
 .../internal/processors/cache/GridCacheUtils.java  |  50 +++++
 .../cluster/GridClusterStateProcessor.java         | 232 ++++++++++++++-------
 .../cache/IgniteClusterActivateDeactivateTest.java |   5 +-
 ...usterActivateDeactivateTestWithPersistence.java | 220 ++++++++++++++++++-
 5 files changed, 428 insertions(+), 84 deletions(-)

diff --git a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerTest.java b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerTest.java
index 12a142d..809d78a 100644
--- a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerTest.java
+++ b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerTest.java
@@ -968,7 +968,10 @@ public class GridCommandHandlerTest extends GridCommandHandlerClusterPerMethodAb
 
         CountDownLatch latch = getNewStateLatch(ignite.cluster().state(), state);
 
-        assertEquals(EXIT_CODE_OK, execute("--set-state", strState));
+        if (state == INACTIVE)
+            assertEquals(EXIT_CODE_OK, execute("--set-state", strState, "--force"));
+        else
+            assertEquals(EXIT_CODE_OK, execute("--set-state", strState));
 
         latch.await(getTestTimeout(), TimeUnit.MILLISECONDS);
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheUtils.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheUtils.java
index 1369eff..01eb84a 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheUtils.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheUtils.java
@@ -1968,6 +1968,56 @@ public class GridCacheUtils {
     }
 
     /**
+     * Finds and returns a data region configuration with the specified name.
+     *
+     * @param dsCfg Data storage configuration.
+     * @param name Name of data region configuration to find.
+     * @return Data region configuration with the specified name
+     *          or {@code null} if the given data storage configuration does not contain such data region.
+     */
+    @Nullable public static DataRegionConfiguration findDataRegionConfiguration(
+        @Nullable DataStorageConfiguration dsCfg,
+        @Nullable String name
+    ) {
+        if (dsCfg == null || name == null)
+            return null;
+
+        if (dsCfg.getDefaultDataRegionConfiguration().getName().equals(name))
+            return dsCfg.getDefaultDataRegionConfiguration();
+
+        DataRegionConfiguration[] regions = dsCfg.getDataRegionConfigurations();
+
+        if (regions == null)
+            return null;
+
+        for (int i = 0; i < regions.length; ++i) {
+            if (regions[i].getName().equals(name))
+                return regions[i];
+        }
+
+        return null;
+    }
+
+    /**
+     * Finds and returns a data region configuration with the specified name that is configured on remote node.
+     *
+     * @param node Remote node.
+     * @param marshaller JDK marshaller that is used in order to extract data storage configuration.
+     * @param clsLdr Classloader  that is used in order to extract data storage configuration.
+     * @param name Name of data region configuration to find.
+     * @return Data region configuration with the specified name
+     *          or {@code null} if the given data storage configuration does not contain such data region.
+     */
+    @Nullable public static DataRegionConfiguration findRemoteDataRegionConfiguration(
+        ClusterNode node,
+        JdkMarshaller marshaller,
+        ClassLoader clsLdr,
+        @Nullable String name
+    ) {
+        return findDataRegionConfiguration(extractDataStorage(node, marshaller, clsLdr), name);
+    }
+
+    /**
      * @return {@code true} if persistence is enabled for a default data region, {@code false} if not.
      */
     public static boolean isDefaultDataRegionPersistent(DataStorageConfiguration cfg) {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cluster/GridClusterStateProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cluster/GridClusterStateProcessor.java
index caa7855..a57dd5c 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cluster/GridClusterStateProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cluster/GridClusterStateProcessor.java
@@ -40,6 +40,7 @@ import org.apache.ignite.IgniteLogger;
 import org.apache.ignite.cluster.BaselineNode;
 import org.apache.ignite.cluster.ClusterNode;
 import org.apache.ignite.cluster.ClusterState;
+import org.apache.ignite.configuration.DataRegionConfiguration;
 import org.apache.ignite.configuration.DataStorageConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.events.BaselineConfigurationChangedEvent;
@@ -62,6 +63,7 @@ import org.apache.ignite.internal.managers.systemview.walker.BaselineNodeAttribu
 import org.apache.ignite.internal.managers.systemview.walker.BaselineNodeViewWalker;
 import org.apache.ignite.internal.processors.GridProcessorAdapter;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
+import org.apache.ignite.internal.processors.cache.DynamicCacheDescriptor;
 import org.apache.ignite.internal.processors.cache.ExchangeActions;
 import org.apache.ignite.internal.processors.cache.GridCacheProcessor;
 import org.apache.ignite.internal.processors.cache.GridCacheSharedContext;
@@ -88,6 +90,7 @@ import org.apache.ignite.internal.util.typedef.internal.A;
 import org.apache.ignite.internal.util.typedef.internal.CU;
 import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.lang.IgniteBiPredicate;
 import org.apache.ignite.lang.IgniteFuture;
 import org.apache.ignite.lang.IgniteInClosure;
 import org.apache.ignite.lang.IgniteProductVersion;
@@ -114,7 +117,6 @@ import static org.apache.ignite.internal.IgniteFeatures.SAFE_CLUSTER_DEACTIVATIO
 import static org.apache.ignite.internal.IgniteFeatures.allNodesSupports;
 import static org.apache.ignite.internal.managers.communication.GridIoPolicy.SYSTEM_POOL;
 import static org.apache.ignite.internal.processors.cache.GridCacheUtils.extractDataStorage;
-import static org.apache.ignite.internal.processors.cache.GridCacheUtils.isPersistentCache;
 import static org.apache.ignite.internal.processors.metric.impl.MetricUtils.metricName;
 import static org.apache.ignite.internal.util.IgniteUtils.toStringSafe;
 
@@ -709,103 +711,105 @@ public class GridClusterStateProcessor extends GridProcessorAdapter implements I
                 }
             }
         }
-        else {
-            if (isApplicable(msg, state)) {
-                if (msg.state() == INACTIVE && !msg.forceDeactivation() && hasInMemoryCache() &&
-                    allNodesSupports(ctx.discovery().serverNodes(topVer), SAFE_CLUSTER_DEACTIVATION)) {
+        else if (isApplicable(msg, state)) {
+            if (msg.state() == INACTIVE && !msg.forceDeactivation() &&
+                allNodesSupports(ctx.discovery().serverNodes(topVer), SAFE_CLUSTER_DEACTIVATION)) {
+                List<String> inMemCaches = listInMemoryUserCaches();
+
+                if (!inMemCaches.isEmpty()) {
                     GridChangeGlobalStateFuture stateFut = changeStateFuture(msg);
 
                     if (stateFut != null) {
                         stateFut.onDone(new IgniteException(DATA_LOST_ON_DEACTIVATION_WARNING
-                            + " To deactivate cluster pass flag 'force'."));
+                            + " In memory caches: " + inMemCaches + " .To deactivate cluster pass '--force' flag."));
                     }
 
                     return false;
                 }
+            }
 
-                ExchangeActions exchangeActions;
+            ExchangeActions exchangeActions;
 
-                try {
-                    exchangeActions = ctx.cache().onStateChangeRequest(msg, topVer, state);
-                }
-                catch (IgniteCheckedException e) {
-                    GridChangeGlobalStateFuture fut = changeStateFuture(msg);
+            try {
+                exchangeActions = ctx.cache().onStateChangeRequest(msg, topVer, state);
+            }
+            catch (IgniteCheckedException e) {
+                GridChangeGlobalStateFuture fut = changeStateFuture(msg);
 
-                    if (fut != null)
-                        fut.onDone(e);
+                if (fut != null)
+                    fut.onDone(e);
 
-                    return false;
-                }
+                return false;
+            }
 
-                Set<UUID> nodeIds = U.newHashSet(discoCache.allNodes().size());
+            Set<UUID> nodeIds = U.newHashSet(discoCache.allNodes().size());
 
-                for (ClusterNode node : discoCache.allNodes())
-                    nodeIds.add(node.id());
+            for (ClusterNode node : discoCache.allNodes())
+                nodeIds.add(node.id());
 
-                GridChangeGlobalStateFuture fut = changeStateFuture(msg);
+            GridChangeGlobalStateFuture fut = changeStateFuture(msg);
 
-                if (fut != null)
-                    fut.setRemaining(nodeIds, topVer.nextMinorVersion());
+            if (fut != null)
+                fut.setRemaining(nodeIds, topVer.nextMinorVersion());
 
-                if (log.isInfoEnabled())
-                    log.info("Started state transition: " + prettyStr(msg.state()));
-
-                BaselineTopologyHistoryItem bltHistItem = BaselineTopologyHistoryItem.fromBaseline(
-                    state.baselineTopology());
-
-                transitionFuts.put(msg.requestId(), new GridFutureAdapter<Void>());
-
-                DiscoveryDataClusterState newState = globalState = DiscoveryDataClusterState.createTransitionState(
-                    msg.state(),
-                    state,
-                    activate(state.state(), msg.state()) || msg.forceChangeBaselineTopology()
-                        ? msg.baselineTopology()
-                        : state.baselineTopology(),
-                    msg.requestId(),
-                    topVer,
-                    nodeIds
-                );
+            if (log.isInfoEnabled())
+                log.info("Started state transition: " + prettyStr(msg.state()));
 
-                ctx.durableBackgroundTask().onStateChangeStarted(msg);
+            BaselineTopologyHistoryItem bltHistItem = BaselineTopologyHistoryItem.fromBaseline(
+                state.baselineTopology());
 
-                if (msg.forceChangeBaselineTopology())
-                    newState.setTransitionResult(msg.requestId(), msg.state());
+            transitionFuts.put(msg.requestId(), new GridFutureAdapter<Void>());
 
-                AffinityTopologyVersion stateChangeTopVer = topVer.nextMinorVersion();
+            DiscoveryDataClusterState newState = globalState = DiscoveryDataClusterState.createTransitionState(
+                msg.state(),
+                state,
+                activate(state.state(), msg.state()) || msg.forceChangeBaselineTopology()
+                    ? msg.baselineTopology()
+                    : state.baselineTopology(),
+                msg.requestId(),
+                topVer,
+                nodeIds
+            );
 
-                StateChangeRequest req = new StateChangeRequest(
-                    msg,
-                    bltHistItem,
-                    state.state(),
-                    stateChangeTopVer
-                );
+            ctx.durableBackgroundTask().onStateChangeStarted(msg);
 
-                exchangeActions.stateChangeRequest(req);
+            if (msg.forceChangeBaselineTopology())
+                newState.setTransitionResult(msg.requestId(), msg.state());
 
-                msg.exchangeActions(exchangeActions);
+            AffinityTopologyVersion stateChangeTopVer = topVer.nextMinorVersion();
 
-                if (newState.state() != state.state()) {
-                    if (ctx.event().isRecordable(EventType.EVT_CLUSTER_STATE_CHANGE_STARTED)) {
-                        ctx.pools().getStripedExecutorService().execute(
-                            () -> ctx.event().record(new ClusterStateChangeStartedEvent(
-                                state.state(),
-                                newState.state(),
-                                ctx.discovery().localNode(),
-                                "Cluster state change started."
-                            ))
-                        );
-                    }
-                }
+            StateChangeRequest req = new StateChangeRequest(
+                msg,
+                bltHistItem,
+                state.state(),
+                stateChangeTopVer
+            );
 
-                return true;
-            }
-            else {
-                // State already changed.
-                GridChangeGlobalStateFuture stateFut = changeStateFuture(msg);
+            exchangeActions.stateChangeRequest(req);
+
+            msg.exchangeActions(exchangeActions);
 
-                if (stateFut != null)
-                    stateFut.onDone();
+            if (newState.state() != state.state()) {
+                if (ctx.event().isRecordable(EventType.EVT_CLUSTER_STATE_CHANGE_STARTED)) {
+                    ctx.pools().getStripedExecutorService().execute(
+                        () -> ctx.event().record(new ClusterStateChangeStartedEvent(
+                            state.state(),
+                            newState.state(),
+                            ctx.discovery().localNode(),
+                            "Cluster state change started."
+                        ))
+                    );
+                }
             }
+
+            return true;
+        }
+        else {
+            // State already changed.
+            GridChangeGlobalStateFuture stateFut = changeStateFuture(msg);
+
+            if (stateFut != null)
+                stateFut.onDone();
         }
 
         return false;
@@ -1925,16 +1929,6 @@ public class GridClusterStateProcessor extends GridProcessorAdapter implements I
     }
 
     /**
-     * @return {@code True} if cluster has in-memory caches (without persistence) including the system caches.
-     * {@code False} otherwise.
-     */
-    private boolean hasInMemoryCache() {
-        return ctx.cache().cacheDescriptors().values().stream()
-            .anyMatch(desc -> !isPersistentCache(desc.cacheConfiguration(), ctx.config().getDataStorageConfiguration())
-                && (!desc.cacheConfiguration().isWriteBehindEnabled() || !desc.cacheConfiguration().isReadThrough()));
-    }
-
-    /**
      * Gets state of given two with minimal number of features.
      * <p/>
      * The order: {@link ClusterState#ACTIVE} > {@link ClusterState#ACTIVE_READ_ONLY} > {@link ClusterState#INACTIVE}.
@@ -1970,6 +1964,82 @@ public class GridClusterStateProcessor extends GridProcessorAdapter implements I
     }
 
     /**
+     * @return Lists in-memory user defined caches.
+     */
+    private List<String> listInMemoryUserCaches() {
+        IgniteBiPredicate<DynamicCacheDescriptor, DataRegionConfiguration> inMemoryPred = (desc, dataRegionCfg) -> {
+            return !(dataRegionCfg != null && dataRegionCfg.isPersistenceEnabled())
+                && (!desc.cacheConfiguration().isWriteThrough() || !desc.cacheConfiguration().isReadThrough());
+        };
+
+        if (ctx.discovery().localNode().isClient()) {
+            // Need to check cache descriptors using server node storage configurations.
+            // The reason for this is that client node may not be configured with the required data region configuration.
+            List<ClusterNode> srvs = ctx.discovery().discoCache().serverNodes();
+
+            if (F.isEmpty(srvs))
+                return Collections.emptyList();
+
+            return ctx.cache().cacheDescriptors().values().stream()
+                // Filter out system caches
+                .filter(desc -> !CU.isSystemCache(desc.cacheName()))
+                .filter(desc -> {
+                    String dataRegionName = desc.cacheConfiguration().getDataRegionName();
+
+                    // We always should use server data storage configurations instead of client node config,
+                    // because it is not validated when the client node joins the cluster
+                    // and so it cannot be the source of truth.
+                    DataRegionConfiguration dataRegionCfg = null;
+
+                    // Need to find out the first server node that knows about this data region and its configuration.
+                    for (ClusterNode n : srvs) {
+                        dataRegionCfg = CU.findRemoteDataRegionConfiguration(
+                            n,
+                            ctx.marshallerContext().jdkMarshaller(),
+                            U.resolveClassLoader(ctx.config()),
+                            dataRegionName);
+
+                        if (dataRegionCfg != null)
+                            break;
+                    }
+
+                    return inMemoryPred.apply(desc, dataRegionCfg);
+                })
+                .map(DynamicCacheDescriptor::cacheName)
+                .collect(Collectors.toList());
+        }
+
+        return ctx.cache().cacheDescriptors().values().stream()
+            .filter(desc -> !CU.isSystemCache(desc.cacheName()))
+            .filter(desc -> {
+                String dataRegionName = desc.cacheConfiguration().getDataRegionName();
+
+                DataRegionConfiguration dataRegionCfg =
+                    CU.findDataRegionConfiguration(ctx.config().getDataStorageConfiguration(), dataRegionName);
+
+                if (dataRegionCfg == null) {
+                    List<ClusterNode> srvs = ctx.discovery().discoCache().serverNodes();
+
+                    // Need to find out the first server node that knows about this data region and its configuration.
+                    for (ClusterNode n : srvs) {
+                        dataRegionCfg = CU.findRemoteDataRegionConfiguration(
+                            n,
+                            ctx.marshallerContext().jdkMarshaller(),
+                            U.resolveClassLoader(ctx.config()),
+                            dataRegionName);
+
+                        if (dataRegionCfg != null)
+                            break;
+                    }
+                }
+
+                return inMemoryPred.apply(desc, dataRegionCfg);
+            })
+            .map(DynamicCacheDescriptor::cacheName)
+            .collect(Collectors.toList());
+    }
+
+    /**
      *
      */
     private class GridChangeGlobalStateFuture extends GridFutureAdapter<Void> {
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteClusterActivateDeactivateTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteClusterActivateDeactivateTest.java
index 3136782..2ad649e 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteClusterActivateDeactivateTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteClusterActivateDeactivateTest.java
@@ -83,7 +83,7 @@ public class IgniteClusterActivateDeactivateTest extends GridCommonAbstractTest
     static final String CACHE_NAME_PREFIX = "cache-";
 
     /** Non-persistent data region name. */
-    private static final String NO_PERSISTENCE_REGION = "no-persistence-region";
+    protected static final String NO_PERSISTENCE_REGION = "no-persistence-region";
 
     /** */
     private static final int DEFAULT_CACHES_COUNT = 2;
@@ -1419,6 +1419,9 @@ public class IgniteClusterActivateDeactivateTest extends GridCommonAbstractTest
         if (persistenceEnabled())
             ignite.cluster().state(ACTIVE);
 
+        // Create a new cache in order to trigger all needed checks on deactivation.
+        ignite.getOrCreateCache("test-partitioned-cache");
+
         checkDeactivation(ignite, () -> mxBean.active(false), false);
 
         checkDeactivation(ignite, () -> mxBean.clusterState(INACTIVE.name()), false);
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteClusterActivateDeactivateTestWithPersistence.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteClusterActivateDeactivateTestWithPersistence.java
index 6649072..bb05795 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteClusterActivateDeactivateTestWithPersistence.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteClusterActivateDeactivateTestWithPersistence.java
@@ -18,9 +18,11 @@
 package org.apache.ignite.internal.processors.cache;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -33,6 +35,7 @@ import org.apache.ignite.cache.CacheWriteSynchronizationMode;
 import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
 import org.apache.ignite.cluster.ClusterState;
 import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.DataRegionConfiguration;
 import org.apache.ignite.configuration.DataStorageConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.IgniteEx;
@@ -48,10 +51,12 @@ import org.junit.Assert;
 import org.junit.Assume;
 import org.junit.Test;
 
+import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.apache.ignite.cluster.ClusterState.ACTIVE;
 import static org.apache.ignite.cluster.ClusterState.ACTIVE_READ_ONLY;
 import static org.apache.ignite.cluster.ClusterState.INACTIVE;
 import static org.apache.ignite.testframework.GridTestUtils.assertActive;
+import static org.apache.ignite.testframework.GridTestUtils.assertThrows;
 import static org.apache.ignite.testframework.GridTestUtils.assertThrowsAnyCause;
 import static org.apache.ignite.testframework.GridTestUtils.assertThrowsWithCause;
 
@@ -59,6 +64,12 @@ import static org.apache.ignite.testframework.GridTestUtils.assertThrowsWithCaus
  *
  */
 public class IgniteClusterActivateDeactivateTestWithPersistence extends IgniteClusterActivateDeactivateTest {
+    /** Indicates that additional data region configuration should be added on server node. */
+    private boolean addAdditionalDataRegion;
+
+    /** Persistent data region name. */
+    private static final String ADDITIONAL_PERSISTENT_DATA_REGION = "additional-persistent-region";
+
     /** {@inheritDoc} */
     @Override protected boolean persistenceEnabled() {
         return true;
@@ -80,7 +91,214 @@ public class IgniteClusterActivateDeactivateTestWithPersistence extends IgniteCl
 
     /** {@inheritDoc} */
     @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
-        return super.getConfiguration(igniteInstanceName).setAutoActivationEnabled(false);
+        IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName).setAutoActivationEnabled(false);
+
+        if (addAdditionalDataRegion) {
+            DataRegionConfiguration[] originRegions = cfg.getDataStorageConfiguration().getDataRegionConfigurations();
+
+            DataRegionConfiguration[] regions = Arrays.copyOf(originRegions, originRegions.length + 1);
+
+            regions[originRegions.length] = new DataRegionConfiguration()
+                .setName(ADDITIONAL_PERSISTENT_DATA_REGION)
+                .setPersistenceEnabled(true);
+
+            cfg.getDataStorageConfiguration().setDataRegionConfigurations(regions);
+        }
+
+        if (cfg.isClientMode())
+            cfg.setDataStorageConfiguration(null);
+
+        return cfg;
+    }
+
+    /**
+     * Tests "soft" deactivation (without using the --force flag)
+     * when the client node does not have the configured data storage and the cluster contains persistent caches.
+     *
+     * Expected behavior: the cluster should be deactivated successfully (there is no data loss).
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testDeactivateClusterWithPersistentCache() throws Exception {
+        IgniteEx srv = startGrid(0);
+
+        IgniteEx clientNode = startClientGrid(1);
+
+        clientNode.cluster().state(ACTIVE);
+
+        DataRegionConfiguration dfltDataRegion = srv
+            .configuration()
+            .getDataStorageConfiguration()
+            .getDefaultDataRegionConfiguration();
+
+        assertTrue(
+            "It is assumed that the default data storage region is persistent.",
+            dfltDataRegion.isPersistenceEnabled());
+
+        // Create a new cache that is placed into pesristent data region.
+        clientNode.getOrCreateCache(new CacheConfiguration<>("test-client-cache")
+            .setDataRegionName(dfltDataRegion.getName()));
+
+        // Try to deactivate the cluster without the `force` flag.
+        IgniteInternalFuture<?> deactivateFut = srv
+            .context()
+            .state()
+            .changeGlobalState(INACTIVE, false, Collections.emptyList(), false);
+
+        try {
+            deactivateFut.get(10, SECONDS);
+        }
+        catch (IgniteCheckedException e) {
+            log.error("Failed to deactivate the cluster.", e);
+
+            fail("Failed to deactivate the cluster. [err=" + e.getMessage() + ']');
+        }
+
+        awaitPartitionMapExchange();
+
+        // Let's check that all nodes in the cluster have the same state.
+        for (Ignite node : G.allGrids()) {
+            IgniteEx n = (IgniteEx)node;
+
+            ClusterState state = n.context().state().clusterState().state();
+
+            assertTrue(
+                "Node must be in inactive state. " +
+                    "[node=" + n.configuration().getIgniteInstanceName() + ", actual=" + state + ']',
+                INACTIVE == state
+            );
+        }
+    }
+
+    /**
+     * Tests "soft" deactivation (without using the --force flag)
+     * when the client node does not have the configured data storage and the cluster contains in-memory caches.
+     *
+     * Expected behavior: deactivation should fail due to potential data loss.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testDeactivateClusterWithInMemoryCaches() throws Exception {
+        IgniteEx srv = startGrid(0);
+
+        IgniteEx clientNode = startClientGrid(1);
+
+        clientNode.cluster().state(ACTIVE);
+
+        DataStorageConfiguration dsCfg = srv.configuration().getDataStorageConfiguration();
+
+        DataRegionConfiguration nonPersistentRegion = Arrays.stream(dsCfg.getDataRegionConfigurations())
+            .filter(region -> NO_PERSISTENCE_REGION.equals(region.getName()))
+            .findFirst()
+            .orElse(null);
+
+        assertTrue(
+            "It is assumed that the '" + NO_PERSISTENCE_REGION + "' data storage region exists and non-persistent.",
+            nonPersistentRegion != null && !nonPersistentRegion.isPersistenceEnabled());
+
+        // Create a new cache that is placed into non persistent data region.
+        clientNode.getOrCreateCache(new CacheConfiguration<>("test-client-cache")
+            .setDataRegionName(nonPersistentRegion.getName()));
+
+        // Try to deactivate the cluster without the `force` flag.
+        IgniteInternalFuture<?> deactivateFut = srv
+            .context()
+            .state()
+            .changeGlobalState(INACTIVE, false, Collections.emptyList(), false);
+
+        assertThrows(
+            log,
+            () -> deactivateFut.get(10, SECONDS),
+            IgniteCheckedException.class,
+            "Deactivation stopped. Deactivation clears in-memory caches (without persistence) including the system caches.");
+
+        awaitPartitionMapExchange();
+
+        // Let's check that all nodes in the cluster have the same state.
+        for (Ignite node : G.allGrids()) {
+            IgniteEx n = (IgniteEx)node;
+
+            ClusterState state = n.context().state().clusterState().state();
+
+            assertTrue(
+                "Node must be in active state. " +
+                    "[node=" + n.configuration().getIgniteInstanceName() + ", actual=" + state + ']',
+                ACTIVE == state
+            );
+        }
+    }
+
+    /**
+     * Tests "soft" deactivation (without using the --force flag)
+     * when the cluster contains persistent caches and cluster nodes "support" different lists of data regions.
+     *
+     * Expected behavior: the cluster should be deactivated successfully (there is no data loss)..
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testDeactivateClusterWithPersistentCachesAndDifferentDataRegions() throws Exception {
+        IgniteEx srv = startGrid(0);
+
+        addAdditionalDataRegion = true;
+
+        IgniteEx srv1 = startGrid(1);
+
+        IgniteEx clientNode = startClientGrid(2);
+
+        clientNode.cluster().state(ACTIVE);
+
+        DataStorageConfiguration dsCfg = srv1.configuration().getDataStorageConfiguration();
+
+        DataRegionConfiguration persistentRegion = Arrays.stream(dsCfg.getDataRegionConfigurations())
+            .filter(region -> ADDITIONAL_PERSISTENT_DATA_REGION.equals(region.getName()))
+            .findFirst()
+            .orElse(null);
+
+        assertTrue(
+            "It is assumed that the '" + ADDITIONAL_PERSISTENT_DATA_REGION + "' data storage region exists and persistent.",
+            persistentRegion != null && persistentRegion.isPersistenceEnabled());
+
+        final UUID srv1NodeId = srv1.localNode().id();
+
+        // Create a new cache that is placed into persistent data region.
+        srv.getOrCreateCache(new CacheConfiguration<>("test-client-cache")
+            .setDataRegionName(persistentRegion.getName())
+            .setAffinity(new RendezvousAffinityFunction(false, 1))
+            // This cache should only be started on srv1 node.
+            .setNodeFilter(node -> node.id().equals(srv1NodeId)));
+
+        // Try to deactivate the cluster without the `force` flag.
+        IgniteInternalFuture<?> deactivateFut = srv
+            .context()
+            .state()
+            .changeGlobalState(INACTIVE, false, Collections.emptyList(), false);
+
+        try {
+            deactivateFut.get(10, SECONDS);
+        }
+        catch (IgniteCheckedException e) {
+            log.error("Failed to deactivate the cluster.", e);
+
+            fail("Failed to deactivate the cluster. [err=" + e.getMessage() + ']');
+        }
+
+        awaitPartitionMapExchange();
+
+        // Let's check that all nodes in the cluster have the same state.
+        for (Ignite node : G.allGrids()) {
+            IgniteEx n = (IgniteEx)node;
+
+            ClusterState state = n.context().state().clusterState().state();
+
+            assertTrue(
+                "Node must be in inactive state. " +
+                    "[node=" + n.configuration().getIgniteInstanceName() + ", actual=" + state + ']',
+                INACTIVE == state
+            );
+        }
     }
 
     /**