// This file is part of OpenTSDB. // Copyright (C) 2010-2012 The OpenTSDB Authors. // // This program is free software: you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published by // the Free Software Foundation, either version 2.1 of the License, or (at your // option) any later version. This program is distributed in the hope that it // will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty // of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser // General Public License for more details. You should have received a copy // of the GNU Lesser General Public License along with this program. If not, // see <http://www.gnu.org/licenses/>. package net.opentsdb.tools; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.hbase.async.AtomicIncrementRequest; import org.hbase.async.Bytes; import org.hbase.async.DeleteRequest; import org.hbase.async.GetRequest; import org.hbase.async.HBaseClient; import org.hbase.async.HBaseException; import org.hbase.async.KeyValue; import org.hbase.async.PutRequest; import org.hbase.async.Scanner; import net.opentsdb.core.TSDB; import net.opentsdb.meta.TSMeta; import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.uid.UniqueId.UniqueIdType; import net.opentsdb.utils.Config; /** * Command line tool to manipulate UIDs. * Can be used to find or assign UIDs. */ final class UidManager { private static final Logger LOG = LoggerFactory.getLogger(UidManager.class); /** Prints usage. */ static void usage(final String errmsg) { usage(null, errmsg); } /** Prints usage. */ static void usage(final ArgP argp, final String errmsg) { System.err.println(errmsg); System.err.println("Usage: uid <subcommand> args\n" + "Sub commands:\n" + " grep [kind] <RE>: Finds matching IDs.\n" + " assign <kind> <name> [names]:" + " Assign an ID for the given name(s).\n" + " rename <kind> <name> <newname>: Renames this UID.\n" + " delete <kind> <name>: Deletes this UID.\n" + " fsck: [fix] [delete_unknown] Checks the consistency of UIDs.\n" + " fix - Fix errors. By default errors are logged.\n" + " delete_unknown - Remove columns with unknown qualifiers.\n" + " The \"fix\" flag must be supplied as well.\n" + "\n" + " [kind] <name>: Lookup the ID of this name.\n" + " [kind] <ID>: Lookup the name of this ID.\n" + " metasync: Generates missing TSUID and UID meta entries, updates\n" + " created timestamps\n" + " metapurge: Removes meta data entries from the UID table\n" + " treesync: Process all timeseries meta objects through tree rules\n" + " treepurge <id> [definition]: Purge a tree and/or the branches\n" + " from storage. Provide an integer Tree ID and optionally\n" + " add \"true\" to delete the tree definition\n\n" + "Example values for [kind]:" + " metrics, tagk (tag name), tagv (tag value)."); if (argp != null) { System.err.print(argp.usage()); } } public static void main(String[] args) throws Exception { ArgP argp = new ArgP(); CliOptions.addCommon(argp); CliOptions.addVerbose(argp); argp.addOption("--idwidth", "N", "Number of bytes on which the UniqueId is encoded."); argp.addOption("--ignore-case", "Ignore case distinctions when matching a regexp."); argp.addOption("-i", "Short for --ignore-case."); args = CliOptions.parse(argp, args); if (args == null) { usage(argp, "Invalid usage"); System.exit(2); } else if (args.length < 1) { usage(argp, "Not enough arguments"); System.exit(2); } final short idwidth = (argp.has("--idwidth") ? Short.parseShort(argp.get("--idwidth")) : 3); if (idwidth <= 0) { usage(argp, "Negative or 0 --idwidth"); System.exit(3); } final boolean ignorecase = argp.has("--ignore-case") || argp.has("-i"); // get a config object Config config = CliOptions.getConfig(argp); final byte[] table = config.getString("tsd.storage.hbase.uid_table") .getBytes(); final TSDB tsdb = new TSDB(config); tsdb.getClient().ensureTableExists( config.getString("tsd.storage.hbase.uid_table")).joinUninterruptibly(); argp = null; int rc; try { rc = runCommand(tsdb, table, idwidth, ignorecase, args); } finally { try { LOG.info("Shutting down TSD...."); tsdb.getClient().shutdown().joinUninterruptibly(); LOG.info("Gracefully shutdown the TSD"); } catch (Exception e) { LOG.error("Unexpected exception while shutting down", e); rc = 42; } } System.exit(rc); } private static int runCommand(final TSDB tsdb, final byte[] table, final short idwidth, final boolean ignorecase, final String[] args) { final int nargs = args.length; if (args[0].equals("grep")) { if (2 <= nargs && nargs <= 3) { try { return grep(tsdb.getClient(), table, ignorecase, args); } catch (HBaseException e) { return 3; } } else { usage("Wrong number of arguments"); return 2; } } else if (args[0].equals("assign")) { if (nargs < 3) { usage("Wrong number of arguments"); return 2; } return assign(tsdb, table, idwidth, args); } else if (args[0].equals("rename")) { if (nargs != 4) { usage("Wrong number of arguments"); return 2; } return rename(tsdb.getClient(), table, idwidth, args); } else if (args[0].equals("delete")) { if (nargs != 3) { usage("Wrong number of arguments"); return 2; } try { return delete(tsdb, table, args); } catch (Exception e) { LOG.error("Unexpected exception", e); return 4; } } else if (args[0].equals("fsck")) { boolean fix = false; boolean fix_unknowns = false; if (args.length > 1) { for (String arg : args) { if (arg.equals("fix")) { fix = true; } else if (arg.equals("delete_unknown")) { fix_unknowns = true; } } } return fsck(tsdb.getClient(), table, fix, fix_unknowns); } else if (args[0].equals("metasync")) { // check for the data table existence and initialize our plugins // so that update meta data can be pushed to search engines try { tsdb.getClient().ensureTableExists( tsdb.getConfig().getString( "tsd.storage.hbase.data_table")).joinUninterruptibly(); tsdb.initializePlugins(false); return metaSync(tsdb); } catch (Exception e) { LOG.error("Unexpected exception", e); return 3; } } else if (args[0].equals("metapurge")) { // check for the data table existence and initialize our plugins // so that update meta data can be pushed to search engines try { tsdb.getClient().ensureTableExists( tsdb.getConfig().getString( "tsd.storage.hbase.uid_table")).joinUninterruptibly(); return metaPurge(tsdb); } catch (Exception e) { LOG.error("Unexpected exception", e); return 3; } } else if (args[0].equals("treesync")) { // check for the UID table existence try { tsdb.getClient().ensureTableExists( tsdb.getConfig().getString( "tsd.storage.hbase.uid_table")).joinUninterruptibly(); if (!tsdb.getConfig().enable_tree_processing()) { LOG.warn("Tree processing is disabled"); return 0; } return treeSync(tsdb); } catch (Exception e) { LOG.error("Unexpected exception", e); return 3; } } else if (args[0].equals("treepurge")) { if (nargs < 2) { usage("Wrong number of arguments"); return 2; } try { tsdb.getClient().ensureTableExists( tsdb.getConfig().getString( "tsd.storage.hbase.uid_table")).joinUninterruptibly(); final int tree_id = Integer.parseInt(args[1]); final boolean delete_definitions; if (nargs < 3) { delete_definitions = false; } else { final String delete_all = args[2]; if (delete_all.toLowerCase().equals("true")) { delete_definitions = true; } else { delete_definitions = false; } } return purgeTree(tsdb, tree_id, delete_definitions); } catch (Exception e) { LOG.error("Unexpected exception", e); return 3; } } else { if (1 <= nargs && nargs <= 2) { final String kind = nargs == 2 ? args[0] : null; try { final long id = Long.parseLong(args[nargs - 1]); return lookupId(tsdb.getClient(), table, idwidth, id, kind); } catch (NumberFormatException e) { return lookupName(tsdb.getClient(), table, idwidth, args[nargs - 1], kind); } } else { usage("Wrong number of arguments"); return 2; } } } /** * Implements the {@code grep} subcommand. * @param client The HBase client to use. * @param table The name of the HBase table to use. * @param ignorecase Whether or not to ignore the case while grepping. * @param args Command line arguments ({@code [kind] RE}). * @return The exit status of the command (0 means at least 1 match). */ private static int grep(final HBaseClient client, final byte[] table, final boolean ignorecase, final String[] args) { final Scanner scanner = client.newScanner(table); scanner.setMaxNumRows(1024); String regexp; scanner.setFamily(CliUtils.ID_FAMILY); if (args.length == 3) { scanner.setQualifier(CliUtils.toBytes(args[1])); regexp = args[2]; } else { regexp = args[1]; } if (ignorecase) { regexp = "(?i)" + regexp; } scanner.setKeyRegexp(regexp, CliUtils.CHARSET); boolean found = false; try { ArrayList<ArrayList<KeyValue>> rows; while ((rows = scanner.nextRows().joinUninterruptibly()) != null) { for (final ArrayList<KeyValue> row : rows) { found |= printResult(row, CliUtils.ID_FAMILY, true); } } } catch (HBaseException e) { LOG.error("Error while scanning HBase, scanner=" + scanner, e); throw e; } catch (Exception e) { LOG.error("WTF? Unexpected exception type, scanner=" + scanner, e); throw new AssertionError("Should never happen"); } return found ? 0 : 1; } /** * Helper to print the cells in a given family for a given row, if any. * @param row The row to print. * @param family Only cells in this family (if any) will be printed. * @param formard If true, this row contains a forward mapping (name to ID). * Otherwise the row is assumed to contain a reverse mapping (ID to name). * @return {@code true} if at least one cell was printed. */ private static boolean printResult(final ArrayList<KeyValue> row, final byte[] family, final boolean formard) { final byte[] key = row.get(0).key(); String name = formard ? CliUtils.fromBytes(key) : null; String id = formard ? null : Arrays.toString(key); boolean printed = false; for (final KeyValue kv : row) { if (!Bytes.equals(kv.family(), family)) { continue; } printed = true; if (formard) { id = Arrays.toString(kv.value()); } else { name = CliUtils.fromBytes(kv.value()); } System.out.println(CliUtils.fromBytes(kv.qualifier()) + ' ' + name + ": " + id); } return printed; } /** * Implements the {@code assign} subcommand. * @param tsdb The TSDB to use. * @param table The name of the HBase table to use. * @param idwidth Number of bytes on which the UIDs should be. * @param args Command line arguments ({@code assign name [names]}). * @return The exit status of the command (0 means success). */ private static int assign(final TSDB tsdb, final byte[] table, final short idwidth, final String[] args) { boolean randomize = false; if (UniqueId.stringToUniqueIdType(args[1]) == UniqueIdType.METRIC) { randomize = tsdb.getConfig().getBoolean("tsd.core.uid.random_metrics"); } final UniqueId uid = new UniqueId(tsdb.getClient(), table, args[1], (int) idwidth, randomize); for (int i = 2; i < args.length; i++) { try { uid.getOrCreateId(args[i]); // Lookup again the ID we've just created and print it. extactLookupName(tsdb.getClient(), table, idwidth, args[1], args[i]); } catch (HBaseException e) { LOG.error("error while processing " + args[i], e); return 3; } } return 0; } /** * Implements the {@code rename} subcommand. * @param client The HBase client to use. * @param table The name of the HBase table to use. * @param idwidth Number of bytes on which the UIDs should be. * @param args Command line arguments ({@code assign name [names]}). * @return The exit status of the command (0 means success). */ private static int rename(final HBaseClient client, final byte[] table, final short idwidth, final String[] args) { final String kind = args[1]; final String oldname = args[2]; final String newname = args[3]; final UniqueId uid = new UniqueId(client, table, kind, (int) idwidth); try { uid.rename(oldname, newname); } catch (HBaseException e) { LOG.error("error while processing renaming " + oldname + " to " + newname, e); return 3; } catch (NoSuchUniqueName e) { LOG.error(e.getMessage()); return 1; } System.out.println(kind + ' ' + oldname + " -> " + newname); return 0; } /** * Implements the {@code delete} subcommand. * @param client The HBase client to use. * @param table The name of the HBase table to use. * @param args Command line arguments ({@code assign name [names]}). * @return The exit status of the command (0 means success). */ private static int delete(final TSDB tsdb, final byte[] table, final String[] args) throws Exception { final String kind = args[1]; final String name = args[2]; try { tsdb.deleteUidAsync(kind, name).join(); } catch (HBaseException e) { LOG.error("error while processing delete " + name, e); return 3; } catch (NoSuchUniqueName e) { LOG.error(e.getMessage()); return 1; } LOG.info("UID " + kind + ' ' + name + " deleted."); return 0; } /** * Implements the {@code fsck} subcommand. * @param client The HBase client to use. * @param table The name of the HBase table to use. * @return The exit status of the command (0 means success). */ private static int fsck(final HBaseClient client, final byte[] table, final boolean fix, final boolean fix_unknowns) { if (fix) { LOG.info("----------------------------------"); LOG.info("- Running fsck in FIX mode -"); LOG.info("- Remove Unknowns: " + fix_unknowns + " -"); LOG.info("----------------------------------"); } else { LOG.info("Running in log only mode"); } final class Uids { int errors; long maxid; long max_found_id; short width; final HashMap<String, String> id2name = new HashMap<String, String>(); final HashMap<String, String> name2id = new HashMap<String, String>(); void error(final KeyValue kv, final String msg) { error(msg + ". kv=" + kv); } void error(final String msg) { LOG.error(msg); errors++; } /* * Replaces or creates the reverse map in storage and in the local map */ void restoreReverseMap(final String kind, final String name, final String uid) { final PutRequest put = new PutRequest(table, UniqueId.stringToUid(uid), CliUtils.NAME_FAMILY, CliUtils.toBytes(kind), CliUtils.toBytes(name)); client.put(put); id2name.put(uid, name); LOG.info("FIX: Restoring " + kind + " reverse mapping: " + uid + " -> " + name); } /* * Removes the reverse map from storage only */ void removeReverseMap(final String kind, final String name, final String uid) { // clean up meta data too final byte[][] qualifiers = new byte[2][]; qualifiers[0] = CliUtils.toBytes(kind); if (Bytes.equals(CliUtils.METRICS, qualifiers[0])) { qualifiers[1] = CliUtils.METRICS_META; } else if (Bytes.equals(CliUtils.TAGK, qualifiers[0])) { qualifiers[1] = CliUtils.TAGK_META; } else if (Bytes.equals(CliUtils.TAGV, qualifiers[0])) { qualifiers[1] = CliUtils.TAGV_META; } final DeleteRequest delete = new DeleteRequest(table, UniqueId.stringToUid(uid), CliUtils.NAME_FAMILY, qualifiers); client.delete(delete); // can't remove from the id2name map as this will be called while looping LOG.info("FIX: Removed " + kind + " reverse mapping: " + uid + " -> " + name); } } final long start_time = System.nanoTime(); final HashMap<String, Uids> name2uids = new HashMap<String, Uids>(); final Scanner scanner = client.newScanner(table); scanner.setMaxNumRows(1024); int kvcount = 0; try { ArrayList<ArrayList<KeyValue>> rows; while ((rows = scanner.nextRows().joinUninterruptibly()) != null) { for (final ArrayList<KeyValue> row : rows) { for (final KeyValue kv : row) { kvcount++; final byte[] qualifier = kv.qualifier(); // TODO - validate meta data in the future, for now skip it if (Bytes.equals(qualifier, TSMeta.META_QUALIFIER()) || Bytes.equals(qualifier, TSMeta.COUNTER_QUALIFIER()) || Bytes.equals(qualifier, CliUtils.METRICS_META) || Bytes.equals(qualifier, CliUtils.TAGK_META) || Bytes.equals(qualifier, CliUtils.TAGV_META)) { continue; } if (!Bytes.equals(qualifier, CliUtils.METRICS) && !Bytes.equals(qualifier, CliUtils.TAGK) && !Bytes.equals(qualifier, CliUtils.TAGV)) { LOG.warn("Unknown qualifier " + UniqueId.uidToString(qualifier) + " in row " + UniqueId.uidToString(kv.key())); if (fix && fix_unknowns) { final DeleteRequest delete = new DeleteRequest(table, kv.key(), kv.family(), qualifier); client.delete(delete); LOG.info("FIX: Removed unknown qualifier " + UniqueId.uidToString(qualifier) + " in row " + UniqueId.uidToString(kv.key())); } continue; } final String kind = CliUtils.fromBytes(kv.qualifier()); Uids uids = name2uids.get(kind); if (uids == null) { uids = new Uids(); name2uids.put(kind, uids); } final byte[] key = kv.key(); final byte[] family = kv.family(); final byte[] value = kv.value(); if (Bytes.equals(key, CliUtils.MAXID_ROW)) { if (value.length != 8) { uids.error(kv, "Invalid maximum ID for " + kind + ": should be on 8 bytes: "); // TODO - a fix would be to find the max used ID for the type // and store that in the max row. } else { uids.maxid = Bytes.getLong(value); LOG.info("Maximum ID for " + kind + ": " + uids.maxid); } } else { short idwidth = 0; if (Bytes.equals(family, CliUtils.ID_FAMILY)) { idwidth = (short) value.length; final String skey = CliUtils.fromBytes(key); final String svalue = UniqueId.uidToString(value); final long max_found_id; if (Bytes.equals(qualifier, CliUtils.METRICS)) { max_found_id = UniqueId.uidToLong(value, TSDB.metrics_width()); } else if (Bytes.equals(qualifier, CliUtils.TAGK)) { max_found_id = UniqueId.uidToLong(value, TSDB.tagk_width()); } else { max_found_id = UniqueId.uidToLong(value, TSDB.tagv_width()); } if (uids.max_found_id < max_found_id) { uids.max_found_id = max_found_id; } final String id = uids.name2id.put(skey, svalue); if (id != null) { uids.error(kv, "Duplicate forward " + kind + " mapping: " + skey + " -> " + id + " and " + skey + " -> " + svalue); } } else if (Bytes.equals(family, CliUtils.NAME_FAMILY)) { final String skey = UniqueId.uidToString(key); final String svalue = CliUtils.fromBytes(value); idwidth = (short) key.length; final String name = uids.id2name.put(skey, svalue); if (name != null) { uids.error(kv, "Duplicate reverse " + kind + " mapping: " + svalue + " -> " + name + " and " + svalue + " -> " + skey); } } if (uids.width == 0) { uids.width = idwidth; } else if (uids.width != idwidth) { uids.error(kv, "Invalid " + kind + " ID of length " + idwidth + " (expected: " + uids.width + ')'); } } } } } } catch (HBaseException e) { LOG.error("Error while scanning HBase, scanner=" + scanner, e); throw e; } catch (Exception e) { LOG.error("WTF? Unexpected exception type, scanner=" + scanner, e); throw new AssertionError("Should never happen"); } // Match up all forward mappings with their reverse mappings and vice // versa and make sure they agree. int errors = 0; for (final Map.Entry<String, Uids> entry : name2uids.entrySet()) { final String kind = entry.getKey(); final Uids uids = entry.getValue(); // This will be used in the event that we run into an inconsistent forward // mapping that could mean a single UID was assigned to different names. // It SHOULD NEVER HAPPEN, but it could. HashMap<String, TreeSet<String>> uid_collisions = null; // Look for forward mappings without the corresponding reverse mappings. // These are harmful and shouldn't exist. for (final Map.Entry<String, String> nameid : uids.name2id.entrySet()) { final String name = nameid.getKey(); final String id = nameid.getValue(); final String found = uids.id2name.get(id); if (found == null) { uids.error("Forward " + kind + " mapping is missing reverse" + " mapping: " + name + " -> " + id); if (fix) { uids.restoreReverseMap(kind, name, id); } } else if (!found.equals(name)) { uids.error("Forward " + kind + " mapping " + name + " -> " + id + " is different than reverse mapping: " + id + " -> " + found); final String id2 = uids.name2id.get(found); if (id2 != null) { uids.error("Inconsistent forward " + kind + " mapping " + name + " -> " + id + " vs " + name + " -> " + found + " / " + found + " -> " + id2); // This shouldn't happen but there are two possible states: // 1) The reverse map name is wrong // 2) Multiple names are mapped to the same UID, which is REALLY // as we're sending data from different sources to the same time // series. if (fix) { // using the name2id map, build an id -> set of names map. Do it // once, as needed, since it's expensive. if (uid_collisions == null) { uid_collisions = new HashMap<String, TreeSet<String>>(uids.name2id.size()); for (final Map.Entry<String, String> row : uids.name2id.entrySet()) { TreeSet<String> names = uid_collisions.get(row.getValue()); if (names == null) { names = new TreeSet<String>(); uid_collisions.put(row.getValue(), names); } names.add(row.getKey()); } } // if there was only one name to UID map found, then the time // series *should* be OK and we can just fix the reverse map. if (uid_collisions.containsKey(id) && uid_collisions.get(id).size() <= 1) { uids.restoreReverseMap(kind, name, id); } } } else { uids.error("Duplicate forward " + kind + " mapping " + name + " -> " + id + " and " + id2 + " -> " + found); if (fix) { uids.restoreReverseMap(kind, name, id); } } } } // Scan through the UID collisions map and fix the screw ups if (uid_collisions != null) { for (Map.Entry<String, TreeSet<String>> collision : uid_collisions.entrySet()) { if (collision.getValue().size() <= 1) { continue; } // The data in any time series with the errant UID is // a mashup of with all of the names. The best thing to do is // start over. We'll rename the old time series so the user can // still see it if they want to, but delete the forward mappings // so that UIDs can be reassigned and clean series started. // - concatenate all of the names into // "fsck.<name1>.<name2>[...<nameN>]" // - delete the forward mappings for all of the names // - create a mapping with the fsck'd name pointing to the id final StringBuilder fsck_builder = new StringBuilder("fsck"); final String id = collision.getKey(); // compile the new fsck'd name and remove each of the duplicate keys for (String name : collision.getValue()) { fsck_builder.append(".") .append(name); final DeleteRequest delete = new DeleteRequest(table, CliUtils.toBytes(name), CliUtils.ID_FAMILY, CliUtils.toBytes(kind)); client.delete(delete); uids.name2id.remove(name); LOG.info("FIX: Removed forward " + kind + " mapping for " + name + " -> " + id); } // write the new forward map final String fsck_name = fsck_builder.toString(); final PutRequest put = new PutRequest(table, CliUtils.toBytes(fsck_name), CliUtils.ID_FAMILY, CliUtils.toBytes(kind), UniqueId.stringToUid(id)); client.put(put); LOG.info("FIX: Created forward " + kind + " mapping for fsck'd UID " + fsck_name + " -> " + collision.getKey()); // we still need to fix the uids map for the reverse run through below uids.name2id.put(fsck_name, collision.getKey()); uids.restoreReverseMap(kind, fsck_name, id); LOG.error("----------------------------------"); LOG.error("- UID COLLISION DETECTED -"); LOG.error("Corrupted UID [" + collision.getKey() + "] renamed to [" + fsck_name +"]"); LOG.error("----------------------------------"); } } // Look for reverse mappings without the corresponding forward mappings. // These are harmless but shouldn't frequently occur. for (final Map.Entry<String, String> idname : uids.id2name.entrySet()) { final String name = idname.getValue(); final String id = idname.getKey(); final String found = uids.name2id.get(name); if (found == null) { LOG.warn("Reverse " + kind + " mapping is missing forward" + " mapping: " + name + " -> " + id); if (fix) { uids.removeReverseMap(kind, name, id); } } else if (!found.equals(id)) { final String name2 = uids.id2name.get(found); if (name2 != null) { uids.error("Inconsistent reverse " + kind + " mapping " + id + " -> " + name + " vs " + found + " -> " + name + " / " + name2 + " -> " + found); if (fix) { uids.removeReverseMap(kind, name, id); } } else { uids.error("Duplicate reverse " + kind + " mapping " + id + " -> " + name + " and " + found + " -> " + name2); if (fix) { uids.removeReverseMap(kind, name, id); } } } } final int maxsize = Math.max(uids.id2name.size(), uids.name2id.size()); if (uids.maxid > maxsize) { LOG.warn("Max ID for " + kind + " is " + uids.maxid + " but only " + maxsize + " entries were found. Maybe " + (uids.maxid - maxsize) + " IDs were deleted?"); } else if (uids.maxid < uids.max_found_id) { uids.error("We found an ID of " + uids.max_found_id + " for " + kind + " but the max ID is only " + uids.maxid + "! Future IDs may be double-assigned!"); if (fix) { // increment the max UID by the difference. It could be that a TSD may // increment the id immediately before our call, and if so, that would // put us over a little, but that's OK as it's better to waste a few // IDs than to under-run. if (uids.max_found_id == Long.MAX_VALUE) { LOG.error("Ran out of UIDs for " + kind + ". Unable to fix max ID"); } else { final long diff = uids.max_found_id - uids.maxid; final AtomicIncrementRequest air = new AtomicIncrementRequest(table, CliUtils.MAXID_ROW, CliUtils.ID_FAMILY, CliUtils.toBytes(kind), diff); client.atomicIncrement(air); LOG.info("FIX: Updated max ID for " + kind + " to " + uids.max_found_id); } } } if (uids.errors > 0) { LOG.error(kind + ": Found " + uids.errors + " errors."); errors += uids.errors; } } final long timing = (System.nanoTime() - start_time) / 1000000; LOG.info(kvcount + " KVs analyzed in " + timing + "ms (~" + (kvcount * 1000 / timing) + " KV/s)"); if (errors == 0) { LOG.info("No errors found."); return 0; } LOG.warn(errors + " errors found."); return errors; } /** * Looks up an ID and finds the corresponding name(s), if any. * @param client The HBase client to use. * @param table The name of the HBase table to use. * @param idwidth Number of bytes on which the UIDs should be. * @param lid The ID to look for. * @param kind The 'kind' of the ID (can be {@code null}). * @return The exit status of the command (0 means at least 1 found). */ private static int lookupId(final HBaseClient client, final byte[] table, final short idwidth, final long lid, final String kind) { final byte[] id = idInBytes(idwidth, lid); if (id == null) { return 1; } else if (kind != null) { return extactLookupId(client, table, idwidth, kind, id); } return findAndPrintRow(client, table, id, CliUtils.NAME_FAMILY, false); } /** * Gets a given row in HBase and prints it on standard output. * @param client The HBase client to use. * @param table The name of the HBase table to use. * @param key The row key to attempt to get from HBase. * @param family The family in which we're interested. * @return 0 if at least one cell was found and printed, 1 otherwise. */ private static int findAndPrintRow(final HBaseClient client, final byte[] table, final byte[] key, final byte[] family, boolean formard) { final GetRequest get = new GetRequest(table, key); get.family(family); ArrayList<KeyValue> row; try { row = client.get(get).joinUninterruptibly(); } catch (HBaseException e) { LOG.error("Get failed: " + get, e); return 1; } catch (Exception e) { LOG.error("WTF? Unexpected exception type, get=" + get, e); return 42; } return printResult(row, family, formard) ? 0 : 1; } /** * Looks up an ID for a given kind, and prints it if found. * @param client The HBase client to use. * @param table The name of the HBase table to use. * @param idwidth Number of bytes on which the UIDs should be. * @param kind The 'kind' of the ID (must not be {@code null}). * @param id The ID to look for. * @return 0 if the ID for this kind was found, 1 otherwise. */ private static int extactLookupId(final HBaseClient client, final byte[] table, final short idwidth, final String kind, final byte[] id) { final UniqueId uid = new UniqueId(client, table, kind, (int) idwidth); try { final String name = uid.getName(id); System.out.println(kind + ' ' + name + ": " + Arrays.toString(id)); return 0; } catch (NoSuchUniqueId e) { LOG.error(e.getMessage()); return 1; } } /** * Transforms an ID into the corresponding byte array. * @param idwidth Number of bytes on which the UIDs should be. * @param lid The ID to transform. * @return The ID represented in {@code idwidth} bytes, or * {@code null} if {@code lid} couldn't fit in {@code idwidth} bytes. */ private static byte[] idInBytes(final short idwidth, final long lid) { if (idwidth <= 0) { throw new AssertionError("negative idwidth: " + idwidth); } final byte[] id = Bytes.fromLong(lid); for (int i = 0; i < id.length - idwidth; i++) { if (id[i] != 0) { System.err.println(lid + " is too large to fit on " + idwidth + " bytes. Maybe you forgot to adjust --idwidth?"); return null; } } return Arrays.copyOfRange(id, id.length - idwidth, id.length); } /** * Looks up a name and finds the corresponding UID(s), if any. * @param client The HBase client to use. * @param table The name of the HBase table to use. * @param idwidth Number of bytes on which the UIDs should be. * @param name The name to look for. * @param kind The 'kind' of the ID (can be {@code null}). * @return The exit status of the command (0 means at least 1 found). */ private static int lookupName(final HBaseClient client, final byte[] table, final short idwidth, final String name, final String kind) { if (kind != null) { return extactLookupName(client, table, idwidth, kind, name); } return findAndPrintRow(client, table, CliUtils.toBytes(name), CliUtils.ID_FAMILY, true); } /** * Looks up a name for a given kind, and prints it if found. * @param client The HBase client to use. * @param idwidth Number of bytes on which the UIDs should be. * @param kind The 'kind' of the ID (must not be {@code null}). * @param name The name to look for. * @return 0 if the name for this kind was found, 1 otherwise. */ private static int extactLookupName(final HBaseClient client, final byte[] table, final short idwidth, final String kind, final String name) { final UniqueId uid = new UniqueId(client, table, kind, (int) idwidth); try { final byte[] id = uid.getId(name); System.out.println(kind + ' ' + name + ": " + Arrays.toString(id)); return 0; } catch (NoSuchUniqueName e) { LOG.error(e.getMessage()); return 1; } } /** * Runs through the entire data table and creates TSMeta objects for unique * timeseries and/or updates {@code created} timestamps * The process is as follows: * <ul><li>Fetch the max number of Metric UIDs as we'll use those to match * on the data rows</li> * <li>Split the # of UIDs amongst worker threads</li> * <li>Setup a scanner in each thread for the range it will be working on and * start iterating</li> * <li>Fetch the TSUID from the row key</li> * <li>For each unprocessed TSUID: * <ul><li>Check if the metric UID mapping is present, if not, log an error * and continue</li> * <li>See if the meta for the metric UID exists, if not, create it</li> * <li>See if the row timestamp is less than the metric UID meta's created * time. This means we have a record of the UID being used earlier than the * meta data indicates. Update it.</li> * <li>Repeat the previous three steps for each of the TAGK and TAGV tags</li> * <li>Check to see if meta data exists for the timeseries</li> * <li>If not, create the counter column if it's missing, and create the meta * column</li> * <li>If it did exist, check the {@code created} timestamp and if the row's * time is less, update the meta data</li></ul></li> * <li>Continue on to the next unprocessed timeseries data row</li></ul> * <b>Note:</b> Updates or new entries will also be sent to the search plugin * if configured. * @param tsdb The tsdb to use for processing, including a search plugin * @return 0 if completed successfully, something else if it dies */ private static int metaSync(final TSDB tsdb) throws Exception { final long start_time = System.currentTimeMillis() / 1000; // now figure out how many IDs to divy up between the workers final int workers = Runtime.getRuntime().availableProcessors() * 2; final Set<Integer> processed_tsuids = Collections.synchronizedSet(new HashSet<Integer>()); final ConcurrentHashMap<String, Long> metric_uids = new ConcurrentHashMap<String, Long>(); final ConcurrentHashMap<String, Long> tagk_uids = new ConcurrentHashMap<String, Long>(); final ConcurrentHashMap<String, Long> tagv_uids = new ConcurrentHashMap<String, Long>(); final List<Scanner> scanners = CliUtils.getDataTableScanners(tsdb, workers); LOG.info("Spooling up [" + scanners.size() + "] worker threads"); final List<Thread> threads = new ArrayList<Thread>(scanners.size()); int i = 0; for (final Scanner scanner : scanners) { final MetaSync worker = new MetaSync(tsdb, scanner, processed_tsuids, metric_uids, tagk_uids, tagv_uids, i++); worker.setName("Sync #" + i); worker.start(); threads.add(worker); } for (final Thread thread : threads) { thread.join(); LOG.info("Thread [" + thread + "] Finished"); } LOG.info("All metasync threads have completed"); // make sure buffered data is flushed to storage before exiting tsdb.flush().joinUninterruptibly(); final long duration = (System.currentTimeMillis() / 1000) - start_time; LOG.info("Completed meta data synchronization in [" + duration + "] seconds"); return 0; } /** * Runs through the tsdb-uid table and removes TSMeta, UIDMeta and TSUID * counter entries from the table * The process is as follows: * <ul><li>Fetch the max number of Metric UIDs</li> * <li>Split the # of UIDs amongst worker threads</li> * <li>Create a delete request with the qualifiers of any matching meta data * columns</li></ul> * <li>Continue on to the next unprocessed timeseries data row</li></ul> * @param tsdb The tsdb to use for processing, including a search plugin * @return 0 if completed successfully, something else if it dies */ private static int metaPurge(final TSDB tsdb) throws Exception { final long start_time = System.currentTimeMillis() / 1000; final long max_id = CliUtils.getMaxMetricID(tsdb); // now figure out how many IDs to divy up between the workers final int workers = Runtime.getRuntime().availableProcessors() * 2; final double quotient = (double)max_id / (double)workers; long index = 1; LOG.info("Max metric ID is [" + max_id + "]"); LOG.info("Spooling up [" + workers + "] worker threads"); final Thread[] threads = new Thread[workers]; for (int i = 0; i < workers; i++) { threads[i] = new MetaPurge(tsdb, index, quotient, i); threads[i].setName("MetaSync # " + i); threads[i].start(); index += quotient; if (index < max_id) { index++; } } // wait till we're all done for (int i = 0; i < workers; i++) { threads[i].join(); LOG.info("[" + i + "] Finished"); } // make sure buffered data is flushed to storage before exiting tsdb.flush().joinUninterruptibly(); final long duration = (System.currentTimeMillis() / 1000) - start_time; LOG.info("Completed meta data synchronization in [" + duration + "] seconds"); return 0; } /** * Runs through all TSMeta objects in the UID table and passes them through * each of the Trees configured in the system. * First, the method loads all trees in the system, compiles them into * TreeBuilders, then scans the UID table, passing each TSMeta through each * of the TreeBuilder objects. * @param tsdb The TSDB to use for access * @return 0 if completed successfully, something else if an error occurred */ private static int treeSync(final TSDB tsdb) throws Exception { final long start_time = System.currentTimeMillis() / 1000; final long max_id = CliUtils.getMaxMetricID(tsdb); // now figure out how many IDs to divy up between the workers final int workers = Runtime.getRuntime().availableProcessors() * 2; final double quotient = (double)max_id / (double)workers; long index = 1; LOG.info("Max metric ID is [" + max_id + "]"); LOG.info("Spooling up [" + workers + "] worker threads"); final Thread[] threads = new Thread[workers]; for (int i = 0; i < workers; i++) { threads[i] = new TreeSync(tsdb, index, quotient, i); threads[i].setName("TreeSync # " + i); threads[i].start(); index += quotient; if (index < max_id) { index++; } } // wait till we're all done for (int i = 0; i < workers; i++) { threads[i].join(); LOG.info("[" + i + "] Finished"); } // make sure buffered data is flushed to storage before exiting tsdb.flush().joinUninterruptibly(); final long duration = (System.currentTimeMillis() / 1000) - start_time; LOG.info("Completed meta data synchronization in [" + duration + "] seconds"); return 0; } /** * Attempts to delete the branches, leaves, collisions and not-matched entries * for a given tree. Optionally removes the tree definition itself * @param tsdb The TSDB to use for access * @param tree_id ID of the tree to delete * @param delete_definition Whether or not to delete the tree definition as * well * @return 0 if completed successfully, something else if an error occurred */ private static int purgeTree(final TSDB tsdb, final int tree_id, final boolean delete_definition) throws Exception { final TreeSync sync = new TreeSync(tsdb, 0, 1, 0); return sync.purgeTree(tree_id, delete_definition); } }