/*
* Copyright 2008-2013 LinkedIn, Inc
*
* Licensed 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 voldemort.client.rebalance;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import voldemort.ClusterTestUtils;
import voldemort.ServerTestUtils;
import voldemort.VoldemortException;
import voldemort.VoldemortTestConstants;
import voldemort.cluster.Cluster;
import voldemort.routing.RoutingStrategyType;
import voldemort.store.StoreDefinition;
import voldemort.xml.StoreDefinitionsMapper;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
/**
* Tests rebalance batch plan for non-zoned cluster. These tests existed before
* the RebalancePlan was re-written in April/May 2013. That is why these tests
* follow a different format than those in ZonedRebalanceBatchPlanTest.
*/
public class NonZonedRebalanceBatchPlanTest {
private static String storeDefFile = "test/common/voldemort/config/stores.xml";
private Cluster currentCluster;
private Cluster finalCluster;
private List<StoreDefinition> storeDefList;
private List<StoreDefinition> storeDefList2;
private List<StoreDefinition> test211StoreDef;
@Before
public void setUp() {
try {
storeDefList = new StoreDefinitionsMapper().readStoreList(new FileReader(new File(storeDefFile)));
storeDefList2 = new StoreDefinitionsMapper().readStoreList(new StringReader(VoldemortTestConstants.getSingleStore322Xml()));
test211StoreDef = Lists.newArrayList(ServerTestUtils.getStoreDef("test",
2,
1,
1,
1,
1,
RoutingStrategyType.CONSISTENT_STRATEGY));
} catch(FileNotFoundException e) {
throw new RuntimeException("Failed to find storeDefFile:" + storeDefFile, e);
}
}
/**
* Tests the scenario where-in a migration causes a drop in the number of
* replicas
*/
@Test
public void testInsufficientNodes() {
currentCluster = ServerTestUtils.getLocalCluster(3, new int[][] { { 0 }, { 1 }, { 2 } });
finalCluster = ServerTestUtils.getLocalCluster(2, new int[][] { { 1 }, { 0 }, { 2 } });
try {
new RebalanceBatchPlan(currentCluster, finalCluster, storeDefList);
fail("Should have thrown an exception since the migration should result in decrease in replication factor");
} catch(VoldemortException e) {}
}
/**
* confirm that a shuffle of a cluster of size 2 for a 211 store is a no op.
*/
@Test
public void testShuffleNoop() {
int numServers = 2;
int ports[] = ServerTestUtils.findFreePorts(3 * numServers);
currentCluster = ServerTestUtils.getLocalCluster(numServers, ports, new int[][] {
{ 0, 1, 2, 3 }, { 4, 5, 6, 7 } });
finalCluster = ServerTestUtils.getLocalCluster(numServers, ports, new int[][] {
{ 1, 2, 3 }, { 4, 5, 6, 7, 0 } });
List<RebalanceTaskInfo> batchPlan = ClusterTestUtils.getBatchPlan(currentCluster,
finalCluster,
test211StoreDef);
assertTrue("Batch plan should be empty.", batchPlan.isEmpty());
}
/**
* Expand on to an empty server.
*/
@Test
public void testClusterExpansion() {
int numServers = 3;
int ports[] = ServerTestUtils.findFreePorts(3 * numServers);
currentCluster = ServerTestUtils.getLocalCluster(numServers, ports, new int[][] {
{ 0, 1, 2, 3 }, { 4, 5, 6, 7 }, {} });
finalCluster = ServerTestUtils.getLocalCluster(numServers, ports, new int[][] {
{ 1, 2, 3 }, { 4, 5, 6, 7 }, { 0 } });
List<RebalanceTaskInfo> batchPlan = ClusterTestUtils.getBatchPlan(currentCluster,
finalCluster,
test211StoreDef);
// data should only move from node 0 to node 2 for node 2 to host
// everything needed. no other movement should occur.
assertEquals("There should be one move in this plan.", 1, batchPlan.size());
assertEquals("There should be exactly 1 rebalancing nodes",
1,
getUniqueNodeCount(batchPlan, false));
assertEquals("Stealer 2 should have 1 entry",
1,
getStealerNodePartitionTaskCount(2, batchPlan));
// Partitions to move
HashMap<String, List<Integer>> storeToPartitionsToMove = Maps.newHashMap();
storeToPartitionsToMove.put("test", Lists.newArrayList(0, 4, 5, 6, 7));
checkAllRebalanceInfoPresent(getStealerNodePartitionTaskList(2, batchPlan),
Arrays.asList(new RebalanceTaskInfo(2,
0,
storeToPartitionsToMove,
currentCluster)));
}
/**
* Tests the case where-in we delete all the partitions from the last node
*/
@Test
public void testDeleteLastNode() {
int numServers = 4;
int ports[] = ServerTestUtils.findFreePorts(3 * numServers);
currentCluster = ServerTestUtils.getLocalCluster(numServers, ports, new int[][] {
{ 3, 6, 9, 12, 15 }, { 1, 4, 7, 10, 13, 16 }, { 2, 5, 8, 11, 14, 17 }, { 0 } });
finalCluster = ServerTestUtils.getLocalCluster(numServers, ports, new int[][] {
{ 0, 3, 6, 9, 12, 15 }, { 1, 4, 7, 10, 13, 16 }, { 2, 5, 8, 11, 14, 17 }, {} });
List<RebalanceTaskInfo> orderedRebalanceTaskInfoList = ClusterTestUtils.getBatchPlan(currentCluster,
finalCluster,
storeDefList2);
assertEquals("There should have exactly 1 rebalancing node",
1,
getUniqueNodeCount(orderedRebalanceTaskInfoList, false));
assertEquals("There should be exactly 1 rebalancing partition info",
1,
orderedRebalanceTaskInfoList.size());
assertEquals("Stealer 0 should have 1 entry",
1,
getStealerNodePartitionTaskCount(0, orderedRebalanceTaskInfoList));
HashMap<String, List<Integer>> storeToPartitionsToMove = Maps.newHashMap();
storeToPartitionsToMove.put("test", Lists.newArrayList(0, 16, 17));
checkAllRebalanceInfoPresent(getStealerNodePartitionTaskList(0,
orderedRebalanceTaskInfoList),
Arrays.asList(new RebalanceTaskInfo(0,
3,
storeToPartitionsToMove,
currentCluster)));
}
/**
* Tests the scenario where-in we delete the first node
*/
@Test
public void testDeleteFirstNode() {
int numServers = 4;
int ports[] = ServerTestUtils.findFreePorts(3 * numServers);
currentCluster = ServerTestUtils.getLocalCluster(numServers, ports, new int[][] { { 0, 4 },
{ 1, 5 }, { 2, 6 }, { 3, 7 } });
finalCluster = ServerTestUtils.getLocalCluster(numServers, ports, new int[][] { { 4 },
{ 0, 1, 5 }, { 2, 6 }, { 3, 7 } });
// PHASE 1 - move partition 0 off of node 0 to node 1
List<RebalanceTaskInfo> batchPlan = ClusterTestUtils.getBatchPlan(currentCluster,
finalCluster,
storeDefList2);
assertFalse("Batch plan should not be empty.", batchPlan.isEmpty());
// Cannot do other tests because with partition 1 already on node 1, its
// unclear which partitions will actual move.
// PHASE 2 - Move partition 4 off of node 0 to node 2
currentCluster = ServerTestUtils.getLocalCluster(numServers, ports, new int[][] { { 4 },
{ 0, 1, 5 }, { 2 }, { 3, 6, 7 } });
finalCluster = ServerTestUtils.getLocalCluster(numServers, ports, new int[][] { {},
{ 0, 1, 5 }, { 4, 2 }, { 3, 6, 7 } });
batchPlan = ClusterTestUtils.getBatchPlan(currentCluster, finalCluster, storeDefList2);
assertFalse("Batch plan should not be empty.", batchPlan.isEmpty());
assertFalse("Batch plan for server 2 should not be empty.",
getStealerNodePartitionTaskList(2, batchPlan).isEmpty());
boolean hasTheMove = false;
// Confirm partition 4 is moved from server 0 to server 2
for(RebalanceTaskInfo info: getStealerNodePartitionTaskList(2, batchPlan)) {
assertTrue(info.getStealerId() == 2);
if(info.getDonorId() == 0) {
hasTheMove = true;
assertTrue(info.getPartitionStores().size() == 1);
assertTrue(info.getPartitionIds("test").contains(4));
}
}
assertTrue(hasTheMove);
}
@Test
public void testRebalanceDeletingMiddleNode() {
int numServers = 4;
int ports[] = ServerTestUtils.findFreePorts(3 * numServers);
// PHASE 1 - move partition 2 off of node 2 and onto node 1
currentCluster = ServerTestUtils.getLocalCluster(numServers, ports, new int[][] { { 0, 4 },
{ 1, 5 }, { 2, 6 }, { 3, 7 } });
finalCluster = ServerTestUtils.getLocalCluster(numServers, ports, new int[][] { { 0, 4 },
{ 2, 1, 5 }, { 6 }, { 3, 7 } });
List<RebalanceTaskInfo> batchPlan = ClusterTestUtils.getBatchPlan(currentCluster,
finalCluster,
storeDefList2);
assertFalse("Batch plan should not be empty.", batchPlan.isEmpty());
assertFalse("Batch plan for server 1 should not be empty.",
getStealerNodePartitionTaskList(1, batchPlan).isEmpty());
boolean hasTheMove = false;
// Confirm partition 2 is moved from server 2 to server 1
for(RebalanceTaskInfo info: getStealerNodePartitionTaskList(1, batchPlan)) {
assertTrue(info.getStealerId() == 1);
if(info.getDonorId() == 2) {
hasTheMove = true;
assertTrue(info.getPartitionStores().size() == 1);
assertTrue(info.getPartitionIds("test").contains(2));
}
}
assertTrue(hasTheMove);
// PHASE 2 - move partition 6 off of node 2 and onto node 3
currentCluster = ServerTestUtils.getLocalCluster(numServers, ports, new int[][] { { 0, 4 },
{ 2, 1, 5 }, { 6 }, { 3, 7 } });
finalCluster = ServerTestUtils.getLocalCluster(numServers, ports, new int[][] { { 0, 4 },
{ 2, 1, 5 }, {}, { 6, 3, 7 } });
batchPlan = ClusterTestUtils.getBatchPlan(currentCluster, finalCluster, storeDefList2);
assertFalse("Batch plan should not be empty.", batchPlan.isEmpty());
// Cannot do other tests because with partition 7 already on node 3, its
// unclear which partitions will actual move when partitoin 6 also moves
// to node 3.
}
@Test
public void testManyStoreClusterExpansion() {
int numServers = 4;
int ports[] = ServerTestUtils.findFreePorts(3 * numServers);
currentCluster = ServerTestUtils.getLocalCluster(numServers, ports, new int[][] {
{ 0, 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 }, {} });
finalCluster = ServerTestUtils.getLocalCluster(numServers, ports, new int[][] {
{ 0, 2, 3 }, { 4, 6 }, { 7, 8, 9 }, { 1, 5 } });
List<RebalanceTaskInfo> batchPlan = ClusterTestUtils.getBatchPlan(currentCluster,
finalCluster,
storeDefList);
assertFalse("Batch plan should not be empty.", batchPlan.isEmpty());
assertFalse("Batch plan for server 3 should not be empty.",
getStealerNodePartitionTaskList(3, batchPlan).isEmpty());
boolean hasTheMove = false;
// Confirm partition 1 is moved from server 0 to server 3
for(RebalanceTaskInfo info: getStealerNodePartitionTaskList(3, batchPlan)) {
assertTrue(info.getStealerId() == 3);
if(info.getDonorId() == 0) {
hasTheMove = true;
for(String storeName: info.getPartitionStores()) {
assertTrue(info.getPartitionIds(storeName).contains(1));
}
}
}
assertTrue(hasTheMove);
hasTheMove = false;
// Confirm partition 5 is moved from server 1 to server 3
for(RebalanceTaskInfo info: getStealerNodePartitionTaskList(3, batchPlan)) {
assertTrue(info.getStealerId() == 3);
if(info.getDonorId() == 1) {
hasTheMove = true;
for(String storeName: info.getPartitionStores()) {
assertTrue(info.getPartitionIds(storeName).contains(5));
}
}
}
assertTrue(hasTheMove);
}
/**
* Issue 288
*/
@Test
public void testRebalanceAllReplicasBeingMigrated() {
int numServers = 4;
int ports[] = ServerTestUtils.findFreePorts(3 * numServers);
currentCluster = ServerTestUtils.getLocalCluster(numServers, ports, new int[][] { { 0, 4 },
{ 2, 3 }, { 1, 5 }, {} });
finalCluster = ServerTestUtils.getLocalCluster(numServers, ports, new int[][] { { 4 },
{ 2, 3 }, { 1, 5 }, { 0 } });
List<RebalanceTaskInfo> orderedRebalanceTaskInfoList = ClusterTestUtils.getBatchPlan(currentCluster,
finalCluster,
storeDefList2);
assertEquals("There should have exactly 1 rebalancing node",
1,
this.getUniqueNodeCount(orderedRebalanceTaskInfoList, false));
assertEquals("There should have exactly 2 rebalancing partition info",
2,
orderedRebalanceTaskInfoList.size());
assertEquals("Stealer 3 should have 2 entry",
2,
this.getStealerNodePartitionTaskCount(3, orderedRebalanceTaskInfoList));
HashMap<String, List<Integer>> storeToPartitionsToMove1 = Maps.newHashMap();
storeToPartitionsToMove1.put("test", Lists.newArrayList(0, 5));
HashMap<String, List<Integer>> storeToPartitionsToMove2 = Maps.newHashMap();
storeToPartitionsToMove2.put("test", Lists.newArrayList(4));
checkAllRebalanceInfoPresent(this.getStealerNodePartitionTaskList(3,
orderedRebalanceTaskInfoList),
Arrays.asList(new RebalanceTaskInfo(3,
0,
storeToPartitionsToMove1,
currentCluster),
new RebalanceTaskInfo(3,
1,
storeToPartitionsToMove2,
currentCluster)));
}
private int getUniqueNodeCount(List<RebalanceTaskInfo> rebalanceInfoList,
boolean isDonorBased) {
HashSet<Integer> uniqueNodeSet = Sets.newHashSet();
for(RebalanceTaskInfo partitionInfo: rebalanceInfoList) {
int nodeId;
if(isDonorBased) {
nodeId = partitionInfo.getDonorId();
} else {
nodeId = partitionInfo.getStealerId();
}
if(!uniqueNodeSet.contains(nodeId)) {
uniqueNodeSet.add(nodeId);
}
}
return uniqueNodeSet.size();
}
private int getStealerNodePartitionTaskCount(int stealerId,
List<RebalanceTaskInfo> rebalanceInfoList) {
int count = 0;
for(RebalanceTaskInfo partitionInfo: rebalanceInfoList) {
if(partitionInfo.getStealerId() == stealerId) {
count++;
}
}
return count;
}
private List<RebalanceTaskInfo> getStealerNodePartitionTaskList(int stealerId,
List<RebalanceTaskInfo> rebalanceTaskList) {
ArrayList<RebalanceTaskInfo> partitionList = Lists.newArrayList();
for(RebalanceTaskInfo partitionInfo: rebalanceTaskList) {
if(partitionInfo.getStealerId() == stealerId) {
partitionList.add(partitionInfo);
}
}
return partitionList;
}
private void checkAllRebalanceInfoPresent(List<RebalanceTaskInfo> toCheckRebalanceTaskInfoList,
List<RebalanceTaskInfo> rebalanceTaskInfoList) {
for(RebalanceTaskInfo rebalanceInfo: rebalanceTaskInfoList) {
boolean match = false;
for(RebalanceTaskInfo nodeRebalanceTaskInfo: toCheckRebalanceTaskInfoList) {
if(rebalanceInfo.getDonorId() == nodeRebalanceTaskInfo.getDonorId()) {
assertEquals("Store lists should match",
rebalanceInfo.getPartitionStores(),
nodeRebalanceTaskInfo.getPartitionStores());
assertEquals("Clusters to be same",
rebalanceInfo.getInitialCluster(),
nodeRebalanceTaskInfo.getInitialCluster());
for(String storeName: rebalanceInfo.getPartitionStores()) {
assertEquals("add partition mapping for store " + storeName
+ " should be same ",
rebalanceInfo.getPartitionIds(storeName),
nodeRebalanceTaskInfo.getPartitionIds(storeName));
}
match = true;
}
}
assertNotSame("rebalancePartition Info " + rebalanceInfo
+ " should be present in the nodePlan "
+ toCheckRebalanceTaskInfoList,
false,
match);
}
}
}