diff --git a/modules/core/src/main/java/org/apache/ignite/cache/affinity/rendezvous/MdcAffinityBackupFilter.java b/modules/core/src/main/java/org/apache/ignite/cache/affinity/rendezvous/MdcAffinityBackupFilter.java new file mode 100644 index 0000000000000..d800728056e66 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/cache/affinity/rendezvous/MdcAffinityBackupFilter.java @@ -0,0 +1,128 @@ +/* + * 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.cache.affinity.rendezvous; + +import java.util.List; +import org.apache.ignite.cluster.ClusterNode; +import org.apache.ignite.internal.util.typedef.internal.S; +import org.apache.ignite.lang.IgniteBiPredicate; + +/** + * Multi-data center affinity backup filter that ensures each partition's data is distributed across multiple data centers, + * providing high availability and fault tolerance. This implementation guarantees at least one copy of the data in each + * data center and attempts to maintain the configured backup factor without discarding copies. + *

+ * The filter works by grouping nodes based on their data center identification attribute (@see {@link ClusterNode#dataCenterId()}) + * and ensuring that for every partition, at least one node from each data center is included in the primary-backup set. + *

+ * The filter will discard backup copies only if the number of available nodes in a given data center is less + * than the number of copies assigned to that data center. + * For example, if a partition has 4 copies (1 primary and 3 backups) and the cluster has 2 data centers, + * than 2 copies are assigned to each data center. The only scenario when just a single copy is assigned to a node in a data center is when + * the number of nodes in that data center is one. + *

+ * This class is constructed with a number of data centers the cluster spans and a number of backups of the cache this filter is applied to. + * Implementation expects that all copies can be spread evenly across all data centers. In other words, (backups + 1) is divisible by + * number of data centers without remainder. Uneven distributions of copies are not supported. + *

+ * Warning: Ensure that all nodes have a consistent and valid data center identifier attribute. Missing or inconsistent values + * may lead to unexpected placement of data. + * + *

Spring Example

+ * Create a partitioned cache template where each data center has at least one copy of the data, and the backup count is maintained. + *
+ * <property name="cacheConfiguration">
+ *     <list>
+ *         <bean id="cache-template-bean" abstract="true" class="org.apache.ignite.configuration.CacheConfiguration">
+ *             <property name="name" value="JobcaseDefaultCacheConfig*"/>
+ *             <property name="cacheMode" value="PARTITIONED" />
+ *             <property name="backups" value="3" />
+ *             <property name="affinity">
+ *                 <bean class="org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction">
+ *                     <property name="affinityBackupFilter">
+ *                         <bean class="org.apache.ignite.cache.affinity.rendezvous.MdcAffinityBackupFilter">
+ *                             <constructor-arg value="2"/> 
+ *                             <constructor-arg value="3"/> 
+ *                         </bean>
+ *                     </property>
+ *                 </bean>
+ *             </property>
+ *         </bean>
+ *     </list>
+ * </property>
+ * 
+ *

+ * With more backups, additional replicas can be distributed across different data centers to further improve redundancy. + */ +public class MdcAffinityBackupFilter implements IgniteBiPredicate> { + /** */ + private static final long serialVersionUID = 1L; + + /** */ + private final int partCopiesPerDc; + + /** + * @param dcsNum Number of data centers. + * @param backups Number of backups. + */ + public MdcAffinityBackupFilter(int dcsNum, int backups) { + if (dcsNum < 2) { + throw new IllegalArgumentException("MdcAffinityBackupFilter cannot be used in an environment with only one datacenter. " + + "Number of datacenters must be at least 2."); + } + + int numCopies = backups + 1; + + partCopiesPerDc = numCopies / dcsNum; + int remainder = numCopies % dcsNum; + + if (remainder != 0) { + String suggestion = "recommended "; + if (numCopies - remainder <= 0) + suggestion += "value is " + (backups + (dcsNum - remainder)); + else + suggestion += "values are " + (backups - remainder) + " and " + (backups + (dcsNum - remainder)); + + throw new IllegalArgumentException("Number of copies is not completely divisible by number of datacenters, " + + "copies cannot be distributed evenly across DCs. " + + "Please adjust the number of backups, " + suggestion); + } + } + + /** {@inheritDoc} */ + @Override public boolean apply(ClusterNode candidate, List previouslySelected) { + String candidateDcId = candidate.dataCenterId(); + int candDcCopiesAssigned = 0; + + for (int i = 0; i < previouslySelected.size(); i++) { + String prevDcId = previouslySelected.get(i).dataCenterId(); + + if (prevDcId == null) + return false; + + candDcCopiesAssigned += prevDcId.equals(candidateDcId) ? 1 : 0; + } + + return candDcCopiesAssigned < partCopiesPerDc; + } + + /** {@inheritDoc} */ + @Override public String toString() { + return S.toString(MdcAffinityBackupFilter.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 fc18b6565300b..4d4fb35575d31 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 @@ -434,6 +434,8 @@ private void checkCache(CacheJoinNodeDiscoveryData.CacheInfo locInfo, CacheData "Affinity partitions count", locAttr.affinityPartitionsCount(), rmtAttr.affinityPartitionsCount(), true); + // TODO IGNITE-26967 - implement validation of affinity backup filter. + CU.validateKeyConfigiration(rmtAttr.groupName(), rmtAttr.cacheName(), rmt, rmtAttr.configuration().getKeyConfiguration(), locAttr.configuration().getKeyConfiguration(), log, true); diff --git a/modules/core/src/test/java/org/apache/ignite/cache/affinity/rendezvous/MdcAffinityBackupFilterSelfTest.java b/modules/core/src/test/java/org/apache/ignite/cache/affinity/rendezvous/MdcAffinityBackupFilterSelfTest.java new file mode 100644 index 0000000000000..a45f3ccd90124 --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/cache/affinity/rendezvous/MdcAffinityBackupFilterSelfTest.java @@ -0,0 +1,460 @@ +/* + * 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.cache.affinity.rendezvous; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.ignite.IgniteCache; +import org.apache.ignite.IgniteCheckedException; +import org.apache.ignite.IgniteSystemProperties; +import org.apache.ignite.cache.affinity.Affinity; +import org.apache.ignite.cluster.ClusterNode; +import org.apache.ignite.configuration.DataRegionConfiguration; +import org.apache.ignite.configuration.DataStorageConfiguration; +import org.apache.ignite.configuration.IgniteConfiguration; +import org.apache.ignite.internal.IgniteEx; +import org.apache.ignite.lang.IgniteBiPredicate; +import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; +import org.junit.Ignore; +import org.junit.Test; + +import static org.apache.ignite.cluster.ClusterState.ACTIVE; +import static org.apache.ignite.testframework.GridTestUtils.assertThrows; + +/** + * Verifies behaviour of {@link MdcAffinityBackupFilter} - guarantees that each DC has at least one copy of every partition. + * Verified distribution uniformity in each DC separately. + */ +public class MdcAffinityBackupFilterSelfTest extends GridCommonAbstractTest { + /** */ + private static final int PARTS_CNT = 512; + + /** */ + private int backups; + + /** */ + private String[] dcIds; + + /** */ + private IgniteBiPredicate> backupFilter; + + /** */ + private boolean persistenceEnabled; + + /** {@inheritDoc} */ + @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception { + IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName); + + cfg.setCacheConfiguration(defaultCacheConfiguration() + .setBackups(backups) + .setAffinity( + new RendezvousAffinityFunction(false, PARTS_CNT) + .setAffinityBackupFilter(backupFilter))); + + if (persistenceEnabled) { + cfg.setDataStorageConfiguration( + new DataStorageConfiguration() + .setDefaultDataRegionConfiguration( + new DataRegionConfiguration() + .setPersistenceEnabled(true) + ) + ); + } + + return cfg; + } + + /** {@inheritDoc} */ + @Override protected void afterTest() throws Exception { + super.afterTest(); + + stopAllGrids(); + + cleanPersistenceDir(); + + System.clearProperty(IgniteSystemProperties.IGNITE_DATA_CENTER_ID); + } + + /** + * Verifies that {@link MdcAffinityBackupFilter} prohibits single data center deployment. + */ + @Test + public void testSingleDcDeploymentIsProhibited() { + assertThrows(null, + () -> new MdcAffinityBackupFilter(1, 1), + IllegalArgumentException.class, + "Number of datacenters must be at least 2."); + } + + /** + * Verifies that {@link MdcAffinityBackupFilter} enforces even number of partition copies per datacenter. + */ + @Test + public void testUniformNumberOfPartitionCopiesPerDcIsEnforced() { + assertThrows(null, + () -> new MdcAffinityBackupFilter(3, 1), + IllegalArgumentException.class, + "recommended value is 2"); + + assertThrows(null, + () -> new MdcAffinityBackupFilter(3, 7), + IllegalArgumentException.class, + "recommended values are 5 and 8"); + } + + /** + * Verifies that filter doesn't assing additional copies if baseline topology is not changed, + * and it could lead to breaking of the guarantee `at least one copy of partition per datacenter`. + *

+ * Verifies that after baseline topology change, guarantee is restored. + * + * @throws Exception If failed. + */ + @Test + public void testMdcFilterWithBaselineTopology() throws Exception { + persistenceEnabled = true; + dcIds = new String[] {"DC_0", "DC_1"}; + int nodesPerDc = 2; + backups = 1; + backupFilter = new MdcAffinityBackupFilter(dcIds.length, backups); + + IgniteEx srv = startClusterAcrossDataCenters(dcIds, nodesPerDc); + + srv.cluster().state(ACTIVE); + + IgniteCache cache = srv.getOrCreateCache(DEFAULT_CACHE_NAME); + + Map> dc1OldAffDistr = affinityForPartitions(cache, node -> node.dataCenterId().equals(dcIds[1])); + + UUID srv0Id = grid(0).localNode().id(); + + Map> dc0AffDistr = affinityForPartitions(cache, node -> node.dataCenterId().equals(dcIds[0])); + + Set srv0Partitions = dc0AffDistr + .entrySet().stream() + .filter(entry -> entry.getValue().contains(srv0Id)) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + + stopGrid(0); + + dc0AffDistr = affinityForPartitions(cache, node -> node.dataCenterId().equals(dcIds[0])); + + // Partitions from stopped node are not assigned to any node without baseline topology change. + assertFalse(dc0AffDistr.keySet().containsAll(srv0Partitions)); + + Map> dc1NewAffDistr = affinityForPartitions(cache, node -> node.dataCenterId().equals(dcIds[1])); + + // Assignment of partitions in remote DC has not changed. + assertEquals(dc1OldAffDistr, dc1NewAffDistr); + + srv.cluster().setBaselineTopology(srv.cluster().topologyVersion()); + awaitPartitionMapExchange(); + + Set allParts = Stream.iterate(0, i -> i + 1).limit(PARTS_CNT).collect(Collectors.toSet()); + dc0AffDistr = affinityForPartitions(cache, node -> node.dataCenterId().equals(dcIds[0])); + // After baseline topology change, guarantee is restored. + assertTrue(allParts.containsAll(dc0AffDistr.keySet())); + } + + /** + * Verifies that partition copies are assigned evenly across a cluster in two data centers. + *

+ * When a node from one data center is stopped, partition distribution is that data center should stay uniform. + * + * @throws Exception If failed. + */ + @Test + public void test2DcDistribution() throws Exception { + dcIds = new String[] {"DC_0", "DC_1"}; + int nodesPerDc = 4; + backups = 3; + backupFilter = new MdcAffinityBackupFilter(dcIds.length, backups); + + IgniteEx srv = startClusterAcrossDataCenters(dcIds, nodesPerDc); + + verifyDistributionGuarantees(srv, dcIds, nodesPerDc, backups); + + //stopping one node in DC_1 should not compromise distribution as there are additional nodes in the same DC + stopGrid(5); + + awaitPartitionMapExchange(); + + verifyDistributionGuarantees(srv, dcIds, nodesPerDc, backups); + + //stopping another node in DC_1 should not compromise distribution as well + stopGrid(6); + + awaitPartitionMapExchange(); + + verifyDistributionGuarantees(srv, dcIds, nodesPerDc, backups); + } + + /** + * Verifies that partition copies are assigned evenly across a cluster in three data centers. + * + * @throws Exception If failed. + */ + @Test + public void test3DcDistribution() throws Exception { + dcIds = new String[] {"DC_0", "DC_1", "DC_2"}; + int nodesPerDc = 2; + backups = 5; + backupFilter = new MdcAffinityBackupFilter(dcIds.length, backups); + + IgniteEx srv = startClusterAcrossDataCenters(dcIds, 2); + + verifyDistributionGuarantees(srv, dcIds, nodesPerDc, backups); + } + + /** + * Verifies that node is prohibited from joining cluster if its affinityBackupFilter configuration differs + * from the one specified in the cluster. + * + * @throws Exception If failed. + */ + @Test + @Ignore("https://issues.apache.org/jira/browse/IGNITE-26967") + public void testAffinityFilterConfigurationValidation() throws Exception { + dcIds = new String[] {"DC_0", "DC_1"}; + backups = 3; + backupFilter = new MdcAffinityBackupFilter(dcIds.length, backups); + startGrid(0); + + backupFilter = new ClusterNodeAttributeAffinityBackupFilter("DC_ID"); + try { + startGrid(1); + + fail("Expected exception was not thrown."); + } + catch (IgniteCheckedException e) { + String errMsg = e.getMessage(); + + assertNotNull(errMsg); + + assertTrue(errMsg.contains("Affinity backup filter class mismatch")); + } + + backupFilter = new MdcAffinityBackupFilter(dcIds.length, backups + dcIds.length); + try { + startGrid(1); + + fail("Expected exception was not thrown."); + } + catch (IgniteCheckedException e) { + String errMsg = e.getMessage(); + + assertNotNull(errMsg); + + assertTrue(errMsg.contains("Affinity backup filter mismatch")); + } + } + + /** + * Verifies that distribution of partitions in one datacenter + * doesn't change if nodes leave in another or if the other DC goes down completely. + * In other words, no rebalance is triggered in one dc if some topology changes happen in another. + * + * @throws Exception If failed. + */ + @Test + public void testNoRebalanceInOneDcIfTopologyChangesInAnother() throws Exception { + dcIds = new String[] {"DC_0", "DC_1"}; + int nodesPerDc = 3; + backups = 3; + backupFilter = new MdcAffinityBackupFilter(dcIds.length, backups); + Predicate dc1NodesFilter = node -> dcIds[1].equals(node.dataCenterId()); + + IgniteEx srv = startClusterAcrossDataCenters(dcIds, nodesPerDc); + awaitPartitionMapExchange(); + Map> oldDistribution = affinityForPartitions(srv.getOrCreateCache(DEFAULT_CACHE_NAME), dc1NodesFilter); + + oldDistribution = verifyNoRebalancing(0, true, null, srv, oldDistribution, dc1NodesFilter); + oldDistribution = verifyNoRebalancing(1, true, null, srv, oldDistribution, dc1NodesFilter); + // Start srv0 back in DC_0 to make sure that neither starting nor stopping a server doesn't affect + // affinity distribution in remote DC. + oldDistribution = verifyNoRebalancing(0, false, dcIds[0], srv, oldDistribution, dc1NodesFilter); + // Stop it again. + oldDistribution = verifyNoRebalancing(0, true, null, srv, oldDistribution, dc1NodesFilter); + verifyNoRebalancing(2, true, null, srv, oldDistribution, dc1NodesFilter); + } + + /** Starts specified number of nodes in each DC. */ + private IgniteEx startClusterAcrossDataCenters(String[] dcIds, int nodesPerDc) throws Exception { + int nodeIdx = 0; + IgniteEx lastNode = null; + + for (String dcId : dcIds) { + System.setProperty(IgniteSystemProperties.IGNITE_DATA_CENTER_ID, dcId); + + for (int i = 0; i < nodesPerDc; i++) + lastNode = startGrid(nodeIdx++); + } + + return lastNode; + } + + /** */ + private Map> verifyNoRebalancing( + int srvIdx, + boolean stopSrv, + String startingSrvDcId, + IgniteEx srv, + Map> oldDistribution, + Predicate dc1NodesFilter + ) throws Exception { + if (stopSrv) + stopGrid(srvIdx); + else { + System.setProperty(IgniteSystemProperties.IGNITE_DATA_CENTER_ID, startingSrvDcId); + startGrid(srvIdx); + } + awaitPartitionMapExchange(); + IgniteCache cache = srv.getOrCreateCache(DEFAULT_CACHE_NAME); + + Map> newDistribution = affinityForPartitions(cache, dc1NodesFilter); + + assertEquals( + String.format("Affinity distribution changed after server node %d was stopped", srvIdx), + oldDistribution, + newDistribution); + + return newDistribution; + } + + /** */ + private Map> affinityForPartitions(IgniteCache cache, Predicate dcFilter) { + Map> result = new HashMap<>(PARTS_CNT); + Affinity aff = affinity(cache); + + for (int i = 0; i < PARTS_CNT; i++) { + int j = i; + + // For each partition, collect UUID of all its affinity nodes passing the provided filter. + aff.mapKeyToPrimaryAndBackups(i) + .stream() + .filter(dcFilter) + .forEach( + node -> result.compute(j, + (k, v) -> { + if (v == null) { + Set s = new HashSet<>(); + s.add(node.id()); + return s; + } + else { + v.add(node.id()); + return v; + } + })); + } + + return result; + } + + /** + * Checks that copies of each partition are distributed evenly across data centers and copies are spread evenly across nodes. + */ + private void verifyDistributionGuarantees( + IgniteEx srv, + String[] dcIds, + int nodesPerDc, + int backups + ) { + IgniteCache cache = srv.getOrCreateCache(DEFAULT_CACHE_NAME); + + int partCnt = cacheConfiguration(srv.configuration(), cache.getName()).getAffinity().partitions(); + Affinity aff = affinity(cache); + int expectedCopiesPerNode = (backups + 1) / dcIds.length; + + Map overallCopiesPerNode = new HashMap<>(); + int[] copiesPerNode = new int[dcIds.length * nodesPerDc]; + + for (int partId = 0; partId < partCnt; partId++) { + int[] partCopiesPerDc = new int[dcIds.length]; + + Collection nodes = aff.mapKeyToPrimaryAndBackups(partId); + + //calculate actual number of copies in each data center + //aggregate copies per each node + for (ClusterNode node : nodes) { + copiesPerNode[(int)(node.order() - 1)]++; + + overallCopiesPerNode.compute(node, (k, v) -> { + if (v == null) + return 1; + else + return v + 1; + }); + + for (int j = 0; j < dcIds.length; j++) { + if (node.dataCenterId().equals(dcIds[j])) { + partCopiesPerDc[j]++; + break; + } + } + } + + verifyCopyInEachDcGuarantee(partId, expectedCopiesPerNode, partCopiesPerDc); + } + + verifyDistributionUniformity(dcIds, overallCopiesPerNode); + } + + /** */ + private void verifyCopyInEachDcGuarantee(int partId, int expectedCopiesPerNode, int[] partCopiesPerDc) { + for (int dcIdx = 0; dcIdx < dcIds.length; dcIdx++) { + assertEquals(String.format("Unexpected number of copies of partition %d in data center %s", partId, dcIds[dcIdx]), + expectedCopiesPerNode, + partCopiesPerDc[dcIdx]); + } + } + + /** */ + private void verifyDistributionUniformity(String[] dcIds, Map overallCopiesPerNode) { + for (String dcId : dcIds) { + long nodesInDc = overallCopiesPerNode.entrySet().stream().filter(e -> e.getKey().dataCenterId().equals(dcId)).count(); + + double idealCopiesPerNode = (double)((PARTS_CNT * (backups + 1)) / (dcIds.length * nodesInDc)); + + List numOfCopiesPerNode = overallCopiesPerNode.entrySet().stream() + .filter(e -> e.getKey().dataCenterId().equals(dcId)).map(Map.Entry::getValue).collect(Collectors.toList()); + + for (int copiesOnNode : numOfCopiesPerNode) { + double deviation = (Math.abs(copiesOnNode - idealCopiesPerNode) / idealCopiesPerNode); + + assertTrue( + String.format("Too big deviation from ideal distribution: partitions assigned = %d, " + + "ideal partitions assigned = %d, deviation = %d", + copiesOnNode, + (int)idealCopiesPerNode, + (int)(deviation * 100) + ), + deviation < 0.1); + } + } + } +} diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteCacheTestSuite2.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteCacheTestSuite2.java index 0c44992d8e4df..97a6ab93f69e4 100644 --- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteCacheTestSuite2.java +++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteCacheTestSuite2.java @@ -22,6 +22,7 @@ import java.util.List; import org.apache.ignite.cache.affinity.rendezvous.ClusterNodeAttributeAffinityBackupFilterSelfTest; import org.apache.ignite.cache.affinity.rendezvous.ClusterNodeAttributeColocatedBackupFilterSelfTest; +import org.apache.ignite.cache.affinity.rendezvous.MdcAffinityBackupFilterSelfTest; import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunctionBackupFilterSelfTest; import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunctionExcludeNeighborsSelfTest; import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunctionFastPowerOfTwoHashSelfTest; @@ -348,6 +349,7 @@ public static List> suite(Collection ignoredTests) { GridTestUtils.addTestIfNeeded(suite, RendezvousAffinityFunctionBackupFilterSelfTest.class, ignoredTests); GridTestUtils.addTestIfNeeded(suite, ClusterNodeAttributeAffinityBackupFilterSelfTest.class, ignoredTests); GridTestUtils.addTestIfNeeded(suite, ClusterNodeAttributeColocatedBackupFilterSelfTest.class, ignoredTests); + GridTestUtils.addTestIfNeeded(suite, MdcAffinityBackupFilterSelfTest.class, ignoredTests); GridTestUtils.addTestIfNeeded(suite, CachePartitionStateTest.class, ignoredTests); GridTestUtils.addTestIfNeeded(suite, CacheComparatorTest.class, ignoredTests);