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 0d62fd96ad8 IGNITE-24535 Compatibility test for Ignite snapshot
(#11939)
0d62fd96ad8 is described below
commit 0d62fd96ad8e9608aef4283ce9f84ea3bdb1137e
Author: Vladislav Novikov <[email protected]>
AuthorDate: Mon Mar 31 23:28:13 2025 +0300
IGNITE-24535 Compatibility test for Ignite snapshot (#11939)
---
.../persistence/SnapshotCompatibilityTest.java | 453 +++++++++++++++++++++
.../IgniteCompatibilityBasicTestSuite.java | 2 +
.../cache/persistence/filename/NodeFileTree.java | 10 +-
.../cache/persistence/snapshot/dump/Dump.java | 5 +-
4 files changed, 468 insertions(+), 2 deletions(-)
diff --git
a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/persistence/SnapshotCompatibilityTest.java
b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/persistence/SnapshotCompatibilityTest.java
new file mode 100644
index 00000000000..18cf026b31c
--- /dev/null
+++
b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/persistence/SnapshotCompatibilityTest.java
@@ -0,0 +1,453 @@
+/*
+ * 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.compatibility.persistence;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import javax.annotation.Nullable;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.binary.BinaryType;
+import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
+import org.apache.ignite.cdc.TypeMapping;
+import org.apache.ignite.cluster.ClusterState;
+import org.apache.ignite.compatibility.IgniteReleasedVersion;
+import
org.apache.ignite.compatibility.testframework.junits.IgniteCompatibilityAbstractTest;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.dump.DumpConsumer;
+import org.apache.ignite.dump.DumpEntry;
+import org.apache.ignite.dump.DumpReader;
+import org.apache.ignite.dump.DumpReaderConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.processors.cache.StoredCacheData;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.lang.IgniteInClosure;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ *
+ */
+@RunWith(Parameterized.class)
+public class SnapshotCompatibilityTest extends IgniteCompatibilityAbstractTest
{
+ /** */
+ private static final String OLD_IGNITE_VERSION =
Arrays.stream(IgniteReleasedVersion.values())
+ .max(Comparator.comparing(IgniteReleasedVersion::version))
+ .map(IgniteReleasedVersion::toString)
+ .orElseThrow(() -> new IllegalStateException("Enum is empty"));
+
+ /** */
+ private static final String SNAPSHOT_NAME = "test_snapshot";
+
+ /** */
+ private static final String CACHE_DUMP_NAME = "test_cache_dump";
+
+ /** */
+ private static final int BASE_CACHE_SIZE = 100;
+
+ /** */
+ private static final int ENTRIES_CNT_FOR_INCREMENT = 100;
+
+ /** */
+ private static final String CUSTOM_SNP_RELATIVE_PATH = "ex_snapshots";
+
+ /** */
+ private static final String CONSISTENT_ID = UUID.randomUUID().toString();
+
+ /** */
+ @Parameterized.Parameter
+ public boolean incSnp;
+
+ /** */
+ @Parameterized.Parameter(1)
+ @Nullable public String consId;
+
+ /** */
+ @Parameterized.Parameter(2)
+ public int oldNodesCnt;
+
+ /** */
+ @Parameterized.Parameter(3)
+ public boolean cacheDump;
+
+ /** */
+ @Parameterized.Parameter(4)
+ public boolean customSnpPath;
+
+ /** */
+ @Parameterized.Parameter(5)
+ public boolean testCacheGrp;
+
+ /** */
+ private CacheGroupInfo cacheGrpInfo;
+
+ /**
+ * The test is parameterized by whether an incremental snapshot is taken
and by consistentId.
+ * Restore incremental snapshot if consistentId is null is fixed in
2.17.0, see here https://issues.apache.org/jira/browse/IGNITE-23222.
+ * Also restoring cache dump and any kind of snapshot is pointless.
+ */
+ @Parameters(name = "incrementalSnp={0}, consistentID={1}, oldNodesCnt={2},
cacheDump={3}, customSnpPath={4}, testCacheGrp={5}")
+ public static Collection<Object[]> data() {
+ List<Object[]> data = new ArrayList<>();
+
+ for (boolean incSnp : Arrays.asList(true, false))
+ for (String consId : Arrays.asList(CONSISTENT_ID, null))
+ for (int oldNodesCnt : Arrays.asList(1, 3))
+ for (boolean cacheDump : Arrays.asList(true, false))
+ for (boolean customSnpPath : Arrays.asList(true,
false))
+ for (boolean testCacheGrp : Arrays.asList(true,
false))
+ if ((!incSnp || !cacheDump) && (!incSnp ||
consId != null))
+ data.add(new Object[]{incSnp, consId,
oldNodesCnt, cacheDump, customSnpPath, testCacheGrp});
+
+ return data;
+ }
+
+ /** */
+ @Before
+ public void setUp() {
+ cacheGrpInfo = new CacheGroupInfo("test-cache", testCacheGrp ? 2 : 1);
+ }
+
+ /** */
+ @Test
+ public void testSnapshotRestore() throws Exception {
+ try {
+ startGrid(
+ oldNodesCnt,
+ OLD_IGNITE_VERSION,
+ new ConfigurationClosure(incSnp, consId, customSnpPath, true,
cacheGrpInfo),
+ new CreateSnapshotClosure(incSnp, cacheDump, cacheGrpInfo)
+ );
+
+ stopAllGrids();
+
+ cleanPersistenceDir(true);
+
+ IgniteEx node = startGrid(currentIgniteConfiguration(incSnp,
consId, customSnpPath));
+
+ node.cluster().state(ClusterState.ACTIVE);
+
+ if (cacheDump)
+ checkCacheDump(node);
+ else if (incSnp)
+ checkIncrementalSnapshot(node);
+ else
+ checkSnapshot(node);
+ }
+ finally {
+ stopAllGrids();
+
+ cleanPersistenceDir();
+ }
+ }
+
+ /** */
+ private void checkSnapshot(IgniteEx node) {
+ node.snapshot().restoreSnapshot(SNAPSHOT_NAME,
Collections.singleton(cacheGrpInfo.name())).get();
+
+ cacheGrpInfo.checkCaches(node, BASE_CACHE_SIZE);
+ }
+
+ /** */
+ private void checkIncrementalSnapshot(IgniteEx node) {
+ node.snapshot().restoreSnapshot(SNAPSHOT_NAME,
Collections.singleton(cacheGrpInfo.name()), 1).get();
+
+ cacheGrpInfo.checkCaches(node, BASE_CACHE_SIZE +
ENTRIES_CNT_FOR_INCREMENT);
+ }
+
+ /** */
+ private void checkCacheDump(IgniteEx node) throws IgniteCheckedException {
+ Map<String, Integer> foundCacheSizes = new ConcurrentHashMap<>();
+
+ Set<String> foundCacheNames = ConcurrentHashMap.newKeySet();
+
+ DumpConsumer consumer = new DumpConsumer() {
+ @Override public void start() {
+ // No-op.
+ }
+
+ @Override public void onMappings(Iterator<TypeMapping> mappings) {
+ // No-op.
+ }
+
+ @Override public void onTypes(Iterator<BinaryType> types) {
+ // No-op.
+ }
+
+ @Override public void onCacheConfigs(Iterator<StoredCacheData>
caches) {
+ assertNotNull(cacheGrpInfo);
+
+ caches.forEachRemaining(cache -> {
+ CacheConfiguration<?, ?> ccfg = cache.config();
+
+ assertNotNull(ccfg);
+
+ assertEquals(cacheGrpInfo.name(), ccfg.getGroupName());
+
+ foundCacheNames.add(ccfg.getName());
+ });
+ }
+
+ @Override public void onPartition(int grp, int part,
Iterator<DumpEntry> data) {
+ assertNotNull(cacheGrpInfo);
+
+ data.forEachRemaining(de -> {
+ assertNotNull(de);
+
+ Integer key = (Integer)de.key();
+ String val = (String)de.value();
+
+ for (String cacheName : cacheGrpInfo.cacheNamesList()) {
+ if (val.startsWith(cacheName)) {
+ assertEquals(calcValue(cacheName, key), val);
+
+ foundCacheSizes.put(cacheName,
foundCacheSizes.getOrDefault(cacheName, 0) + 1);
+
+ break;
+ }
+ }
+ });
+ }
+
+ @Override public void stop() {
+ // No-op.
+ }
+ };
+
+ new DumpReader(new DumpReaderConfiguration(
+ CACHE_DUMP_NAME,
+ customSnpPath ? customSnapshotPath(CUSTOM_SNP_RELATIVE_PATH,
false) : null,
+ node.configuration(),
+ consumer
+ ), log).run();
+
+ cacheGrpInfo.cacheNamesList().forEach(
+ cacheName -> assertEquals(BASE_CACHE_SIZE,
(int)foundCacheSizes.get(cacheName))
+ );
+
+ assertTrue(cacheGrpInfo.cacheNamesList().containsAll(foundCacheNames));
+ assertEquals(cacheGrpInfo.cacheNamesList().size(),
foundCacheNames.size());
+ }
+
+ /** */
+ private @NotNull IgniteConfiguration currentIgniteConfiguration(
+ boolean incSnp,
+ String consId,
+ boolean customSnpPath
+ ) throws Exception {
+ IgniteConfiguration cfg =
getConfiguration(getTestIgniteInstanceName(0));
+
+ // We configure current Ignite version in the same way as the old one.
+ new ConfigurationClosure(incSnp, consId, customSnpPath, false,
cacheGrpInfo).apply(cfg);
+
+ return cfg;
+ }
+
+ /** */
+ private static String customSnapshotPath(String relativePath, boolean
delIfExist) throws IgniteCheckedException {
+ return U.resolveWorkDirectory(U.defaultWorkDirectory(), relativePath,
delIfExist).getAbsolutePath();
+ }
+
+ /** */
+ private static String calcValue(String cacheName, int key) {
+ return cacheName + "-organization-" + key;
+ }
+
+ /**
+ * Configuration closure both for old and current Ignite version.
+ */
+ private static class ConfigurationClosure implements
IgniteInClosure<IgniteConfiguration> {
+ /** */
+ private final String consId;
+
+ /** */
+ private final boolean incSnp;
+
+ /** */
+ private final boolean customSnpPath;
+
+ /** */
+ private final boolean delIfExist;
+
+ /** */
+ private final CacheGroupInfo cacheGrpInfo;
+
+ /** */
+ public ConfigurationClosure(
+ boolean incSnp,
+ String consId,
+ boolean customSnpPath,
+ boolean delIfExist,
+ CacheGroupInfo cacheGrpInfo
+ ) {
+ this.consId = consId;
+ this.incSnp = incSnp;
+ this.customSnpPath = customSnpPath;
+ this.delIfExist = delIfExist;
+ this.cacheGrpInfo = cacheGrpInfo;
+ }
+
+ /** {@inheritDoc} */
+ @Override public void apply(IgniteConfiguration cfg) {
+ DataStorageConfiguration storageCfg = new
DataStorageConfiguration();
+
+
storageCfg.getDefaultDataRegionConfiguration().setPersistenceEnabled(true);
+
+ cfg.setDataStorageConfiguration(storageCfg);
+
+ cfg.setConsistentId(consId);
+
+ storageCfg.setWalCompactionEnabled(incSnp);
+
+ if (delIfExist) {
+ cfg.setCacheConfiguration(
+ cacheGrpInfo.cacheNamesList().stream()
+ .map(cacheName -> new CacheConfiguration<Integer,
String>(cacheName)
+ .setGroupName(cacheGrpInfo.name())
+ .setAffinity(new RendezvousAffinityFunction(false,
10))
+ )
+ .toArray(CacheConfiguration[]::new)
+ );
+ }
+
+ if (customSnpPath) {
+ try {
+
cfg.setSnapshotPath(customSnapshotPath(CUSTOM_SNP_RELATIVE_PATH, delIfExist));
+ }
+ catch (IgniteCheckedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Snapshot creating closure for old Ignite version.
+ */
+ private static class CreateSnapshotClosure implements
IgniteInClosure<Ignite> {
+ /** */
+ private final boolean incSnp;
+
+ /** */
+ private final boolean cacheDump;
+
+ /** */
+ private final CacheGroupInfo cacheGrpInfo;
+
+ /** */
+ public CreateSnapshotClosure(boolean incSnp, boolean cacheDump,
CacheGroupInfo cacheGrpInfo) {
+ this.incSnp = incSnp;
+ this.cacheDump = cacheDump;
+ this.cacheGrpInfo = cacheGrpInfo;
+ }
+
+ /** {@inheritDoc} */
+ @Override public void apply(Ignite ign) {
+ ign.cluster().state(ClusterState.ACTIVE);
+
+ cacheGrpInfo.addItemsToCacheGrp(ign, 0, BASE_CACHE_SIZE);
+
+ if (cacheDump)
+ ign.snapshot().createDump(CACHE_DUMP_NAME,
Collections.singleton(cacheGrpInfo.name())).get();
+ else
+ ign.snapshot().createSnapshot(SNAPSHOT_NAME).get();
+
+ if (incSnp) {
+ cacheGrpInfo.addItemsToCacheGrp(ign, BASE_CACHE_SIZE,
ENTRIES_CNT_FOR_INCREMENT);
+
+ ign.snapshot().createIncrementalSnapshot(SNAPSHOT_NAME).get();
+ }
+ }
+ }
+
+ /** */
+ private static class CacheGroupInfo {
+ /** */
+ private final String name;
+
+ /** */
+ private final List<String> cacheNames;
+
+ /** */
+ public CacheGroupInfo(String name, int cachesCnt) {
+ this.name = name;
+
+ cacheNames = new ArrayList<>();
+
+ for (int i = 0; i < cachesCnt; ++i)
+ cacheNames.add("test-cache-" + i);
+ }
+
+ /** */
+ public String name() {
+ return name;
+ }
+
+ /** */
+ public List<String> cacheNamesList() {
+ return cacheNames;
+ }
+
+ /** */
+ public void addItemsToCacheGrp(Ignite ign, int startIdx, int cnt) {
+ for (String cacheName : cacheNames)
+ addItemsToCache(ign.cache(cacheName), startIdx, cnt);
+ }
+
+ /** */
+ private void addItemsToCache(IgniteCache<Integer, String> cache, int
startIdx, int cnt) {
+ for (int i = startIdx; i < startIdx + cnt; ++i)
+ cache.put(i, calcValue(cache.getName(), i));
+ }
+
+ /** */
+ public void checkCaches(Ignite ign, int expectedCacheSize) {
+ for (String cacheName : cacheNames) {
+ IgniteCache<Integer, String> cache = ign.cache(cacheName);
+
+ assertNotNull(cache);
+
+ checkCache(cache, expectedCacheSize);
+ }
+ }
+
+ /** */
+ private void checkCache(IgniteCache<Integer, String> cache, int
expectedSize) {
+ assertEquals(expectedSize, cache.size());
+
+ for (int i = 0; i < expectedSize; ++i)
+ assertEquals(calcValue(cache.getName(), i), cache.get(i));
+ }
+ }
+}
diff --git
a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testsuites/IgniteCompatibilityBasicTestSuite.java
b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testsuites/IgniteCompatibilityBasicTestSuite.java
index ee0f1d09ba9..5b422b79f81 100644
---
a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testsuites/IgniteCompatibilityBasicTestSuite.java
+++
b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testsuites/IgniteCompatibilityBasicTestSuite.java
@@ -28,6 +28,7 @@ import
org.apache.ignite.compatibility.persistence.MetaStorageCompatibilityTest;
import
org.apache.ignite.compatibility.persistence.MigratingToWalV2SerializerWithCompactionTest;
import
org.apache.ignite.compatibility.persistence.MoveBinaryMetadataCompatibility;
import
org.apache.ignite.compatibility.persistence.PersistenceBasicCompatibilityTest;
+import org.apache.ignite.compatibility.persistence.SnapshotCompatibilityTest;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@@ -47,6 +48,7 @@ import org.junit.runners.Suite;
JavaThinCompatibilityTest.class,
IgnitePKIndexesMigrationToUnwrapPkTest.class,
CompoundIndexCompatibilityTest.class,
+ SnapshotCompatibilityTest.class
})
public class IgniteCompatibilityBasicTestSuite {
}
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 f2eb4862bd6..488cb6258b9 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
@@ -728,7 +728,7 @@ public class NodeFileTree extends SharedFileTree {
/**
* @param root Root directory.
- * @return Array of cache data files.
+ * @return List of cache data files.
*/
public static List<File> existingCacheConfigFiles(File root) {
if (cacheDir(root)) {
@@ -737,6 +737,14 @@ public class NodeFileTree extends SharedFileTree {
return cfg.exists() ? Collections.singletonList(cfg) :
Collections.emptyList();
}
+ return allExisingConfigFiles(root);
+ }
+
+ /**
+ * @param root Root directory.
+ * @return List of cache data files regardless directory name.
+ */
+ public static List<File> allExisingConfigFiles(File root) {
return
F.asList(root.listFiles(NodeFileTree::cacheOrCacheGroupConfigFile));
}
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 f14d72a22ba..f04efd1724a 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
@@ -150,7 +150,10 @@ public class Dump implements AutoCloseable {
public List<StoredCacheData> configs(String node, int grp) {
JdkMarshaller marsh = cctx.marshallerContext().jdkMarshaller();
- return
NodeFileTree.existingCacheConfigFiles(sft(node).existingCacheDirectory(grp)).stream().map(f
-> {
+ // 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());
}