// 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.core; import java.util.Arrays; import java.util.Comparator; import org.hbase.async.Bytes; import com.stumbleupon.async.Deferred; /** Helper functions to deal with the row key. */ final public class RowKey { private RowKey() { // Can't create instances of this utility class. } /** * Extracts the name of the metric ID contained in a row key. * @param tsdb The TSDB to use. * @param row The actual row key. * @return The name of the metric. * @throws NoSuchUniqueId if the UID could not resolve to a string */ static String metricName(final TSDB tsdb, final byte[] row) { try { return metricNameAsync(tsdb, row).joinUninterruptibly(); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException("Should never be here", e); } } /** * Extracts the name of the metric ID contained in a row key. * @param tsdb The TSDB to use. * @param row The actual row key. * @return A deferred to wait on that will return the name of the metric. * @throws IllegalArgumentException if the row key is too short due to missing * salt or metric or if it's null/empty. * @throws NoSuchUniqueId if the UID could not resolve to a string * @since 1.2 */ public static Deferred<String> metricNameAsync(final TSDB tsdb, final byte[] row) { if (row == null || row.length < 1) { throw new IllegalArgumentException("Row key cannot be null or empty"); } if (row.length < Const.SALT_WIDTH() + tsdb.metrics.width()) { throw new IllegalArgumentException("Row key is too short"); } final byte[] id = Arrays.copyOfRange( row, Const.SALT_WIDTH(), tsdb.metrics.width() + Const.SALT_WIDTH()); return tsdb.metrics.getNameAsync(id); } /** * Generates a row key given a TSUID and an absolute timestamp. The timestamp * will be normalized to an hourly base time. If salting is enabled then * empty salt bytes will be prepended to the key and must be filled in later. * @param tsdb The TSDB to use for fetching tag widths * @param tsuid The TSUID to use for the key * @param timestamp An absolute time from which we generate the row base time * @return A row key for use in fetching data from OpenTSDB * @throws IllegalArgumentException if the TSUID is too short, i.e. doesn't * contain a metric * @since 2.0 */ public static byte[] rowKeyFromTSUID(final TSDB tsdb, final byte[] tsuid, final long timestamp) { if (tsuid.length < tsdb.metrics.width()) { throw new IllegalArgumentException("TSUID appears to be missing the metric"); } final long base_time; if ((timestamp & Const.SECOND_MASK) != 0) { // drop the ms timestamp to seconds to calculate the base timestamp base_time = ((timestamp / 1000) - ((timestamp / 1000) % Const.MAX_TIMESPAN)); } else { base_time = (timestamp - (timestamp % Const.MAX_TIMESPAN)); } final byte[] row = new byte[Const.SALT_WIDTH() + tsuid.length + Const.TIMESTAMP_BYTES]; System.arraycopy(tsuid, 0, row, Const.SALT_WIDTH(), tsdb.metrics.width()); Bytes.setInt(row, (int) base_time, Const.SALT_WIDTH() + tsdb.metrics.width()); System.arraycopy(tsuid, tsdb.metrics.width(), row, Const.SALT_WIDTH() + tsdb.metrics.width() + Const.TIMESTAMP_BYTES, tsuid.length - tsdb.metrics.width()); RowKey.prefixKeyWithSalt(row); return row; } /** * Returns the byte array for the given salt id * WARNING: Don't use this one unless you know what you're doing. It's here * for unit testing. * @param bucket The ID of the bucket to get the salt for * @return The salt as a byte array based on the width in bytes * @since 2.2 */ public static byte[] getSaltBytes(final int bucket) { final byte[] bytes = new byte[Const.SALT_WIDTH()]; int shift = 0; for (int i = 1;i <= Const.SALT_WIDTH(); i++) { bytes[Const.SALT_WIDTH() - i] = (byte) (bucket >>> shift); shift += 8; } return bytes; } /** * Calculates and writes an array of one or more salt bytes at the front of * the given row key. * * The salt is calculated by taking the Java hash code of the metric and * tag UIDs and returning a modulo based on the number of salt buckets. * The result will always be a positive integer from 0 to salt buckets. * * NOTE: The row key passed in MUST have allocated the {@link width} number of * bytes at the front of the row key or this call will overwrite data. * * WARNING: If the width is set to a positive value, then the bucket must be * at least 1 or greater. * @param row_key The pre-allocated row key to write the salt to * @since 2.2 */ public static void prefixKeyWithSalt(final byte[] row_key) { if (Const.SALT_WIDTH() > 0) { if (row_key.length < (Const.SALT_WIDTH() + TSDB.metrics_width()) || (Bytes.memcmp(row_key, new byte[Const.SALT_WIDTH() + TSDB.metrics_width()], Const.SALT_WIDTH(), TSDB.metrics_width()) == 0)) { // ^ Don't salt the global annotation row, leave it at zero return; } final int tags_start = Const.SALT_WIDTH() + TSDB.metrics_width() + Const.TIMESTAMP_BYTES; // we want the metric and tags, not the timestamp final byte[] salt_base = new byte[row_key.length - Const.SALT_WIDTH() - Const.TIMESTAMP_BYTES]; System.arraycopy(row_key, Const.SALT_WIDTH(), salt_base, 0, TSDB.metrics_width()); System.arraycopy(row_key, tags_start,salt_base, TSDB.metrics_width(), row_key.length - tags_start); int modulo = Arrays.hashCode(salt_base) % Const.SALT_BUCKETS(); if (modulo < 0) { // make sure we return a positive salt. modulo = modulo * -1; } final byte[] salt = getSaltBytes(modulo); System.arraycopy(salt, 0, row_key, 0, Const.SALT_WIDTH()); } // else salting is disabled so it's a no-op } /** * Checks a row key to determine if it contains the metric UID. If salting is * enabled, we skip the salt bytes. * @param metric The metric UID to match * @param row_key The row key to match on * @return 0 if the two arrays are identical, otherwise the difference * between the first two different bytes (treated as unsigned), otherwise * the different between their lengths. * @throws IndexOutOfBoundsException if either array isn't large enough. */ public static int rowKeyContainsMetric(final byte[] metric, final byte[] row_key) { int idx = Const.SALT_WIDTH(); for (int i = 0; i < metric.length; i++, idx++) { if (metric[i] != row_key[idx]) { return (metric[i] & 0xFF) - (row_key[idx] & 0xFF); // "promote" to unsigned. } } return 0; } /** * A comparator that ignores the salt in row keys */ public static class SaltCmp implements Comparator<byte[]> { public int compare(final byte[] a, final byte[] b) { final int length = Math.min(a.length, b.length); if (a == b) { // Do this after accessing a.length and b.length return 0; // in order to NPE if either a or b is null. } // Skip salt for (int i = Const.SALT_WIDTH(); i < length; i++) { if (a[i] != b[i]) { return (a[i] & 0xFF) - (b[i] & 0xFF); // "promote" to unsigned. } } return a.length - b.length; } } }