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

nizhikov 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 a18c76b631e IGNITE-25389 Explicit index cache path (#12133)
a18c76b631e is described below

commit a18c76b631e615c4da84e3992bede9e307aec07f
Author: Nikolay <[email protected]>
AuthorDate: Wed Jun 18 12:58:35 2025 +0300

    IGNITE-25389 Explicit index cache path (#12133)
---
 .../ignite/configuration/CacheConfiguration.java   |  31 +++
 .../processors/cache/ClusterCachesInfo.java        |   4 +
 .../cache/ValidationOnNodeJoinUtils.java           |  35 ++--
 .../cache/persistence/filename/FileTreeUtils.java  |  25 +++
 .../cache/persistence/filename/NodeFileTree.java   |  90 ++++++--
 .../persistence/filename/SnapshotFileTree.java     |   7 +-
 .../snapshot/IgniteSnapshotManager.java            |   2 +
 .../snapshot/SnapshotResponseRemoteFutureTask.java |  25 ++-
 .../cache/persistence/snapshot/SnapshotSender.java |   2 +
 .../cache/persistence/snapshot/dump/Dump.java      |  47 +++--
 .../AbstractDataRegionRelativeStoragePathTest.java |  80 ++++++-
 .../filename/CacheConfigStoragePathTest.java       | 232 ++++++++++++++-------
 .../CustomCacheStorageConfigurationSelfTest.java   |  38 +++-
 .../SnapshotCreationNonDefaultStoragePathTest.java |  49 +++--
 .../filename/SQLCacheConfigStoragePathTest.java    |  16 ++
 15 files changed, 521 insertions(+), 162 deletions(-)

diff --git 
a/modules/core/src/main/java/org/apache/ignite/configuration/CacheConfiguration.java
 
b/modules/core/src/main/java/org/apache/ignite/configuration/CacheConfiguration.java
index 2ae9896496d..c033a533b8f 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/configuration/CacheConfiguration.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/configuration/CacheConfiguration.java
@@ -442,6 +442,14 @@ public class CacheConfiguration<K, V> extends 
MutableConfiguration<K, V> {
     @IgniteExperimental
     @Nullable private String[] storagePaths;
 
+    /**
+     * Root directory where index file are stored.
+     * @see DataStorageConfiguration#setStoragePath(String)
+     * @see DataStorageConfiguration#setExtraStoragePaths(String[])
+     */
+    @IgniteExperimental
+    @Nullable private String idxPath;
+
     /** Empty constructor (all values are initialized to their defaults). */
     public CacheConfiguration() {
         /* No-op. */
@@ -541,6 +549,7 @@ public class CacheConfiguration<K, V> extends 
MutableConfiguration<K, V> {
         sqlOnheapCacheMaxSize = cc.getSqlOnheapCacheMaxSize();
         evtsDisabled = cc.isEventsDisabled();
         storagePaths = cc.getStoragePaths();
+        idxPath = cc.getIndexPath();
     }
 
     /**
@@ -2488,6 +2497,28 @@ public class CacheConfiguration<K, V> extends 
MutableConfiguration<K, V> {
         return this;
     }
 
+    /**
+     * @return A path to the root directory where the Persistent Store for 
cache group will persist index.
+     */
+    @IgniteExperimental
+    @Nullable public String getIndexPath() {
+        return idxPath;
+    }
+
+    /**
+     * Sets a path to the root directory where the Persistent Store will 
persist index partition.
+     * By default, the Persistent Store's files are located under {@link 
DataStorageConfiguration#getStoragePath()}.
+     *
+     * @param idxPath Index path.
+     * @return {@code this} for chaining.
+     */
+    @IgniteExperimental
+    public CacheConfiguration<K, V> setIndexPath(String idxPath) {
+        this.idxPath = idxPath;
+
+        return this;
+    }
+
     /** {@inheritDoc} */
     @Override public String toString() {
         return S.toString(CacheConfiguration.class, this);
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/ClusterCachesInfo.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/ClusterCachesInfo.java
index 30b92aebb2d..86bce4091ea 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/ClusterCachesInfo.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/ClusterCachesInfo.java
@@ -2601,6 +2601,10 @@ public class ClusterCachesInfo {
         CU.validateCacheGroupsAttributesMismatch(log, cfg, startCfg,
             "storagePath", "Storage path",
             cfg.getStoragePaths(), startCfg.getStoragePaths(), true);
+
+        CU.validateCacheGroupsAttributesMismatch(log, cfg, startCfg,
+            "indexPath", "Index path",
+            cfg.getIndexPath(), startCfg.getIndexPath(), true);
     }
 
     /**
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/ValidationOnNodeJoinUtils.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/ValidationOnNodeJoinUtils.java
index aee1189ed02..eb3fd275afc 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/ValidationOnNodeJoinUtils.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/ValidationOnNodeJoinUtils.java
@@ -21,7 +21,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -74,6 +73,7 @@ import static 
org.apache.ignite.internal.IgniteNodeAttributes.ATTR_CONSISTENCY_C
 import static 
org.apache.ignite.internal.IgniteNodeAttributes.ATTR_TX_AWARE_QUERIES_ENABLED;
 import static 
org.apache.ignite.internal.IgniteNodeAttributes.ATTR_TX_SERIALIZABLE_ENABLED;
 import static 
org.apache.ignite.internal.processors.cache.GridCacheUtils.isDefaultDataRegionPersistent;
+import static 
org.apache.ignite.internal.processors.cache.persistence.filename.FileTreeUtils.nodeStorages;
 import static 
org.apache.ignite.internal.processors.security.SecurityUtils.nodeSecurityContext;
 
 /**
@@ -393,24 +393,29 @@ public class ValidationOnNodeJoinUtils {
                     cacheSpec.toString());
         }
 
-        if (!F.isEmpty(cc.getStoragePaths()) && !ctx.clientNode()) {
-            DataStorageConfiguration dsCfg = c.getDataStorageConfiguration();
+        if (!ctx.clientNode()) {
+            if (!F.isEmpty(cc.getStoragePaths())) {
+                List<String> csp = Arrays.asList(cc.getStoragePaths());
 
-            List<String> csp = Arrays.asList(cc.getStoragePaths());
+                Set<String> nodeStorages = 
nodeStorages(c.getDataStorageConfiguration());
 
-            if (dsCfg == null)
-                throw new IgniteCheckedException("Data storage must be 
configured when cache storage path set: " + csp);
-
-            Set<String> nodeStorages = new 
HashSet<>(F.asList(dsCfg.getExtraStoragePaths()));
+                if (!nodeStorages.containsAll(csp)) {
+                    throw new IgniteCheckedException(
+                        "Unknown storage path. Storage path must be from 
DataStorageConfiguration " +
+                            "[cacheStorage=" + csp + ", nodeStorages=" + 
nodeStorages + ']'
+                    );
+                }
+            }
 
-            if (!F.isEmpty(dsCfg.getStoragePath()))
-                nodeStorages.add(dsCfg.getStoragePath());
+            if (!F.isEmpty(cc.getIndexPath())) {
+                Set<String> nodeStorages = 
nodeStorages(c.getDataStorageConfiguration());
 
-            if (!nodeStorages.containsAll(csp)) {
-                throw new IgniteCheckedException(
-                    "Unknown storage path. Storage path must be from 
DataStorageConfiguration " +
-                        "[cacheStorage=" + csp + ", nodeStorages=" + 
nodeStorages + ']'
-                );
+                if (!nodeStorages.contains(cc.getIndexPath())) {
+                    throw new IgniteCheckedException(
+                        "Unknown storage path. Storage path must be from 
DataStorageConfiguration " +
+                            "[indexPath=" + cc.getIndexPath() + ", 
nodeStorages=" + nodeStorages + ']'
+                    );
+                }
             }
         }
     }
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/FileTreeUtils.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/FileTreeUtils.java
index 5bd538a869e..7d1dd7d7f05 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/FileTreeUtils.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/FileTreeUtils.java
@@ -19,14 +19,19 @@ package 
org.apache.ignite.internal.processors.cache.persistence.filename;
 
 import java.io.File;
 import java.io.IOException;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteLogger;
 import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.jetbrains.annotations.Nullable;
 
+import static 
org.apache.ignite.internal.pagemem.PageIdAllocator.INDEX_PARTITION;
+
 /**
  * Utility methods for {@link NodeFileTree}.
  */
@@ -70,6 +75,9 @@ public class FileTreeUtils {
      * @return Storage path from config for partition.
      */
     public static @Nullable String partitionStorage(CacheConfiguration<?, ?> 
ccfg, int part) {
+        if (part == INDEX_PARTITION && !F.isEmpty(ccfg.getIndexPath()))
+            return ccfg.getIndexPath();
+
         String[] csp = ccfg.getStoragePaths();
 
         if (F.isEmpty(csp))
@@ -78,6 +86,23 @@ public class FileTreeUtils {
         return resolveStorage(csp, part);
     }
 
+    /**
+     * @param dsCfg Data storage configuration.
+     * @return All known storages.
+     * @throws IgniteCheckedException
+     */
+    public static Set<String> nodeStorages(DataStorageConfiguration dsCfg) 
throws IgniteCheckedException {
+        if (dsCfg == null)
+            throw new IgniteCheckedException("Data storage must be 
configured");
+
+        Set<String> nodeStorages = new 
HashSet<>(F.asList(dsCfg.getExtraStoragePaths()));
+
+        if (!F.isEmpty(dsCfg.getStoragePath()))
+            nodeStorages.add(dsCfg.getStoragePath());
+
+        return nodeStorages;
+    }
+
     /**
      * @param dir Directory to remove
      * @param err If {@code true} then operation ends with error.
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/NodeFileTree.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/NodeFileTree.java
index c31869ca3fb..c81daecc1e2 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/NodeFileTree.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/NodeFileTree.java
@@ -78,7 +78,8 @@ import static 
org.apache.ignite.internal.processors.cache.persistence.metastorag
  * </ul>
  *
  * Node, user can configure several extra storages by {@link 
DataStorageConfiguration#setExtraStoragePaths(String...)}
- * Which can be used by the cache to store data. See {@link 
CacheConfiguration#setStoragePaths(String...)}.
+ * Which can be used by the cache to store data. See {@link 
CacheConfiguration#setStoragePaths(String...)}
+ * and {@link CacheConfiguration#setIndexPath(String)}.
  * Partition files will be spread evenly among all configured storages.
  * In case extra storages configured, each storage will repeat structure 
described below.
  * {@link NodeFileTree#defaultCacheStorage(CacheConfiguration)} will be used 
to store cache config.
@@ -567,19 +568,34 @@ public class NodeFileTree extends SharedFileTree {
      * @return Store dirs for given cache.
      */
     public File[] cacheStorages(CacheConfiguration<?, ?> ccfg) {
-        String cacheDirName = ccfg.getGroupName() != null
-            ? CACHE_GRP_DIR_PREFIX + ccfg.getGroupName()
-            : CACHE_DIR_PREFIX + ccfg.getName();
+        return cacheStorages(ccfg, true);
+    }
+
+    /**
+     * @param ccfg Cache configuration.
+     * @param includeIdxPath If {@code true} then add optional index store to 
results.
+     * @return Store dirs for given cache.
+     */
+    private File[] cacheStorages(CacheConfiguration<?, ?> ccfg, boolean 
includeIdxPath) {
+        String cacheDirName = cacheDirName(ccfg);
 
         String[] csp = ccfg.getStoragePaths();
+        String idxPath = ccfg.getIndexPath();
+        boolean idxPathEmpty = !includeIdxPath || F.isEmpty(idxPath);
 
-        if (F.isEmpty(csp))
-            return new File[] {new File(cacheStorageRoot(null), cacheDirName)};
+        if (F.isEmpty(csp)) {
+            return idxPathEmpty
+                ? new File[]{cacheStorage(null, cacheDirName)}
+                : new File[]{cacheStorage(null, cacheDirName), 
cacheStorage(idxPath, cacheDirName)};
+        }
+
+        File[] cs = new File[csp.length + (idxPathEmpty ? 0 : 1)];
 
-        File[] cs = new File[csp.length];
+        for (int i = 0; i < csp.length; i++)
+            cs[i] = cacheStorage(csp[i], cacheDirName);
 
-        for (int i = 0; i < cs.length; i++)
-            cs[i] = new File(cacheStorageRoot(csp[i]), cacheDirName);
+        if (!idxPathEmpty)
+            cs[cs.length - 1] = cacheStorage(idxPath, cacheDirName);
 
         return cs;
     }
@@ -640,7 +656,14 @@ public class NodeFileTree extends SharedFileTree {
      * @return Partition file.
      */
     public File partitionFile(CacheConfiguration<?, ?> ccfg, int part) {
-        return new File(resolveStorage(cacheStorages(ccfg), part), 
partitionFileName(part));
+        if (part == INDEX_PARTITION) {
+            String idxPath = ccfg.getIndexPath();
+
+            if (!F.isEmpty(idxPath))
+                return new File(cacheStorage(idxPath, cacheDirName(ccfg)), 
partitionFileName(INDEX_PARTITION));
+        }
+
+        return new File(resolveStorage(cacheStorages(ccfg, false), part), 
partitionFileName(part));
     }
 
     /**
@@ -648,11 +671,20 @@ public class NodeFileTree extends SharedFileTree {
      * @return Store directory for given cache.
      */
     public File[] tmpCacheStorages(CacheConfiguration<?, ?> ccfg) {
-        File[] cacheStorages = cacheStorages(ccfg);
+        return tmpCacheStorages(ccfg, true);
+    }
+
+    /**
+     * @param ccfg Cache configuration.
+     * @param includeIdxPath If {@code true} then add optional index store to 
results.
+     * @return Store directory for given cache.
+     */
+    private File[] tmpCacheStorages(CacheConfiguration<?, ?> ccfg, boolean 
includeIdxPath) {
+        File[] cacheStorages = cacheStorages(ccfg, includeIdxPath);
         File[] tmpCacheStorages = new File[cacheStorages.length];
 
         for (int i = 0; i < cacheStorages.length; i++)
-            tmpCacheStorages[i] = new File(cacheStorages[i].getParentFile(), 
TMP_CACHE_DIR_PREFIX + cacheStorages[i].getName());
+            tmpCacheStorages[i] = tmpCacheStorage(cacheStorages[i]);
 
         return tmpCacheStorages;
     }
@@ -665,9 +697,10 @@ public class NodeFileTree extends SharedFileTree {
      * @param cacheDirName Cache directory name.
      * @return Temp store directory for given cache.
      * @see CacheConfiguration#getStoragePaths()
+     * @see CacheConfiguration#getIndexPath()
      */
     public File tmpCacheStorage(@Nullable String storagePath, String 
cacheDirName) {
-        return new File(cacheStorageRoot(storagePath), TMP_CACHE_DIR_PREFIX + 
cacheDirName);
+        return tmpCacheStorage(cacheStorage(storagePath, cacheDirName));
     }
 
     /**
@@ -702,7 +735,17 @@ public class NodeFileTree extends SharedFileTree {
      * @return Path to the temp partition file.
      */
     public File tmpPartition(CacheConfiguration<?, ?> ccfg, int partId) {
-        return new File(resolveStorage(tmpCacheStorages(ccfg), partId), 
partitionFileName(partId));
+        if (partId == INDEX_PARTITION) {
+            String idxPath = ccfg.getIndexPath();
+
+            if (!F.isEmpty(idxPath)) {
+                File idxPartFile = partitionFile(ccfg, INDEX_PARTITION);
+
+                return new File(tmpCacheStorage(idxPartFile.getParentFile()), 
idxPartFile.getName());
+            }
+        }
+
+        return new File(resolveStorage(tmpCacheStorages(ccfg, false), partId), 
partitionFileName(partId));
     }
 
     /**
@@ -713,6 +756,7 @@ public class NodeFileTree extends SharedFileTree {
      * @param partId partition id.
      * @return Path to the temp partition file.
      * @see CacheConfiguration#getStoragePaths()
+     * @see CacheConfiguration#getIndexPath()
      */
     public File tmpPartition(@Nullable String storagePath, String 
cacheDirName, int partId) {
         return new File(tmpCacheStorage(storagePath, cacheDirName), 
partitionFileName(partId));
@@ -879,11 +923,14 @@ public class NodeFileTree extends SharedFileTree {
 
     /**
      * @param storagePath Value from config.
+     * @param cacheDirName Regular or temporary directory name for cache.
      * @return File storage.
      * @see CacheConfiguration#getStoragePaths()
+     * @see CacheConfiguration#getIndexPath()
+     * @see #cacheDirName(CacheConfiguration)
      */
-    private File cacheStorageRoot(@Nullable String storagePath) {
-        return storagePath == null ? nodeStorage : 
extraStorages.getOrDefault(storagePath, nodeStorage);
+    private File cacheStorage(@Nullable String storagePath, String 
cacheDirName) {
+        return new File(storagePath == null ? nodeStorage : 
extraStorages.getOrDefault(storagePath, nodeStorage), cacheDirName);
     }
 
     /**
@@ -1004,6 +1051,7 @@ public class NodeFileTree extends SharedFileTree {
      */
     public static int partId(File part) {
         String name = part.getName();
+
         if (name.equals(INDEX_FILE_NAME))
             return INDEX_PARTITION;
 
@@ -1046,6 +1094,16 @@ public class NodeFileTree extends SharedFileTree {
         return Paths.get(root.getAbsolutePath(), path, folderName).toFile();
     }
 
+    /**
+     * @param ccfg Cache configuration.
+     * @return Cache directory name.
+     */
+    private static String cacheDirName(CacheConfiguration<?, ?> ccfg) {
+        return ccfg.getGroupName() != null
+            ? CACHE_GRP_DIR_PREFIX + ccfg.getGroupName()
+            : CACHE_DIR_PREFIX + ccfg.getName();
+    }
+
     /**
      * @param typeId Type id.
      * @return Binary metadata file name.
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/SnapshotFileTree.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/SnapshotFileTree.java
index e41197b277f..52b12a4d0db 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/SnapshotFileTree.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/SnapshotFileTree.java
@@ -30,7 +30,6 @@ import 
org.apache.ignite.configuration.DataStorageConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.dump.DumpReader;
 import org.apache.ignite.internal.GridKernalContext;
-import org.apache.ignite.internal.util.typedef.F;
 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;
@@ -244,10 +243,10 @@ public class SnapshotFileTree extends NodeFileTree {
 
     /**
      * @param grpId Cache group id.
-     * @return Files that match cache or cache group pattern.
+     * @return Directories that match cache or cache group pattern.
      */
-    public File existingCacheDirectory(int grpId) {
-        return F.first(existingCacheDirs(true, f -> CU.cacheId(cacheName(f)) 
== grpId));
+    public List<File> existingCacheDirectories(int grpId) {
+        return existingCacheDirs(true, f -> CU.cacheId(cacheName(f)) == grpId);
     }
 
     /**
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotManager.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotManager.java
index e869ce66706..c42c302ad42 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotManager.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/IgniteSnapshotManager.java
@@ -309,6 +309,7 @@ public class IgniteSnapshotManager extends 
GridCacheSharedManagerAdapter
     /**
      * File transmission parameter of a storage path for given group id.
      * @see CacheConfiguration#getStoragePaths()
+     * @see CacheConfiguration#getIndexPath()
      */
     private static final String SNP_CACHE_STORAGE_PATH_PARAM = 
"cacheStoragePath";
 
@@ -3725,6 +3726,7 @@ public class IgniteSnapshotManager extends 
GridCacheSharedManagerAdapter
          * @param pair Cache group id with corresponding partition id.
          * @return Map of params.
          * @see CacheConfiguration#getStoragePaths()
+         * @see CacheConfiguration#getIndexPath()
          */
         private Map<String, Serializable> transmissionParams(
             String rqId,
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/SnapshotResponseRemoteFutureTask.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/SnapshotResponseRemoteFutureTask.java
index b4c9a35fa17..12b589f1d6b 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/SnapshotResponseRemoteFutureTask.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/SnapshotResponseRemoteFutureTask.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.internal.processors.cache.persistence.snapshot;
 
 import java.io.File;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
@@ -204,21 +205,25 @@ public class SnapshotResponseRemoteFutureTask extends 
AbstractSnapshotFutureTask
             if (cacheData.containsKey(grpId))
                 return cacheData.get(grpId);
 
-            File cacheDir = sft.existingCacheDirectory(grpId);
+            List<File> cacheDirs = sft.existingCacheDirectories(grpId);
 
-            if (cacheDir == null) {
+            if (F.isEmpty(cacheDirs)) {
                 throw new IgniteException("Cache directory not found 
[snpName=" + snpName + ", meta=" + meta +
                     ", grp=" + grpId + ']');
             }
 
-            List<StoredCacheData> res = 
NodeFileTree.existingCacheConfigFiles(cacheDir).stream().map(f -> {
-                try {
-                    return cctx.cache().configManager().readCacheData(f);
-                }
-                catch (IgniteCheckedException e) {
-                    throw new IgniteException(e);
-                }
-            }).collect(Collectors.toList());
+            List<StoredCacheData> res = new ArrayList<>();
+
+            for (File cacheDir : cacheDirs) {
+                
res.addAll(NodeFileTree.existingCacheConfigFiles(cacheDir).stream().map(f -> {
+                    try {
+                        return cctx.cache().configManager().readCacheData(f);
+                    }
+                    catch (IgniteCheckedException e) {
+                        throw new IgniteException(e);
+                    }
+                }).collect(Collectors.toList()));
+            }
 
             if (res.isEmpty()) {
                 throw new IgniteException("Cache configs not found [snpName=" 
+ snpName + ", meta=" + meta +
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/SnapshotSender.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/SnapshotSender.java
index be42b5503a9..1b53ef36a8b 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/SnapshotSender.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/SnapshotSender.java
@@ -130,6 +130,7 @@ public abstract class SnapshotSender {
      * @param pair Group id with partition id pair.
      * @param length Partition length.
      * @see CacheConfiguration#getStoragePaths()
+     * @see CacheConfiguration#getIndexPath()
      */
     public final void sendPart(File from, File to, @Nullable String 
storagePath, GroupPartitionId pair, Long length) {
         if (!lock.readLock().tryLock())
@@ -197,6 +198,7 @@ public abstract class SnapshotSender {
      * @param pair Group id with partition id pair.
      * @param length Partition length.
      * @see CacheConfiguration#getStoragePaths()
+     * @see CacheConfiguration#getIndexPath()
      */
     protected abstract void sendPart0(File from, File to, @Nullable String 
storagePath, GroupPartitionId pair, Long length);
 
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/dump/Dump.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/dump/Dump.java
index 6dcc392fedf..9df3977f2d9 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/dump/Dump.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/snapshot/dump/Dump.java
@@ -21,6 +21,7 @@ import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Iterator;
@@ -152,19 +153,25 @@ public class Dump implements AutoCloseable {
     public List<StoredCacheData> configs(String node, int grp, @Nullable 
Set<Integer> cacheIds) {
         JdkMarshaller marsh = cctx.marshallerContext().jdkMarshaller();
 
+        List<StoredCacheData> res = new ArrayList<>();
+
         // Searching for ALL config files regardless directory name.
         // Initial version of Cache dump contains a bug:
         // For a group with one cache cache-xxx directory created, but 
cacheGroup-xxx expected.
-        return 
NodeFileTree.allExisingConfigFiles(sft(node).existingCacheDirectory(grp)).stream().map(f
 -> {
-            try {
-                return readCacheData(f, marsh, cctx.config());
-            }
-            catch (IgniteCheckedException e) {
-                throw new IgniteException(e);
-            }
-        // Keep only caches from filter.
-        }).filter(scd -> cacheIds == null || cacheIds.contains(scd.cacheId()))
-          .collect(Collectors.toList());
+        for (File cacheDir : sft(node).existingCacheDirectories(grp)) {
+            
res.addAll(NodeFileTree.allExisingConfigFiles(cacheDir).stream().map(f -> {
+                try {
+                    return readCacheData(f, marsh, cctx.config());
+                }
+                catch (IgniteCheckedException e) {
+                    throw new IgniteException(e);
+                }
+                // Keep only caches from filter.
+            }).filter(scd -> cacheIds == null || 
cacheIds.contains(scd.cacheId()))
+                .collect(Collectors.toList()));
+        }
+
+        return res;
     }
 
     /**
@@ -173,10 +180,10 @@ public class Dump implements AutoCloseable {
      * @return Dump iterator.
      */
     public List<Integer> partitions(String node, int grp) {
-        List<File> parts = 
sft(node).existingCachePartitionFiles(sft(node).existingCacheDirectory(grp), 
true, comprParts);
+        List<File> parts = new ArrayList<>();
 
-        if (parts == null)
-            return Collections.emptyList();
+        for (File cacheDir : sft(node).existingCacheDirectories(grp))
+            parts.addAll(sft(node).existingCachePartitionFiles(cacheDir, true, 
comprParts));
 
         return parts.stream()
             .map(NodeFileTree::partId)
@@ -197,7 +204,7 @@ public class Dump implements AutoCloseable {
         FileIO dumpFile;
 
         try {
-            dumpFile = ioFactory.create(new 
File(sft(node).existingCacheDirectory(grp), dumpPartFileName(part, 
comprParts)));
+            dumpFile = ioFactory.create(dumpFile(node, grp, part));
         }
         catch (IOException e) {
             throw new RuntimeException(e);
@@ -272,6 +279,18 @@ public class Dump implements AutoCloseable {
         };
     }
 
+    /** */
+    private File dumpFile(String node, int grp, int part) {
+        for (File cacheDir : sft(node).existingCacheDirectories(grp)) {
+            File partFile = new File(cacheDir, dumpPartFileName(part, 
comprParts));
+
+            if (partFile.exists())
+                return partFile;
+        }
+
+        throw new IllegalStateException("Part file not found[node=" + node + 
", grp=" + grp + ", part=" + part + ']');
+    }
+
     /** @return Dump directories. */
     public List<SnapshotFileTree> fileTrees() {
         return sfts;
diff --git 
a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/AbstractDataRegionRelativeStoragePathTest.java
 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/AbstractDataRegionRelativeStoragePathTest.java
index f40d9e40621..1db94184f8a 100644
--- 
a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/AbstractDataRegionRelativeStoragePathTest.java
+++ 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/AbstractDataRegionRelativeStoragePathTest.java
@@ -30,6 +30,8 @@ import org.apache.ignite.IgniteCheckedException;
 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.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.IgniteEx;
 import org.apache.ignite.internal.IgnitionEx;
 import org.apache.ignite.internal.util.typedef.F;
@@ -50,34 +52,63 @@ public abstract class 
AbstractDataRegionRelativeStoragePathTest extends GridComm
     /** Second custom storage path. */
     static final String STORAGE_PATH_2 = "storage2";
 
+    /** Custom indexes path. */
+    static final String IDX_PATH = "idxs";
+
     /** */
     static final String SNP_PATH = "ex_snapshots";
 
     /** */
     protected static final int PARTS_CNT = 15;
 
+    /** */
+    protected static final int GRID_CNT = 3;
+
     /** */
     @Parameterized.Parameter()
-    public boolean absPath;
+    public PathMode pathMode;
 
     /** */
     @Parameterized.Parameter(1)
     public boolean severalCacheStorages;
 
     /** */
-    @Parameterized.Parameters(name = "absPath={0},severalCacheStorages={1}")
+    @Parameterized.Parameter(2)
+    public boolean idxStorage;
+
+    /** */
+    @Parameterized.Parameters(name = 
"path={0},severalCacheStorages={1},idxStorage={2}")
     public static List<Object[]> params() {
         List<Object[]> res = new ArrayList<>();
 
-        for (boolean absPath : new boolean[]{true, false}) {
+        for (PathMode absPathMode : PathMode.values()) {
             for (boolean severalCacheStorages : new boolean[]{true, false}) {
-                res.add(new Object[] {absPath, severalCacheStorages});
+                for (boolean idxStorage : new boolean[]{true, false})
+                    res.add(new Object[]{absPathMode, severalCacheStorages, 
idxStorage});
             }
         }
 
         return res;
     }
 
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String 
igniteInstanceName) throws Exception {
+        DataStorageConfiguration dsCfg = dataStorageConfiguration();
+
+        dsCfg.getDefaultDataRegionConfiguration()
+            .setPersistenceEnabled(true);
+
+        IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);
+
+        return cfg
+            .setConsistentId(consId(cfg))
+            .setDataStorageConfiguration(dsCfg)
+            .setCacheConfiguration(ccfgs())
+            .setWorkDirectory(pathMode == PathMode.SEPARATE_ROOT
+                ? new File(U.defaultWorkDirectory(), 
consId(cfg)).getAbsolutePath()
+                : null);
+    }
+
     /** {@inheritDoc} */
     @Override protected void afterTest() throws Exception {
         super.afterTest();
@@ -86,14 +117,22 @@ public abstract class 
AbstractDataRegionRelativeStoragePathTest extends GridComm
 
         cleanPersistenceDir();
 
-        if (absPath) {
+        if (pathMode == PathMode.ABS) {
             U.delete(new File(storagePath(STORAGE_PATH)).getParentFile());
             U.delete(new File(storagePath(STORAGE_PATH_2)).getParentFile());
+            U.delete(new File(storagePath(IDX_PATH)).getParentFile());
         }
-        else {
+        else if (pathMode == PathMode.RELATIVE) {
             U.delete(new File(U.defaultWorkDirectory(), STORAGE_PATH));
             U.delete(new File(U.defaultWorkDirectory(), STORAGE_PATH_2));
+            U.delete(new File(U.defaultWorkDirectory(), IDX_PATH));
         }
+        else if (pathMode == PathMode.SEPARATE_ROOT) {
+            for (File f : new File(U.defaultWorkDirectory()).listFiles(f -> 
!f.getName().equals("log")))
+                U.delete(f);
+        }
+        else
+            throw new IllegalStateException("Unknown path mode");
 
         U.delete(new File(U.defaultWorkDirectory(), SNP_PATH));
     }
@@ -116,7 +155,7 @@ public abstract class 
AbstractDataRegionRelativeStoragePathTest extends GridComm
             ft.extraStorages().values().forEach(U::delete);
         });
 
-        U.delete(F.first(fts).db());
+        fts.forEach(ft -> U.delete(ft.db()));
 
         IgniteEx srv = startAndActivate();
 
@@ -170,7 +209,7 @@ public abstract class 
AbstractDataRegionRelativeStoragePathTest extends GridComm
 
     /** */
     IgniteEx startAndActivate() throws Exception {
-        IgniteEx srv = startGrids(3);
+        IgniteEx srv = startGrids(GRID_CNT);
 
         srv.cluster().state(ClusterState.ACTIVE);
 
@@ -193,6 +232,9 @@ public abstract class 
AbstractDataRegionRelativeStoragePathTest extends GridComm
         if (!F.isEmpty(storagePath))
             ccfg.setStoragePaths(storagePath);
 
+        if (idxStorage)
+            ccfg.setIndexPath(storagePath(IDX_PATH));
+
         return ccfg;
     }
 
@@ -213,7 +255,7 @@ public abstract class 
AbstractDataRegionRelativeStoragePathTest extends GridComm
     /** */
     String storagePath(String storagePath) {
         try {
-            return absPath ? new File(U.defaultWorkDirectory(), "abs/" + 
storagePath).getAbsolutePath() : storagePath;
+            return pathMode == PathMode.ABS ? new 
File(U.defaultWorkDirectory(), "abs/" + storagePath).getAbsolutePath() : 
storagePath;
         }
         catch (IgniteCheckedException e) {
             throw new RuntimeException(e);
@@ -223,6 +265,26 @@ public abstract class 
AbstractDataRegionRelativeStoragePathTest extends GridComm
     /** @param fts Nodes file trees. */
     abstract void checkFileTrees(List<NodeFileTree> fts) throws 
IgniteCheckedException;
 
+    /** Data storage configuration. */
+    abstract DataStorageConfiguration dataStorageConfiguration();
+
     /** Cache configs. */
     abstract CacheConfiguration[] ccfgs();
+
+    /** */
+    public enum PathMode {
+        /** Absolute path for custom directories. */
+        ABS,
+
+        /** Relative path for custom directories. Same root. */
+        RELATIVE,
+
+        /** Relative path for custom directories. Separate root for each node. 
*/
+        SEPARATE_ROOT
+    }
+
+    /** */
+    static String consId(IgniteConfiguration cfg) {
+        return U.maskForFileName(cfg.getIgniteInstanceName());
+    }
 }
diff --git 
a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/CacheConfigStoragePathTest.java
 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/CacheConfigStoragePathTest.java
index b62ab5766c8..d5054060571 100644
--- 
a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/CacheConfigStoragePathTest.java
+++ 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/CacheConfigStoragePathTest.java
@@ -21,19 +21,23 @@ import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Function;
+import java.util.function.IntConsumer;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
+import org.apache.ignite.Ignition;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.DataStorageConfiguration;
-import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.IgniteEx;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.CU;
@@ -41,24 +45,17 @@ import org.apache.ignite.internal.util.typedef.internal.U;
 import org.junit.Test;
 
 import static 
org.apache.ignite.configuration.IgniteConfiguration.DFLT_SNAPSHOT_DIRECTORY;
+import static 
org.apache.ignite.internal.pagemem.PageIdAllocator.INDEX_PARTITION;
 import static 
org.apache.ignite.internal.processors.cache.persistence.metastorage.MetaStorage.METASTORAGE_CACHE_NAME;
 
 /**
  * Test cases when {@link CacheConfiguration#setStoragePaths(String...)} used 
to set custom data region storage path.
  */
 public class CacheConfigStoragePathTest extends 
AbstractDataRegionRelativeStoragePathTest {
-
     /** {@inheritDoc} */
-    @Override protected IgniteConfiguration getConfiguration(String 
igniteInstanceName) throws Exception {
-        DataStorageConfiguration dsCfg = new DataStorageConfiguration()
-            .setExtraStoragePaths(storagePath(STORAGE_PATH), 
storagePath(STORAGE_PATH_2));
-
-        dsCfg.getDefaultDataRegionConfiguration().setPersistenceEnabled(true);
-
-        return super.getConfiguration(igniteInstanceName)
-            .setConsistentId(U.maskForFileName(igniteInstanceName))
-            .setDataStorageConfiguration(dsCfg)
-            .setCacheConfiguration(ccfgs());
+    @Override protected DataStorageConfiguration dataStorageConfiguration() {
+        return new DataStorageConfiguration()
+            .setExtraStoragePaths(storagePath(STORAGE_PATH), 
storagePath(STORAGE_PATH_2), storagePath(IDX_PATH));
     }
 
     /** {@inheritDoc} */
@@ -75,6 +72,39 @@ public class CacheConfigStoragePathTest extends 
AbstractDataRegionRelativeStorag
         };
     }
 
+    /** Sanity checks - all paths for all partitions are different and contain 
cacheDir. */
+    @Test
+    public void testPathGeneration() throws Exception {
+        IgniteEx srv = startAndActivate();
+
+        NodeFileTree ft = srv.context().pdsFolderResolver().fileTree();
+
+        IntConsumer checkPart = i -> {
+            Set<File> parts = new HashSet<>();
+
+            Set<String> grps = new HashSet<>();
+
+            for (CacheConfiguration<?, ?> ccfg : ccfgs()) {
+                if (!grps.add(CU.cacheOrGroupName(ccfg)))
+                    continue;
+
+                File part = ft.partitionFile(ccfg, i);
+
+                
assertTrue(Arrays.asList(ft.cacheStorages(ccfg)).contains(part.getParentFile()));
+                assertTrue(parts.add(part));
+            }
+
+            assertEquals(grpCount(), grps.size());
+        };
+
+        for (int i = 0; i < PARTS_CNT; i++)
+            checkPart.accept(i);
+
+        checkPart.accept(INDEX_PARTITION);
+
+        stopAllGrids();
+    }
+
     /** */
     @Test
     public void testCaches() throws Exception {
@@ -129,110 +159,152 @@ public class CacheConfigStoragePathTest extends 
AbstractDataRegionRelativeStorag
      * @param path Snapshot path.
      */
     private void checkSnapshotFiles(String name, String path) {
-        NodeFileTree ft = grid(0).context().pdsFolderResolver().fileTree();
+        // Only in "separate root" mode snapshots for nodes will have 
different roots.
+        List<NodeFileTree> fts = pathMode == PathMode.SEPARATE_ROOT
+            ? Ignition.allGrids().stream().map(n -> 
((IgniteEx)n).context().pdsFolderResolver().fileTree()).collect(Collectors.toList())
+            : 
Collections.singletonList(grid(0).context().pdsFolderResolver().fileTree());
 
-        Function<String, File> snpRootF = storage -> {
-            File snpRoot;
+        Map<Integer, Set<Integer>> partsMap = new HashMap<>();
 
-            if (path != null)
-                snpRoot = new File(path);
-            else if (storage == null)
-                snpRoot = ft.snapshotsRoot();
-            else {
-                File nodeStorage = absPath
-                    ? new File(storage)
-                    : new File(ft.root(), storage);
+        for (NodeFileTree ft : fts) {
+            Function<String, File> snpRootF = storage -> {
+                File snpRoot;
+
+                if (path != null)
+                    snpRoot = new File(path);
+                else if (storage == null)
+                    snpRoot = ft.snapshotsRoot();
+                else {
+                    File nodeStorage = pathMode == PathMode.ABS
+                        ? new File(storage)
+                        : new File(ft.root(), storage);
+
+                    snpRoot = new File(nodeStorage, DFLT_SNAPSHOT_DIRECTORY);
+                }
 
-                snpRoot = new File(nodeStorage, DFLT_SNAPSHOT_DIRECTORY);
-            }
+                return new File(snpRoot, name);
+            };
 
-            return new File(snpRoot, name);
-        };
+            // Snapshot root directories.
+            Set<File> roots = new HashSet<>(Arrays.asList(
+                snpRootF.apply(null),
+                snpRootF.apply(storagePath(STORAGE_PATH)),
+                snpRootF.apply(storagePath(STORAGE_PATH_2))
+            ));
 
-        // Snapshot root directories.
-        Set<File> roots = new HashSet<>(Arrays.asList(
-            snpRootF.apply(null),
-            snpRootF.apply(storagePath(STORAGE_PATH)),
-            snpRootF.apply(storagePath(STORAGE_PATH_2))
-        ));
+            boolean idxPathUsed = idxStorage && idxPartMustExistsInSnapshot();
 
-        assertEquals(path != null ? 1 : 3, roots.size());
+            if (idxPathUsed)
+                roots.add(snpRootF.apply(storagePath(IDX_PATH)));
 
-        // Root -> cache -> partition set.
-        Map<File, Map<String, Set<Integer>>> snpFiles = new HashMap<>();
+            // Sanity check storagePath returns different paths.
+            assertEquals(path != null ? 1 : (idxPathUsed ? 4 : 3), 
roots.size());
 
-        // Collecting all partition files under each snapshot root.
-        roots.forEach(snpRoot -> {
-            assertTrue(snpRoot.exists());
-            assertTrue(snpRoot.isDirectory());
+            // Root -> cache -> partition set.
+            Map<File, Map<String, Set<Integer>>> snpFiles = new HashMap<>();
 
-            try (Stream<Path> files = Files.walk(snpRoot.toPath())) {
-                files.filter(p -> 
NodeFileTree.partitionFile(p.toFile())).forEach(partFile -> {
-                    File root = roots.stream().filter(r -> 
partFile.startsWith(r.toPath())).findFirst().orElseThrow();
+            // Collecting all partition files under each snapshot root.
+            for (File snpRoot : roots) {
+                assertTrue(snpRoot.exists());
+                assertTrue(snpRoot.isDirectory());
 
-                    String cacheName = 
NodeFileTree.cacheName(partFile.getParent().toFile());
+                Predicate<Path> pathPredicate = p -> 
NodeFileTree.partitionFile(p.toFile())
+                    || 
p.getFileName().toString().equals(NodeFileTree.partitionFileName(INDEX_PARTITION));
 
-                    if (cacheName.equals(METASTORAGE_CACHE_NAME))
-                        return;
+                try (Stream<Path> files = Files.walk(snpRoot.toPath())) {
+                    files.filter(pathPredicate).forEach(partFile -> {
+                        File root = roots.stream().filter(r -> 
partFile.startsWith(r.toPath())).findFirst().orElseThrow();
 
-                    int part = NodeFileTree.partId(partFile.toFile());
+                        String cacheName = 
NodeFileTree.cacheName(partFile.getParent().toFile());
 
-                    String[] cs = Arrays.stream(ccfgs())
-                        .filter(ccfg -> 
CU.cacheOrGroupName(ccfg).equals(cacheName))
-                        .findFirst().orElseThrow().getStoragePaths();
+                        if (cacheName.equals(METASTORAGE_CACHE_NAME))
+                            return;
 
-                    File expStorage = snpRootF.apply(F.isEmpty(cs) ? null : 
cs[part % cs.length]);
+                        int part = NodeFileTree.partId(partFile.toFile());
 
-                    assertEquals(expStorage, root);
+                        String[] cs = Arrays.stream(ccfgs())
+                            .filter(ccfg -> 
CU.cacheOrGroupName(ccfg).equals(cacheName))
+                            .findFirst().orElseThrow().getStoragePaths();
 
-                    snpFiles
-                        .computeIfAbsent(root, r -> new HashMap<>())
-                        .computeIfAbsent(cacheName, c -> new HashSet<>())
-                        .add(part);
-                });
-            }
-            catch (IOException e) {
-                throw new RuntimeException(e);
+                        File expStorage = (idxPathUsed && part == 
INDEX_PARTITION)
+                            ? snpRootF.apply(storagePath(IDX_PATH))
+                            : snpRootF.apply(F.isEmpty(cs) ? null : cs[part % 
cs.length]);
+
+                        assertEquals(expStorage, root);
+
+                        snpFiles
+                            .computeIfAbsent(root, r -> new HashMap<>())
+                            .computeIfAbsent(cacheName, c -> new HashSet<>())
+                            .add(part);
+                    });
+                }
+                catch (IOException e) {
+                    throw new RuntimeException(e);
+                }
             }
-        });
 
-        Set<String> seenGrps = new HashSet<>();
+            Set<String> seenGrps = new HashSet<>();
 
-        Arrays.stream(ccfgs()).forEach(ccfg -> {
-            String cname = CU.cacheOrGroupName(ccfg);
+            Arrays.stream(ccfgs()).forEach(ccfg -> {
+                String cname = CU.cacheOrGroupName(ccfg);
 
-            if (seenGrps.contains(cname))
-                return;
+                if (seenGrps.contains(cname))
+                    return;
 
-            seenGrps.add(cname);
+                seenGrps.add(cname);
 
-            Set<Integer> parts = new HashSet<>();
+                Set<Integer> parts = new HashSet<>();
 
-            String[] storagePaths = ccfg.getStoragePaths();
+                List<String> storagePaths = new 
ArrayList<>(F.isEmpty(ccfg.getStoragePaths())
+                    ? Collections.singletonList(null)
+                    : Arrays.asList(ccfg.getStoragePaths()));
 
-            if (F.isEmpty(storagePaths)) {
-                storagePaths = new String[1];
-            }
+                if (!F.isEmpty(ccfg.getIndexPath()) && 
idxPartMustExistsInSnapshot())
+                    storagePaths.add(ccfg.getIndexPath());
 
-            for (String storagePath : storagePaths) {
-                File expRoot = snpRootF.apply(storagePath);
+                for (String storagePath : storagePaths) {
+                    File expRoot = snpRootF.apply(storagePath);
 
-                assertTrue(cname + " must be found", 
snpFiles.containsKey(expRoot));
+                    assertTrue(cname + " must be found", 
snpFiles.containsKey(expRoot));
 
-                parts.addAll(snpFiles.get(expRoot).get(cname));
-            }
+                    parts.addAll(snpFiles.get(expRoot).get(cname));
+                }
+
+                assertFalse(cname + " partitions must be found", 
parts.isEmpty());
+
+                partsMap.computeIfAbsent(CU.cacheGroupId(ccfg), key -> new 
HashSet<>()).addAll(parts);
+            });
+
+            assertEquals(grpCount(), seenGrps.size());
+        }
+
+        Arrays.stream(ccfgs()).forEach(ccfg -> {
+            String cname = CU.cacheOrGroupName(ccfg);
+
+            Set<Integer> parts = partsMap.get(CU.cacheGroupId(ccfg));
 
             assertFalse(cname + " partitions must be found", parts.isEmpty());
-            assertEquals("All partitions for " + cname + " must be found", 
PARTS_CNT, parts.size());
+
+            assertEquals(
+                "All partitions for " + cname + " must be found",
+                PARTS_CNT + (idxPartMustExistsInSnapshot() ? 1 : 0),
+                parts.size()
+            );
 
             IntStream.range(0, PARTS_CNT).forEach(i -> assertTrue(i + " 
partition must be found", parts.contains(i)));
-        });
 
-        assertEquals(grpCount(), seenGrps.size());
+            if (idxPartMustExistsInSnapshot())
+                assertTrue(parts.contains(INDEX_PARTITION));
+        });
     }
 
     /** */
     private int grpCount() {
         return 
Arrays.stream(ccfgs()).map(CU::cacheOrGroupName).collect(Collectors.toSet()).size();
     }
+
+    /** */
+    protected boolean idxPartMustExistsInSnapshot() {
+        return false;
+    }
 }
diff --git 
a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/CustomCacheStorageConfigurationSelfTest.java
 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/CustomCacheStorageConfigurationSelfTest.java
index ff0f145b1a5..e52e6cb322c 100644
--- 
a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/CustomCacheStorageConfigurationSelfTest.java
+++ 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/CustomCacheStorageConfigurationSelfTest.java
@@ -111,6 +111,11 @@ public class CustomCacheStorageConfigurationSelfTest 
extends GridCommonAbstractT
                 () -> srv.createCache(new 
CacheConfiguration<>("my-cache").setStoragePaths("other")),
                 IgniteCheckedException.class
             );
+
+            assertThrowsWithCause(
+                () -> srv.createCache(new 
CacheConfiguration<>("my-cache").setIndexPath("other")),
+                IgniteCheckedException.class
+            );
         };
 
         try (IgniteEx srv = startGrid(new 
IgniteConfiguration().setDataStorageConfiguration(new DataStorageConfiguration()
@@ -157,6 +162,13 @@ public class CustomCacheStorageConfigurationSelfTest 
extends GridCommonAbstractT
                 IgniteCheckedException.class
             );
 
+            assertThrowsWithCause(
+                () -> srv.createCache(new 
CacheConfiguration<>("my-cache2").setGroupName("grp")
+                    .setStoragePaths(myPath3.getAbsolutePath())
+                    .setIndexPath(myPath.getAbsolutePath())),
+                IgniteCheckedException.class
+            );
+
             srv.createCache(new CacheConfiguration<>("my-cache2")
                 .setGroupName("grp-2"));
 
@@ -174,6 +186,13 @@ public class CustomCacheStorageConfigurationSelfTest 
extends GridCommonAbstractT
                 IgniteCheckedException.class
             );
 
+            assertThrowsWithCause(
+                () -> srv.createCache(new CacheConfiguration<>("my-cache3")
+                    .setGroupName("grp-2")
+                    .setIndexPath(myPath.getAbsolutePath())),
+                IgniteCheckedException.class
+            );
+
             srv.createCache(new CacheConfiguration<>("my-cache3")
                 .setStoragePaths(myPath2.getAbsolutePath(), 
myPath3.getAbsolutePath())
                 .setGroupName("grp-3"));
@@ -191,6 +210,14 @@ public class CustomCacheStorageConfigurationSelfTest 
extends GridCommonAbstractT
                     .setStoragePaths(myPath3.getAbsolutePath(), 
myPath2.getAbsolutePath())),
                 IgniteCheckedException.class
             );
+
+            assertThrowsWithCause(
+                () -> srv.createCache(new CacheConfiguration<>("my-cache4")
+                    .setGroupName("grp-3")
+                    .setStoragePaths(myPath2.getAbsolutePath(), 
myPath3.getAbsolutePath())
+                    .setIndexPath(myPath.getAbsolutePath())),
+                IgniteCheckedException.class
+            );
         };
 
         try (IgniteEx srv = startGrid(new 
IgniteConfiguration().setDataStorageConfiguration(new DataStorageConfiguration()
@@ -227,6 +254,14 @@ public class CustomCacheStorageConfigurationSelfTest 
extends GridCommonAbstractT
 
             srv.createCache(new CacheConfiguration<>("my-cache2")
                 
.setGroupName("grp").setStoragePaths(myPath3.getAbsolutePath()));
+
+            srv.createCache(new CacheConfiguration<>("my-cache3")
+                
.setGroupName("grp2").setStoragePaths(myPath3.getAbsolutePath())
+                .setIndexPath(myPath.getAbsolutePath()));
+
+            srv.createCache(new CacheConfiguration<>("my-cache4")
+                
.setGroupName("grp2").setStoragePaths(myPath3.getAbsolutePath())
+                .setIndexPath(myPath.getAbsolutePath()));
         }
     }
 
@@ -238,7 +273,8 @@ public class CustomCacheStorageConfigurationSelfTest 
extends GridCommonAbstractT
             .setDefaultDataRegionConfiguration(new 
DataRegionConfiguration().setPersistenceEnabled(true));
 
         CacheConfiguration<Object, Object> ccfg = new 
CacheConfiguration<>(DEFAULT_CACHE_NAME)
-            .setStoragePaths(myPath.getAbsolutePath());
+            .setStoragePaths(myPath.getAbsolutePath())
+            .setIndexPath(myPath.getAbsolutePath());
 
         try (IgniteEx srv = startGrid(getConfiguration("srv")
             .setDataStorageConfiguration(dsCfg)
diff --git 
a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/SnapshotCreationNonDefaultStoragePathTest.java
 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/SnapshotCreationNonDefaultStoragePathTest.java
index e22d7a0cd11..b670774e5b1 100644
--- 
a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/SnapshotCreationNonDefaultStoragePathTest.java
+++ 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/SnapshotCreationNonDefaultStoragePathTest.java
@@ -18,18 +18,21 @@
 package org.apache.ignite.internal.processors.cache.persistence.filename;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.List;
 import java.util.function.BiConsumer;
+import org.apache.commons.io.FileUtils;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteException;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.DataStorageConfiguration;
-import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.IgniteEx;
 import org.apache.ignite.internal.util.typedef.G;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.junit.Test;
 
+import static 
org.apache.ignite.configuration.IgniteConfiguration.DFLT_SNAPSHOT_DIRECTORY;
+import static 
org.apache.ignite.testframework.GridTestUtils.assertThrowsAnyCause;
 import static 
org.apache.ignite.testframework.GridTestUtils.assertThrowsWithCause;
 
 /**
@@ -37,18 +40,10 @@ import static 
org.apache.ignite.testframework.GridTestUtils.assertThrowsWithCaus
  */
 public class SnapshotCreationNonDefaultStoragePathTest extends 
AbstractDataRegionRelativeStoragePathTest {
     /** {@inheritDoc} */
-    @Override protected IgniteConfiguration getConfiguration(String 
igniteInstanceName) throws Exception {
-        DataStorageConfiguration dsCfg = new DataStorageConfiguration()
+    @Override protected DataStorageConfiguration dataStorageConfiguration() {
+        return new DataStorageConfiguration()
             .setStoragePath(storagePath(STORAGE_PATH))
-            .setExtraStoragePaths(storagePath(STORAGE_PATH_2));
-
-        dsCfg.getDefaultDataRegionConfiguration()
-            .setPersistenceEnabled(true);
-
-        return super.getConfiguration(igniteInstanceName)
-            .setConsistentId(U.maskForFileName(igniteInstanceName))
-            .setDataStorageConfiguration(dsCfg)
-            .setCacheConfiguration(ccfgs());
+            .setExtraStoragePaths(storagePath(STORAGE_PATH_2), 
storagePath(IDX_PATH));
     }
 
     /** {@inheritDoc} */
@@ -126,6 +121,8 @@ public class SnapshotCreationNonDefaultStoragePathTest 
extends AbstractDataRegio
 
         srv.context().cache().context().snapshotMgr().createSnapshot("mysnp2", 
fullPathSnp.getAbsolutePath(), false, false).get();
 
+        String grid1ConsId = consId(grid(1).configuration());
+
         stopGrid(1);
 
         resetBaselineTopology();
@@ -146,6 +143,32 @@ public class SnapshotCreationNonDefaultStoragePathTest 
extends AbstractDataRegio
             checkDataExists();
         };
 
+        if (pathMode == PathMode.SEPARATE_ROOT) {
+            // Must move snapshot file from grid1 to other so it can be 
restored.
+            assertThrowsAnyCause(
+                log,
+                () -> {
+                    check.accept("mysnp", null);
+                    return null;
+                },
+                IgniteException.class,
+                "No snapshot metadatas found for the baseline nodes with 
consistent ids: "
+            );
+
+            Path[] copyStoppedNodeData = new Path[] {
+                Path.of(DFLT_SNAPSHOT_DIRECTORY, "mysnp"),
+                Path.of(STORAGE_PATH_2, DFLT_SNAPSHOT_DIRECTORY, "mysnp"),
+                Path.of(IDX_PATH, DFLT_SNAPSHOT_DIRECTORY, "mysnp")
+            };
+
+            for (Path copyPath : copyStoppedNodeData) {
+                FileUtils.copyDirectory(
+                    Path.of(U.defaultWorkDirectory(), grid1ConsId, 
copyPath.toString()).toFile(),
+                    Path.of(U.defaultWorkDirectory(), 
consId(grid(0).configuration()), copyPath.toString()).toFile()
+                );
+            }
+        }
+
         check.accept("mysnp", null);
         check.accept("mysnp2", fullPathSnp.getAbsolutePath());
     }
@@ -158,7 +181,7 @@ public class SnapshotCreationNonDefaultStoragePathTest 
extends AbstractDataRegio
 
                 for (String cs : ccfg.getStoragePaths()) {
 
-                    File customRoot = ensureExists(absPath
+                    File customRoot = ensureExists(pathMode == PathMode.ABS
                         ? new File(cs)
                         : new File(ft.root(), cs)
                     );
diff --git 
a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/SQLCacheConfigStoragePathTest.java
 
b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/SQLCacheConfigStoragePathTest.java
index eb48daab65d..7517d4dea0d 100644
--- 
a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/SQLCacheConfigStoragePathTest.java
+++ 
b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/SQLCacheConfigStoragePathTest.java
@@ -89,6 +89,22 @@ public class SQLCacheConfigStoragePathTest extends 
CacheConfigStoragePathTest {
 
     }
 
+    /** {@inheritDoc} */
+    @Override void restoreAndCheck(String name, String path) throws Exception {
+        super.restoreAndCheck(name, path);
+
+        for (int i = 0; i < GRID_CNT; i++) {
+            List<List<?>> rows = executeSql(grid(0), "SELECT TABLE_NAME FROM 
SYS.TABLES WHERE IS_INDEX_REBUILD_IN_PROGRESS = 'TRUE'");
+
+            assertTrue(rows.isEmpty());
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override protected boolean idxPartMustExistsInSnapshot() {
+        return true;
+    }
+
     /** */
     private static String dep(int i) {
         return i + " dep";

Reply via email to