/* * Copyright 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.tools; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Set; import joptsimple.OptionException; import joptsimple.OptionParser; import joptsimple.OptionSet; import org.apache.log4j.Logger; import voldemort.client.protocol.admin.AdminClient; import voldemort.cluster.Cluster; import voldemort.cluster.Node; import voldemort.routing.RoutingStrategy; import voldemort.routing.RoutingStrategyFactory; import voldemort.store.StoreDefinition; import voldemort.store.readonly.ReadOnlyStorageConfiguration; import voldemort.utils.CmdUtils; import voldemort.utils.Utils; import com.google.common.base.Joiner; import com.google.common.collect.Lists; public class ReadOnlyReplicationHelperCLI { private static Logger logger = Logger.getLogger(ReadOnlyReplicationHelperCLI.class); public final static int DEFAULT_NODE_ID = 0; public final static String SPLIT_LITERAL = "_"; private final static String OPT_HELP = "help"; private final static String OPT_URL = "url"; private final static String OPT_NODE = "node"; private final static String OPT_OUTPUT = "output"; private final static String OPT_LOCAL = "local"; private static OptionParser parser; private static void setupParser() { parser = new OptionParser(); parser.accepts(OPT_HELP, "Print usage information"); parser.accepts(OPT_URL, "Voldemort cluster bootstrap url") .withRequiredArg() .describedAs("bootstrap-url") .ofType(String.class); parser.accepts(OPT_NODE, "Destination node id on which files will be copied") .withRequiredArg() .describedAs("node-id") .ofType(Integer.class); parser.accepts(OPT_OUTPUT, "Output file that contains lines of: store_name,src_node_id,src_file_name,dest_file_name") .withRequiredArg() .describedAs("output-file-path") .ofType(String.class); parser.accepts(OPT_LOCAL, "Get output without actually querying data files exist on remote nodes"); } private static void printUsage() { StringBuilder help = new StringBuilder(); help.append("\n"); help.append("NAME\n"); help.append(" ReadOnlyReplicationHelper - Get read-only data files to replicate for a node \n"); help.append("\n"); help.append("SYNOPSIS\n"); help.append(" --" + OPT_URL + " <bootstrap-url> --" + OPT_NODE + " <node-id> [--" + OPT_OUTPUT + " <output-file-path>]\n"); help.append("\n"); System.out.print(help.toString()); try { parser.printHelpOn(System.out); } catch(IOException e) { e.printStackTrace(); } } private static void printUsageAndDie(String errMessage) { printUsage(); Utils.croak("\n" + errMessage + "\n"); } private static OptionSet getValidOptions(String[] args) { OptionSet options = null; try { options = parser.parse(args); } catch(OptionException oe) { printUsageAndDie("Exception when parsing arguments : " + oe.getMessage()); } if(options.has(OPT_HELP)) { printUsage(); System.exit(0); } Set<String> missing = CmdUtils.missing(options, OPT_URL, OPT_NODE); if(missing.size() > 0) { printUsageAndDie("Missing required arguments: " + Joiner.on(", ").join(missing)); } return options; } private static List<String> getROStorageFileListLocally(List<Integer> nodePartitionIds, RoutingStrategy routingStrategy) { List<String> fileList = Lists.newArrayList(); // Go over every partition and find out all buckets ( pair of master // partition id and replica type ) for(Node node: routingStrategy.getNodes()) { for(int partitionId: node.getPartitionIds()) { List<Integer> routingPartitionList = routingStrategy.getReplicatingPartitionList(partitionId); // Find intersection with nodes partition ids for(int replicatingPartition = 0; replicatingPartition < routingPartitionList.size(); replicatingPartition++) { if(nodePartitionIds.contains(routingPartitionList.get(replicatingPartition))) { fileList.add(Integer.toString(routingPartitionList.get(0)) + "_" + Integer.toString(replicatingPartition) + "_" + "*"); break; } } } } return fileList; } /** * This method take a list of fileName of the type partitionId_Replica_Chunk * and returns file names that match the regular expression * masterPartitionId_ */ private static List<String> parseAndCompare(List<String> fileNames, int masterPartitionId) { List<String> sourceFileNames = new ArrayList<String>(); for(String fileName: fileNames) { String[] partitionIdReplicaChunk = fileName.split(SPLIT_LITERAL); if(Integer.parseInt(partitionIdReplicaChunk[0]) == masterPartitionId) { sourceFileNames.add(fileName); } } return sourceFileNames; } /** * Analyze read-only storage file replication info, for replicating * the partitions on the given nodeId. Note that because that node * is being replicated, we cannot rely on the node being up or * consistent, so we carefully avoid talking to it. * * @param cluster * @param nodeId * @return List of read-only replication info in format of: * store_name,src_node_id,src_file_name,dest_file_name */ public static List<String> getReadOnlyReplicationInfo(AdminClient adminClient, Integer nodeId, Boolean local) { List<String> infoList = Lists.newArrayList(); Cluster cluster = adminClient.getAdminClientCluster(); List<StoreDefinition> storeDefs = adminClient.rebalanceOps.getCurrentStoreDefinitionsExcept(cluster, nodeId); for(StoreDefinition storeDef: storeDefs) { String storeName = storeDef.getName(); // Skip a store without replication, or has wrong ro storage format. String storageFormat = null; if(storeDef.getReplicationFactor() <= 1) { logger.error("Store " + storeName + " cannot be restored, as it has replication factor = " + storeDef.getReplicationFactor()); continue; } if(!storeDef.getType().equals(ReadOnlyStorageConfiguration.TYPE_NAME)) { logger.error("Store " + storeName + " cannot be restored, as it has type = " + storeDef.getType() + " instead of " + ReadOnlyStorageConfiguration.TYPE_NAME); continue; } logger.info("Processing store " + storeName); RoutingStrategy strategy = new RoutingStrategyFactory().updateRoutingStrategy(storeDef, cluster); // Go over the entire partitions and find if the destination node // belong in the replication partition list for(int masterPartitionId = 0; masterPartitionId < cluster.getNumberOfPartitions(); ++masterPartitionId) { List<Integer> naryPartitionIds = strategy.getReplicatingPartitionList(masterPartitionId); int nary = 0; Boolean hasPartition = false; for(int naryPartitionId: naryPartitionIds) { Node naryNode = cluster.getNodeForPartitionId(naryPartitionId); if(naryNode.getId() == nodeId) { // Test if destination node has the partition of // masterPartitionId, and record what kind of replica it // is and break out of the loop hasPartition = true; naryPartitionIds.remove(nary); break; } nary++; } if(!hasPartition) { logger.trace("Node " + nodeId + " doesn't have partition " + masterPartitionId); continue; } // Now find out which node hosts one of these replicas Node sourceNode = cluster.getNodeForPartitionId(naryPartitionIds.get(0)); Integer sourceNodeId = sourceNode.getId(); Long version = adminClient.readonlyOps.getROCurrentVersion(sourceNodeId, Arrays.asList(storeName)) .get(storeName); // Now get all the file names from this node. List<String> fileNames = null; if(local) { List<Integer> srcPartitionIds = Lists.newArrayList(); srcPartitionIds.addAll(sourceNode.getPartitionIds()); fileNames = getROStorageFileListLocally(srcPartitionIds, strategy); } else { fileNames = adminClient.readonlyOps.getROStorageFileList(sourceNode.getId(), storeName); } List<String> sourceFileNames = parseAndCompare(fileNames, masterPartitionId); // Compare if there are files that relate to the // masterPartitionId if(sourceFileNames.size() > 0) { for(String sourceFileName: sourceFileNames) { String[] partitionIdReplicaChunk = sourceFileName.split(SPLIT_LITERAL); // At the destination node the replicaId will be // different, so only change it to nary String partitionId = partitionIdReplicaChunk[0]; String replicaId = String.valueOf(nary); String chunkId = partitionIdReplicaChunk[2]; // Now concat the parts together to create the file name // on the destination node String destFileName = partitionId.concat(SPLIT_LITERAL) .concat(replicaId) .concat(SPLIT_LITERAL) .concat(chunkId); String sourceRelPath = storeName + "/version-" + version + "/" + sourceFileName; String destRelPath = storeName + "/version-" + version + "/" + destFileName; infoList.add(sourceNode.getHost() + "," + sourceNode.getId() + "," + sourceRelPath + "," + destRelPath); } } else { logger.warn("Cannot find file for partition " + masterPartitionId + " on source node " + sourceNode.getId()); } } } return infoList; } public static void main(String[] args) throws Exception { setupParser(); OptionSet options = getValidOptions(args); // Required args String url = (String) options.valueOf(OPT_URL); Integer nodeId = (Integer) options.valueOf(OPT_NODE); PrintStream outputStream; if(options.has(OPT_OUTPUT)) { String output = (String) options.valueOf(OPT_OUTPUT); outputStream = new PrintStream(output); } else { outputStream = System.out; } Boolean local = options.has(OPT_LOCAL); AdminClient adminClient = new AdminClient(url); outputStream.println("src_host_name,src_node_id,src_rel_path,dest_rel_path"); List<String> infoList = getReadOnlyReplicationInfo(adminClient, nodeId, local); for(String info: infoList) { outputStream.println(info); } if(outputStream != System.out) { outputStream.close(); } } }