/*
* 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.fail;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import org.junit.After;
import voldemort.ROTestUtils;
import voldemort.ServerTestUtils;
import voldemort.TestUtils;
import voldemort.VoldemortException;
import voldemort.client.protocol.RequestFormatType;
import voldemort.client.protocol.admin.AdminClient;
import voldemort.client.protocol.admin.QueryKeyResult;
import voldemort.cluster.Cluster;
import voldemort.cluster.Node;
import voldemort.routing.RoutingStrategy;
import voldemort.routing.RoutingStrategyFactory;
import voldemort.routing.StoreRoutingPlan;
import voldemort.server.VoldemortConfig;
import voldemort.server.VoldemortServer;
import voldemort.store.Store;
import voldemort.store.StoreDefinition;
import voldemort.store.metadata.MetadataStore;
import voldemort.store.metadata.MetadataStore.VoldemortState;
import voldemort.store.socket.SocketStoreFactory;
import voldemort.store.socket.clientrequest.ClientRequestExecutorPool;
import voldemort.utils.ByteArray;
import voldemort.utils.ByteUtils;
import voldemort.utils.Pair;
import voldemort.utils.Utils;
import voldemort.versioning.VectorClock;
import voldemort.versioning.Versioned;
public abstract class AbstractRebalanceTest {
Map<Integer, VoldemortServer> serverMap;
HashMap<String, String> testEntries;
protected SocketStoreFactory socketStoreFactory;
public AbstractRebalanceTest() {
this.serverMap = new HashMap<Integer, VoldemortServer>();
testEntries = ServerTestUtils.createRandomKeyValueString(getNumKeys());
socketStoreFactory = new ClientRequestExecutorPool(2, 1000, 1000, 32 * 1024);
}
// This method is susceptible to BindException issues due to TOCTOU
// problem with getLocalCluster (which is used to construct cluster that is
// passed in).
// TODO: (refactor) AbstractRebalanceTest to take advantage of
// ServerTestUtils.startVoldemortCluster.
protected Cluster startServers(Cluster cluster,
String storeXmlFile,
List<Integer> nodeToStart,
Map<String, String> configProps) throws Exception {
for(int node: nodeToStart) {
Properties properties = new Properties();
if(null != configProps) {
for(Entry<String, String> property: configProps.entrySet()) {
properties.put(property.getKey(), property.getValue());
}
}
// turn proxy puts on
properties.put("proxy.puts.during.rebalance", "true");
properties.put("bdb.cache.size", "" + (5 * 1024 * 1024));
properties.put("bdb.one.env.per.store", "true");
VoldemortConfig config = ServerTestUtils.createServerConfig(true,
node,
TestUtils.createTempDir()
.getAbsolutePath(),
null,
storeXmlFile,
properties);
VoldemortServer server = ServerTestUtils.startVoldemortServer(socketStoreFactory,
config,
cluster);
serverMap.put(node, server);
}
return cluster;
}
@After
public void tearDown() throws Exception {
for(VoldemortServer vs: serverMap.values()) {
vs.stop();
}
}
protected Store<ByteArray, byte[], byte[]> getSocketStore(String storeName,
String host,
int port) {
return getSocketStore(storeName, host, port, false);
}
protected Store<ByteArray, byte[], byte[]> getSocketStore(String storeName,
String host,
int port,
boolean isRouted) {
return ServerTestUtils.getSocketStore(socketStoreFactory,
storeName,
host,
port,
RequestFormatType.PROTOCOL_BUFFERS,
isRouted);
}
protected VoldemortState getCurrentState(int nodeId) {
VoldemortServer server = serverMap.get(nodeId);
if(server == null) {
throw new VoldemortException("Node id " + nodeId + " does not exist");
} else {
return server.getMetadataStore().getServerStateUnlocked();
}
}
protected Cluster getCurrentCluster(int nodeId) {
VoldemortServer server = serverMap.get(nodeId);
if(server == null) {
throw new VoldemortException("Node id " + nodeId + " does not exist");
} else {
return server.getMetadataStore().getCluster();
}
}
public void checkConsistentMetadata(Cluster finalCluster, List<Integer> serverList) {
for(int nodeId: serverList) {
assertEquals(finalCluster, getCurrentCluster(nodeId));
assertEquals(MetadataStore.VoldemortState.NORMAL_SERVER, getCurrentState(nodeId));
}
}
protected void stopServer(List<Integer> nodesToStop) throws Exception {
for(int node: nodesToStop) {
try {
ServerTestUtils.stopVoldemortServer(serverMap.get(node));
} catch(VoldemortException e) {
// ignore these at stop time
}
}
}
/**
* This method determines the "size" of the test to run...
*
* @return
*/
protected abstract int getNumKeys();
protected String getBootstrapUrl(Cluster cluster, int nodeId) {
Node node = cluster.getNodeById(nodeId);
return "tcp://" + node.getHost() + ":" + node.getSocketPort();
}
/**
* Does the rebalance and then checks that it succeeded.
*
* @param rebalancePlan
* @param rebalanceClient
* @param nodeCheckList
*/
protected void rebalanceAndCheck(RebalancePlan rebalancePlan,
RebalanceController rebalanceClient,
List<Integer> nodeCheckList) {
rebalanceClient.rebalance(rebalancePlan);
checkEntriesPostRebalance(rebalancePlan.getCurrentCluster(),
rebalancePlan.getFinalCluster(),
rebalancePlan.getCurrentStores(),
nodeCheckList,
testEntries,
null);
}
/**
* Makes sure that all expected partition-stores are on each server after
* the rebalance.
*
* @param currentCluster
* @param finalCluster
* @param storeDefs
* @param nodeCheckList
* @param baselineTuples
* @param baselineVersions
*/
protected void checkEntriesPostRebalance(Cluster currentCluster,
Cluster finalCluster,
List<StoreDefinition> storeDefs,
List<Integer> nodeCheckList,
HashMap<String, String> baselineTuples,
HashMap<String, VectorClock> baselineVersions) {
for(StoreDefinition storeDef: storeDefs) {
Map<Integer, Set<Pair<Integer, Integer>>> currentNodeToPartitionTuples = ROTestUtils.getNodeIdToAllPartitions(currentCluster,
storeDef,
true);
Map<Integer, Set<Pair<Integer, Integer>>> finalNodeToPartitionTuples = ROTestUtils.getNodeIdToAllPartitions(finalCluster,
storeDef,
true);
for(int nodeId: nodeCheckList) {
Set<Pair<Integer, Integer>> currentPartitionTuples = currentNodeToPartitionTuples.get(nodeId);
Set<Pair<Integer, Integer>> finalPartitionTuples = finalNodeToPartitionTuples.get(nodeId);
HashMap<Integer, List<Integer>> flattenedPresentTuples = ROTestUtils.flattenPartitionTuples(Utils.getAddedInTarget(currentPartitionTuples,
finalPartitionTuples));
Store<ByteArray, byte[], byte[]> store = getSocketStore(storeDef.getName(),
finalCluster.getNodeById(nodeId)
.getHost(),
finalCluster.getNodeById(nodeId)
.getSocketPort());
checkGetEntries(finalCluster.getNodeById(nodeId),
finalCluster,
storeDef,
store,
flattenedPresentTuples,
baselineTuples,
baselineVersions);
}
}
}
protected void checkGetEntries(Node node,
Cluster cluster,
StoreDefinition def,
Store<ByteArray, byte[], byte[]> store,
HashMap<Integer, List<Integer>> flattenedPresentTuples,
HashMap<String, String> baselineTuples,
HashMap<String, VectorClock> baselineVersions) {
RoutingStrategy routing = new RoutingStrategyFactory().updateRoutingStrategy(def, cluster);
for(Entry<String, String> entry: baselineTuples.entrySet()) {
ByteArray keyBytes = new ByteArray(ByteUtils.getBytes(entry.getKey(), "UTF-8"));
List<Integer> partitions = routing.getPartitionList(keyBytes.get());
if(StoreRoutingPlan.checkKeyBelongsToPartition(partitions,
node.getPartitionIds(),
flattenedPresentTuples)) {
List<Versioned<byte[]>> values = store.get(keyBytes, null);
// expecting exactly one version
if(values.size() == 0) {
fail("unable to find value for key=" + entry.getKey() + " on node="
+ node.getId());
}
assertEquals("Expecting exactly one version", 1, values.size());
Versioned<byte[]> value = values.get(0);
// check version matches
if(baselineVersions == null) {
// expecting base version for all
assertEquals("Value version should match",
new VectorClock(),
value.getVersion());
} else {
assertEquals("Value version should match",
baselineVersions.get(entry.getKey()),
value.getVersion());
}
// check value matches.
assertEquals("Value bytes should match",
entry.getValue(),
ByteUtils.getString(value.getValue(), "UTF-8"));
}
}
}
protected List<ByteArray> sampleKeysFromPartition(AdminClient admin,
int serverId,
String store,
List<Integer> partitionsToSample,
int numSamples) {
List<ByteArray> samples = new ArrayList<ByteArray>(numSamples);
Iterator<ByteArray> keys = admin.bulkFetchOps.fetchKeys(serverId,
store,
partitionsToSample,
null,
false);
int count = 0;
while(keys.hasNext() && count < numSamples) {
samples.add(keys.next());
count++;
}
return samples;
}
/**
* REFACTOR: these should belong AdminClient so existence checks can be done
* easily across the board
*
* @param admin
* @param serverId
* @param store
* @param keyList
*/
protected void checkForKeyExistence(AdminClient admin,
int serverId,
String store,
List<ByteArray> keyList) {
// do the positive tests
Iterator<QueryKeyResult> positiveTestResultsItr = admin.streamingOps.queryKeys(serverId,
store,
keyList.iterator());
while(positiveTestResultsItr.hasNext()) {
QueryKeyResult item = positiveTestResultsItr.next();
ByteArray key = item.getKey();
List<Versioned<byte[]>> vals = item.getValues();
Exception e = item.getException();
assertEquals("Error fetching key " + key, null, e);
assertEquals("Value not found for key " + key, true, vals != null & vals.size() != 0);
}
}
/**
* REFACTOR: these should belong AdminClient so existence checks can be done
* easily across the board
*
* @param admin
* @param serverId
* @param store
* @param keyList
*/
protected void checkForTupleEquivalence(AdminClient admin,
int serverId,
String store,
List<ByteArray> keyList,
HashMap<String, String> baselineTuples,
HashMap<String, VectorClock> baselineVersions) {
// do the positive tests
Iterator<QueryKeyResult> positiveTestResultsItr = admin.streamingOps.queryKeys(serverId,
store,
keyList.iterator());
while(positiveTestResultsItr.hasNext()) {
QueryKeyResult item = positiveTestResultsItr.next();
ByteArray key = item.getKey();
List<Versioned<byte[]>> vals = item.getValues();
Exception e = item.getException();
assertEquals("Error fetching key " + key, null, e);
assertEquals("Value not found for key " + key, true, vals != null & vals.size() != 0);
String keyStr = ByteUtils.getString(key.get(), "UTF-8");
if(baselineTuples != null)
assertEquals("Value does not match up ",
baselineTuples.get(keyStr),
ByteUtils.getString(vals.get(0).getValue(), "UTF-8"));
if(baselineVersions != null)
assertEquals("Version does not match up",
baselineVersions.get(keyStr),
vals.get(0).getVersion());
}
}
/**
* REFACTOR: these should belong AdminClient so existence checks can be done
* easily across the board
*
* @param admin
* @param serverId
* @param store
* @param keyList
*/
protected void checkForKeyNonExistence(AdminClient admin,
int serverId,
String store,
List<ByteArray> keyList) {
Iterator<QueryKeyResult> negativeTestResultsItr = admin.streamingOps.queryKeys(serverId,
store,
keyList.iterator());
while(negativeTestResultsItr.hasNext()) {
QueryKeyResult item = negativeTestResultsItr.next();
ByteArray key = item.getKey();
List<Versioned<byte[]>> vals = item.getValues();
Exception e = item.getException();
assertEquals("Error fetching key " + key, null, e);
assertEquals("Value " + vals + "found for key " + key,
true,
vals == null || vals.size() == 0);
}
}
}