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 07307c91527 IGNITE-27380 DataStorageConfiguration#extraSnapshotPaths 
added (#12602)
07307c91527 is described below

commit 07307c9152793b9b632218a10e11c012d9026732
Author: Nikolay <[email protected]>
AuthorDate: Wed Dec 24 18:41:02 2025 +0300

    IGNITE-27380 DataStorageConfiguration#extraSnapshotPaths added (#12602)
---
 .../configuration/DataStorageConfiguration.java    |  36 +-
 .../org/apache/ignite/internal/IgnitionEx.java     |  13 +
 .../cache/persistence/filename/NodeFileTree.java   |  14 +-
 .../persistence/filename/SnapshotFileTree.java     |  59 +-
 .../CustomCacheStorageConfigurationSelfTest.java   |  42 ++
 .../SnapshotCreationNonDefaultStoragePathTest.java |  30 +-
 .../filename/SnapshotExtraStoragesTest.java        |  35 ++
 .../filename/SnapshotFileTreeSelfTest.java         | 687 +++++++++++++++++++++
 .../ignite/testsuites/IgnitePdsTestSuite8.java     |   4 +
 9 files changed, 889 insertions(+), 31 deletions(-)

diff --git 
a/modules/core/src/main/java/org/apache/ignite/configuration/DataStorageConfiguration.java
 
b/modules/core/src/main/java/org/apache/ignite/configuration/DataStorageConfiguration.java
index b81b0d8bbc7..a6e7a2db23b 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/configuration/DataStorageConfiguration.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/configuration/DataStorageConfiguration.java
@@ -216,14 +216,26 @@ public class DataStorageConfiguration implements 
Serializable {
 
     /** 
      * Additional directories where index and partition files are stored.
-     * User may want to use dedicated storage for cache is server has several 
physical disks.
+     * User may want to use dedicated storage for cache if server has several 
physical disks.
      * Spreading load across several disks can improve performance.
      *
+     * @see #getStoragePath()
      * @see CacheConfiguration#setStoragePaths(String...)
      */
     @IgniteExperimental
     private String[] extraStoragePaths;
 
+    /**
+     * Additional directories where snapshot files are stored.
+     * User may want to use dedicated storage for cache if server has several 
physical disks.
+     * Spreading snapshot across several disks can improve performance.
+     *
+     * @see IgniteConfiguration#getSnapshotPath()
+     * @see CacheConfiguration#setStoragePaths(String...)
+     */
+    @IgniteExperimental
+    private String[] extraSnapshotPaths;
+
     /** Checkpoint frequency. */
     private long checkpointFreq = DFLT_CHECKPOINT_FREQ;
 
@@ -574,6 +586,14 @@ public class DataStorageConfiguration implements 
Serializable {
         return extraStoragePaths;
     }
 
+    /**
+     * @return Additional directories for snapshots.
+     */
+    @IgniteExperimental
+    public String[] getExtraSnapshotPaths() {
+        return extraSnapshotPaths;
+    }
+
     /**
      * Sets a path to the root directory where the Persistent Store will 
persist data and indexes.
      * By default, the Persistent Store's files are located under Ignite work 
directory.
@@ -601,6 +621,20 @@ public class DataStorageConfiguration implements 
Serializable {
         return this;
     }
 
+    /**
+     * Sets a paths to the root directories where the snapshot files stored.
+     * By default, {@link IgniteConfiguration#getSnapshotPath()} used.
+     * Length of {@code extraSnapshotPaths} must be equal to the length of 
{@link #getExtraStoragePaths()}.
+     *
+     * @param extraSnapshotPaths Extra snapshot paths where snapshot files can 
be stored.
+     * @return {@code this} for chaining.
+     */
+    public DataStorageConfiguration setExtraSnapshotPaths(String... 
extraSnapshotPaths) {
+        this.extraSnapshotPaths = extraSnapshotPaths;
+
+        return this;
+    }
+
     /**
      * Gets checkpoint frequency.
      *
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/IgnitionEx.java 
b/modules/core/src/main/java/org/apache/ignite/internal/IgnitionEx.java
index ed5e0f55a99..2864009c84a 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/IgnitionEx.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/IgnitionEx.java
@@ -1959,6 +1959,19 @@ public class IgnitionEx {
                     throw new IgniteCheckedException("DataStorageConfiguration 
contains duplicates " +
                         "[storagePath=" + dsCfg.getStoragePath() + ", 
extraStoragePaths=" + extraStorages + ']');
                 }
+
+                List<String> extraSnapshotStorages = 
F.asList(dsCfg.getExtraSnapshotPaths());
+
+                if (!extraSnapshotStorages.isEmpty() && extraStorages.size() 
!= extraSnapshotStorages.size()) {
+                    throw new IgniteCheckedException("DataStorageConfiguration 
error. " +
+                        "Size of extraSnapshotPaths must be equal to 
extraStoragePath " +
+                        "[extraStoragePaths=" + extraStorages + ", 
extraSnapshotPaths=" + extraSnapshotStorages + ']');
+                }
+
+                if (extraSnapshotStorages.size() != new 
HashSet<>(extraSnapshotStorages).size()) {
+                    throw new IgniteCheckedException("DataStorageConfiguration 
contains duplicates " +
+                        "[extraSnapshotPaths=" + extraSnapshotStorages + ']');
+                }
             }
 
             if (cfg.getMemoryConfiguration() != null || 
cfg.getPersistentStoreConfiguration() != null)
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 dbf07261067..159eeb77db7 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
@@ -403,10 +403,7 @@ public class NodeFileTree extends SharedFileTree {
             walCdc = rootRelative(DFLT_WAL_CDC_PATH);
         }
 
-        extraStorages = extraStorages(
-            dsCfg,
-            storagePath -> resolveDirectory(Path.of(storagePath, 
DB_DIR).toString())
-        );
+        extraStorages = extraStorages(dsCfg);
     }
 
     /** @return Node storage directory. */
@@ -989,12 +986,15 @@ public class NodeFileTree extends SharedFileTree {
      * @return Node storages.
      * @see DataStorageConfiguration#setExtraStoragePaths(String...)
      */
-    private Map<String, File> extraStorages(@Nullable DataStorageConfiguration 
dsCfg, Function<String, File> resolver) {
+    private Map<String, File> extraStorages(@Nullable DataStorageConfiguration 
dsCfg) {
         if (dsCfg == null || F.isEmpty(dsCfg.getExtraStoragePaths()))
             return Collections.emptyMap();
 
         return Arrays.stream(dsCfg.getExtraStoragePaths())
-            .collect(Collectors.toMap(Function.identity(), resolver));
+            .collect(Collectors.toMap(
+                Function.identity(),
+                p -> resolveDirectory(Path.of(p, DB_DIR).toString()))
+            );
     }
 
     /**
@@ -1129,7 +1129,7 @@ public class NodeFileTree extends SharedFileTree {
      * @param cfg Configured directory path.
      * @return Initialized directory.
      */
-    private File resolveDirectory(String cfg) {
+    protected File resolveDirectory(String cfg) {
         File sharedDir = new File(cfg);
 
         return sharedDir.isAbsolute()
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 52b12a4d0db..f53abc94ba4 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
@@ -134,7 +134,7 @@ public class SnapshotFileTree extends NodeFileTree {
         this.path = path;
         this.consId = consId;
 
-        Map<String, File> snpExtraStorages = snapshotExtraStorages(ft, 
cfg.getSnapshotPath());
+        Map<String, File> snpExtraStorages = snapshotExtraStorages(ft, cfg);
 
         if (snpExtraStorages.isEmpty())
             extraStorages.clear();
@@ -380,27 +380,54 @@ public class SnapshotFileTree extends NodeFileTree {
      * This will distribute workload to all physical device on host.
      *
      * @param ft Node file tree.
-     * @param snpDfltPath Snapshot default path.
+     * @param cfg Ignite configuration.
      */
-    private Map<String, File> snapshotExtraStorages(NodeFileTree ft, String 
snpDfltPath) {
+    private Map<String, File> snapshotExtraStorages(NodeFileTree ft, 
IgniteConfiguration cfg) {
+        DataStorageConfiguration dsCfg = cfg.getDataStorageConfiguration();
+
         // If path provided then create snapshot inside it, only.
-        // Same rule applies if absolute path to the snapshot root dir 
configured.
-        if (path != null || new File(snpDfltPath).isAbsolute())
+        if (path != null || dsCfg == null)
+            return Collections.emptyMap();
+
+        String[] extraStorages = dsCfg.getExtraStoragePaths();
+
+        if (extraStorages == null)
             return Collections.emptyMap();
 
-        Map<String, File> snpExtraStorages = new HashMap<>();
+        String[] extraSnpPaths = dsCfg.getExtraSnapshotPaths();
+
+        boolean hasExtraSnpPaths = extraSnpPaths != null && 
extraSnpPaths.length > 0;
 
-        ft.extraStorages().forEach((cfgStoragePath, storagePath) -> {
-            // In case we want to make snapshot in several folders the paths 
will be the following:
-            // {storage_path}/db/{folder_name} - node cache storage.
-            // {storage_path}/snapshots/{snp_name}/db/{folder_name} - snapshot 
cache storage.
-            snpExtraStorages.put(
-                cfgStoragePath,
-                new File(storagePath.getParentFile().getParentFile(), 
Path.of(snpDfltPath, name, DB_DIR, folderName()).toString())
-            );
-        });
+        String snpDfltPath = cfg.getSnapshotPath();
+
+        boolean snpDfltPathAbsolute = new File(snpDfltPath).isAbsolute();
+
+        // If absolute path to the snapshot root dir configured and no extra 
snapshot paths, then use snapshot root, only.
+        if (snpDfltPathAbsolute && !hasExtraSnpPaths)
+            return Collections.emptyMap();
+
+        Map<String, File> snpRoots = new HashMap<>();
+
+        String snpUniqSuffix = snpDfltPathAbsolute
+            ? Path.of(name, DB_DIR, folderName()).toString()
+            : Path.of(snpDfltPath, name, DB_DIR, folderName()).toString();
+
+        for (int i = 0; i < extraStorages.length; i++) {
+            File snpRoot = hasExtraSnpPaths
+                // Extra snapshot paths configured as "root" related.
+                // folderName can be different from local (NodeFileTree ft) 
one in case snapshot restored on smaller topology
+                // and data from some node copied to local.
+                // resolveDirectory returns in form 
{root}/{extra_snp_path}/{folder_name} so parent required.
+                ? ft.resolveDirectory(extraSnpPaths[i]).getParentFile()
+                // In case we want to make snapshot in several folders the 
paths will be the following:
+                // {storage_path}/db/{folder_name} - node cache storage.
+                // {storage_path} - snapshot root.
+                : 
ft.extraStorages.get(extraStorages[i]).getParentFile().getParentFile();
+
+            snpRoots.put(extraStorages[i], new File(snpRoot, snpUniqSuffix));
+        }
 
-        return snpExtraStorages;
+        return snpRoots;
     }
 
     /**
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 7a0527f7931..d908e83c925 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
@@ -112,6 +112,48 @@ public class CustomCacheStorageConfigurationSelfTest 
extends GridCommonAbstractT
         );
     }
 
+    /** */
+    @Test
+    public void testIncorrectSnapshotPathsThrows() {
+        assertThrows(
+            log,
+            () -> startGrid(new 
IgniteConfiguration().setDataStorageConfiguration(new DataStorageConfiguration()
+                .setStoragePath(myPath.getAbsolutePath())
+                .setExtraStoragePaths(myPath2.getAbsolutePath())
+                .setExtraSnapshotPaths("snppath1", "snppath2"))),
+            IgniteCheckedException.class,
+            "DataStorageConfiguration error. Size of extraSnapshotPaths must 
be equal to extraStoragePath"
+        );
+
+        assertThrows(
+            log,
+            () -> startGrid(new 
IgniteConfiguration().setDataStorageConfiguration(new DataStorageConfiguration()
+                .setStoragePath(myPath.getAbsolutePath())
+                .setExtraSnapshotPaths("snppath1"))),
+            IgniteCheckedException.class,
+            "DataStorageConfiguration error. Size of extraSnapshotPaths must 
be equal to extraStoragePath"
+        );
+
+        assertThrows(
+            log,
+            () -> startGrid(new 
IgniteConfiguration().setDataStorageConfiguration(new DataStorageConfiguration()
+                .setStoragePath(myPath.getAbsolutePath())
+                .setExtraSnapshotPaths("snppath1", "snppath2"))),
+            IgniteCheckedException.class,
+            "DataStorageConfiguration error. Size of extraSnapshotPaths must 
be equal to extraStoragePath"
+        );
+
+        assertThrows(
+            log,
+            () -> startGrid(new 
IgniteConfiguration().setDataStorageConfiguration(new DataStorageConfiguration()
+                .setStoragePath(myPath.getAbsolutePath())
+                .setExtraStoragePaths(myPath2.getAbsolutePath(), 
myPath3.getAbsolutePath())
+                .setExtraSnapshotPaths("snppath1", "snppath1"))),
+            IgniteCheckedException.class,
+            "DataStorageConfiguration contains duplicates [extraSnapshotPaths="
+        );
+    }
+
     /** */
     @Test
     public void testCacheUnknownStoragePathThrows() throws Exception {
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 b670774e5b1..739f23d3c2a 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
@@ -39,6 +39,19 @@ import static 
org.apache.ignite.testframework.GridTestUtils.assertThrowsWithCaus
  * Test snapshot can be created when {@link 
DataStorageConfiguration#setStoragePath(String)} used.
  */
 public class SnapshotCreationNonDefaultStoragePathTest extends 
AbstractDataRegionRelativeStoragePathTest {
+    /** */
+    protected String[] extraSnpPaths = new String[] {
+        STORAGE_PATH_2,
+        IDX_PATH
+    };
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        U.delete(new File(U.defaultWorkDirectory()));
+    }
+
     /** {@inheritDoc} */
     @Override protected DataStorageConfiguration dataStorageConfiguration() {
         return new DataStorageConfiguration()
@@ -121,12 +134,14 @@ public class SnapshotCreationNonDefaultStoragePathTest 
extends AbstractDataRegio
 
         srv.context().cache().context().snapshotMgr().createSnapshot("mysnp2", 
fullPathSnp.getAbsolutePath(), false, false).get();
 
-        String grid1ConsId = consId(grid(1).configuration());
+        NodeFileTree ft = grid(1).context().pdsFolderResolver().fileTree();
 
         stopGrid(1);
 
         resetBaselineTopology();
 
+        ft.allStorages().forEach(U::delete);
+
         BiConsumer<String, String> check = (name, path) -> {
             for (CacheConfiguration<?, ?> ccfg : ccfgs())
                 grid(0).destroyCache(ccfg.getName());
@@ -155,15 +170,16 @@ public class SnapshotCreationNonDefaultStoragePathTest 
extends AbstractDataRegio
                 "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")
-            };
+            Path[] copyStoppedNodeData = new Path[extraSnpPaths.length + 1];
+
+            copyStoppedNodeData[0] = Path.of(DFLT_SNAPSHOT_DIRECTORY, "mysnp");
+
+            for (int i = 0; i < extraSnpPaths.length; i++)
+                copyStoppedNodeData[i + 1] = Path.of(extraSnpPaths[i], 
DFLT_SNAPSHOT_DIRECTORY, "mysnp");
 
             for (Path copyPath : copyStoppedNodeData) {
                 FileUtils.copyDirectory(
-                    Path.of(U.defaultWorkDirectory(), grid1ConsId, 
copyPath.toString()).toFile(),
+                    Path.of(U.defaultWorkDirectory(), ft.folderName(), 
copyPath.toString()).toFile(),
                     Path.of(U.defaultWorkDirectory(), 
consId(grid(0).configuration()), copyPath.toString()).toFile()
                 );
             }
diff --git 
a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/SnapshotExtraStoragesTest.java
 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/SnapshotExtraStoragesTest.java
new file mode 100644
index 00000000000..4250e5dd7f4
--- /dev/null
+++ 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/SnapshotExtraStoragesTest.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.cache.persistence.filename;
+
+import org.apache.ignite.configuration.DataStorageConfiguration;
+
+/**
+ * Test snapshot can be created when {@link 
DataStorageConfiguration#setStoragePath(String)} used.
+ */
+public class SnapshotExtraStoragesTest extends 
SnapshotCreationNonDefaultStoragePathTest {
+    /** {@inheritDoc} */
+    @Override protected DataStorageConfiguration dataStorageConfiguration() {
+        extraSnpPaths = new String[] {
+            "snp_path",
+            "snp_path2"
+        };
+
+        return 
super.dataStorageConfiguration().setExtraSnapshotPaths(storagePath("snp_path"), 
storagePath("snp_path2"));
+    }
+}
diff --git 
a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/SnapshotFileTreeSelfTest.java
 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/SnapshotFileTreeSelfTest.java
new file mode 100644
index 00000000000..99bb32fddcf
--- /dev/null
+++ 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/filename/SnapshotFileTreeSelfTest.java
@@ -0,0 +1,687 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.cache.persistence.filename;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.configuration.DataRegionConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/** */
+public class SnapshotFileTreeSelfTest {
+    /** */
+    private static final String TEST_CONSISTENT_ID = "node1";
+
+    /** */
+    public String workDir;
+
+    /** */
+    public boolean snpAbsPath;
+
+    /** */
+    @Before
+    public void setUp() throws IgniteCheckedException {
+        workDir = U.defaultWorkDirectory();
+        snpAbsPath = false;
+    }
+
+    /** */
+    @Test
+    public void testNamedSnapshotPaths() {
+        IgniteConfiguration cfg = config(
+            "/path/to/storage1",
+            "/path/to/storage2",
+            "/path/to/storage3"
+        );
+
+        NodeFileTree ft = fileTree(cfg);
+
+        checkSnapshotTempDirs(
+            ft,
+            "/path/to/storage1/node1/",
+            "/path/to/storage2/db/node1/",
+            "/path/to/storage3/db/node1/"
+        );
+
+        checkSnapshotDirs(
+            cfg,
+            ft,
+            workDir + "/snapshots/snap/db/node1",
+            "/path/to/storage2/snapshots/snap/db/node1",
+            "/path/to/storage3/snapshots/snap/db/node1"
+        );
+
+        checkSnapshotDirs(
+            "node2",
+            cfg,
+            ft,
+            workDir + "/snapshots/snap/db/node2",
+            "/path/to/storage2/snapshots/snap/db/node2",
+            "/path/to/storage3/snapshots/snap/db/node2"
+        );
+    }
+
+    /** */
+    @Test
+    public void testNamedSnapshotPaths_extraSnapshotPaths() {
+        IgniteConfiguration cfg = config(
+            "/path/to/storage1",
+            new String[]{
+                "/path/to/storage2",
+                "/path/to/storage3"
+            },
+            new String[]{
+                "/extra_snp2",
+                "/extra_snp3"
+            }
+        );
+
+        checkSnapshotDirs(
+            cfg,
+            fileTree(cfg),
+            workDir + "/snapshots/snap/db/node1",
+            "/extra_snp2/snapshots/snap/db/node1",
+            "/extra_snp3/snapshots/snap/db/node1"
+        );
+
+        checkSnapshotDirs(
+            "node2",
+            cfg,
+            fileTree(cfg),
+            workDir + "/snapshots/snap/db/node2",
+            "/extra_snp2/snapshots/snap/db/node2",
+            "/extra_snp3/snapshots/snap/db/node2"
+        );
+    }
+
+    /** */
+    @Test
+    public void testNamedSnapshotPaths2() {
+        IgniteConfiguration cfg = config(
+            "storage1",
+            "storage2",
+            "storage3"
+        );
+
+        NodeFileTree ft = fileTree(cfg);
+
+        checkSnapshotTempDirs(
+            ft,
+            workDir + "/storage1/node1",
+            workDir + "/storage2/db/node1",
+            workDir + "/storage3/db/node1"
+        );
+
+        checkSnapshotDirs(
+            cfg,
+            ft,
+            workDir + "/snapshots/snap/db/node1",
+            workDir + "/storage2/snapshots/snap/db/node1",
+            workDir + "/storage3/snapshots/snap/db/node1"
+        );
+
+        checkSnapshotDirs(
+            "node2",
+            cfg,
+            ft,
+            workDir + "/snapshots/snap/db/node2",
+            workDir + "/storage2/snapshots/snap/db/node2",
+            workDir + "/storage3/snapshots/snap/db/node2"
+        );
+    }
+
+    /** */
+    @Test
+    public void testNamedSnapshotPaths2_extraSnapshotPaths() {
+        IgniteConfiguration cfg = config(
+            "storage1",
+            new String[]{
+                "storage2",
+                "storage3"
+            },
+            new String[]{
+                "extra_snp2",
+                "extra_snp3"
+            }
+        );
+
+        checkSnapshotDirs(
+            cfg,
+            fileTree(cfg),
+            workDir + "/snapshots/snap/db/node1",
+            workDir + "/extra_snp2/snapshots/snap/db/node1",
+            workDir + "/extra_snp3/snapshots/snap/db/node1"
+        );
+
+        checkSnapshotDirs(
+            "node2",
+            cfg,
+            fileTree(cfg),
+            workDir + "/snapshots/snap/db/node2",
+            workDir + "/extra_snp2/snapshots/snap/db/node2",
+            workDir + "/extra_snp3/snapshots/snap/db/node2"
+        );
+    }
+
+    /** */
+    @Test
+    public void testNamedSnapshotPaths3() {
+        IgniteConfiguration cfg = config(
+            "/path/to/storage1",
+            "storage2",
+            "storage3"
+        );
+
+        NodeFileTree ft = fileTree(cfg);
+
+        checkSnapshotTempDirs(
+            ft,
+            "/path/to/storage1/node1/",
+            workDir + "/storage2/db/node1",
+            workDir + "/storage3/db/node1"
+        );
+
+        checkSnapshotDirs(
+            cfg,
+            ft,
+            workDir + "/snapshots/snap/db/node1",
+            workDir + "/storage2/snapshots/snap/db/node1",
+            workDir + "/storage3/snapshots/snap/db/node1"
+        );
+
+        checkSnapshotDirs(
+            "node2",
+            cfg,
+            ft,
+            workDir + "/snapshots/snap/db/node2",
+            workDir + "/storage2/snapshots/snap/db/node2",
+            workDir + "/storage3/snapshots/snap/db/node2"
+        );
+    }
+
+    /** */
+    @Test
+    public void testNamedSnapshotPaths3_extraSnapshotPaths() {
+        IgniteConfiguration cfg = config(
+            "/path/to/storage1",
+            new String[]{
+                "storage2",
+                "storage3"
+            },
+            new String[]{
+                "extra_snp2",
+                "extra_snp3"
+            }
+        );
+
+        checkSnapshotDirs(
+            cfg,
+            fileTree(cfg),
+            workDir + "/snapshots/snap/db/node1",
+            workDir + "/extra_snp2/snapshots/snap/db/node1",
+            workDir + "/extra_snp3/snapshots/snap/db/node1"
+        );
+
+        checkSnapshotDirs(
+            "node2",
+            cfg,
+            fileTree(cfg),
+            workDir + "/snapshots/snap/db/node2",
+            workDir + "/extra_snp2/snapshots/snap/db/node2",
+            workDir + "/extra_snp3/snapshots/snap/db/node2"
+        );
+    }
+
+    /** */
+    @Test
+    public void testNamedSnapshotPaths4() {
+        IgniteConfiguration cfg = config(
+            "storage1",
+            "/path/to/storage2",
+            "/path/to/storage3"
+        );
+
+        NodeFileTree ft = fileTree(cfg);
+
+        checkSnapshotTempDirs(
+            ft,
+            workDir + "/storage1/node1/",
+            "/path/to/storage2/db/node1/",
+            "/path/to/storage3/db/node1/"
+        );
+
+        checkSnapshotDirs(
+            cfg,
+            ft,
+            workDir + "/snapshots/snap/db/node1",
+            "/path/to/storage2/snapshots/snap/db/node1",
+            "/path/to/storage3/snapshots/snap/db/node1"
+        );
+
+        checkSnapshotDirs(
+            "node2",
+            cfg,
+            ft,
+            workDir + "/snapshots/snap/db/node2",
+            "/path/to/storage2/snapshots/snap/db/node2",
+            "/path/to/storage3/snapshots/snap/db/node2"
+        );
+    }
+
+    /** */
+    @Test
+    public void testNamedSnapshotPaths4_extraSnapshotPaths() {
+        IgniteConfiguration cfg = config(
+            "storage1",
+            new String[]{
+                "/path/to/storage2",
+                "/path/to/storage3"
+            },
+            new String[]{
+                "/path/to/extra_snp2",
+                "/path/to/extra_snp3"
+            }
+        );
+
+        checkSnapshotDirs(
+            cfg,
+            fileTree(cfg),
+            workDir + "/snapshots/snap/db/node1",
+            "/path/to/extra_snp2/snapshots/snap/db/node1",
+            "/path/to/extra_snp3/snapshots/snap/db/node1"
+        );
+
+        checkSnapshotDirs(
+            "node2",
+            cfg,
+            fileTree(cfg),
+            workDir + "/snapshots/snap/db/node2",
+            "/path/to/extra_snp2/snapshots/snap/db/node2",
+            "/path/to/extra_snp3/snapshots/snap/db/node2"
+        );
+    }
+
+    /** */
+    @Test
+    public void testNamedSnapshotPaths5() {
+        IgniteConfiguration cfg = config(
+            null,
+            "/path/to/storage2",
+            "/path/to/storage3"
+        );
+
+        NodeFileTree ft = fileTree(cfg);
+
+        checkSnapshotTempDirs(
+            ft,
+            workDir + "/db/node1/",
+            "/path/to/storage2/db/node1/",
+            "/path/to/storage3/db/node1/"
+        );
+
+        checkSnapshotDirs(
+            cfg,
+            ft,
+            workDir + "/snapshots/snap/db/node1",
+            "/path/to/storage2/snapshots/snap/db/node1",
+            "/path/to/storage3/snapshots/snap/db/node1"
+        );
+
+        checkSnapshotDirs(
+            "node2",
+            cfg,
+            ft,
+            workDir + "/snapshots/snap/db/node2",
+            "/path/to/storage2/snapshots/snap/db/node2",
+            "/path/to/storage3/snapshots/snap/db/node2"
+        );
+    }
+
+    /** */
+    @Test
+    public void testNamedSnapshotPaths5_extraSnapshotPaths() {
+        IgniteConfiguration cfg = config(
+            null,
+            new String[]{
+                "/path/to/storage2",
+                "/path/to/storage3"
+            },
+            new String[]{
+                "/path/to/extra_snp2",
+                "/path/to/extra_snp3"
+            }
+        );
+
+        checkSnapshotDirs(
+            cfg,
+            fileTree(cfg),
+            workDir + "/snapshots/snap/db/node1",
+            "/path/to/extra_snp2/snapshots/snap/db/node1",
+            "/path/to/extra_snp3/snapshots/snap/db/node1"
+        );
+
+        checkSnapshotDirs(
+            "node2",
+            cfg,
+            fileTree(cfg),
+            workDir + "/snapshots/snap/db/node2",
+            "/path/to/extra_snp2/snapshots/snap/db/node2",
+            "/path/to/extra_snp3/snapshots/snap/db/node2"
+        );
+    }
+
+    /** */
+    @Test
+    public void testNamedSnapshotPaths6() {
+        IgniteConfiguration cfg = config(
+            null,
+            "storage2",
+            "storage3"
+        );
+
+        NodeFileTree ft = fileTree(cfg);
+
+        checkSnapshotTempDirs(
+            ft,
+            workDir + "/db/node1/",
+            workDir + "/storage2/db/node1/",
+            workDir + "/storage3/db/node1/"
+        );
+
+        checkSnapshotDirs(
+            cfg,
+            ft,
+            workDir + "/snapshots/snap/db/node1",
+            workDir + "/storage2/snapshots/snap/db/node1",
+            workDir + "/storage3/snapshots/snap/db/node1"
+        );
+
+        checkSnapshotDirs(
+            "node2",
+            cfg,
+            ft,
+            workDir + "/snapshots/snap/db/node2",
+            workDir + "/storage2/snapshots/snap/db/node2",
+            workDir + "/storage3/snapshots/snap/db/node2"
+        );
+    }
+
+    /** */
+    @Test
+    public void testNamedSnapshotPaths6_extraSnapshotPaths() {
+        IgniteConfiguration cfg = config(
+            null,
+            new String[]{
+                "storage2",
+                "storage3"
+            },
+            new String[]{
+                "extra_snp2",
+                "extra_snp3"
+            }
+        );
+
+        checkSnapshotDirs(
+            cfg,
+            fileTree(cfg),
+            workDir + "/snapshots/snap/db/node1",
+            workDir + "/extra_snp2/snapshots/snap/db/node1",
+            workDir + "/extra_snp3/snapshots/snap/db/node1"
+        );
+
+        checkSnapshotDirs(
+            "node2",
+            cfg,
+            fileTree(cfg),
+            workDir + "/snapshots/snap/db/node2",
+            workDir + "/extra_snp2/snapshots/snap/db/node2",
+            workDir + "/extra_snp3/snapshots/snap/db/node2"
+        );
+    }
+
+    /** */
+    @Test
+    public void testNamedSnapshotPaths7() {
+        IgniteConfiguration cfg = config(
+            "/path/to/storage1",
+            "storage2",
+            "/path/to/storage3"
+        );
+
+        NodeFileTree ft = fileTree(cfg);
+
+        checkSnapshotTempDirs(
+            ft,
+            "/path/to/storage1/node1/",
+            workDir + "/storage2/db/node1",
+            "/path/to/storage3/db/node1"
+        );
+
+        checkSnapshotDirs(
+            cfg,
+            ft,
+            workDir + "/snapshots/snap/db/node1",
+            workDir + "/storage2/snapshots/snap/db/node1",
+            "/path/to/storage3/snapshots/snap/db/node1"
+        );
+
+        checkSnapshotDirs(
+            "node2",
+            cfg,
+            ft,
+            workDir + "/snapshots/snap/db/node2",
+            workDir + "/storage2/snapshots/snap/db/node2",
+            "/path/to/storage3/snapshots/snap/db/node2"
+        );
+    }
+
+    /** */
+    @Test
+    public void testNamedSnapshotPaths7_extraSnapshotPaths() {
+        IgniteConfiguration cfg = config(
+            "/path/to/storage1",
+            new String[]{
+                "storage2",
+                "/path/to/storage3"
+            },
+            new String[]{
+                "extra_snp2",
+                "/path/to/extra_snp3"
+            }
+        );
+
+        checkSnapshotDirs(
+            cfg,
+            fileTree(cfg),
+            workDir + "/snapshots/snap/db/node1",
+            workDir + "/extra_snp2/snapshots/snap/db/node1",
+            "/path/to/extra_snp3/snapshots/snap/db/node1"
+        );
+
+        checkSnapshotDirs(
+            "node2",
+            cfg,
+            fileTree(cfg),
+            workDir + "/snapshots/snap/db/node2",
+            workDir + "/extra_snp2/snapshots/snap/db/node2",
+            "/path/to/extra_snp3/snapshots/snap/db/node2"
+        );
+    }
+
+    /** */
+    @Test
+    public void testAbsoluteDeafultSnapshotPath() {
+        IgniteConfiguration cfg = config(
+            "/path/to/storage1",
+            "/path/to/storage2",
+            "/path/to/storage3"
+        ).setSnapshotPath("/path/to/snapshots");
+
+        NodeFileTree ft = fileTree(cfg);
+
+        checkSnapshotTempDirs(
+            ft,
+            "/path/to/storage1/node1/",
+            "/path/to/storage2/db/node1/",
+            "/path/to/storage3/db/node1/"
+        );
+
+        checkSnapshotDirs(
+            cfg,
+            ft,
+            "/path/to/snapshots/snap/db/node1"
+        );
+
+        checkSnapshotDirs(
+            "node2",
+            cfg,
+            ft,
+            "/path/to/snapshots/snap/db/node2"
+        );
+    }
+
+    /** */
+    @Test
+    public void testAbsoluteDeafultSnapshotPath_extraSnapshotPaths() {
+        IgniteConfiguration cfg = config(
+            "/path/to/storage1",
+            new String[]{
+                "/path/to/storage2",
+                "/path/to/storage3"
+            },
+            new String[]{
+                "/extra_snp2",
+                "/extra_snp3"
+            }
+        ).setSnapshotPath("/extra_snp");
+
+        NodeFileTree ft = fileTree(cfg);
+
+        checkSnapshotDirs(
+            cfg,
+            ft,
+            "/extra_snp/snap/db/node1",
+            "/extra_snp2/snap/db/node1",
+            "/extra_snp3/snap/db/node1"
+        );
+
+        checkSnapshotDirs(
+            "node2",
+            cfg,
+            ft,
+            "/extra_snp/snap/db/node2",
+            "/extra_snp2/snap/db/node2",
+            "/extra_snp3/snap/db/node2"
+        );
+    }
+
+    /** */
+    private NodeFileTree fileTree(IgniteConfiguration cfg) {
+        NodeFileTree ft = new NodeFileTree(cfg, TEST_CONSISTENT_ID);
+
+        if (new File(cfg.getSnapshotPath()).isAbsolute())
+            assertEquals(new File(cfg.getSnapshotPath()), ft.snapshotsRoot());
+        else
+            assertEquals(new File(workDir, cfg.getSnapshotPath()), 
ft.snapshotsRoot());
+
+        return ft;
+    }
+
+    /** */
+    private IgniteConfiguration config(String storagePath, String... 
extraPaths) {
+        return new IgniteConfiguration()
+            .setConsistentId(TEST_CONSISTENT_ID)
+            .setDataStorageConfiguration(new DataStorageConfiguration()
+                .setStoragePath(storagePath)
+                .setExtraStoragePaths(extraPaths)
+                .setDefaultDataRegionConfiguration(new 
DataRegionConfiguration().setPersistenceEnabled(true)));
+    }
+
+    /** */
+    private IgniteConfiguration config(String storagePath, String[] 
extraPaths, String[] extraSnpPaths) {
+        return new IgniteConfiguration()
+            .setConsistentId(TEST_CONSISTENT_ID)
+            .setDataStorageConfiguration(new DataStorageConfiguration()
+                .setStoragePath(storagePath)
+                .setExtraStoragePaths(extraPaths)
+                .setExtraSnapshotPaths(extraSnpPaths)
+                .setDefaultDataRegionConfiguration(new 
DataRegionConfiguration().setPersistenceEnabled(true)));
+    }
+
+    /** */
+    private void checkSnapshotTempDirs(NodeFileTree ft, String... 
expStoragesStr) {
+        List<File> expStorages = 
Arrays.stream(expStoragesStr).map(File::new).collect(Collectors.toList());
+        Set<File> ftStorages = ft.allStorages().collect(Collectors.toSet());
+
+        assertEquals(expStorages.size(), ftStorages.size());
+        assertTrue(
+            ftStorages + " must contains all " + expStorages,
+            ftStorages.containsAll(expStorages)
+        );
+
+        List<File> snpTmps = ft.snapshotsTempRoots();
+
+        assertEquals(expStorages.size(), snpTmps.size());
+        assertTrue(snpTmps.containsAll(
+            expStorages.stream().map(s -> new File(s, 
NodeFileTree.SNAPSHOT_TMP_DIR)).collect(Collectors.toList())));
+    }
+
+    /** */
+    private void checkSnapshotDirs(IgniteConfiguration cfg, NodeFileTree ft, 
String... expSnpStoragesStr) {
+        checkSnapshotDirs(ft.folderName(), cfg, ft, expSnpStoragesStr);
+    }
+
+    /** */
+    private void checkSnapshotDirs(String folderName, IgniteConfiguration cfg, 
NodeFileTree ft, String... expSnpStoragesStr) {
+        SnapshotFileTree sft = new SnapshotFileTree(cfg, ft, "snap", 
snpAbsPath ? "/snpdir" : null, folderName, TEST_CONSISTENT_ID);
+
+        Set<File> snpStorages = sft.allStorages().collect(Collectors.toSet());
+        List<File> expSnpStorages = 
Arrays.stream(expSnpStoragesStr).map(File::new).collect(Collectors.toList());
+
+        assertEquals(expSnpStorages.size(), snpStorages.size());
+        assertTrue(expSnpStorages + " must contains all " + snpStorages, 
expSnpStorages.containsAll(snpStorages));
+
+        // With absolute path any configuration must return one snapshot dir.
+        if (!snpAbsPath) {
+            snpAbsPath = true;
+
+            try {
+                checkSnapshotDirs(
+                    folderName,
+                    cfg,
+                    ft,
+                    "/snpdir/snap/db/" + folderName
+                );
+            }
+            finally {
+                snpAbsPath = false;
+            }
+        }
+    }
+}
diff --git 
a/modules/core/src/test/java/org/apache/ignite/testsuites/IgnitePdsTestSuite8.java
 
b/modules/core/src/test/java/org/apache/ignite/testsuites/IgnitePdsTestSuite8.java
index 914156bb9af..a15208e8ac1 100644
--- 
a/modules/core/src/test/java/org/apache/ignite/testsuites/IgnitePdsTestSuite8.java
+++ 
b/modules/core/src/test/java/org/apache/ignite/testsuites/IgnitePdsTestSuite8.java
@@ -51,6 +51,8 @@ import 
org.apache.ignite.internal.processors.cache.persistence.diagnostic.pagelo
 import 
org.apache.ignite.internal.processors.cache.persistence.filename.CacheConfigStoragePathTest;
 import 
org.apache.ignite.internal.processors.cache.persistence.filename.CustomCacheStorageConfigurationSelfTest;
 import 
org.apache.ignite.internal.processors.cache.persistence.filename.SnapshotCreationNonDefaultStoragePathTest;
+import 
org.apache.ignite.internal.processors.cache.persistence.filename.SnapshotExtraStoragesTest;
+import 
org.apache.ignite.internal.processors.cache.persistence.filename.SnapshotFileTreeSelfTest;
 import 
org.apache.ignite.internal.processors.cache.persistence.filename.SnapshotRestoreIndexPathTest;
 import 
org.apache.ignite.internal.processors.cache.warmup.LoadAllWarmUpStrategySelfTest;
 import org.apache.ignite.internal.processors.cache.warmup.WarmUpSelfTest;
@@ -115,8 +117,10 @@ public class IgnitePdsTestSuite8 {
         GridTestUtils.addTestIfNeeded(suite, 
IgnitePdsCheckpointRecoveryTest.class, ignoredTests);
         GridTestUtils.addTestIfNeeded(suite, CacheConfigStoragePathTest.class, 
ignoredTests);
         GridTestUtils.addTestIfNeeded(suite, 
SnapshotCreationNonDefaultStoragePathTest.class, ignoredTests);
+        GridTestUtils.addTestIfNeeded(suite, SnapshotExtraStoragesTest.class, 
ignoredTests);
         GridTestUtils.addTestIfNeeded(suite, 
SnapshotRestoreIndexPathTest.class, ignoredTests);
         GridTestUtils.addTestIfNeeded(suite, 
CustomCacheStorageConfigurationSelfTest.class, ignoredTests);
+        GridTestUtils.addTestIfNeeded(suite, SnapshotFileTreeSelfTest.class, 
ignoredTests);
 
         return suite;
     }


Reply via email to