/* * Copyright 2008-2014 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.admin.command; import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintStream; import java.io.StringReader; import java.io.StringWriter; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import joptsimple.OptionParser; import joptsimple.OptionSet; import org.apache.avro.Schema; import org.apache.avro.generic.GenericDatumReader; import org.apache.avro.generic.GenericRecord; import org.apache.avro.io.JsonDecoder; import org.apache.commons.codec.DecoderException; import org.codehaus.jackson.JsonFactory; import org.codehaus.jackson.map.ObjectMapper; import voldemort.VoldemortException; import voldemort.client.protocol.admin.AdminClient; import voldemort.client.protocol.admin.QueryKeyResult; import voldemort.cluster.Cluster; import voldemort.cluster.Zone; import voldemort.routing.BaseStoreRoutingPlan; import voldemort.routing.StoreRoutingPlan; import voldemort.serialization.DefaultSerializerFactory; import voldemort.serialization.SerializationException; import voldemort.serialization.Serializer; import voldemort.serialization.SerializerDefinition; import voldemort.serialization.SerializerFactory; import voldemort.serialization.json.JsonReader; import voldemort.store.InvalidMetadataException; import voldemort.store.StoreDefinition; import voldemort.store.compress.CompressionStrategy; import voldemort.store.compress.CompressionStrategyFactory; import voldemort.tools.admin.AdminParserUtils; import voldemort.tools.admin.AdminToolUtils; import voldemort.utils.ByteArray; import voldemort.utils.ByteUtils; import voldemort.utils.StoreDefinitionUtils; import voldemort.utils.Utils; import voldemort.versioning.VectorClock; import voldemort.versioning.Versioned; import com.google.common.base.Objects; import com.sleepycat.persist.StoreNotFoundException; /** * Implements all debug commands. */ public class AdminCommandDebug extends AbstractAdminCommand { /** * Parses command-line and directs to sub-commands. * * @param args Command-line input * @throws Exception */ public static void executeCommand(String[] args) throws Exception { String subCmd = (args.length > 0) ? args[0] : ""; args = AdminToolUtils.copyArrayCutFirst(args); if(subCmd.equals("query-keys")) { SubCommandDebugQueryKeys.executeCommand(args); } else if(subCmd.equals("route")) { SubCommandDebugRoute.executeCommand(args); } else { printHelp(System.out); } } /** * Prints command-line help menu. */ public static void printHelp(PrintStream stream) { stream.println(); stream.println("Voldemort Admin Tool Debug Commands"); stream.println("-----------------------------------"); stream.println("query-keys Query stores for a set of keys."); stream.println("route Show detailed routing plan for a given set of keys on a store."); stream.println(); stream.println("To get more information on each command,"); stream.println("please try \'help debug <command-name>\'."); stream.println(); } /** * Parses command-line input and prints help menu. * * @throws Exception */ public static void executeHelp(String[] args, PrintStream stream) throws Exception { String subCmd = (args.length > 0) ? args[0] : ""; if(subCmd.equals("query-keys")) { SubCommandDebugQueryKeys.printHelp(stream); } else if(subCmd.equals("route")) { SubCommandDebugRoute.printHelp(stream); } else { printHelp(stream); } } /** * debug query-keys command */ public static class SubCommandDebugQueryKeys extends AbstractAdminCommand { /** * Initializes parser * * @return OptionParser object with all available options */ protected static OptionParser getParser() { OptionParser parser = new OptionParser(); // help options AdminParserUtils.acceptsHelp(parser); // required options AdminParserUtils.acceptsHex(parser); // either --hex or // --json AdminParserUtils.acceptsJson(parser); // either --hex or // --json AdminParserUtils.acceptsStoreMultiple(parser); AdminParserUtils.acceptsUrl(parser); // optional options AdminParserUtils.acceptsNodeMultiple(parser); // either // --node or // --all-nodes AdminParserUtils.acceptsAllNodes(parser); // either --node or // --all-nodes return parser; } /** * Prints help menu for command. * * @param stream PrintStream object for output * @throws IOException */ public static void printHelp(PrintStream stream) throws IOException { stream.println(); stream.println("NAME"); stream.println(" debug query-keys - Query stores for a set of keys"); stream.println(); stream.println("SYNOPSIS"); stream.println(" debug query-keys ((-x | -j) <key-list>) -s <store-name-list> -u <url>"); stream.println(" [-n <node-id-list> | --all-nodes]"); stream.println(); getParser().printHelpOn(stream); stream.println(); } /** * Parses command-line and queries stores for a set of keys * * @param args Command-line input * @throws IOException * */ @SuppressWarnings("unchecked") public static void executeCommand(String[] args) throws IOException { OptionParser parser = getParser(); // declare parameters String keyType = null; List<String> keyStrings = null; List<String> storeNames = null; String url = null; List<Integer> nodeIds = null; Boolean allNodes = true; // parse command-line input OptionSet options = parser.parse(args); if(options.has(AdminParserUtils.OPT_HELP)) { printHelp(System.out); return; } // check required options and/or conflicting options AdminParserUtils.checkRequired(options, AdminParserUtils.OPT_STORE); AdminParserUtils.checkRequired(options, AdminParserUtils.OPT_URL); AdminParserUtils.checkRequired(options, AdminParserUtils.OPT_HEX, AdminParserUtils.OPT_JSON); AdminParserUtils.checkOptional(options, AdminParserUtils.OPT_NODE, AdminParserUtils.OPT_ALL_NODES); // load parameters if(options.has(AdminParserUtils.OPT_HEX)) { keyType = AdminParserUtils.OPT_HEX; keyStrings = (List<String>) options.valuesOf(AdminParserUtils.OPT_HEX); } else if(options.has(AdminParserUtils.OPT_JSON)) { keyType = AdminParserUtils.OPT_JSON; keyStrings = (List<String>) options.valuesOf(AdminParserUtils.OPT_JSON); } storeNames = (List<String>) options.valuesOf(AdminParserUtils.OPT_STORE); url = (String) options.valueOf(AdminParserUtils.OPT_URL); if(options.has(AdminParserUtils.OPT_NODE)) { nodeIds = (List<Integer>) options.valuesOf(AdminParserUtils.OPT_NODE); allNodes = false; } // execute command AdminClient adminClient = AdminToolUtils.getAdminClient(url); if(allNodes) { nodeIds = AdminToolUtils.getAllNodeIds(adminClient); } doDebugQueryKeys(adminClient, nodeIds, storeNames, keyStrings, keyType); } private static void doDebugQueryKey(AdminClient adminClient, List<Integer> queryingNodes, List<String> storeNames, String keyString, String keyFormat) throws IOException { Map<String, StoreDefinition> storeDefinitions = AdminToolUtils.getUserStoreDefMapOnNode(adminClient, queryingNodes.get(0)); BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out)); // iterate through stores for(final String storeName: storeNames) { // store definition StoreDefinition storeDefinition = storeDefinitions.get(storeName); if(storeDefinition == null) { throw new StoreNotFoundException("Store " + storeName + " not found"); } out.write("STORE_NAME: " + storeDefinition.getName() + "\n"); // k-v serializer final SerializerDefinition keySerializerDef = storeDefinition.getKeySerializer(); final SerializerDefinition valueSerializerDef = storeDefinition.getValueSerializer(); SerializerFactory serializerFactory = new DefaultSerializerFactory(); @SuppressWarnings("unchecked") final Serializer<Object> keySerializer = (Serializer<Object>) serializerFactory.getSerializer(keySerializerDef); @SuppressWarnings("unchecked") final Serializer<Object> valueSerializer = (Serializer<Object>) serializerFactory.getSerializer(valueSerializerDef); // compression strategy final CompressionStrategy keyCompressionStrategy; final CompressionStrategy valueCompressionStrategy; if(keySerializerDef != null && keySerializerDef.hasCompression()) { keyCompressionStrategy = new CompressionStrategyFactory().get(keySerializerDef.getCompression()); } else { keyCompressionStrategy = null; } if(valueSerializerDef != null && valueSerializerDef.hasCompression()) { valueCompressionStrategy = new CompressionStrategyFactory().get(valueSerializerDef.getCompression()); } else { valueCompressionStrategy = null; } if(keyCompressionStrategy == null) { out.write("KEY_COMPRESSION_STRATEGY: None\n"); } else { out.write("KEY_COMPRESSION_STRATEGY: " + keyCompressionStrategy.getType() + "\n"); } out.write("KEY_SERIALIZER_NAME: " + keySerializerDef.getName() + "\n"); for(Map.Entry<Integer, String> entry: keySerializerDef.getAllSchemaInfoVersions() .entrySet()) { out.write(String.format("KEY_SCHEMA VERSION=%d\n", entry.getKey())); out.write("====================================\n"); out.write(entry.getValue()); out.write("\n====================================\n"); } out.write("\n"); if(valueCompressionStrategy == null) { out.write("VALUE_COMPRESSION_STRATEGY: None\n"); } else { out.write("VALUE_COMPRESSION_STRATEGY: " + valueCompressionStrategy.getType() + "\n"); } out.write("VALUE_SERIALIZER_NAME: " + valueSerializerDef.getName() + "\n"); for(Map.Entry<Integer, String> entry: valueSerializerDef.getAllSchemaInfoVersions() .entrySet()) { out.write(String.format("VALUE_SCHEMA %d\n", entry.getKey())); out.write("====================================\n"); out.write(entry.getValue()); out.write("\n====================================\n"); } out.write("\n"); // although the streamingOps support multiple keys, we only // query // one key here ByteArray key; try { if(keyFormat.equals(AdminParserUtils.OPT_JSON)) { Object keyObject; String keySerializerName = keySerializerDef.getName(); if(isAvroSchema(keySerializerName)) { Schema keySchema = Schema.parse(keySerializerDef.getCurrentSchemaInfo()); JsonDecoder decoder = new JsonDecoder(keySchema, keyString); GenericDatumReader<Object> datumReader = new GenericDatumReader<Object>(keySchema); keyObject = datumReader.read(null, decoder); } else if(keySerializerName.equals(DefaultSerializerFactory.JSON_SERIALIZER_TYPE_NAME)) { JsonReader jsonReader = new JsonReader(new StringReader(keyString)); keyObject = jsonReader.read(); } else { keyObject = keyString; } key = new ByteArray(keySerializer.toBytes(keyObject)); } else { key = new ByteArray(ByteUtils.fromHexString(keyString)); } } catch(SerializationException se) { System.err.println("Error serializing key " + keyString); System.err.println("If this is a JSON key, you need to include escaped quotation marks in the command line if it is a string"); se.printStackTrace(); return; } catch(DecoderException de) { System.err.println("Error decoding key " + keyString); de.printStackTrace(); return; } catch(IOException io) { System.err.println("Error parsing avro string " + keyString); io.printStackTrace(); return; } boolean printedKey = false; // A Map<> could have been used instead of List<Entry<>> if // Versioned supported correct hash codes. Read the comment in // Versioned about the issue List<Entry<List<Versioned<byte[]>>, List<Integer>>> nodeValues = new ArrayList<Entry<List<Versioned<byte[]>>, List<Integer>>>(); for(final Integer queryNodeId: queryingNodes) { Iterator<QueryKeyResult> iterator; iterator = adminClient.streamingOps.queryKeys(queryNodeId, storeName, Arrays.asList(key).iterator()); final StringWriter stringWriter = new StringWriter(); QueryKeyResult queryKeyResult = iterator.next(); if(!printedKey) { // de-serialize and write key byte[] keyBytes = queryKeyResult.getKey().get(); Object keyObject = keySerializer.toObject((null == keyCompressionStrategy) ? keyBytes : keyCompressionStrategy.inflate(keyBytes)); writeVoldKeyOrValueInternal(keyBytes, keySerializer, keyCompressionStrategy, "KEY", out); printedKey = true; } // iterate through, de-serialize and write values if(queryKeyResult.hasValues() && queryKeyResult.getValues().size() > 0) { int elementId = -1; for(int i = 0; i < nodeValues.size(); i++) { if(Objects.equal(nodeValues.get(i).getKey(), queryKeyResult.getValues())) { elementId = i; break; } } if(elementId == -1) { ArrayList<Integer> nodes = new ArrayList<Integer>(); nodes.add(queryNodeId); nodeValues.add(new AbstractMap.SimpleEntry<List<Versioned<byte[]>>, List<Integer>>(queryKeyResult.getValues(), nodes)); } else { nodeValues.get(elementId).getValue().add(queryNodeId); } out.write(String.format("\nQueried node %d on store %s\n", queryNodeId, storeName)); int versionCount = 0; if(queryKeyResult.getValues().size() > 1) { out.write("VALUE " + versionCount + "\n"); } for(Versioned<byte[]> versioned: queryKeyResult.getValues()) { // write version VectorClock version = (VectorClock) versioned.getVersion(); out.write("VECTOR_CLOCK_BYTE: " + ByteUtils.toHexString(version.toBytes()) + "\n"); out.write("VECTOR_CLOCK_TEXT: " + version.toString() + '[' + new Date(version.getTimestamp()).toString() + "]\n"); // write value byte[] valueBytes = versioned.getValue(); writeVoldKeyOrValueInternal(valueBytes, valueSerializer, valueCompressionStrategy, "VALUE", out); versionCount++; } } // If a node does not host a key, it returns // invalidmetdata // exception. else if(queryKeyResult.hasException()) { boolean isInvalidMetadataException = queryKeyResult.getException() instanceof InvalidMetadataException; // Print the exception if not InvalidMetadataException // or // you are querying only a single node. if(!isInvalidMetadataException || queryingNodes.size() == 1) { out.write(String.format("\nNode %d on store %s returned exception\n", queryNodeId, storeName)); out.write(queryKeyResult.getException().toString()); out.write("\n====================================\n"); } } else { if(queryingNodes.size() == 1) { out.write(String.format("\nNode %d on store %s returned NULL\n", queryNodeId, storeName)); out.write("\n====================================\n"); } } out.flush(); } out.write("\n====================================\n"); for(Map.Entry<List<Versioned<byte[]>>, List<Integer>> nodeValue: nodeValues) { out.write("Nodes with same Value " + Arrays.toString(nodeValue.getValue().toArray())); out.write("\n====================================\n"); } if(nodeValues.size() > 1) { out.write("\n*** Multiple (" + nodeValues.size() + ") versions of key/value exist for the key ***\n"); } out.flush(); } } private static void writeVoldKeyOrValueInternal(byte[] input, Serializer<Object> serializer, CompressionStrategy compressionStrategy, String prefix, BufferedWriter out) throws IOException { out.write(prefix + "_BYTES\n====================================\n"); out.write(ByteUtils.toHexString(input)); out.write("\n====================================\n"); try { Object inputObject = serializer.toObject((null == compressionStrategy) ? input : compressionStrategy.inflate(input)); out.write(prefix + "_TEXT\n====================================\n"); if(inputObject instanceof GenericRecord) { out.write(inputObject.toString()); } else { new JsonFactory(new ObjectMapper()).createJsonGenerator(out) .writeObject(inputObject); } out.write("\n====================================\n\n"); } catch(SerializationException e) {} } /** * Queries stores for a set of keys * * @param adminClient An instance of AdminClient points to given cluster * @param nodeIds Node ids to query keys from * @param storeNames Stores to be queried * @param keyStrings Keys to be queried * @param keyType Format of the keys: hex, json * @throws IOException * */ public static void doDebugQueryKeys(AdminClient adminClient, List<Integer> nodeIds, List<String> storeNames, List<String> keyStrings, String keyType) throws IOException { // decide queryNode for storeDef Integer storeDefNodeId = nodeIds.get(0); Map<String, StoreDefinition> storeDefinitions = AdminToolUtils.getUserStoreDefMapOnNode(adminClient, storeDefNodeId); for(String keyString: keyStrings) { doDebugQueryKey(adminClient, nodeIds, storeNames, keyString, keyType); } } /** * Tells if serializer is avro schema * * @param serializerName Serializer to be checked * @return */ private static boolean isAvroSchema(String serializerName) { if(serializerName.equals(DefaultSerializerFactory.AVRO_GENERIC_VERSIONED_TYPE_NAME) || serializerName.equals(DefaultSerializerFactory.AVRO_GENERIC_TYPE_NAME) || serializerName.equals(DefaultSerializerFactory.AVRO_REFLECTIVE_TYPE_NAME) || serializerName.equals(DefaultSerializerFactory.AVRO_SPECIFIC_TYPE_NAME)) { return true; } else { return false; } } } /** * debug route command */ public static class SubCommandDebugRoute extends AbstractAdminCommand { /** * Initializes parser * * @return OptionParser object with all available options */ protected static OptionParser getParser() { OptionParser parser = new OptionParser(); // help options AdminParserUtils.acceptsHelp(parser); // required options AdminParserUtils.acceptsHex(parser); // either --hex or // --json AdminParserUtils.acceptsJson(parser); // either --hex or // --json AdminParserUtils.acceptsStoreSingle(parser); AdminParserUtils.acceptsUrl(parser); return parser; } /** * Prints help menu for command. * * @param stream PrintStream object for output * @throws IOException */ public static void printHelp(PrintStream stream) throws IOException { stream.println(); stream.println("NAME"); stream.println(" debug route - Show detailed routing plan for a given set of keys on a store"); stream.println(); stream.println("SYNOPSIS"); stream.println(" debug route ((-x | -j) <key-list>) -s <store-name> -u <url>"); stream.println(); getParser().printHelpOn(stream); stream.println(); } /** * Parses command-line and shows detailed routing information for a * given set of keys on a store. * * Usage: debug route ((-h | -j) <key-list>) -s <store-name> -u <url> * * @param args Command-line input * @throws Exception * */ @SuppressWarnings("unchecked") public static void executeCommand(String[] args) throws Exception { OptionParser parser = getParser(); // declare parameters String keyType = null; List<String> keyStrings = null; String storeName = null; String url = null; // parse command-line input OptionSet options = parser.parse(args); if(options.has(AdminParserUtils.OPT_HELP)) { printHelp(System.out); return; } // check required options and/or conflicting options AdminParserUtils.checkRequired(options, AdminParserUtils.OPT_STORE); AdminParserUtils.checkRequired(options, AdminParserUtils.OPT_URL); AdminParserUtils.checkRequired(options, AdminParserUtils.OPT_HEX, AdminParserUtils.OPT_JSON); // load parameters if(options.has(AdminParserUtils.OPT_HEX)) { keyType = AdminParserUtils.OPT_HEX; keyStrings = (List<String>) options.valuesOf(AdminParserUtils.OPT_HEX); } else if(options.has(AdminParserUtils.OPT_JSON)) { keyType = AdminParserUtils.OPT_JSON; keyStrings = (List<String>) options.valuesOf(AdminParserUtils.OPT_JSON); throw new VoldemortException("Key type OPT_JSON not supported."); } storeName = (String) options.valueOf(AdminParserUtils.OPT_STORE); url = (String) options.valueOf(AdminParserUtils.OPT_URL); // execute command AdminClient adminClient = AdminToolUtils.getAdminClient(url); doDebugRoute(adminClient, storeName, keyStrings, keyType); } /** * Shows detailed routing information for a given set of keys on a * store. * * @param adminClient An instance of AdminClient points to given cluster * @param storeName Store that contains keys * @param keyStrings Keys to show routing plan * @param keyType Format of the keys: hex * @throws DecoderException * @throws IOException * */ public static void doDebugRoute(AdminClient adminClient, String storeName, List<String> keyStrings, String keyType) throws DecoderException { Cluster cluster = adminClient.getAdminClientCluster(); List<StoreDefinition> storeDefs = adminClient.metadataMgmtOps.getRemoteStoreDefList() .getValue(); StoreDefinition storeDef = StoreDefinitionUtils.getStoreDefinitionWithName(storeDefs, storeName); StoreRoutingPlan routingPlan = new StoreRoutingPlan(cluster, storeDef); BaseStoreRoutingPlan bRoutingPlan = new BaseStoreRoutingPlan(cluster, storeDef); final int COLUMN_WIDTH = 30; for(String keyStr: keyStrings) { byte[] key = ByteUtils.fromHexString(keyStr); System.out.println("Key :" + keyStr); System.out.println("Replicating Partitions :" + routingPlan.getReplicatingPartitionList(key)); System.out.println("Replicating Nodes :"); List<Integer> nodeList = routingPlan.getReplicationNodeList(routingPlan.getMasterPartitionId(key)); for(int i = 0; i < nodeList.size(); i++) { System.out.println(nodeList.get(i) + "\t" + cluster.getNodeById(nodeList.get(i)).getHost()); } System.out.println("Zone Nary information :"); HashMap<Integer, Integer> zoneRepMap = storeDef.getZoneReplicationFactor(); for(Zone zone: cluster.getZones()) { System.out.println("\tZone #" + zone.getId()); int numReplicas = -1; if(zoneRepMap == null) { // non zoned cluster numReplicas = storeDef.getReplicationFactor(); } else { // zoned cluster if(!zoneRepMap.containsKey(zone.getId())) { Utils.croak("Repfactor for Zone " + zone.getId() + " not found in storedef"); } numReplicas = zoneRepMap.get(zone.getId()); } String FormatString = "%s %s %s\n"; System.out.format(FormatString, Utils.paddedString("REPLICA#", COLUMN_WIDTH), Utils.paddedString("PARTITION", COLUMN_WIDTH), Utils.paddedString("NODE", COLUMN_WIDTH)); for(int i = 0; i < numReplicas; i++) { Integer nodeId = bRoutingPlan.getNodeIdForZoneNary(zone.getId(), i, key); Integer partitionId = routingPlan.getNodesPartitionIdForKey(nodeId, key); System.out.format(FormatString, Utils.paddedString(i + "", COLUMN_WIDTH), Utils.paddedString(partitionId.toString(), COLUMN_WIDTH), Utils.paddedString(nodeId + "(" + cluster.getNodeById(nodeId) .getHost() + ")", COLUMN_WIDTH)); } System.out.println(); } System.out.println("-----------------------------------------------"); System.out.println(); } } } }